diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 20999b3a54e..7bff8f0cfa0 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -13,6 +13,7 @@ clickable clig CMMI copyable +Counterintuitively CtrlDToClose cybersecurity dalet @@ -94,6 +95,7 @@ shcha slnt Sos ssh +sustainability stakeholders sxn timeline diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 71b2922ca03..4ce5fe88d6d 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -28,6 +28,7 @@ CYICON Dacl dataobject dcomp +debugbreak delayimp DERR dlldata @@ -219,16 +220,19 @@ UFIELD ULARGE UOI UPDATEINIFILE +urlmon userenv USEROBJECTFLAGS Vcpp Viewbox virtualalloc vsnwprintf +wcsnlen wcsstr wcstoui WDJ winhttp +wininet winmain winsta winstamin diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index b79f49c3781..1ed1c6da897 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -4,6 +4,7 @@ advapi altform altforms appendwttlogging +appinstaller appx appxbundle appxerror @@ -59,12 +60,13 @@ PRIINFO propkey pscustomobject QWORD +rdpclip regedit resfiles robocopy SACLs -segoe sdkddkver +segoe Shobjidl sid Skype diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 2826e125a02..4339b17eb04 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -188,8 +188,10 @@ changelist chaof charinfo CHARSETINFO +chcbpat chh chk +chshdng CHT Cic cielab @@ -749,7 +751,6 @@ gfx GGI GHIJK GHIJKL -GHIJKLM gitfilters gitmodules gle @@ -1000,7 +1001,6 @@ listptrsize lld llx LMENU -LMNOP lnk lnkd lnkfile @@ -1223,7 +1223,6 @@ nonspace NOOWNERZORDER Nop NOPAINT -NOPQRST noprofile NOREDRAW NOREMOVE @@ -1498,7 +1497,6 @@ pwsz pythonw Qaabbcc qos -QRSTU QUERYOPEN QUESTIONMARK quickedit @@ -1861,7 +1859,6 @@ testname TESTNULL testpass testpasses -testtimeout TEXCOORD texel TExpected @@ -1908,6 +1905,7 @@ touchpad Tpp Tpqrst tprivapi +tput tracelog tracelogging traceloggingprovider @@ -2018,7 +2016,6 @@ utext UText UTEXT utr -UVWX UVWXY UVWXYZ uwa @@ -2077,7 +2074,6 @@ VTRGBTo vtseq vtterm vttest -VWX waitable WANSUNG WANTARROWS @@ -2273,6 +2269,7 @@ YCast YCENTER YCount YDPI +YLimit YOffset YSubstantial YVIRTUALSCREEN diff --git a/OpenConsole.sln b/OpenConsole.sln index 1a48fa2a001..5c2b55ccd4b 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -239,8 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{89CDCC EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Types.Unit.Tests", "src\types\ut_types\Types.Unit.Tests.vcxproj", "{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PublicTerminalCore", "src\cascadia\PublicTerminalCore\PublicTerminalCore.vcxproj", "{84848BFA-931D-42CE-9ADF-01EE54DE7890}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfTerminalControl", "src\cascadia\WpfTerminalControl\WpfTerminalControl.csproj", "{376FE273-6B84-4EB5-8B30-8DE9D21B022C}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests_TerminalApp", "src\cascadia\ut_app\TerminalApp.UnitTests.vcxproj", "{CA5CAD1A-9333-4D05-B12A-1905CBF112F9}" @@ -334,7 +332,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "src\dep\fmt\fmt.vcxp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfTerminalTestNetCore", "src\cascadia\WpfTerminalTestNetCore\WpfTerminalTestNetCore.csproj", "{1588FD7C-241E-4E7D-9113-43735F3E6BAD}" ProjectSection(ProjectDependencies) = postProject - {84848BFA-931D-42CE-9ADF-01EE54DE7890} = {84848BFA-931D-42CE-9ADF-01EE54DE7890} + {CA5CAD1A-F542-4635-A069-7CAEFB930070} = {CA5CAD1A-F542-4635-A069-7CAEFB930070} {A22EC5F6-7851-4B88-AC52-47249D437A52} = {A22EC5F6-7851-4B88-AC52-47249D437A52} EndProjectSection EndProject @@ -1711,34 +1709,6 @@ Global {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x64.Build.0 = Release|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.ActiveCfg = Release|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.Build.0 = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x64.ActiveCfg = AuditMode|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x64.Build.0 = AuditMode|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x86.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x86.Build.0 = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|Any CPU.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM64.Build.0 = Debug|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x64.ActiveCfg = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x64.Build.0 = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x86.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x86.Build.0 = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|Any CPU.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM64.ActiveCfg = Release|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM64.Build.0 = Release|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x64.ActiveCfg = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x64.Build.0 = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x86.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x86.Build.0 = Release|Win32 {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|Any CPU.ActiveCfg = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM.Build.0 = Debug|Any CPU @@ -2894,7 +2864,6 @@ Global {05500DEF-2294-41E3-AF9A-24E580B82836} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {1E4A062E-293B-4817-B20D-BF16B979E350} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} - {84848BFA-931D-42CE-9ADF-01EE54DE7890} = {4DAF0299-495E-4CD1-A982-9BAC16A45932} {376FE273-6B84-4EB5-8B30-8DE9D21B022C} = {4DAF0299-495E-4CD1-A982-9BAC16A45932} {CA5CAD1A-9333-4D05-B12A-1905CBF112F9} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {CA5CAD1A-9A12-429C-B551-8562EC954746} = {59840756-302F-44DF-AA47-441A9D673202} diff --git a/README.md b/README.md index b174aeaa5a9..2ace5f109b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![terminal-logos](https://user-images.githubusercontent.com/48369326/115790869-4c852b00-a37c-11eb-97f1-f61972c7800c.png) +![terminal-logos](https://github.com/microsoft/terminal/assets/91625426/333ddc76-8ab2-4eb4-a8c0-4d7b953b1179) # Welcome to the Windows Terminal, Console and Command-Line repo @@ -118,6 +118,28 @@ repository. --- +## Installing Windows Terminal Canary +Windows Terminal Canary is a nightly build of Windows Terminal. This build has the latest code from our `main` branch, giving you an opportunity to try features before they make it to Windows Terminal Preview. + +Windows Terminal Canary is our least stable offering, so you may discover bugs before we have had a chance to find them. + +Windows Terminal Canary is available as an App Installer distribution and a Portable ZIP distribution. + +The App Installer distribution supports automatic updates. Due to platform limitations, this installer only works on Windows 11. + +The Portable ZIP distribution is a portable application. It will not automatically update and will not automatically check for updates. This portable ZIP distribution works on Windows 10 (19041+) and Windows 11. + +| Distribution | Architecture | Link | +|---------------|:---------------:|------------------------------------------------------| +| App Installer | x64, arm64, x86 | [download](https://aka.ms/terminal-canary-installer) | +| Portable ZIP | x64 | [download](https://aka.ms/terminal-canary-zip-x64) | +| Portable ZIP | ARM64 | [download](https://aka.ms/terminal-canary-zip-arm64) | +| Portable ZIP | x86 | [download](https://aka.ms/terminal-canary-zip-x86) | + +_Learn more about the [types of Windows Terminal distributions](https://learn.microsoft.com/windows/terminal/distributions)._ + +--- + ## Windows Terminal Roadmap The plan for the Windows Terminal [is described here](/doc/roadmap-2023.md) and diff --git a/build/config/esrp.build.batch.wpf.json b/build/config/esrp.build.batch.wpf.json index e9c9af87891..62432046664 100644 --- a/build/config/esrp.build.batch.wpf.json +++ b/build/config/esrp.build.batch.wpf.json @@ -1,7 +1,7 @@ [ { "MatchedPath": [ - "PublicTerminalCore.dll" + "Microsoft.Terminal.Control/Microsoft.Terminal.Control.dll" ], "SigningInfo": { "Operations": [ diff --git a/build/config/release.gdnbaselines b/build/config/release.gdnbaselines new file mode 100644 index 00000000000..b8f686e3a94 --- /dev/null +++ b/build/config/release.gdnbaselines @@ -0,0 +1,1127 @@ +{ + "hydrated": false, + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines", + "hydrationStatus": "This file does not contain identifying data. It is safe to check into your repo. To hydrate this file with identifying data, run `guardian hydrate --help` and follow the guidance." + }, + "version": "1.0.0", + "baselines": { + "default": { + "name": "default", + "createdDate": "2023-09-29 19:39:12Z", + "lastUpdatedDate": "2023-09-29 19:39:12Z" + } + }, + "results": { + "b1e592dc96e9286d806682680cffcb44b7ba2f7ea883b022371037689fe2bb5b": { + "signature": "b1e592dc96e9286d806682680cffcb44b7ba2f7ea883b022371037689fe2bb5b", + "alternativeSignatures": [ + "25d5f2d86325f3e6666cf720c3b0827c7b5773c42f3770a6e83ab8bc0f716686" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "fd608e2146a69491516290ae3b65d4d094e5f9ba53c6624ff457c3b21f67e433": { + "signature": "fd608e2146a69491516290ae3b65d4d094e5f9ba53c6624ff457c3b21f67e433", + "alternativeSignatures": [ + "528526d5f81c7387e9eca88c1dc1e7886bb3189d9c53f7678c63a44a7e78d2b8" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "104eb68a26c9a422f9973b85603046c42bb6894f398e1ebd454d829a386c47cd": { + "signature": "104eb68a26c9a422f9973b85603046c42bb6894f398e1ebd454d829a386c47cd", + "alternativeSignatures": [ + "33b75434f899dc72a71c55e6667420b5f3406641eae1a784bad8632fe80eccc6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "fa8ab295467d81fd138402f12899da84cdf312e2e84712a539e08361b6900651": { + "signature": "fa8ab295467d81fd138402f12899da84cdf312e2e84712a539e08361b6900651", + "alternativeSignatures": [ + "b0ef4eae3f158488f99f7705cc93d037d55e8bd39b173c40e25581a35c6889d2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2d5cf239e62a256384275134ea1e1180b927f4cd53e51193751b0b5671045be8": { + "signature": "2d5cf239e62a256384275134ea1e1180b927f4cd53e51193751b0b5671045be8", + "alternativeSignatures": [ + "bd73f2f051d6525c01db61a4c4a6c3b9a80083da83ed602eae2a70e17286a7ed" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e88b3902e68d0315f7bef3a24860a7d3d9abd2d960fde06a0c650c6bb569df74": { + "signature": "e88b3902e68d0315f7bef3a24860a7d3d9abd2d960fde06a0c650c6bb569df74", + "alternativeSignatures": [ + "143ade735d3d6d19f999555f2ce706f647a64688054b5f19052f99992ab325fd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "01306239b92e4508b0ab60e025cadc4c974c33c29f1eb28d2b3fdda8d4019e09": { + "signature": "01306239b92e4508b0ab60e025cadc4c974c33c29f1eb28d2b3fdda8d4019e09", + "alternativeSignatures": [ + "dca24210a3064ca7069a737f9a88f4a25b1cc1cd4c9e2a095156297246611de5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "675ea9bd7809b2f210105e9703dffe8e48ee134ab21677e7cc4556bf22782e8d": { + "signature": "675ea9bd7809b2f210105e9703dffe8e48ee134ab21677e7cc4556bf22782e8d", + "alternativeSignatures": [ + "7d5287a33645767969627a20e3a65fb2f1bb2db0e79a689b5a804a98f65d01fa" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6e040bc57aa08165f8af6007e3794149d3a2c0325021e746e9705ada7c05b30c": { + "signature": "6e040bc57aa08165f8af6007e3794149d3a2c0325021e746e9705ada7c05b30c", + "alternativeSignatures": [ + "218c0d7d5332113e33632d06eb0f209bac711ac515bd76a0fcfda2a0cbed885d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "73e85b3af96a27c797fbe9f36f00feccd4118204615b9477da67e5868dd5dfb9": { + "signature": "73e85b3af96a27c797fbe9f36f00feccd4118204615b9477da67e5868dd5dfb9", + "alternativeSignatures": [ + "49c44c3dc27233eba172214cb302de4b5c9b2a845e8595e7930fad74805183ac" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "15bd7ecdb19cdd263867db23c12a98029cede5476c89e870b811fce45abb8db4": { + "signature": "15bd7ecdb19cdd263867db23c12a98029cede5476c89e870b811fce45abb8db4", + "alternativeSignatures": [ + "2705f7f6c9e83d2a812e264f39878e0e0173a1a5efea16e7206c4d02dc419af8" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3f7d3360f0b185361b1e67b94c22b2a319b7c2d5aa2c8fd7fc3e4626394cba54": { + "signature": "3f7d3360f0b185361b1e67b94c22b2a319b7c2d5aa2c8fd7fc3e4626394cba54", + "alternativeSignatures": [ + "71f05133f86c279daee4f1b85f997955e36857bcc5945dbae71f8bbb91b5df6d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3bcb157fd01d81513a15da41b4fc8b2820247331689fef5d0026578d95b85716": { + "signature": "3bcb157fd01d81513a15da41b4fc8b2820247331689fef5d0026578d95b85716", + "alternativeSignatures": [ + "67e145d1f3bf6eb205e385884ca6356decffc3cac3e15339cc7ca43441cff99b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "27ae6a9a7047c5ca98d1dc3a9b0885733875387d475f1426afc4d031b357fc1b": { + "signature": "27ae6a9a7047c5ca98d1dc3a9b0885733875387d475f1426afc4d031b357fc1b", + "alternativeSignatures": [ + "b87d72abe37f6144614469221a0ce60e389792843f602805afe149b3ad1dd098" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "bd2aea1d24e36c1dcc128eb87a2c27292ed251d95aedf5f11204647bc68719f8": { + "signature": "bd2aea1d24e36c1dcc128eb87a2c27292ed251d95aedf5f11204647bc68719f8", + "alternativeSignatures": [ + "ca29329aae819543d357e86acf0ccce914dbc3a15e0f91016091c3079638ad34" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b836a6e0d58634003e054c88f3ed615d8e92182ebc78e9dd2168d8126c33e694": { + "signature": "b836a6e0d58634003e054c88f3ed615d8e92182ebc78e9dd2168d8126c33e694", + "alternativeSignatures": [ + "850aa5d2e92dac06fc119ed297e84dfde9fdb3f9b116bdb2c83e8d66ea442865" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e54972bb37d35b3b9c7c764cdf5351f0254ae2b8526465fe03c9ba8a04b06651": { + "signature": "e54972bb37d35b3b9c7c764cdf5351f0254ae2b8526465fe03c9ba8a04b06651", + "alternativeSignatures": [ + "9e8723695743edaddfc5b188ec6bde240ddde68516f5c2342bd1c4d2a8c68430" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3786d03543e25eec1f68beb21a3884953460239c269ba4071baf9fd4f564e763": { + "signature": "3786d03543e25eec1f68beb21a3884953460239c269ba4071baf9fd4f564e763", + "alternativeSignatures": [ + "05e0c9bc16b8ade4d40d4e4371864da4ee7d9c8d68b19c3319543dbebea97a39" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5f7c1c0c1f9290d3afad4bb3a17d5e5d33f2176e18b8ec406df71e8245ec0b7f": { + "signature": "5f7c1c0c1f9290d3afad4bb3a17d5e5d33f2176e18b8ec406df71e8245ec0b7f", + "alternativeSignatures": [ + "952d91003467bdf2dbf5e72f56b9c69de52c4abf44cc82c0833ba8d89d9befac" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "605037e76782777be2f16c12c023247671ebad0afea868ed0c4bf939c9ff443a": { + "signature": "605037e76782777be2f16c12c023247671ebad0afea868ed0c4bf939c9ff443a", + "alternativeSignatures": [ + "4f77f69716e098fec8214948428cfe9d2c2324d0719250b25e5c084a21d05e69" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4a173deb8aeab5f4f2463774f117b909dee0168c99bc54b29b1141fca64598bd": { + "signature": "4a173deb8aeab5f4f2463774f117b909dee0168c99bc54b29b1141fca64598bd", + "alternativeSignatures": [ + "66526149b57bbdaa8d4e3437a0099a4e3b7818c911b2110fa40d7d0264ce99f6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e557e9c4d1c36f69fd4a2310d22bbeadc1dff3dd2404449b8424662bb1c6adc7": { + "signature": "e557e9c4d1c36f69fd4a2310d22bbeadc1dff3dd2404449b8424662bb1c6adc7", + "alternativeSignatures": [ + "6b93c58cd83ba89289c12b408c2988dfd54d7936c0920981cf690725155caf7a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "70eb589101b9b57d2e47f6879348a8b93cd0c2f223e2c20d2289c5e1e8be0b9d": { + "signature": "70eb589101b9b57d2e47f6879348a8b93cd0c2f223e2c20d2289c5e1e8be0b9d", + "alternativeSignatures": [ + "e9fe7fc2a31de263521f635058984dd81b4bab9cba174949fc5582fbeef6f693" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3e5c0b1cad192bf5fa00d6b0f46f4cfaa7251ebaea30177d2d6891890f0dafab": { + "signature": "3e5c0b1cad192bf5fa00d6b0f46f4cfaa7251ebaea30177d2d6891890f0dafab", + "alternativeSignatures": [ + "9a1dd0e934ddd3a33f3cf37372052731f5c157c13cac05ef0539815f845db9bf" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "dd3b2ddb278bb70559efcfc10e19c2f3a3bfe805cbbecd6a16dd141433f0b433": { + "signature": "dd3b2ddb278bb70559efcfc10e19c2f3a3bfe805cbbecd6a16dd141433f0b433", + "alternativeSignatures": [ + "3fcb3a01715c6d244d26ef49fa4c06f8a92616a9c6465c26c4398c554f7fd67f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a635cb613925ab6c9f6b133890169bdf77acee212b6ac8f83cca7253faafaec7": { + "signature": "a635cb613925ab6c9f6b133890169bdf77acee212b6ac8f83cca7253faafaec7", + "alternativeSignatures": [ + "2ff5cc7a3bf6cd63862cffc5088690fc824eb1d2f9e42c732fcd1611e9e25bfc" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4ecdfc33e6267bfeadb1507d5304a9737f3ba0ee3132e6d7bc6f3fd68a47824e": { + "signature": "4ecdfc33e6267bfeadb1507d5304a9737f3ba0ee3132e6d7bc6f3fd68a47824e", + "alternativeSignatures": [ + "23dc3273d54a3db12ad729569696b875523201dc13d4cbba9e3642dde17492bb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a9b9f171556057eaf197eb19780abfac5df82154f6c06c07240ce73970127df7": { + "signature": "a9b9f171556057eaf197eb19780abfac5df82154f6c06c07240ce73970127df7", + "alternativeSignatures": [ + "0a9f4d3a4aa85e918744a5af36851791eb4b2f44fc132560cc56cf5661ba28fb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "ad9da2b392f1622c6cff4a2b63eadf0da2ff31811dbbf8c9f2b2c4e8d9fefa1f": { + "signature": "ad9da2b392f1622c6cff4a2b63eadf0da2ff31811dbbf8c9f2b2c4e8d9fefa1f", + "alternativeSignatures": [ + "b1fafebeb213b04346d56182ce395ccb9a0ad866dc28bda801c222499e550396" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b8f591f33a84c38402239941484ead425c3e37bfcfd486b9be3a15d2213d9fa3": { + "signature": "b8f591f33a84c38402239941484ead425c3e37bfcfd486b9be3a15d2213d9fa3", + "alternativeSignatures": [ + "de56fbe02702da318ea6a113e592b2efee3f942e04e31c5e9300cf6798abea0c" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "37adbb986601cf75f2d1eaebbe6d430a70a0886e60685a880e7d1f2c48f9b125": { + "signature": "37adbb986601cf75f2d1eaebbe6d430a70a0886e60685a880e7d1f2c48f9b125", + "alternativeSignatures": [ + "7ebe5c4d5a144a3f356114bf5e062295ee90d5cdc7ca615dc729b7dcba8bed33" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e0de291c50fb0cae3bdaf75237ac8e3f34acc6a8de18ba589dde4e76352ab849": { + "signature": "e0de291c50fb0cae3bdaf75237ac8e3f34acc6a8de18ba589dde4e76352ab849", + "alternativeSignatures": [ + "52c0187472041933b5d850e47cf28ac6a7ef5ba0a6a85bca65af3b3531ddb06a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6b163fce31228367e0323768ab0dcc1224f8416cc856ddc03ccc2b8a2cf9a08e": { + "signature": "6b163fce31228367e0323768ab0dcc1224f8416cc856ddc03ccc2b8a2cf9a08e", + "alternativeSignatures": [ + "0e6f9f2e1008091bcfbf520d7407c3690564ea5118580fd8cfca965946f53a78" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "25012b8cab7c6dddc276e2c0d7888554e8542dd686891914d2c2e82dfd3f697a": { + "signature": "25012b8cab7c6dddc276e2c0d7888554e8542dd686891914d2c2e82dfd3f697a", + "alternativeSignatures": [ + "ea05d52f5ee2b483e83c14216a3e56bc8a4474396b71cbb426ced61242d81919" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "bfc7ac3b82698c138a970968cce2c849ed0e45bb8930d42215f6704659f63762": { + "signature": "bfc7ac3b82698c138a970968cce2c849ed0e45bb8930d42215f6704659f63762", + "alternativeSignatures": [ + "c59c7529464c717d1909058d1f4fbc5109881effafab3c67bbbf02ccdffddd4e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "25582fa1ab3be3a375a0d053304e65b50ebfa590f81a5d957759304ba29e3053": { + "signature": "25582fa1ab3be3a375a0d053304e65b50ebfa590f81a5d957759304ba29e3053", + "alternativeSignatures": [ + "e2628baf1e09340a2063cd39c238e541014a65f4433018cc36cd25c5c8c3043d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "38b73c3711c0c9cd958195f318fd2e6ab1f10cfa94abdfb7505d784ba022cfae": { + "signature": "38b73c3711c0c9cd958195f318fd2e6ab1f10cfa94abdfb7505d784ba022cfae", + "alternativeSignatures": [ + "6688a9492755e33e541513bf297d0b4f459f70a43486fd8411749a2caf5cb91c" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "250a0184e9feee525e9a54930f5714965bd2bc29d0fe91c502c560443fa0e10a": { + "signature": "250a0184e9feee525e9a54930f5714965bd2bc29d0fe91c502c560443fa0e10a", + "alternativeSignatures": [ + "d7b1083c67959b8f095acd532a10244a0a5afcbc93eefc66be20c19841b2588e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2c64c980db98b04a7daeedf4c5d358b8ecaecca81a8e0f70911fd211e06c8e20": { + "signature": "2c64c980db98b04a7daeedf4c5d358b8ecaecca81a8e0f70911fd211e06c8e20", + "alternativeSignatures": [ + "7778c4704d515b92d4dc77a9b612c151e6bb9e7cd88e100c7d0419fb150f34e6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3bb5f422737103df8f018ec0789d2b7793ce5b1641927d5646e963126e73e1b5": { + "signature": "3bb5f422737103df8f018ec0789d2b7793ce5b1641927d5646e963126e73e1b5", + "alternativeSignatures": [ + "225e02e47ea3f999bc43279246ee51b4287e4e2b4b7dac899da91c365d8acd0e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6e39bdcfe28e2cb6868a5dcbe12fa712d2a60762bb57fd82734089e19ca85b52": { + "signature": "6e39bdcfe28e2cb6868a5dcbe12fa712d2a60762bb57fd82734089e19ca85b52", + "alternativeSignatures": [ + "f6ebab2bea88d78f69c8ae78f4ec641edd0f0796b9ca193260c44cee9d543a18" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e9a5cff46470c62be3e1db1e43785b71daa88e466c802d4393bf9248a9e6fb56": { + "signature": "e9a5cff46470c62be3e1db1e43785b71daa88e466c802d4393bf9248a9e6fb56", + "alternativeSignatures": [ + "3bdabc64cff83a1322f107a97ce6eab6e8cb41b8492643e9a0df3561d0169b95" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9f19392621589641c13fc5762e36429cc4d38fc04f98297006be89e5ca3e5631": { + "signature": "9f19392621589641c13fc5762e36429cc4d38fc04f98297006be89e5ca3e5631", + "alternativeSignatures": [ + "f448a0535740dcb9bddebe7765ea55c819472efdb9cd63761dc156b8af11a260" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "60b306912ae3399a167514dabf495a7f57e0ff17ec9a9491e66cf39c1ecbf60b": { + "signature": "60b306912ae3399a167514dabf495a7f57e0ff17ec9a9491e66cf39c1ecbf60b", + "alternativeSignatures": [ + "fbd4d1fe2e8e21ed5fe000fc592ef9fa98ff52f529b849c9ceca12c85c3cf4f3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2498be9bbe27f393fad6af89f23cf228fe5c263e38fa3d20d3652b68c356243b": { + "signature": "2498be9bbe27f393fad6af89f23cf228fe5c263e38fa3d20d3652b68c356243b", + "alternativeSignatures": [ + "750b9e1ff2f1b443502036d226678bcdda2f22a3879e36b61e3bcec24c0eec3e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "147fa7e9b0187ab163dfe5049876c838b2b599a8ac2c243226ab4fed8d9e3513": { + "signature": "147fa7e9b0187ab163dfe5049876c838b2b599a8ac2c243226ab4fed8d9e3513", + "alternativeSignatures": [ + "5fe9a0ee3ada52e4dbee1aa6f45683a82d14ed812d9d7c1634b786f28fc2ca51" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a2fe2c57ef4efc03e72dfd1fa653fc9d731b69bda91711f8ab8d46edc3b12667": { + "signature": "a2fe2c57ef4efc03e72dfd1fa653fc9d731b69bda91711f8ab8d46edc3b12667", + "alternativeSignatures": [ + "42ab6b70626c246ddea96b060cb7a79248576d19f003c4f4eedf171f4f5992f4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e7973a3b90d74d4ff6ee544dee45bb35ac66214a3e92eadecbf8fafccca9abb1": { + "signature": "e7973a3b90d74d4ff6ee544dee45bb35ac66214a3e92eadecbf8fafccca9abb1", + "alternativeSignatures": [ + "3359d44d5bbf533487fc65e4a98220af62eef0cc6b883bc35b8286ee1a31ca4f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "190440674b3fec2fcc77b341a684a9e9100c8dec0911a8c83f19f6ccedcb2b78": { + "signature": "190440674b3fec2fcc77b341a684a9e9100c8dec0911a8c83f19f6ccedcb2b78", + "alternativeSignatures": [ + "c7aab0deb9b97032353766e961f4b23bc694e85b8b06709beaddfa2e5743e742" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4d258afa7d015f4fc96c8866e2e43cb00290526d56394eec3848ed650cec840d": { + "signature": "4d258afa7d015f4fc96c8866e2e43cb00290526d56394eec3848ed650cec840d", + "alternativeSignatures": [ + "e23b41b521b849057b8c5ba0594af11fc5ceec59714f3d0ebbeda708511d5803" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "530d02de7ec8a3c9f575cf3770e34665a58b127e01555000e611ee4c5e37e24d": { + "signature": "530d02de7ec8a3c9f575cf3770e34665a58b127e01555000e611ee4c5e37e24d", + "alternativeSignatures": [ + "23f217577bcf9d3a6f87c0ac259cf891a80e78f2b77dd58e631816f3183101bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "69a35d8bd5d4766e5dc981526229ccc0d27f238b06357bdd8e0469cd35b47d08": { + "signature": "69a35d8bd5d4766e5dc981526229ccc0d27f238b06357bdd8e0469cd35b47d08", + "alternativeSignatures": [ + "cab1cab825e5846acc097221236feaf3a06895e648c2ef16a4c285616b714987" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c78a17b7f8d46b9ddedd34240d76adf92e4f1bf27a978c022e4599fe6a7b150d": { + "signature": "c78a17b7f8d46b9ddedd34240d76adf92e4f1bf27a978c022e4599fe6a7b150d", + "alternativeSignatures": [ + "fa75ccd56697c3073a6ef17e2d959ee24b6afc11eabc4bd5dbea1d74ecdb81cb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "7bd1757994e7f1909ffd25e56393f991d436232150cff2555a97e9b08b347b99": { + "signature": "7bd1757994e7f1909ffd25e56393f991d436232150cff2555a97e9b08b347b99", + "alternativeSignatures": [ + "dbdb5c5ebe211b21d9b25e346dfbc58e6b32500258a1efed248e4630216ce57e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "cef01bb2a477968c8cbc997179d04b90c36febd9cfd21ca973d4a22b3efc4caa": { + "signature": "cef01bb2a477968c8cbc997179d04b90c36febd9cfd21ca973d4a22b3efc4caa", + "alternativeSignatures": [ + "9ec52d86604abde8a13124534ec587d3a1686630ea7bb2f736a66963349abdbd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "11026da7398e29b59d98ffee5ed5317c243407638a77d7a09637ebe2d8bb09f3": { + "signature": "11026da7398e29b59d98ffee5ed5317c243407638a77d7a09637ebe2d8bb09f3", + "alternativeSignatures": [ + "a642f1f37cf06c75c2082d720e317481bb7e73249dcda33dba0b5cdd6933926f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8f2550b4f39a798d217679c04dd7cb21b72ef63e6f6d63b06aadb1c40d8c2546": { + "signature": "8f2550b4f39a798d217679c04dd7cb21b72ef63e6f6d63b06aadb1c40d8c2546", + "alternativeSignatures": [ + "10c95aac8513a0d6cc2c0ef08b51f7f4ac3be4b23ddd90d68d06fc536421acc3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "022eb0065260832f06f12313f8bf99765488c1db9ff9ef8ecf8a0cbf393e878d": { + "signature": "022eb0065260832f06f12313f8bf99765488c1db9ff9ef8ecf8a0cbf393e878d", + "alternativeSignatures": [ + "4801e53b7e46778aa21a8c9a0ec2725e73746fb648e76c9359f6db0f32ac2963" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "36f66ad8b00e978f29a7b1c09c7942247e3b068d131431c3b321190aa7cb2978": { + "signature": "36f66ad8b00e978f29a7b1c09c7942247e3b068d131431c3b321190aa7cb2978", + "alternativeSignatures": [ + "7d3ee020a3a1601c49be0c5d687f46858f679ecf6d4150b97ba9ae5abfa742e4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c50b78c8370aa9d767f783bddf8c57b2f1dbd749073ccde60b3b210b116b6951": { + "signature": "c50b78c8370aa9d767f783bddf8c57b2f1dbd749073ccde60b3b210b116b6951", + "alternativeSignatures": [ + "87568cc368760fe9f38d9fb95e93297d8bb5a83587d20d071f97713443df2e04" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8e2be08973d6028cf58e152fba8dcd5616b7938591d2292eaf3bb5f80bdcf004": { + "signature": "8e2be08973d6028cf58e152fba8dcd5616b7938591d2292eaf3bb5f80bdcf004", + "alternativeSignatures": [ + "d9df5f887062a990099a2cb70e46778aad25304d9758db556040beb4b23a8776" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "be36363b685b28e3ac4457b7a415120cab5a01c14d5c9a19192ef5ebd71c176a": { + "signature": "be36363b685b28e3ac4457b7a415120cab5a01c14d5c9a19192ef5ebd71c176a", + "alternativeSignatures": [ + "13779d67c6e5f5eba285b83093a2c9eec2d79b3b36ee86400873191310088255" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a6e48721d67f94b2876a97fe4b385ee37adc1b345825f751e87d6bd05095bfb9": { + "signature": "a6e48721d67f94b2876a97fe4b385ee37adc1b345825f751e87d6bd05095bfb9", + "alternativeSignatures": [ + "f6a5898d9652b23b269283d4bc1a8e95865ee9f7125479ab78087e3dd3b3b12f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b1b1aba985591c904b0faf09b54ddc4763890d25e5dcfa2d3ca758165a616970": { + "signature": "b1b1aba985591c904b0faf09b54ddc4763890d25e5dcfa2d3ca758165a616970", + "alternativeSignatures": [ + "b7ad084f971c1fae38509e87d50fd4984185ea6915d501d6cc0d1eac8bd93abf" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "078e8e5c43a51876fb09d7fab78a07fe896bbbb74c9507f3b6313ed3fe7586d4": { + "signature": "078e8e5c43a51876fb09d7fab78a07fe896bbbb74c9507f3b6313ed3fe7586d4", + "alternativeSignatures": [ + "7046bab04be2bfce75f0a34461871d3dccddab009c2a223a78c08696610c7a55" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9443465290e747bbefb4435296af78a82a948222097df6559db0d0488f7df81a": { + "signature": "9443465290e747bbefb4435296af78a82a948222097df6559db0d0488f7df81a", + "alternativeSignatures": [ + "47a1795431bb9f78383c54fd33cf3427a774ae9802aa994cbd688f81286904bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "01a56e39bd6a3652d95b21035efc6d165eb16451f9558d79ff13d297cd345cd9": { + "signature": "01a56e39bd6a3652d95b21035efc6d165eb16451f9558d79ff13d297cd345cd9", + "alternativeSignatures": [ + "2169e62964a8fa3bb4ccde41e33232efa8b7ddfb6d5a45688c28318c96ccbe52" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a9c67896453ec9e7fb742e05ec0a47f13c5c6b7bd3886497afe3c3bb860ef006": { + "signature": "a9c67896453ec9e7fb742e05ec0a47f13c5c6b7bd3886497afe3c3bb860ef006", + "alternativeSignatures": [ + "5973211461980400fe2aeea7dafe6079e6f771d799c347a0bcb7c631dc379d5e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2689cc88d4721d762e3783920aa7150cd00e4b5f7f15bdd81476dbb56876aa08": { + "signature": "2689cc88d4721d762e3783920aa7150cd00e4b5f7f15bdd81476dbb56876aa08", + "alternativeSignatures": [ + "ba41ff939940ab49d1a4b39dfb5fac6108111616c0d7fb87e6f64f1b82cbfbd2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "64d08c256034d4907fa6655e4914850c945bf1f6db4e14f52a8032eefe8e1245": { + "signature": "64d08c256034d4907fa6655e4914850c945bf1f6db4e14f52a8032eefe8e1245", + "alternativeSignatures": [ + "639e5b7c91137d49bd041d7f241bd2403067f445de5058048c4c6c926af226bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "689ed96056f498ca67a5a056efa247ee9aea196362e693a0b196e6e9eb2cba0f": { + "signature": "689ed96056f498ca67a5a056efa247ee9aea196362e693a0b196e6e9eb2cba0f", + "alternativeSignatures": [ + "9473554d81655d5cf548e8033fc5f34cd42cec8697306b75fc2ec31e8c19b29e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f497cbbab92fbbd65c27b122c7ceaf0d181859365ea121a885d32898dec963c6": { + "signature": "f497cbbab92fbbd65c27b122c7ceaf0d181859365ea121a885d32898dec963c6", + "alternativeSignatures": [ + "2fe25bb29ed8a90e5dba2e8e899f42713b61d178fd18231d560d4873656fe8a4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "669b421bcaea46b4f3a2b63097a357920df16b02075d1b86a476f9ee6fc4314a": { + "signature": "669b421bcaea46b4f3a2b63097a357920df16b02075d1b86a476f9ee6fc4314a", + "alternativeSignatures": [ + "5647958533f690de004b8f82064ed0fc779fe1206b64a1813f90988481dfb72e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8bb84450c75dcbee365ee2b2eb2ff83ef91925afb50d9c96aec845729f3a9be1": { + "signature": "8bb84450c75dcbee365ee2b2eb2ff83ef91925afb50d9c96aec845729f3a9be1", + "alternativeSignatures": [ + "85d7ccfcf73dbbc98b78cbd81581f3e81e9bc9a632a589fd79abb6f9cdd3236d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9beca9c8ed341ed7dbc682811ca43ec7174d945df99613c85e76e45ad55ac7f3": { + "signature": "9beca9c8ed341ed7dbc682811ca43ec7174d945df99613c85e76e45ad55ac7f3", + "alternativeSignatures": [ + "147836cc11afe71d92bdf75ef6d5b1b97ae5f5709d07a9d8c58ef8bce0631a86" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a0f61b31ca15ee653301086835bfb62889b4f4b537fd703ccfc21df9aa280a59": { + "signature": "a0f61b31ca15ee653301086835bfb62889b4f4b537fd703ccfc21df9aa280a59", + "alternativeSignatures": [ + "62828b4278eba310bc7982e5afdac6b0a865bb647f8d3cb078a6391e1a48054f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "235159b32d9022a9e8bf8e37fd5489bb11ca6f1bf56e8ee8737e63358c3610e5": { + "signature": "235159b32d9022a9e8bf8e37fd5489bb11ca6f1bf56e8ee8737e63358c3610e5", + "alternativeSignatures": [ + "ddb5cc4ca95b12e6fcccfd2c5089c798307d3bb664d91e9a87f6efbd1ea78a19" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "48738e93956293a663a2d1f6933bd6856afc4699c22d45da51ca02ffe4781230": { + "signature": "48738e93956293a663a2d1f6933bd6856afc4699c22d45da51ca02ffe4781230", + "alternativeSignatures": [ + "e83017371567ceabc524ef3e26a5520be1aac07e75b1f02a173f2eb847fbc3e5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6d0c84954b066bde9f165d3e7471f621d824bf0f9d920125f2188196c3e7be99": { + "signature": "6d0c84954b066bde9f165d3e7471f621d824bf0f9d920125f2188196c3e7be99", + "alternativeSignatures": [ + "07e93499ac1047ac94bc10474ba57d397bf395f28f8d6e0dd08eeab03f17a539" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b033555adb13ce0c0ec9e65689e4be5f9856f49f9e201f3dbbb24d430d76ec15": { + "signature": "b033555adb13ce0c0ec9e65689e4be5f9856f49f9e201f3dbbb24d430d76ec15", + "alternativeSignatures": [ + "0a59d205aee1fdc5cb027a46c424e4a2da06c5650aadbfdfec77d744278d6b7b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c522ca7bfcad2bc36cfb9567a5bbe36395bb3a5969ce411832197ff9ccfd4b32": { + "signature": "c522ca7bfcad2bc36cfb9567a5bbe36395bb3a5969ce411832197ff9ccfd4b32", + "alternativeSignatures": [ + "32eb8b00cbe99b945a02337c349a0fb97e1763fa9782469531a6859714a7160f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1c58db43c898e119381ecd4a9bb9701f2bbbffd3d0889ca734a905c151b168db": { + "signature": "1c58db43c898e119381ecd4a9bb9701f2bbbffd3d0889ca734a905c151b168db", + "alternativeSignatures": [ + "a87bb1c17c23667355c9b8e17f84f37671b65040a3aa7870a7d169165583211e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "ef7ba70ebe8984da430b1787f439496e14810afd9848a851a3d4cacc37ae7eac": { + "signature": "ef7ba70ebe8984da430b1787f439496e14810afd9848a851a3d4cacc37ae7eac", + "alternativeSignatures": [ + "bbff3ea19b6dd5baa7d56928526a38313685c1d81a30c7c2e7c31d7105d8287f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5bb44f02281666fb7ef18753b4ed0e08923d741efe0361ab1336c18dd0086111": { + "signature": "5bb44f02281666fb7ef18753b4ed0e08923d741efe0361ab1336c18dd0086111", + "alternativeSignatures": [ + "a4f1b29f64aa99073ad3678c41b29ab15bd3af2cbc87ae6b05e6568265734146" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1621b0375d4c98eb6822a7ae37feccda594bbd0137cba47b721f9f9288929947": { + "signature": "1621b0375d4c98eb6822a7ae37feccda594bbd0137cba47b721f9f9288929947", + "alternativeSignatures": [ + "05396ecc761c29e0e0b0a1508c1ed091b213ea18917b9f0ba118c1e7032c51df" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c566d2437202984d3a9d2682ca810d7c52eb78c7b6236c7b9c82db6f7ba63ac8": { + "signature": "c566d2437202984d3a9d2682ca810d7c52eb78c7b6236c7b9c82db6f7ba63ac8", + "alternativeSignatures": [ + "2bcbfb431661bacfa69c2c43c8f09f47444b22353d9e3f7db28f98c7b8126bd6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8f8856d0c4f85bb601ea84e7b9da0d5d636573c772951c4e85bf4cf61fef72bc": { + "signature": "8f8856d0c4f85bb601ea84e7b9da0d5d636573c772951c4e85bf4cf61fef72bc", + "alternativeSignatures": [ + "769ffcc5ae3360b1017647384c184dc56574e0d9ddd00f4083e1c986e9f616b2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b4c31c908facbb22d23b15c3280359184d736901c154d516be16495dbb35ccb0": { + "signature": "b4c31c908facbb22d23b15c3280359184d736901c154d516be16495dbb35ccb0", + "alternativeSignatures": [ + "d4603cccc6c303b5f16f1120b15dc74e8bdbef5877922fdcf57f3b29423c027b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "7e3401dab114451f8ccc26a6ee0ac27639a41cef983b5c7ca7b22f8e03748b53": { + "signature": "7e3401dab114451f8ccc26a6ee0ac27639a41cef983b5c7ca7b22f8e03748b53", + "alternativeSignatures": [ + "32964f7bed67a8e77a61cdc1cdedfb55c8800e04f16e9673dbc1c9dda0d2fcf0" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1599eec39bd8bfe41754c1b87f6d4cf2517107bf55a31fd6f06e57853f0edcb3": { + "signature": "1599eec39bd8bfe41754c1b87f6d4cf2517107bf55a31fd6f06e57853f0edcb3", + "alternativeSignatures": [ + "1d0195a64263b4be03267b1ce650ac0ad6b3589f32639ecd3792df0a39134d2a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c7e28dfe20e2c60575af07324e9960283b6ab567f4d0877763109330077b535d": { + "signature": "c7e28dfe20e2c60575af07324e9960283b6ab567f4d0877763109330077b535d", + "alternativeSignatures": [ + "22da5e4da2ab3befa6aea6c7b562de7057d367a0bf4f05a5791a02c6d7de5886" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "da7db308719d405aa91b08fcda683bc73d9fb73906ad14857b07f1487e2538fb": { + "signature": "da7db308719d405aa91b08fcda683bc73d9fb73906ad14857b07f1487e2538fb", + "alternativeSignatures": [ + "e8a05e57577cfff0f0614b0e2ac77ef86185b636ac9a85322decda6d93bb1025" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "406449365cf5b8a747cda6bcb9f698ab04e5f23c76ea26b15ae5f6dd199e200a": { + "signature": "406449365cf5b8a747cda6bcb9f698ab04e5f23c76ea26b15ae5f6dd199e200a", + "alternativeSignatures": [ + "ab85af1c9f00c18837eddc1a658df6f16759b57337f0c576979a7ead9ca5aea5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2d3985d721950fdc54d6dd67f6da4e2f26da1de752237d6eff2bee79e76e74cf": { + "signature": "2d3985d721950fdc54d6dd67f6da4e2f26da1de752237d6eff2bee79e76e74cf", + "alternativeSignatures": [ + "eb8fe28224b13b24a5fd5dab39c73356ab0e958242fefd3468a632b6f1fc8468" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "73a9b54e10ff814734690c8e99319851d3caffb8d7ee02237ccef08057623c57": { + "signature": "73a9b54e10ff814734690c8e99319851d3caffb8d7ee02237ccef08057623c57", + "alternativeSignatures": [ + "a8395f556a6edc7f633b06820ab22d4ae275f1e2f0fb9f0389520cba4a3fa038" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f5e7a576035cfef04eaefb5c3d64e7575c80aea1449c6528d16bd18f19601938": { + "signature": "f5e7a576035cfef04eaefb5c3d64e7575c80aea1449c6528d16bd18f19601938", + "alternativeSignatures": [ + "e3bf1d972a8173cd7c9e853facdd28b184614cffc18ce2a47e00114894a9a0f3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "85150297f02b3d19a5a032b1ec6b083b4bc2eec2643f60cceac18fd07551134e": { + "signature": "85150297f02b3d19a5a032b1ec6b083b4bc2eec2643f60cceac18fd07551134e", + "alternativeSignatures": [ + "56a1cb19171420c70451d831140f44f8beace9ee605870a54998170e9b9eea44" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "db5e6cce78b5039be2e56eb828caea7fbc1b2c0ddf2204f3c95e90805f480025": { + "signature": "db5e6cce78b5039be2e56eb828caea7fbc1b2c0ddf2204f3c95e90805f480025", + "alternativeSignatures": [ + "38588e5b5cbf9208115ae67488467545f67a83e5ea18d08f65e76318ba2809c1" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "87cbf986f8699bddcd94445159a5772945dd63bb4f55587afaecb5be2846e3cb": { + "signature": "87cbf986f8699bddcd94445159a5772945dd63bb4f55587afaecb5be2846e3cb", + "alternativeSignatures": [ + "229e52e7992660959c3cc4581b73623f6807f87a266ecced4af4343f1f8a2c06" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c3cc891778043c08b6177ace3e90a4af4be52f7936d9df603efcfa77f11df9cf": { + "signature": "c3cc891778043c08b6177ace3e90a4af4be52f7936d9df603efcfa77f11df9cf", + "alternativeSignatures": [ + "4d98cab41917051c77b027daaa0ebaad30ae99b738bb3721331ddacb3d13e990" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "dbd00c32ac388db25b7b4095d242411fb192ecc6d471c80d72f6c83bc5790f5d": { + "signature": "dbd00c32ac388db25b7b4095d242411fb192ecc6d471c80d72f6c83bc5790f5d", + "alternativeSignatures": [ + "5d3a76ad12be60e1fc117dae08b4e56f70e0e905000e6bac82b7c0d7d47efa12" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b68b91f6e33cf24cb0a6b106bad2cccc5f7191510437ce703a42f1fefbb1d13": { + "signature": "5b68b91f6e33cf24cb0a6b106bad2cccc5f7191510437ce703a42f1fefbb1d13", + "alternativeSignatures": [ + "cbd85190a9dc566b475d9d0da29ef2890469d545129695dccd33e12b042c7f87" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "48a3d50b09e89367e161fa2e843b605dbf840ac3264623bad804634b9e17f0b1": { + "signature": "48a3d50b09e89367e161fa2e843b605dbf840ac3264623bad804634b9e17f0b1", + "alternativeSignatures": [ + "ecf7b4e72800b2f2881b924e7c602984c0101738bfe7d901ef9bf68d54f22acc" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b641532bc10265a307b0984e2c1cf7478d345aa73e0b9f52f57de6b6139db0d": { + "signature": "5b641532bc10265a307b0984e2c1cf7478d345aa73e0b9f52f57de6b6139db0d", + "alternativeSignatures": [ + "93f72434bf401ab42b1478aae49174f0f231385f2d190646de90b2f321bf0405" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f5bbdce1f432ce0cc0f8eab2a548e86389c11972a0fd807d5b0218929dc0667f": { + "signature": "f5bbdce1f432ce0cc0f8eab2a548e86389c11972a0fd807d5b0218929dc0667f", + "alternativeSignatures": [ + "c0dd620ed7794312e1f535291893133d2983ef6af266bf1261e08f5a40030184" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b48abf4f4fc439b180232125d632b47c3ca44cde8f518e08d38905578255a76e": { + "signature": "b48abf4f4fc439b180232125d632b47c3ca44cde8f518e08d38905578255a76e", + "alternativeSignatures": [ + "8b1d9c30d610788b227bcf7b1a02f715887adb26d9d476705a864bfbd3eec42d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b91412070b80c595c93a131c6423a78aca0eb174e67b49a72f3a28b2f2e04fc": { + "signature": "5b91412070b80c595c93a131c6423a78aca0eb174e67b49a72f3a28b2f2e04fc", + "alternativeSignatures": [ + "3b52062806f35937f2372d4497d5679d95e9c28d9cef710ac9f0551b3bf6fa69" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4e6e328cd795ad8ffa3d409d0e0313ae91f87124a058e5dfe64dfbc93e236f78": { + "signature": "4e6e328cd795ad8ffa3d409d0e0313ae91f87124a058e5dfe64dfbc93e236f78", + "alternativeSignatures": [ + "656279220733cb5f45e3ef13a50ae9174c9763f7c24ff163245f6bea398d0794" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4dd68b37dee025fa0a77ac00e6e85ce7e1ad8add70a2226e10a6423534359a9b": { + "signature": "4dd68b37dee025fa0a77ac00e6e85ce7e1ad8add70a2226e10a6423534359a9b", + "alternativeSignatures": [ + "c63dd7cfcff47e1a2f686035319925e8b2d3b0f1ac8db86b39d4b1821ae4797d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "62a125c376534b19e9083efbad081f32bc294602b7f19a5210ad940e1c450a5f": { + "signature": "62a125c376534b19e9083efbad081f32bc294602b7f19a5210ad940e1c450a5f", + "alternativeSignatures": [ + "2b2732249979c5e87cc79cbdec660e2b1c8eba1f93a362e5595ec03cdf1e4951" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f918c167a1a70deba61361fd3138b76fba86387a1f41237efeda8f01d464457e": { + "signature": "f918c167a1a70deba61361fd3138b76fba86387a1f41237efeda8f01d464457e", + "alternativeSignatures": [ + "959e93d957e0e5ec521d2f3b9dae02db123812fb8d523d8175329b08694d65d9" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + } + } +} \ No newline at end of file diff --git a/build/config/template.appinstaller b/build/config/template.appinstaller new file mode 100644 index 00000000000..7977fd950b8 --- /dev/null +++ b/build/config/template.appinstaller @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/build/config/tsa.json b/build/config/tsa.json new file mode 100644 index 00000000000..89874d37fc0 --- /dev/null +++ b/build/config/tsa.json @@ -0,0 +1,6 @@ +{ + "instanceUrl": "https://microsoft.visualstudio.com", + "projectName": "OS", + "areaPath": "OS\\Windows Client and Services\\ADEPT\\E4D-Engineered for Developers\\SHINE\\Terminal", + "notificationAliases": ["condev@microsoft.com", "duhowett@microsoft.com"] +} diff --git a/build/pipelines/nightly.yml b/build/pipelines/nightly.yml deleted file mode 100644 index 025a643509f..00000000000 --- a/build/pipelines/nightly.yml +++ /dev/null @@ -1,23 +0,0 @@ -trigger: none -pr: none -schedules: - - cron: "30 3 * * 2-6" # Run at 03:30 UTC Tuesday through Saturday (After the work day in Pacific, Mon-Fri) - displayName: "Nightly Terminal Build" - branches: - include: - - main - always: false # only run if there's code changes! - -name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) - -extends: - template: templates-v2\pipeline-full-release-build.yml - parameters: - branding: Canary - buildTerminal: true - pgoBuildMode: Optimize - codeSign: true - generateSbom: true - publishSymbolsToPublic: true - publishVpackToWindows: false - symbolExpiryTime: 15 # Nightly builds do not keep symbols for very long! diff --git a/build/pipelines/ob-nightly.yml b/build/pipelines/ob-nightly.yml new file mode 100644 index 00000000000..1c90866f9e2 --- /dev/null +++ b/build/pipelines/ob-nightly.yml @@ -0,0 +1,53 @@ +trigger: none +pr: none +schedules: + - cron: "30 3 * * 2-6" # Run at 03:30 UTC Tuesday through Saturday (After the work day in Pacific, Mon-Fri) + displayName: "Nightly Terminal Build" + branches: + include: + - main + always: false # only run if there's code changes! + +parameters: + - name: publishToAzure + displayName: "Deploy to **PUBLIC** Azure Storage" + type: boolean + default: true + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +variables: + - template: templates-v2/variables-nuget-package-version.yml + parameters: + branding: Canary + - template: templates-v2/variables-onebranch-config.yml + +extends: + template: templates-v2/pipeline-onebranch-full-release-build.yml + parameters: + official: true + branding: Canary + buildTerminal: true + pgoBuildMode: Optimize + codeSign: true + publishSymbolsToPublic: true + publishVpackToWindows: false + symbolExpiryTime: 15 + ${{ if eq(true, parameters.publishToAzure) }}: + extraPublishJobs: + - template: build/pipelines/templates-v2/job-deploy-to-azure-storage.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: "$(Build.SourcesDirectory)/_none" + dependsOn: [PublishSymbols] + storagePublicRootURL: $(AppInstallerRootURL) + subscription: $(AzureSubscriptionName) + storageAccount: $(AzureStorageAccount) + storageContainer: $(AzureStorageContainer) + buildConfiguration: Release + buildPlatforms: [x64, x86, arm64] + environment: production-canary + diff --git a/build/pipelines/release.yml b/build/pipelines/ob-release.yml similarity index 81% rename from build/pipelines/release.yml rename to build/pipelines/ob-release.yml index e016270e87f..ca168e6d337 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/ob-release.yml @@ -44,14 +44,6 @@ parameters: - x64 - x86 - arm64 - - name: codeSign - displayName: "Sign all build outputs" - type: boolean - default: true - - name: generateSbom - displayName: "Generate a Bill of Materials" - type: boolean - default: true - name: terminalInternalPackageVersion displayName: "Terminal Internal Package Version" type: string @@ -68,9 +60,16 @@ parameters: name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) +variables: + - template: templates-v2/variables-nuget-package-version.yml + parameters: + branding: ${{ parameters.branding }} + - template: templates-v2/variables-onebranch-config.yml + extends: - template: templates-v2/pipeline-full-release-build.yml + template: templates-v2/pipeline-onebranch-full-release-build.yml parameters: + official: true branding: ${{ parameters.branding }} buildTerminal: ${{ parameters.buildTerminal }} buildConPTY: ${{ parameters.buildConPTY }} @@ -78,8 +77,7 @@ extends: pgoBuildMode: ${{ parameters.pgoBuildMode }} buildConfigurations: ${{ parameters.buildConfigurations }} buildPlatforms: ${{ parameters.buildPlatforms }} - codeSign: ${{ parameters.codeSign }} - generateSbom: ${{ parameters.generateSbom }} + codeSign: true terminalInternalPackageVersion: ${{ parameters.terminalInternalPackageVersion }} publishSymbolsToPublic: ${{ parameters.publishSymbolsToPublic }} publishVpackToWindows: ${{ parameters.publishVpackToWindows }} diff --git a/build/pipelines/templates-v2/job-build-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml index c9329eab519..9d64e1c7cff 100644 --- a/build/pipelines/templates-v2/job-build-package-wpf.yml +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -97,7 +97,7 @@ jobs: flattenFolders: true - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@1 + - task: EsrpCodeSigning@3 displayName: Submit *.nupkg to ESRP for code signing inputs: ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index eda45842bbe..35d342236f9 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -62,6 +62,9 @@ parameters: - name: publishArtifacts type: boolean default: true + - name: removeAllNonSignedFiles + type: boolean + default: false jobs: - job: ${{ parameters.jobName }} @@ -89,6 +92,11 @@ jobs: # Yup. BuildTargetParameter: ' ' SelectedSigningFragments: ' ' + # When building the unpackaged distribution, build it in portable mode if it's Canary-branded + ${{ if eq(parameters.branding, 'Canary') }}: + UnpackagedBuildArguments: -PortableMode + ${{ else }}: + UnpackagedBuildArguments: ' ' JobOutputDirectory: $(Terminal.BinDir) JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} ${{ insert }}: ${{ parameters.variables }} @@ -114,7 +122,7 @@ jobs: $SignFragments += "wpfdotnet" } If ([bool]::Parse("${{ parameters.buildWPF }}")) { - $BuildTargets += "Terminal\wpf\PublicTerminalCore" + $BuildTargets += "Terminal\Control\Microsoft_Terminal_Control" $SignFragments += "wpf" } If ([bool]::Parse("${{ parameters.buildConPTY }}")) { @@ -168,6 +176,7 @@ jobs: # - Directories ending in Lib (static lib projects that we fully linked into DLLs which may also contain unnecessary resources) # - All LocalTests_ project outputs, as they were subsumed into TestHostApp # - All PDB files inside the WindowsTerminal/ output, which do not belong there. + # - console.dll, which apparently breaks XFGCheck? lol. - pwsh: |- $binDir = '$(Terminal.BinDir)' $ImportLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.exp' | ForEach-Object { $_.FullName -Replace "exp$","lib" } @@ -184,6 +193,8 @@ jobs: $Items += Get-ChildItem '$(Terminal.BinDir)' -Filter '*.pdb' -Recurse } + $Items += Get-ChildItem $binDir -Filter 'console.dll' + $Items | Remove-Item -Recurse -Force -Verbose -ErrorAction:Ignore displayName: Clean up static libs and extra symbols errorActionPreference: silentlyContinue # It's OK if this silently fails @@ -224,7 +235,7 @@ jobs: # Code-sign everything we just put together. # We run the signing in Terminal.BinDir, because all of the signing batches are relative to the final architecture/configuration output folder. - - task: EsrpCodeSigning@1 + - task: EsrpCodeSigning@3 displayName: Submit Signing Request inputs: ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a @@ -241,6 +252,14 @@ jobs: Write-Host "##vso[task.setvariable variable=WindowsTerminalPackagePath]${PackageFilename}" displayName: Re-pack the new Terminal package after signing + # Some of our governed pipelines explicitly fail builds that have *any* non-codesigned filed (!) + - ${{ if eq(parameters.removeAllNonSignedFiles, true) }}: + - pwsh: |- + Get-ChildItem "$(Terminal.BinDir)" -Recurse -Include "*.dll","*.exe" | + Where-Object { (Get-AuthenticodeSignature $_).Status -Ne "Valid" } | + Remove-Item -Verbose -Force + displayName: Remove all non-signed output files + - ${{ else }}: # No Signing - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: - pwsh: |- @@ -255,7 +274,7 @@ jobs: - pwsh: |- $XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName $outDir = New-Item -Type Directory "$(Terminal.BinDir)/_unpackaged" -ErrorAction:Ignore - & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination $outDir.FullName + & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 $(UnpackagedBuildArguments) -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination $outDir.FullName displayName: Build Unpackaged Distribution (from MSIX) condition: and(succeeded(), ne(variables.WindowsTerminalPackagePath, '')) diff --git a/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml new file mode 100644 index 00000000000..69b82e57bb3 --- /dev/null +++ b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml @@ -0,0 +1,92 @@ +parameters: + - name: buildConfiguration + type: string + - name: buildPlatforms + type: object + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: variables + type: object + default: {} + - name: environment + type: string + - name: storagePublicRootURL + type: string + - name: subscription + type: string + - name: storageAccount + type: string + - name: storageContainer + type: string + +jobs: +- job: DeployAzure + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + displayName: Publish to Azure Storage (Prod) + dependsOn: ${{ parameters.dependsOn }} + variables: + ${{ insert }}: ${{ parameters.variables }} + steps: + - download: none + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here + submodules: true + persistCredentials: True + + - task: DownloadPipelineArtifact@2 + displayName: Download MSIX Bundle Artifact + inputs: + artifactName: appxbundle-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + downloadPath: '$(Build.SourcesDirectory)/_out' + itemPattern: '**/*.msixbundle' + + - ${{ each platform in parameters.buildPlatforms }}: + - task: DownloadPipelineArtifact@2 + displayName: Download unpackaged build for ${{ platform }} ${{ parameters.buildConfiguration }} + inputs: + artifactName: build-${{ platform }}-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + downloadPath: '$(Build.SourcesDirectory)/_unpackaged' + itemPattern: '**/_unpackaged/*.zip' + + - pwsh: |- + $b = Get-Item _out/*.msixbundle + ./build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 -BundlePath $b.FullName -AppInstallerTemplatePath ./build/config/template.appinstaller -AppInstallerRoot "${{ parameters.storagePublicRootURL }}" -OutputPath _out/Microsoft.WindowsTerminalCanary.appinstaller + displayName: "Produce AppInstaller for MSIX bundle" + + - pwsh: |- + $zips = Get-ChildItem -Recurse -Filter *.zip _unpackaged + $zips | ForEach-Object { + $name = $_.Name + $parts = $name.Split('_') + $parts[1] = "latest" + $name = [String]::Join('_', $parts) + $_ | Move-Item -Destination (Join-Path "_out" $name) + } + displayName: "Wrangle Unpackaged builds into place, rename" + + - powershell: |- + Get-PackageProvider -Name NuGet -ForceBootstrap + Install-Module -Verbose -AllowClobber -Force Az.Accounts, Az.Storage, Az.Network, Az.Resources, Az.Compute + displayName: Install Azure Module Dependencies + + - task: AzureFileCopy@5 + displayName: Publish to Storage Account + inputs: + sourcePath: _out/* + Destination: AzureBlob + azureSubscription: ${{ parameters.subscription }} + storage: ${{ parameters.storageAccount }} + ContainerName: ${{ parameters.storageContainer }} + AdditionalArgumentsForBlobCopy: "--content-type application/octet-stream" + diff --git a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml index 121e07b4417..6d6ad09cdad 100644 --- a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -29,6 +29,9 @@ parameters: - name: publishArtifacts type: boolean default: true + - name: afterBuildSteps + type: stepList + default: [] jobs: - job: ${{ parameters.jobName }} @@ -48,6 +51,8 @@ jobs: BundleStemName: Microsoft.WindowsTerminal ${{ elseif eq(parameters.branding, 'Preview') }}: BundleStemName: Microsoft.WindowsTerminalPreview + ${{ elseif eq(parameters.branding, 'Canary') }}: + BundleStemName: Microsoft.WindowsTerminalCanary ${{ else }}: BundleStemName: WindowsTerminalDev JobOutputDirectory: '$(System.ArtifactsDirectory)/bundle' @@ -83,11 +88,13 @@ jobs: $Components[0] = ([int]$Components[0] + $VersionEpoch) $BundleVersion = $Components -Join "." New-Item -Type Directory "$(System.ArtifactsDirectory)/bundle" - .\build\scripts\Create-AppxBundle.ps1 -InputPath 'bin/' -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + $BundlePath = "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + .\build\scripts\Create-AppxBundle.ps1 -InputPath 'bin/' -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath $BundlePath + Write-Host "##vso[task.setvariable variable=MsixBundlePath]${BundlePath}" displayName: Create msixbundle - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@1 + - task: EsrpCodeSigning@3 displayName: Submit *.msixbundle to ESRP for code signing inputs: ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a @@ -137,6 +144,8 @@ jobs: ValidateSignature: true Verbosity: 'Verbose' + - ${{ parameters.afterBuildSteps }} + - ${{ if eq(parameters.publishArtifacts, true) }}: - publish: $(JobOutputDirectory) artifact: $(JobOutputArtifactName) diff --git a/build/pipelines/templates-v2/job-package-conpty.yml b/build/pipelines/templates-v2/job-package-conpty.yml index 52f3a5c15d9..2f777cdf4f5 100644 --- a/build/pipelines/templates-v2/job-package-conpty.yml +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -82,7 +82,7 @@ jobs: versionEnvVar: XES_PACKAGEVERSIONNUMBER - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@1 + - task: EsrpCodeSigning@3 displayName: Submit *.nupkg to ESRP for code signing inputs: ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a diff --git a/build/pipelines/templates-v2/job-publish-symbols.yml b/build/pipelines/templates-v2/job-publish-symbols.yml index 65663885602..83c472872b8 100644 --- a/build/pipelines/templates-v2/job-publish-symbols.yml +++ b/build/pipelines/templates-v2/job-publish-symbols.yml @@ -17,6 +17,12 @@ parameters: - name: symbolExpiryTime type: string default: 36530 # This is the default from PublishSymbols@2 + - name: variables + type: object + default: {} + - name: symbolPatGoesInTaskInputs + type: boolean + default: false jobs: - job: ${{ parameters.jobName }} @@ -27,6 +33,8 @@ jobs: ${{ else }}: displayName: Publish Symbols Internally dependsOn: ${{ parameters.dependsOn }} + variables: + ${{ insert }}: ${{ parameters.variables }} steps: - checkout: self clean: true @@ -76,6 +84,8 @@ jobs: SymbolsProduct: 'Windows Terminal Converged Symbols' SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} + ${{ if eq(parameters.symbolPatGoesInTaskInputs, true) }}: + Pat: $(ADO_microsoftpublicsymbols_PAT) # The ADO task does not support indexing of GitHub sources. # There is a bug which causes this task to fail if LIB includes an inaccessible path (even though it does not depend on it). # To work around this issue, we just force LIB to be any dir that we know exists. @@ -83,4 +93,5 @@ jobs: env: LIB: $(Build.SourcesDirectory) ArtifactServices_Symbol_AccountName: microsoftpublicsymbols - ArtifactServices_Symbol_PAT: $(ADO_microsoftpublicsymbols_PAT) + ${{ if ne(parameters.symbolPatGoesInTaskInputs, true) }}: + ArtifactServices_Symbol_PAT: $(ADO_microsoftpublicsymbols_PAT) diff --git a/build/pipelines/templates-v2/pipeline-full-release-build.yml b/build/pipelines/templates-v2/pipeline-full-release-build.yml index 42c5c6250d8..06358de5c5b 100644 --- a/build/pipelines/templates-v2/pipeline-full-release-build.yml +++ b/build/pipelines/templates-v2/pipeline-full-release-build.yml @@ -51,6 +51,9 @@ parameters: type: boolean default: false + - name: extraPublishJobs + type: object + default: [] - name: pool type: object default: @@ -58,28 +61,9 @@ parameters: demands: ImageOverride -equals SHINE-VS17-Latest variables: - # If we are building a branch called "release-*", change the NuGet suffix - # to "preview". If we don't do that, XES will set the suffix to "release1" - # because it truncates the value after the first period. - # We also want to disable the suffix entirely if we're Release branded while - # on a release branch. - # main is special, however. XES ignores main. Since we never produce actual - # shipping builds from main, we want to force it to have a beta label as - # well. - # - # In effect: - # BRANCH / BRANDING | Release | Preview - # ------------------|----------------------------|----------------------------- - # release-* | 1.12.20220427 | 1.13.20220427-preview - # main | 1.14.20220427-experimental | 1.14.20220427-experimental - # all others | 1.14.20220427-mybranch | 1.14.20220427-mybranch - ${{ if startsWith(variables['Build.SourceBranchName'], 'release-') }}: - ${{ if eq(parameters.branding, 'Release') }}: - NoNuGetPackBetaVersion: true - ${{ else }}: - NuGetPackBetaVersion: preview - ${{ elseif eq(variables['Build.SourceBranchName'], 'main') }}: - NuGetPackBetaVersion: experimental + - template: variables-nuget-package-version.yml + parameters: + branding: ${{ parameters.branding }} resources: repositories: @@ -193,4 +177,5 @@ stages: includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} symbolExpiryTime: ${{ parameters.symbolExpiryTime }} + - ${{ parameters.extraPublishJobs }} ... diff --git a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml new file mode 100644 index 00000000000..271e7d3e121 --- /dev/null +++ b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml @@ -0,0 +1,265 @@ +parameters: + - name: official + type: boolean + default: false + - name: branding + type: string + default: Release + values: + - Release + - Preview + - Canary + - Dev + - name: buildTerminal + type: boolean + default: true + - name: buildConPTY + type: boolean + default: false + - name: buildWPF + type: boolean + default: false + - name: pgoBuildMode + type: string + default: Optimize + values: + - Optimize + - Instrument + - None + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + - name: codeSign + type: boolean + default: true + - name: terminalInternalPackageVersion + type: string + default: '0.0.8' + + - name: publishSymbolsToPublic + type: boolean + default: true + - name: symbolExpiryTime + type: string + default: 36530 # This is the default from PublishSymbols@2 + - name: publishVpackToWindows + type: boolean + default: false + + - name: extraPublishJobs + type: object + default: [] + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + ${{ if eq(parameters.official, true) }}: + template: v2/Microsoft.Official.yml@templates # https://aka.ms/obpipelines/templates + ${{ else }}: + template: v2/Microsoft.NonOfficial.yml@templates + parameters: + featureFlags: + WindowsHostVersion: 1ESWindows2022 + platform: + name: 'windows_undocked' + product: 'Windows Terminal' + cloudvault: # https://aka.ms/obpipelines/cloudvault + enabled: false + globalSdl: # https://aka.ms/obpipelines/sdl + tsa: + enabled: true + configFile: '$(Build.SourcesDirectory)\build\config\tsa.json' + binskim: + break: false + scanOutputDirectoryOnly: true + policheck: + break: false + severity: Note + baseline: + baselineFile: '$(Build.SourcesDirectory)\build\config\release.gdnbaselines' + suppressionSet: default + + stages: + - stage: Build + displayName: Build + dependsOn: [] + jobs: + - template: ./build/pipelines/templates-v2/job-build-project.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + removeAllNonSignedFiles: true # appease the overlords + codeSign: ${{ parameters.codeSign }} + beforeBuildSteps: # Right before we build, lay down the universal package and localizations + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - task: UniversalPackages@0 + displayName: Download terminal-internal Universal Package + inputs: + feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 + packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 + versionListDownload: ${{ parameters.terminalInternalPackageVersion }} + + - template: ./build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml@self + parameters: + includePseudoLoc: true + + - ${{ if eq(parameters.buildWPF, true) }}: + # Add an Any CPU build flavor for the WPF control bits + - template: ./build/pipelines/templates-v2/job-build-project.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + jobName: BuildWPF + branding: ${{ parameters.branding }} + buildTerminal: false + buildWPFDotNetComponents: true + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: + - Any CPU + generateSbom: false # this is handled by onebranch + removeAllNonSignedFiles: true # appease the overlords + codeSign: ${{ parameters.codeSign }} + beforeBuildSteps: + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + # WPF doesn't need the localizations or the universal package, but if it does... put them here. + + - stage: Package + displayName: Package + dependsOn: [Build] + jobs: + - ${{ if eq(parameters.buildTerminal, true) }}: + - template: ./build/pipelines/templates-v2/job-merge-msix-into-bundle.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + ### This job is also in charge of submitting the vpack to Windows if it's enabled + ob_createvpack_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }} + ob_updateOSManifest_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }} + ### If enabled above, these options are in play. + ob_createvpack_packagename: 'WindowsTerminal.app' + ob_createvpack_owneralias: 'conhost@microsoft.com' + ob_createvpack_description: 'VPack for the Windows Terminal Application' + ob_createvpack_targetDestinationDirectory: '$(Destination)' + ob_createvpack_propsFile: false + ob_createvpack_provData: true + ob_createvpack_metadata: '$(Build.SourceVersion)' + ob_createvpack_topLevelRetries: 0 + ob_createvpack_failOnStdErr: true + ob_createvpack_taskLogVerbosity: Detailed + ob_createvpack_verbose: true + ob_createvpack_vpackdirectory: '$(JobOutputDirectory)\vpack' + ob_updateOSManifest_gitcheckinConfigPath: '$(Build.SourcesDirectory)\build\config\GitCheckin.json' + # We're skipping the 'fetch' part of the OneBranch rules, but that doesn't mean + # that it doesn't expect to have downloaded a manifest directly to some 'destination' + # folder that it can then update and upload. + # Effectively: it says "destination" but it means "source" + # DH: Don't ask why. + ob_updateOSManifest_destination: $(XES_VPACKMANIFESTDIRECTORY) + ob_updateOSManifest_skipFetch: true + publishArtifacts: false # Handled by OneBranch + jobName: Bundle + branding: ${{ parameters.branding }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # Handled by onebranch + codeSign: ${{ parameters.codeSign }} + afterBuildSteps: + # This directory has to exist, even if we aren't using createvpack, because the Guardian rules demand it. + - pwsh: |- + New-Item "$(JobOutputDirectory)/vpack" -Type Directory + displayName: Make sure the vpack directory exists + + - ${{ if parameters.publishVpackToWindows }}: + - pwsh: |- + Copy-Item -Verbose -Path "$(MsixBundlePath)" -Destination (Join-Path "$(JobOutputDirectory)/vpack" 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle') + displayName: Stage msixbundle for vpack + + - ${{ if eq(parameters.buildConPTY, true) }}: + - template: ./build/pipelines/templates-v2/job-package-conpty.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildWPF, true) }}: + - template: ./build/pipelines/templates-v2/job-build-package-wpf.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + codeSign: ${{ parameters.codeSign }} + + - stage: Publish + displayName: Publish + dependsOn: [Build, Package] + jobs: + - template: ./build/pipelines/templates-v2/job-publish-symbols.yml@self + parameters: + pool: { type: windows } + includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} + symbolPatGoesInTaskInputs: true # onebranch tries to muck with the PAT variable, so we need to change how it get the PAT + symbolExpiryTime: ${{ parameters.symbolExpiryTime }} + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(Build.ArtifactStagingDirectory) + # Without this, OneBranch will nerf our symbol tasks + ob_symbolsPublishing_enabled: true + + - ${{ parameters.extraPublishJobs }} diff --git a/build/pipelines/templates-v2/variables-nuget-package-version.yml b/build/pipelines/templates-v2/variables-nuget-package-version.yml new file mode 100644 index 00000000000..d3a137ba0e0 --- /dev/null +++ b/build/pipelines/templates-v2/variables-nuget-package-version.yml @@ -0,0 +1,27 @@ +parameters: + - name: branding + type: string + +variables: + # If we are building a branch called "release-*", change the NuGet suffix + # to "preview". If we don't do that, XES will set the suffix to "release1" + # because it truncates the value after the first period. + # We also want to disable the suffix entirely if we're Release branded while + # on a release branch. + # main is special, however. XES ignores main. Since we never produce actual + # shipping builds from main, we want to force it to have a beta label as + # well. + # + # In effect: + # BRANCH / BRANDING | Release | Preview + # ------------------|----------------------------|----------------------------- + # release-* | 1.12.20220427 | 1.13.20220427-preview + # main | 1.14.20220427-experimental | 1.14.20220427-experimental + # all others | 1.14.20220427-mybranch | 1.14.20220427-mybranch + ${{ if startsWith(variables['Build.SourceBranchName'], 'release-') }}: + ${{ if eq(parameters.branding, 'Release') }}: + NoNuGetPackBetaVersion: true + ${{ else }}: + NuGetPackBetaVersion: preview + ${{ elseif eq(variables['Build.SourceBranchName'], 'main') }}: + NuGetPackBetaVersion: experimental diff --git a/build/pipelines/templates-v2/variables-onebranch-config.yml b/build/pipelines/templates-v2/variables-onebranch-config.yml new file mode 100644 index 00000000000..d7180e3e090 --- /dev/null +++ b/build/pipelines/templates-v2/variables-onebranch-config.yml @@ -0,0 +1,2 @@ +variables: + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' diff --git a/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 b/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 new file mode 100644 index 00000000000..b49ad244ffb --- /dev/null +++ b/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 @@ -0,0 +1,42 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory, + HelpMessage="Path to the .msixbundle")] + [string] + $BundlePath, + + [Parameter(Mandatory, + HelpMessage="Path to the .appinstaller template")] + [string] + $AppInstallerTemplatePath, + + [string] + $AppInstallerRoot, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath +) + +$ErrorActionPreference = "Stop" + +$sentinelFile = New-TemporaryFile +$directory = New-Item -Type Directory "$($sentinelFile.FullName)_Package" +Remove-Item $sentinelFile -Force -EA:Ignore + +$bundle = (Get-Item $BundlePath) +& tar.exe -x -f $bundle.FullName -C $directory AppxMetadata/AppxBundleManifest.xml + +$xml = [xml](Get-Content (Join-Path $directory "AppxMetadata\AppxBundleManifest.xml")) +$name = $xml.Bundle.Identity.Name +$version = $xml.Bundle.Identity.Version + +$doc = (Get-Content -ReadCount 0 $AppInstallerTemplatePath) +$doc = $doc -Replace '\$\$ROOT\$\$',$AppInstallerRoot +$doc = $doc -Replace '\$\$NAME\$\$',$name +$doc = $doc -Replace '\$\$VERSION\$\$',$version +$doc = $doc -Replace '\$\$PACKAGE\$\$',$bundle.Name +$doc | Out-File -Encoding utf8NoBOM $OutputPath + +Get-Item $OutputPath diff --git a/build/scripts/New-UnpackagedTerminalDistribution.ps1 b/build/scripts/New-UnpackagedTerminalDistribution.ps1 index b9ae23a2954..9605f132562 100644 --- a/build/scripts/New-UnpackagedTerminalDistribution.ps1 +++ b/build/scripts/New-UnpackagedTerminalDistribution.ps1 @@ -25,7 +25,12 @@ Param( [Parameter(HelpMessage="Path to makeappx.exe", ParameterSetName='Layout')] [ValidateScript({Test-Path $_ -Type Leaf})] [string] - $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakeAppx.exe" + $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakeAppx.exe", + + [Parameter(HelpMessage="Include the portable mode marker file by default", ParameterSetName='AppX')] + [Parameter(HelpMessage="Include the portable mode marker file by default", ParameterSetName='Layout')] + [switch] + $PortableMode = $PSCmdlet.ParameterSetName -eq 'Layout' ) $filesToRemove = @("*.xml", "*.winmd", "Appx*", "Images/*Tile*", "Images/*Logo*") # Remove from Terminal @@ -128,6 +133,11 @@ $finalTerminalPriFile = Join-Path $terminalAppPath "resources.pri" # Packaging ######## +$portableModeMarkerFile = Join-Path $terminalAppPath ".portable" +If ($PortableMode) { + "" | Out-File $portableModeMarkerFile +} + If ($PSCmdlet.ParameterSetName -Eq "AppX") { # We only produce a ZIP when we're combining two AppX directories. New-Item -ItemType Directory -Path $Destination -ErrorAction:SilentlyContinue | Out-Null diff --git a/custom.props b/custom.props index 6027b31193d..328d7042eb2 100644 --- a/custom.props +++ b/custom.props @@ -5,7 +5,7 @@ true 2023 1 - 19 + 20 Windows Terminal diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index d0c670dcd57..e401b157d67 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -9,7 +9,7 @@ - + diff --git a/doc/ORGANIZATION.md b/doc/ORGANIZATION.md index d220c9034e4..07a6e41ee7e 100644 --- a/doc/ORGANIZATION.md +++ b/doc/ORGANIZATION.md @@ -32,7 +32,6 @@ * `/src/cascadia/TerminalApp` - This DLL represents the implementation of the Windows Terminal application. This includes parsing settings, hosting tabs & panes with Terminals in them, and displaying other UI elements. This DLL is almost entirely UWP-like code, and shouldn't be doing any Win32-like UI work. * `/src/cascadia/WindowsTerminal` - This EXE provides Win32 hosting for the TerminalApp. It will set up XAML islands, and is responsible for drawing the window, either as a standard window or with content in the titlebar (non-client area). * `/src/cascadia/CascadiaPackage` - This is a project for packaging the Windows Terminal and its dependencies into an .appx/.msix for deploying to the machine. - * `/src/cascadia/PublicTerminalCore` - This is a DLL wrapper for the TerminalCore and Renderer, similar to `TermControl`, which exposes some exported functions that so the Terminal can be used from C#. * `/src/cascadia/WpfTerminalControl` - A DLL implementing a WPF version of the Terminal Control. * `/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 diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index d43a9e4e7d8..9dc3a8afd58 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -378,8 +378,12 @@ }, "ShortcutActionName": { "enum": [ + "addMark", "adjustFontSize", + "adjustOpacity", + "clearAllMarks", "clearBuffer", + "clearMark", "closeOtherPanes", "closeOtherTabs", "closePane", @@ -390,6 +394,7 @@ "copy", "duplicateTab", "expandSelectionToWord", + "experimental.colorSelection", "exportBuffer", "find", "findMatch", @@ -397,64 +402,60 @@ "globalSummon", "identifyWindow", "identifyWindows", + "markMode", "moveFocus", "movePane", - "swapPane", - "markMode", "moveTab", "multipleActions", "newTab", "newWindow", "nextTab", + "openAbout", "openNewTabDropdown", "openSettings", + "openSystemMenu", "openTabColorPicker", + "openTabRenamer", "openWindowRenamer", "paste", "prevTab", - "renameTab", - "openSystemMenu", - "openTabRenamer", "quakeMode", + "quit", + "renameTab", + "renameWindow", "resetFontSize", "resizePane", - "renameWindow", + "restoreLastClosed", "scrollDown", "scrollDownPage", - "scrollUp", - "scrollUpPage", "scrollToBottom", + "scrollToMark", "scrollToTop", + "scrollUp", + "scrollUpPage", + "searchWeb", + "selectAll", "sendInput", "setColorScheme", + "setFocusMode", + "setFullScreen", + "setMaximized", "setTabColor", "showSuggestions", "splitPane", + "swapPane", + "switchSelectionEndpoint", "switchToTab", "tabSearch", "toggleAlwaysOnTop", "toggleBlockSelection", "toggleFocusMode", - "selectAll", - "setFocusMode", - "switchSelectionEndpoint", "toggleFullscreen", - "setFullScreen", - "setMaximized", "togglePaneZoom", - "toggleSplitOrientation", "toggleReadOnlyMode", "toggleShaderEffects", + "toggleSplitOrientation", "wt", - "quit", - "adjustOpacity", - "restoreLastClosed", - "addMark", - "scrollToMark", - "clearMark", - "clearAllMarks", - "searchWeb", - "experimental.colorSelection", "unbound" ], "type": "string" @@ -672,7 +673,7 @@ }, "allowEmpty": { "description": "Whether to render a folder without entries, or to hide it", - "default": "false", + "default": false, "type": "boolean" } } @@ -2279,11 +2280,6 @@ "description": "When set to true, the background image for the currently focused profile is expanded to encompass the entire window, beneath other panes.", "type": "boolean" }, - "compatibility.reloadEnvironmentVariables": { - "default": true, - "description": "When set to true, when opening a new tab or pane it will get reloaded environment variables.", - "type": "boolean" - }, "initialCols": { "default": 120, "description": "The number of columns displayed in the window upon first load. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), this property is ignored.", @@ -2532,6 +2528,11 @@ "null" ] }, + "compatibility.reloadEnvironmentVariables": { + "default": true, + "description": "When set to true, when opening a new tab or pane it will get reloaded environment variables.", + "type": "boolean" + }, "unfocusedAppearance": { "$ref": "#/$defs/AppearanceConfig", "description": "Sets the appearance of the terminal when it is unfocused.", diff --git a/doc/specs/#1595 - Suggestions UI/3121-suggestion-menu-2023-000.gif b/doc/specs/#1595 - Suggestions UI/3121-suggestion-menu-2023-000.gif new file mode 100644 index 00000000000..da5f0c4f663 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/3121-suggestion-menu-2023-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/Suggestions-UI.md b/doc/specs/#1595 - Suggestions UI/Suggestions-UI.md new file mode 100644 index 00000000000..84f307c0ce6 --- /dev/null +++ b/doc/specs/#1595 - Suggestions UI/Suggestions-UI.md @@ -0,0 +1,744 @@ +--- +author: Mike Griese +created on: 2022-08-22 +last updated: 2023-08-03 +issue id: 1595 +--- + +# Windows Terminal - Suggestions UI + +## Abstract + +Multiple related scenarios have come up where it would be beneficial to display +actionable UI to the user within the context of the active terminal itself. This +UI would be akin to the Intellisense UI in Visual Studio. It appears right where +the user is typing, and can help provide immediate content for the user, based +on some context. The "Suggestions UI" is this new ephemeral UI within the +Windows Terminal that can display different types of actions, from different +sources. + +## Background + +The Suggestions UI is the singular UI by which the Terminal can display a +variety of suggestions to the user. These include: + +* Recent commands the user has executed in this terminal, powered by shell integration. +* Recent directories, similarly powered by shell integration +* Completions from the shell itself (like the shell completions in PowerShell) +* Tasks, which are `sendInput` actions from the user's settings +* Buffer Completions, which is a dumb type of autocomplete based on words in the buffer +* and more (as provided via extensions) + +All of these scenarios are places where it makes sense to present the user a +menu at the point of text insertion in the terminal control itself. + +### Inspiration + +Primarily, the inspiration is any Intellisense-like experience, in any app. +Visual Studio, VsCode, PowerShell, vim, Sublime any JetBrains IDE - there's more +than enough examples in the wild. + +Ultimately, the inspiration for the Suggestions UI came from a bunch of places +all at once. In the course of a few months though, it became clear that we'd +need a unified UI for displaying a variety of suggestion-like experiences in the +Terminal. Our work with the PowerShell and VsCode teams helped refine these +requests all into the unified design below. + +### User Stories + + Size | Description +-----------|-- + 🐣 Crawl | The user can bring up the Suggestions UI with recent commands, powered by shell integration + 🐣 Crawl | [#12863] The user can bring up the Suggestions UI with recent directories, powered by shell integration + 🚶 Walk | The user can bring up the Suggestions UI with tasks from their settings + 🚶 Walk | CLI apps can invoke the Suggestions UI with a new VT sequence + 🚶 Walk | The Suggestions UI can be opened using the current typed commandline as a filter + 🚶 Walk | Recent commands and directories are stored in `state.json`, across sessions + 🏃‍♂️ Run | Suggestions can have descriptions presented in / alongside the UI + 🏃‍♂️ Run | The Suggestions UI can be opened without any nesting + 🏃‍♂️ Run | The Suggestions UI can be opened, nested by `source` of the suggestion + 🚀 Sprint | Extensions can provide suggestion sources for the Suggestions UI + 🚀 Sprint | The Suggestions UI can be opened in "inline" mode, only showing the text of the first suggestion + +### Elevator Pitch + +The Suggestions UI is a UI element displayed in the Terminal for providing +different types of text suggestions to the user - anything from recently run +commands, to saved commands, to tab-completion suggestions from the shell +itself. + +## Business Justification + +It will delight developers. + +Furthermore, our partners on the Visual Studio team have been requesting similar +functionality for some time now. The way autocompletion menus in PowerShell +currently interact with UIA clients leaves much to be desired. They'd like a way +to provide richer context to screen readers. Something to enable the terminal to +more specifically describe the context of what's being presented to the user. + +## Scenario Details + +### UI/UX Design + +#### Prototypes + +The following gif was a VsCode prototype of [shell-driven autocompletion]. This +is the point of reference we're starting from when talking about what the +suggestions UI might look like. + +![](vscode-shell-suggestions.gif) + +These suggestions are populated by logic within PowerShell itself, and +communicated to the Terminal. The Terminal can then display them in the +Suggestions UI. + +The following demonstrate a prototype of what that might look like for the +Terminal. These are meant to be informative, not normative, representations of +what the UI would look like. + +![](shell-autocomplete-july-2022-000.gif) + +A prototype of the recent commands UI, powered by shell integration: + +![](command-history-suggestions.gif) + +A prototype of the tasks UI, powered by the user's settings: + +![](tasks-suggestions.gif) + +(admittedly, the `TeachingTip` in that gif is a prototype and was later replaced +with a better version.) + +In general, the Suggestions UI will present a list of elements to select from, +near the text cursor. This control might be contain a text box for filtering +these items (a "**palette**"), or it might not (a "**menu**"). + +![An example of the menu mode](3121-suggestion-menu-2023-000.gif) + +#### Palette vs Menu + +Depending on how the suggestions UI is invoked, we may or may not want to +display a text box for filtering these suggestions. Consider the Intellisense +menu in Visual Studio. That's a UI that only allows for up/down for navigation +(and enter/tab for selecting the suggestion). + +For suggestions driven by the Terminal, we'll display a filtering text box in +the Suggestions UI. This is similar to the command palette's search - a fuzzy +search to filter the contents. This is the "**palette**" style of the +suggestions dialog. + +For completions driven by the shell, we should probably not display the +filtering text box. This is the "**menu**" style of the suggestion dialog. The +user is primarily interacting with the shell here, not the Terminal. + +> **Warning** +> TODO! For discussion, possibly with a real UX designer. + +How should we handle completions here? Tab? Enter? Right-Arrow? Should we have +an element selected when we open the menu, or should tab/enter only work once +the user has used the arrows at least once? Sublime allows for tab to +complete the suggestion immediately. + +Consider also that these suggestions might be provided by the shell, as the user +is typing at a commandline shell. For something like PowerShell, the user might +want to start typing a command and have it tab-complete based off the shell's +tab expansion rules. PowerShell's inline suggestions use right-arrow to +differentiate "use this suggestion" vs tab for "tab expand what I'm typing at +the prompt". We should probably preserve this behavior. + +We probably don't want to provide different experiences for the **menu** version +of the Suggestions UI vs. the **palette** version. In the palette version, the +user won't be pressing tab to tab-complete at the shell - the focus is out of +the of terminal and in the Suggestions UI. With the menu version, the focus is +still "in the terminal", and users would expect tab to tab-complete. + +We will want to make sure that there's some semblance of consistency across our +implementation for the Suggestions UI, our own Command Palette, VsCode's +intellisense and their own implementation of shell-completions in the Terminal. + +> **Note** +> In my prototype, for the "Menu" mode, I accepted ALL of right-arrow, tab, and +> enter as "accept completion", and any other key dismissed the UI. This _felt_ +> right for that mode. I'm not sure we could make the same call for "palette" +> mode, where we'd need tab for navigating focus. + +### Implementation Details + +#### Fork the Command Palette + +We're largely going to start with the Command Palette to build the Suggestions +UI[[1](#footnote-1)]. The Command Palette is already a control we've built for displaying a +transient list of commands and dispatching them to the rest of the app. + +Currently, the Command Palette is a single static control, at the top-center of +the Terminal window, and occupying a decent portion of the screen. For the +Suggestions UI, we'll instead want to make sure that the control appears +relative to the current cursor position. + +We'll start by taking the command palette, and copying it over to a new control. +This will allow us to remove large chunks of code dealing with different modes +(i.e. the tab switcher), and code dealing with prefix characters to switch +modes. + +We'll need to make some small modifications to enable the Suggestions UI to +* work as a text cursor-relative control +* exist as a Flyout outside the bounds of the Terminal window +* If the Suggestions UI is too close to the bottom of the screen, we'll need it to open + "upwards", with the search box at the _bottom_ and the list extending above it +* prevent it from switching to command-line mode +* display tooltips / `TeachingTip`s / some secondary flyout with a description + of the suggestion (if provided) + +#### Completion sources + +The Suggestions UI will support suggestions from a variety of different +"sources". As an example, consider the following actions: + +```json + { "command": { "action":"suggestions", "source": "commandHistory" } }, + { "command": { "action":"suggestions", "source": "directoryHistory" } }, + + { "command": { "action":"suggestions", "source": "tasks" } }, + { "command": { "action":"suggestions", "source": "local" } }, + + { "command": { "action":"suggestions", "source": ["local", "tasks", "commandHistory"] } }, + + { "command": { "action":"suggestions", "source": "Microsoft.Terminal.Extensions.BufferComplete" } }, +``` + +Each of these `suggestions` actions would open the Suggestions UI with a +different set of actions. + +* `commandHistory`: Use commands from this session, as identified via shell + integration. This won't be able to return any suggestions if the user has not + configured their shell to support shell integration sequences yet. +* `directoryHistory`: Populate the list with a series of `cd {path}` commands, + where the paths are populated via shell integration. Paths are in MRU order. +* `tasks`: Populate the list with all `sendInput` actions in the user's settings + file. The command structure should remain unchanged. For example, if they have + `sendInput` actions nested under a "git" command, then the "git" entry will + remain in this tasks view with their `sendInput` actions nested inside it. For + more details, see the [Tasks] spec. +* `local`: Populate the list with tasks that are located in the CWD, in a file + named `.wt.json`. For more details, see the [Tasks] spec. +* `Microsoft.Terminal.Extensions.BufferComplete`: As an example, this + demonstrates how an action might be authored to reference a suggestion source + from an extension[[2](#footnote-2)]. + +Each of these different sources will build a different set of `Command`s, +primarily populated with `sendInput` actions. We'll load those `Command`s into +the Suggestions UI control, and open it at the text cursor. + +To drill in on a single example - the `commandHistory` source. In that +particular case, the TerminalPage will query the active TermControl for a list +of its recent commands. If it knows these (via shell integration), then the +TerminalPage will use that list of commands to build a list of `sendInput` +actions. Those will then get fed to the suggestions UI. + +Not listed above is [shell-driven autocompletion]. These aren't something that +the Terminal can invoke all on its own - these are something the shell would +need to invoke themselves. + +#### Pre-populate the current commandline context + +Consider the following scenario. A user has typed `git c` in their shell, and +has [shell integration] enabled for their shell. They want to open the +Suggestions UI filtered to their recent history, but starting with what they've +already typed. To support this scenario, we'll add an additional property: + +* `"useCommandline"`: `bool` (**default**: `true`) + * `true`: the current commandline the user has typed will pre-populate the + filter of the Suggestions UI. This requires that the user has enabled shell + integration in their shell's config. + * `false`: the filter will start empty, regardless of what the user has typed. + +With that setting, the user can achieve their desired UX with the following action: + +```json +{ "command": { "action":"suggestions", "source": "commandHistory", "useCommandline": true } }, +``` + +Now, when they type `git c` and invoke the Suggestions UI, they can immediately +start searching for recent commands that started with `git c`. + +The primary use case for `useCommandline: false` was for `"nesting": "source"`. +When filtering a list of ["Tasks...", "Recent commands...", "Recent +directories...", "Docker...", "Git..."], then there's minimal value to start by +filtering to "git c". + +#### Default actions + +I propose adding the following actions to the Terminal by default: + +```json +{ "command": { "action":"suggestions", "source": "commandHistory", "useCommandline": true } }, +{ "command": { "action":"suggestions", "source": "directoryHistory" } }, +{ "command": { "action":"suggestions", "source": ["local", "tasks", "commandHistory"], "useCommandline": true, "nesting": "disabled" } }, +{ "command": { "action":"suggestions", "source": ["all"], "useCommandline": false, "nesting": "source" } }, +``` + +These actions are colloquially: +* Give me suggestions from my recent commands, using what I've typed +* Give me suggestions of directories I've recently been in +* _(After [Tasks] are implemented)_ Give me suggestions from recent commands, + commands I've saved, and commands for this project. Don't nest any, so they're + all in the top-level menu. Use what I've typed already to start filtering. +* Just open the Suggestions UI with all suggestions sources, and group them by + the source of the suggestions. + +This should cover most of the basic use cases for suggestions. + +#### Who owns this menu? + +There was some discussion of who should own the suggestions menu. The control +itself? Or the app hosting the control? + +A main argument for hosting this UI in the control itself is that any consumer +of the `TermControl` should be able to display the [shell-driven autocompletion] +menu. And they should get the UI from us "for free". Consumers shouldn't need to +reimplement it themselves. This probably could be done without many changes: +* Instead of operating on `Command`s and actions from the terminal settings, + the control could just know that all the entries in the menu are "send + input" "actions". +* The control could offer a method to manually invoke the Suggestions UI for a + list of {suggestion, name, description} objects. +* The app layer could easily translate between sendInput actions and these + pseudo-actions. + +A big argument in favor of having the app layer host the control: Consider an +app like Visual Studio. When they embed the control, they'll want to style the +shell-completions UI in their own way. They already have their own intellisense +menu, and their own UI paradigm. + +For now, we'll leave this as something that's owned by the app layer. When we +get around to finalizing the [shell-driven autocompletion] design, we can +iterate on ideas for supporting both consumers that want to use a pre-built +suggestions control, or consumers who want to bring their own. + +## Tenets + + + + + + + + + + + +
Compatibility + +This shouldn't break any existing flows. This is a general purpose UI element, +to be extended in a variety of ways. Those customizations will all be opt-in by +the user, so I'm not expecting any breaking compatibility changes here. + +
Accessibility + +The Suggestions UI was designed with the goal of making commandline shell +suggestions _more_ accessible. As Carlos previously wrote: + +> Screen readers struggle with this because the entire menu is redrawn every time, making it harder to understand what exactly is "selected" (as the concept of selection in this instance is a shell-side concept represented by visual manipulation). +> +> ... +> +> _\[Shell driven suggestions\]_ can then be leveraged by Windows Terminal to create UI elements. Doing so leverages WinUI's accessible design. + +This will allow the Terminal to provide more context-relevant information to +screen readers. + +
Sustainability + +No sustainability changes expected. + +
Localization + +The localization needs of the Suggestions UI will be effectively the same as the +needs of the Command Palette. + +The Terminal will have no way to localize suggestions that are provided via +[shell-driven autocompletion]. These are just verbatim strings that the shell +told us to use. We don't consider this to be something to worry about, however. +This is no different than the fact that Terminal cannot localize the `Get-Help` +(or any other) output of PowerShell. + +
+ +## Implementation Plan + +This is more of an informative outline, rather than a normative one. Many of the +things from Crawl, Walk, and Run are all already in PRs as of the time of this +spec's review. + +### 🐣 Crawl + +* [ ] Fork the Command palette to a new UI element, the `SuggestionsControl` +* [ ] Enable previewing `sendInput` actions in the Command Palette and `SuggestionsControl` +* [ ] Enable the `SuggestionsControl` to open top-down (aligned to the bottom of the cursor row) or bottom-up (aligned to the top of the cursor row). +* [ ] Disable sorting on the `SuggestionsControl` - elements should presumably be pre-sorted by the source. +* [ ] Expose the recent commands as a accessor on `TermControl` +* [ ] Add a `suggestions` action which accepts a single option `recentCommands`. These should be fed in MRU order to the `SuggestionsControl`. +* [ ] Expose the recent directories as an accessor on `TermControl`, and add a `recentDirectories` source. + +### 🚶 Walk + +* [ ] Add a `tasks` source to `suggestions` which opens the Suggestions UI with + a tree of all `sendInput` commands +* [ ] Enable the `SuggestionsControl` to open with or without a search box +* [ ] Plumb support for shell-driven completions through the core up to the app +* [ ] Expose the _current_ commandline from the `TermControl` +* [ ] Add a `useCommandline` property to `suggestions`, to pre-populate the search with the current commandline. +* [ ] Persist recent commands / directories accordingly + +### 🏃‍♂️ Run + +* [ ] Add a `description` field to `Command` +* [ ] Add a `TeachingTip` (or similar) to the Suggestions UI to display + descriptions (when available) +* [ ] Use the `ToolTip` property of shell-driven suggestions as the description +* [ ] Add a boolean `nesting` property which can be used to disable nesting on the `tasks` source. +* [ ] Add the ability for `nesting` to accept `enabled`/`disabled` as `true`/`false` equivalents +* [ ] Add the ability for `nesting` to accept `source`, which instead groups all + commands to the Suggestions UI by the source of that suggestion. + +### 🚀 Sprint + +The two "sprint" tasks here are much more ambitious than the other listed +scenarios, so breaking them down to atomic tasks sees less reasonable. We'd have +to spend a considerable amount more time figuring out _how_ to do each of these +first. + +For example - extensions. We have yet to fully realize what extensions _are_. +Determining how extensions will provide suggestions is left as something we'll +need to do as a part of the Extensions spec. + +## Conclusion + +Here's a sample json schema for the settings discussed here. + +```json +"OpenSuggestionsAction": { + "description": "Arguments corresponding to a Open Suggestions Action", + "allOf": [ + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "properties": { + "action": { + "type": "string", + "const": "suggestions" + }, + "source": { + "$ref": "#/$defs/SuggestionSource", + "description": "Which suggestion sources to filter." + }, + "useCommandline": { + "default": false, + "description": "When set to `true`, the current commandline the user has typed will pre-populate the filter of the Suggestions UI. This requires that the user has enabled shell integration in their shell's config. When set to false, the filter will start empty." + }, + "nesting": { + "default": true, + "description": "When set to `true`, suggestions will follow the provided nesting structure. For Tasks, these will follow the structure of the Command Palette. When set to `false`, no nesting will be used (and all suggestions will be in the top-level menu.", + "$comment": "This setting is a possible follow-up setting, not required for v1. " + } + } + } + ] +}, +"BuiltinSuggestionSource": { + "enum": [ + "commandHistory", + "directoryHistory", + "tasks", + "local", + "all" + ], + "type": "string" +}, +"SuggestionSource": { + "default": "all", + "description": "Either a single suggestion source, or an array of sources to concatenate. Built-in sources include `commandHistory`, `directoryHistory`, `tasks`, and `local`. Extensions may provide additional values. The special value `all` indicates all suggestion sources should be included", + "$comment": "`tasks` and `local` are sources that would be added by the Tasks feature, as a follow-up" + "oneOf": [ + { + "type": [ "string", "null", "BuiltinSuggestionSource" ] + }, + { + "type": "array", + "items": { "type": "BuiltinSuggestionSource" } + }, + { + "type": "array", + "items": { "type": "string" } + } + ] +}, +``` + +### Future Considerations + +* Another extension idea: `WithFig.FigCompletions`. Imagine an extension that + could parse existing [Fig] completion specs, and provide those as suggestions + in this way. + * This might be a good example of an async suggestion source. The current + commandline is used as the starting filter, and the suggestions would be + populated by some `fig` process / thread / async operation that returns the + suggestions. +* If the user hasn't enabled shell completion, we could add text to the + `commandHistory` or `directoryHistory` menus to inform the user how they could + go enable shell integration. We already have a docs page dedicated to this, so + we could start by linking to that page. More notes on this in [Automatic shell + integration](#Automatic-shell-integration). +* Maybe there could be a per-profile setting for automatic suggestions after + some timeout. Like, as you type, a menu version of the Suggestions UI appears. + So you could just start typing `git c`, and it would automatically give you a + menu with suggestions, implicitly using the typed command as the "filter". + * Maybe we could do this as an `implicit` property on the `suggestions` action + + +#### Description Tooltips + +> **Note**: _This is left as a future consideration for the initial draft of +> this spec. I'd like to flesh out [shell-driven autocompletion] more before +> committing any plans here._ + +It would be beneficial for the Suggestions UI to display additional context to +the user. Consider a extension that provides some commands for the user, like a +hypothetical "Docker" extension. The extension author might be able to give the +commands simplified names, but also want to expose a more detailed description +of the commands to the user. + +Or consider the Suggestions UI when invoked by [shell-driven autocompletion]. +The shell might want to provide help text to the user with each of the +suggestions. This would allow a user to browse through the suggestions that they +might not know about, and learn how they work before committing to one. + +Only the help text for the currently hovered command should be presented to the +user. To support this kind of UX, we'll add an optional flyout of some sort to +display with the Suggestions UI. This flyout will only appear if there's more +information provided to the Terminal. + +This might be in the form of a `TeachingTip`, as in this example: + +![TeachingTip with description](https://user-images.githubusercontent.com/18356694/222244568-243a6482-92d9-4c3c-bffc-54ad97f01f69.gif) + +Actions in the settings could also accept an optional `description` property, to +specify the string that would be presented in that flyout. + +#### Automatic shell integration + +A large portion of these features all rely on shell integration being enabled by +the user. However, this is not a trivial thing for the Terminal to do on behalf +of the user. Shell integration relies on changes to the user's shell config. If +the Terminal were to try and configure those itself, we may accidentally destroy +configuration that the user has already set up. Hence why the Terminal can't +just have a "Light up all the bells and whistles" toggle in the Settings UI. + +This is a non-trivial problem to solve, so it is being left as a future +consideration, for a later spec. It deserves its own spec to sort out how we +should expose this to users and safely implement it. + +#### Pre-filtering the UI & filter by source + +> **Note**: _This is a brainstorm I considered while writing this spec. I would +> not include it in the v1 of this spec. Rather, I'd like to leave it for +> where we might go with this UX in the future._ + +Do want to support different _types_ of nesting? So instead of just the default, +there could be something like `nesting: "source"`, to create a menu structured +like: + +``` +Suggestions UI +├─ Recent Commands... +│ ├─ git checkout main +│ ├─ git fetch +│ └─ git pull +├─ Recent Directories... +│ ├─ d:\dev +│ ├─ d:\dev\public +│ └─ d:\dev\public\terminal +├─ Saved tasks... +│ ├─ Git... +│ │ └─ git commit -m " +│ │ └─ git log... +│ └─ bx & runut +└─ Docker + ├─ docker build --platform linux/amd64 + └─ docker logs -f --tail +``` + +> **Note** +> I'm using `Docker` as an example fragment extension that provides +> some `docker` commands. When grouping by `"source"`, we could pull those into +> a separate top-level entry. When not grouping by `"source"`, those would still +> show up with the rest of `tasks`. ) + +#### Store recent commands across sessions + +> **Note** +> _I'm not sure we really want to put this in this spec or not, hence +> why it is in the "Future considerations" section. I think it is worth +> mentioning. This might be better served in the [shell integration] doc._ + +We'll probably want a way for recent commands to be saved across sessions. That way, your `cmd.exe` command history could persist across sessions. We'd need: + +* A setting to enable this behavior +* A setting to control the context of these saved commandlines. + * Do we want them saved per-profile, or globally? + * If they're saved per-profile, maybe a profile can opt-in to loading all the commands? + * How does defterm play with this? Do we "layer" by concatenating per-profile commands with `profiles.defaults` ones? +* A button in the Settings UI for clearing these commands +* Should fragments be able to pre-populate "recent commands"? + * I'm just gonna say _no_. That would be a better idea for Tasks (aka just a `sendInput` Action that we load from the fragment normally as a Task), or a specific suggestion source for the fragment extension. + +#### Inline mode + +> **Note** +> _This is a half-baked idea with some potential. However, I don't +> think it needs to be a part of the v1 of the Suggestions UI, so I'm leaving it +> under future considerations for a future revision._ + +Do we want to have a suggestions UI "mode", that's just **one** inline +suggestion, "no" UI? Some UX ala the `PsReadline` recent command suggestion +feature. Imagine, we just display the IME ghost text thing for the first result, +given the current prompt? + +Take the following action as an example: + +```json +{ "command": { "action":"suggestions", "source": "commandHistory", "useCommandline": true, "inline": true } }, +``` + +Type the start of some command at the prompt, and press that key. Presto, we do +the `pwsh` thing. Ghost text appears for the first match in the `commandHistory` +for what the user has typed. If they press another key, ~they've typed into the +"hidden" Suggestions UI, which filters the (hidden) list more, and updates the +one inline suggestion.~ + +Or, instead, typed keys go to the shell, and then we re-query the commandline, +and update the filter accordingly. That would allow tab-completion to still +work. We'd use right arrow to accept the suggestion (and dismiss the +ghost text preview). + +This would seemingly SUPER conflict with PowerShell's own handler. Probably not +something someone should enable for PowerShell 7 profiles if they're using that +feature. + +### Rejected ideas + +These are musings from earlier versions of the spec. +* **Asynchronous prompting**: This was rejected because it was so fundamentally + different from the rest of the UX of the Suggestions UI, it didn't make sense + to try and also do that behavior. +* ... + +#### REJECTED: Asynchronous prompting + +Certain suggestion sources might want to provide results asynchronously. +Consider a source that might want to make a web request to populate what strings +to suggest. That source might want to prompt the user for input first, then +dispatch the request, then populate the UI. Or something like a `fig`-like +suggestion source, which would need to parse some files from the disk to +generate the list of suggestions. + +The easiest way to do this would be to provide a secondary UI element for +prompting the user for input, doing the request in the background, then opening +the UI later. However, that feels a little disjointed. Could we instead provide +a more continuous experience? + +The following is a proposal for using the Suggestions UI itself as the control +to prompt the user for input. + +```c++ +TerminalPage::SetUpSuggestionsUI() +{ + const auto& asyncSource{ AsyncSuggestions() }; + + suggestionsUI.OnInputChanged({ asyncSource, AsyncSuggestions::InputChangedHandler}); + // In this example, we don't want the UI to filter item based on the input + // string - the source has already determined the list of relevant matches. + suggestionsUI.FilterByInput(false); + + asyncSource.SuggestionsChanged([](const auto& newCommands){ + suggestionsUI.Loading(false); + suggestionsUI.Commands(newCommands); + }) +} + +void AsyncSuggestions::InputChangedHandler(FilterChangedArgs args) +{ + // kick off a trailing ThrottledFunc to do a new query + _loadNewResults->Run(args.NewInputText()); + // If we get another request, we might want to cancel the pending throttled + // func entirely, and start the timeout fresh. Just so that we only make a + // query for the final string they type. + + args.RequestLoading(true); // pass a boolean back up in the args, so that + // the Suggestions UI can clear out the current commands, and start displaying an + // indeterminate progress wheel. +} +``` + +That would basically _have_ to be special cased for this source, at least for +now. We could refactor that later to better deal with extensions. + +Let's make sure this would work for something `fig`-like, where the "prompt" is +literally the prompt, what the user has already typed at the commandline. + +After some discussion: +* How do we differentiate the prompting version of the Suggestions UI from the + filtering version? + * The prompting version _doesn't_ filter results +* Async modes wouldn't work with sync ones at all. E.g. if you did `source: + ["tasks", "myAsyncSource"]`. It doesn't make sense to start with a list of + `tasks`, then type, find no tasks, but then oh! the UI fills in some other + suggestions too. That's weird. + +## Resources + +These are some other work streams that have a lot of tie-in to the Suggestions +UI. These are all being spec'd at roughly the same time, so links may not be +fully up to date. +* [Shell integration] +* [Shell-driven autocompletion] +* [Tasks] + +### Footnotes + +[1]: We've had discussion in the past ([#7285]) about +possibly creating a more abstract "Live filtering list view" to replace the +Command Palette. We could most certainly use that here too. We've decided to +initially go with a fork for now. + +[2]: Obviously, we're not having a real discussion about +extensions in this doc. This example is solely to show that there's room for +extensions to work with the "source" property in this design. What the final +shape of extensions will be is very much still to be determined. + + +[Fig]: https://github.com/withfig/autocomplete +[Warp]: https://www.warp.dev/ +[workflows]: https://docs.warp.dev/features/workflows +[also working on workflows]: https://fig.io/user-manual/workflows +[winget script]: https://github.com/microsoft/PowerToys/blob/main/.github/workflows/package-submissions.yml +[#1595]: https://github.com/microsoft/terminal/issues/1595 +[#7039]: https://github.com/microsoft/terminal/issues/7039 +[#3121]: https://github.com/microsoft/terminal/issues/3121 +[#10436]: https://github.com/microsoft/terminal/issues/10436 +[#12927]: https://github.com/microsoft/terminal/issues/12927 +[#12863]: https://github.com/microsoft/terminal/issues/12863 +[#7285]: https://github.com/microsoft/terminal/issues/7285 +[#14939]: https://github.com/microsoft/terminal/issues/7285 + +[#keep]: https://github.com/zadjii/keep +[VsCode Tasks]: https://github.com/microsoft/terminal/blob/main/.vscode/tasks.json + + +[Tasks]: https://github.com/microsoft/terminal/issues/12862 + +[shell integration]: https://github.com/microsoft/terminal/pull/14792 + +[shell-driven autocompletion]: https://github.com/microsoft/terminal/issues/3121 diff --git a/doc/specs/#1595 - Suggestions UI/command-history-suggestions.gif b/doc/specs/#1595 - Suggestions UI/command-history-suggestions.gif new file mode 100644 index 00000000000..94e7be1a54e Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/command-history-suggestions.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/shell-autocomplete-july-2022-000.gif b/doc/specs/#1595 - Suggestions UI/shell-autocomplete-july-2022-000.gif new file mode 100644 index 00000000000..0a832691b02 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/shell-autocomplete-july-2022-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/tasks-suggestions.gif b/doc/specs/#1595 - Suggestions UI/tasks-suggestions.gif new file mode 100644 index 00000000000..3629dd5a7b7 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/tasks-suggestions.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/vscode-shell-suggestions.gif b/doc/specs/#1595 - Suggestions UI/vscode-shell-suggestions.gif new file mode 100644 index 00000000000..93f76dc0d2e Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/vscode-shell-suggestions.gif differ diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 6c1c136e947..4036b991d81 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -152,11 +152,15 @@ til::CoordType CharToColumnMapper::GetTrailingColumnAt(ptrdiff_t offset) noexcep return col; } +// If given a pointer inside the ROW's text buffer, this function will return the corresponding column. +// This function in particular returns the glyph's first column. til::CoordType CharToColumnMapper::GetLeadingColumnAt(const wchar_t* str) noexcept { return GetLeadingColumnAt(str - _chars); } +// If given a pointer inside the ROW's text buffer, this function will return the corresponding column. +// This function in particular returns the glyph's last column (this matters for wide glyphs). til::CoordType CharToColumnMapper::GetTrailingColumnAt(const wchar_t* str) noexcept { return GetTrailingColumnAt(str - _chars); @@ -364,11 +368,16 @@ void ROW::TransferAttributes(const til::small_rle& a void ROW::CopyFrom(const ROW& source) { - RowCopyTextFromState state{ .source = source }; - CopyTextFrom(state); - TransferAttributes(source.Attributes(), _columnCount); _lineRendition = source._lineRendition; _wrapForced = source._wrapForced; + + RowCopyTextFromState state{ + .source = source, + .sourceColumnLimit = source.GetReadableColumnCount(), + }; + CopyTextFrom(state); + + TransferAttributes(source.Attributes(), _columnCount); } // Returns the previous possible cursor position, preceding the given column. @@ -382,7 +391,17 @@ til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept // Returns the row width if column is beyond the width of the row. til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept { - return _adjustForward(_clampedColumn(column + 1)); + return _adjustForward(_clampedColumnInclusive(column + 1)); +} + +// Returns the starting column of the glyph at the given column. +// In other words, if you have 3 wide glyphs +// AA BB CC +// 01 23 45 <-- column +// then `AdjustToGlyphStart(3)` returns 2. +til::CoordType ROW::AdjustToGlyphStart(til::CoordType column) const noexcept +{ + return _adjustBackward(_clampedColumn(column)); } // Routine Description: @@ -719,11 +738,12 @@ try if (sourceColBeg < sourceColLimit) { charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast(sourceColLimit) - sourceColBeg + 1); - const auto charsOffset = charOffsets.front() & CharOffsetsMask; + const auto beg = size_t{ charOffsets.front() } & CharOffsetsMask; + const auto end = size_t{ charOffsets.back() } & CharOffsetsMask; // We _are_ using span. But C++ decided that string_view and span aren't convertible. // _chars is a std::span for performance and because it refers to raw, shared memory. #pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). - chars = { source._chars.data() + charsOffset, source._chars.size() - charsOffset }; + chars = { source._chars.data() + beg, end - beg }; } WriteHelper h{ *this, state.columnBegin, state.columnLimit, chars }; @@ -939,6 +959,16 @@ til::CoordType ROW::MeasureLeft() const noexcept til::CoordType ROW::MeasureRight() const noexcept { + if (_wrapForced) + { + auto width = _columnCount; + if (_doubleBytePadded) + { + width--; + } + return width; + } + const auto text = GetText(); const auto beg = text.begin(); const auto end = text.end(); diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index 586aa12b95b..29f7fe18b8c 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -136,6 +136,7 @@ class ROW final til::CoordType NavigateToPrevious(til::CoordType column) const noexcept; til::CoordType NavigateToNext(til::CoordType column) const noexcept; + til::CoordType AdjustToGlyphStart(til::CoordType column) const noexcept; void ClearCell(til::CoordType column); OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt); diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index eef5fac17cb..9707e8fdeaa 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,30 +8,21 @@ using namespace Microsoft::Console::Types; -bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData) -{ - return ResetIfStale(renderData, - _needle, - _step == -1, // this is the opposite of the initializer below - _caseInsensitive); -} - bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive) { const auto& textBuffer = renderData.GetTextBuffer(); const auto lastMutationId = textBuffer.GetLastMutationId(); if (_needle == needle && - _reverse == reverse && _caseInsensitive == caseInsensitive && _lastMutationId == lastMutationId) { + _step = reverse ? -1 : 1; return false; } _renderData = &renderData; _needle = needle; - _reverse = reverse; _caseInsensitive = caseInsensitive; _lastMutationId = lastMutationId; @@ -42,12 +33,38 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c return true; } -void Search::MovePastCurrentSelection() +void Search::MoveToCurrentSelection() { if (_renderData->IsSelectionActive()) { - MovePastPoint(_renderData->GetTextBuffer().ScreenToBufferPosition(_renderData->GetSelectionAnchor())); + MoveToPoint(_renderData->GetTextBuffer().ScreenToBufferPosition(_renderData->GetSelectionAnchor())); + } +} + +void Search::MoveToPoint(const til::point anchor) noexcept +{ + if (_results.empty()) + { + return; + } + + const auto count = gsl::narrow_cast(_results.size()); + ptrdiff_t index = 0; + + if (_step < 0) + { + for (index = count - 1; index >= 0 && til::at(_results, index).start > anchor; --index) + { + } + } + else + { + for (index = 0; index < count && til::at(_results, index).start < anchor; ++index) + { + } } + + _index = (index + count) % count; } void Search::MovePastPoint(const til::point anchor) noexcept @@ -58,18 +75,17 @@ void Search::MovePastPoint(const til::point anchor) noexcept } const auto count = gsl::narrow_cast(_results.size()); - const auto highestIndex = count - 1; - auto index = _reverse ? highestIndex : 0; + ptrdiff_t index = 0; - if (_reverse) + if (_step < 0) { - for (; index >= 0 && til::at(_results, index).start >= anchor; --index) + for (index = count - 1; index >= 0 && til::at(_results, index).start >= anchor; --index) { } } else { - for (; index <= highestIndex && til::at(_results, index).start <= anchor; ++index) + for (index = 0; index < count && til::at(_results, index).start <= anchor; ++index) { } } @@ -119,7 +135,7 @@ const std::vector& Search::Results() const noexcept return _results; } -size_t Search::CurrentMatch() const noexcept +ptrdiff_t Search::CurrentMatch() const noexcept { return _index; } diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index 2c6bc80a724..c2d035e3e9c 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -25,10 +25,10 @@ class Search final public: Search() = default; - bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData); bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive); - void MovePastCurrentSelection(); + void MoveToCurrentSelection(); + void MoveToPoint(til::point anchor) noexcept; void MovePastPoint(til::point anchor) noexcept; void FindNext() noexcept; @@ -36,14 +36,12 @@ class Search final bool SelectCurrent() const; const std::vector& Results() const noexcept; - size_t CurrentMatch() const noexcept; - bool CurrentDirection() const noexcept; + ptrdiff_t CurrentMatch() const noexcept; private: // _renderData is a pointer so that Search() is constexpr default constructable. Microsoft::Console::Render::IRenderData* _renderData = nullptr; std::wstring _needle; - bool _reverse = false; bool _caseInsensitive = false; uint64_t _lastMutationId = 0; diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 84336eaf03e..c4c66ca92e4 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -806,9 +806,9 @@ void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes) // - The viewport //Return value: // - Coordinate position (relative to the text buffer) -til::point TextBuffer::GetLastNonSpaceCharacter(std::optional viewOptional) const +til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) const { - const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize(); + const auto viewport = viewOptional ? *viewOptional : GetSize(); til::point coordEndOfText; // Search the given viewport by starting at the bottom. @@ -1070,46 +1070,40 @@ void TextBuffer::Reset() noexcept // - 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(til::size newSize) noexcept +void TextBuffer::ResizeTraditional(til::size newSize) { // Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text. newSize.width = std::max(newSize.width, 1); newSize.height = std::max(newSize.height, 1); - try - { - TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; - const auto cursorRow = GetCursor().GetPosition().y; - const auto copyableRows = std::min(_height, newSize.height); - til::CoordType srcRow = 0; - til::CoordType dstRow = 0; - - if (cursorRow >= newSize.height) - { - srcRow = cursorRow - newSize.height + 1; - } - - for (; dstRow < copyableRows; ++dstRow, ++srcRow) - { - newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); - } + TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; + const auto cursorRow = GetCursor().GetPosition().y; + const auto copyableRows = std::min(_height, newSize.height); + til::CoordType srcRow = 0; + til::CoordType dstRow = 0; - // NOTE: Keep this in sync with _reserve(). - _buffer = std::move(newBuffer._buffer); - _bufferEnd = newBuffer._bufferEnd; - _commitWatermark = newBuffer._commitWatermark; - _initialAttributes = newBuffer._initialAttributes; - _bufferRowStride = newBuffer._bufferRowStride; - _bufferOffsetChars = newBuffer._bufferOffsetChars; - _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets; - _width = newBuffer._width; - _height = newBuffer._height; + if (cursorRow >= newSize.height) + { + srcRow = cursorRow - newSize.height + 1; + } - _SetFirstRowIndex(0); + for (; dstRow < copyableRows; ++dstRow, ++srcRow) + { + newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); } - CATCH_RETURN(); - return S_OK; + // NOTE: Keep this in sync with _reserve(). + _buffer = std::move(newBuffer._buffer); + _bufferEnd = newBuffer._bufferEnd; + _commitWatermark = newBuffer._commitWatermark; + _initialAttributes = newBuffer._initialAttributes; + _bufferRowStride = newBuffer._bufferRowStride; + _bufferOffsetChars = newBuffer._bufferOffsetChars; + _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets; + _width = newBuffer._width; + _height = newBuffer._height; + + _SetFirstRowIndex(0); } void TextBuffer::SetAsActiveBuffer(const bool isActiveBuffer) noexcept @@ -2201,6 +2195,7 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows, // Routine Description: // - Generates an RTF document based on the passed in text and color data // RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm +// RTF 1.9.1 Spec: https://msopenspecs.azureedge.net/files/Archive_References/[MSFT-RTF].pdf // Arguments: // - rows - the text and color data we will format & encapsulate // - backgroundColor - default background color for characters, also used in padding @@ -2220,10 +2215,18 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi // Standard RTF header. // This is similar to the header generated by WordPad. - // \ansi - specifies that the ANSI char set is used in the current doc - // \ansicpg1252 - represents the ANSI code page which is used to perform the Unicode to ANSI conversion when writing RTF text - // \deff0 - specifies that the default font for the document is the one at index 0 in the font table - // \nouicompat - ? + // \ansi: + // Specifies that the ANSI char set is used in the current doc. + // \ansicpg1252: + // Represents the ANSI code page which is used to perform + // the Unicode to ANSI conversion when writing RTF text. + // \deff0: + // Specifies that the default font for the document is the one + // at index 0 in the font table. + // \nouicompat: + // Some features are blocked by default to maintain compatibility + // with older programs (Eg. Word 97-2003). `nouicompat` disables this + // behavior, and unblocks these features. See: Spec 1.9.1, Pg. 51. rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat"; // font table @@ -2232,17 +2235,25 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi // map to keep track of colors: // keys are colors represented by COLORREF // values are indices of the corresponding colors in the color table - std::unordered_map colorMap; - auto nextColorIndex = 1; // leave 0 for the default color and start from 1. + std::unordered_map colorMap; // RTF color table std::ostringstream colorTableBuilder; colorTableBuilder << "{\\colortbl ;"; - colorTableBuilder << "\\red" << static_cast(GetRValue(backgroundColor)) - << "\\green" << static_cast(GetGValue(backgroundColor)) - << "\\blue" << static_cast(GetBValue(backgroundColor)) - << ";"; - colorMap[backgroundColor] = nextColorIndex++; + + const auto getColorTableIndex = [&](const COLORREF color) -> size_t { + // Exclude the 0 index for the default color, and start with 1. + + const auto [it, inserted] = colorMap.emplace(color, colorMap.size() + 1); + if (inserted) + { + colorTableBuilder << "\\red" << static_cast(GetRValue(color)) + << "\\green" << static_cast(GetGValue(color)) + << "\\blue" << static_cast(GetBValue(color)) + << ";"; + } + return it->second; + }; // content std::ostringstream contentBuilder; @@ -2252,7 +2263,12 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi // \fs specifies font size in half-points i.e. \fs20 results in a font size // of 10 pts. That's why, font size is multiplied by 2 here. contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints) - << "\\highlight1" + // Set the background color for the page. But, the + // standard way (\cbN) to do this isn't supported in Word. + // However, the following control words sequence works + // in Word (and other RTF editors also) for applying the + // text background color. See: Spec 1.9.1, Pg. 23. + << "\\chshdng0\\chcbpat" << getColorTableIndex(backgroundColor) << " "; std::optional fgColor = std::nullopt; @@ -2302,43 +2318,8 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi if (colorChanged) { writeAccumulatedChars(false); - - auto bkColorIndex = 0; - if (colorMap.find(bkColor.value()) != colorMap.end()) - { - // color already exists in the map, just retrieve the index - bkColorIndex = colorMap[bkColor.value()]; - } - else - { - // color not present in the map, so add it - colorTableBuilder << "\\red" << static_cast(GetRValue(bkColor.value())) - << "\\green" << static_cast(GetGValue(bkColor.value())) - << "\\blue" << static_cast(GetBValue(bkColor.value())) - << ";"; - colorMap[bkColor.value()] = nextColorIndex; - bkColorIndex = nextColorIndex++; - } - - auto fgColorIndex = 0; - if (colorMap.find(fgColor.value()) != colorMap.end()) - { - // color already exists in the map, just retrieve the index - fgColorIndex = colorMap[fgColor.value()]; - } - else - { - // color not present in the map, so add it - colorTableBuilder << "\\red" << static_cast(GetRValue(fgColor.value())) - << "\\green" << static_cast(GetGValue(fgColor.value())) - << "\\blue" << static_cast(GetBValue(fgColor.value())) - << ";"; - colorMap[fgColor.value()] = nextColorIndex; - fgColorIndex = nextColorIndex++; - } - - contentBuilder << "\\highlight" << bkColorIndex - << "\\cf" << fgColorIndex + contentBuilder << "\\chshdng0\\chcbpat" << getColorTableIndex(bkColor.value()) + << "\\cf" << getColorTableIndex(fgColor.value()) << " "; } @@ -2412,204 +2393,181 @@ void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::w // the new buffer. The rows's new value is placed back into this parameter. // Return Value: // - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT. -HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo) -try +void TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Viewport* lastCharacterViewport, PositionInformation* positionInfo) { const auto& oldCursor = oldBuffer.GetCursor(); auto& newCursor = newBuffer.GetCursor(); - // 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. - const auto cOldCursorPos = oldCursor.GetPosition(); - const auto cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport); - - const auto cOldRowsTotal = cOldLastChar.y + 1; - - til::point cNewCursorPos; - auto fFoundCursorPos = false; - auto foundOldMutable = false; - auto foundOldVisible = false; - // Loop through all the rows of the old buffer and reprint them into the new buffer - til::CoordType iOldRow = 0; - for (; iOldRow < cOldRowsTotal; iOldRow++) - { - // Fetch the row and its "right" which is the last printable character. - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - const auto cOldColsTotal = oldBuffer.GetLineWidth(iOldRow); - auto iRight = row.MeasureRight(); - - // If we're starting a new row, try and preserve the line rendition - // from the row in the original buffer. - const auto newBufferPos = newCursor.GetPosition(); - if (newBufferPos.x == 0) - { - auto& newRow = newBuffer.GetMutableRowByOffset(newBufferPos.y); - newRow.SetLineRendition(row.GetLineRendition()); - } + til::point oldCursorPos = oldCursor.GetPosition(); + til::point newCursorPos; + + // BODGY: We use oldCursorPos in two critical places below: + // * To compute an oldHeight that includes at a minimum the cursor row + // * For REFLOW_JANK_CURSOR_WRAP (see comment below) + // Both of these would break the reflow algorithm, but the latter of the two in particular + // would cause the main copy loop below to deadlock. In other words, these two lines + // protect this function against yet-unknown bugs in other parts of the code base. + oldCursorPos.x = std::clamp(oldCursorPos.x, 0, oldBuffer._width - 1); + oldCursorPos.y = std::clamp(oldCursorPos.y, 0, oldBuffer._height - 1); + + const auto lastRowWithText = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport).y; + + auto mutableViewportTop = positionInfo ? positionInfo->mutableViewportTop : til::CoordTypeMax; + auto visibleViewportTop = positionInfo ? positionInfo->visibleViewportTop : til::CoordTypeMax; + + til::CoordType oldY = 0; + til::CoordType newY = 0; + til::CoordType newX = 0; + til::CoordType newWidth = newBuffer.GetSize().Width(); + til::CoordType newYLimit = til::CoordTypeMax; + + const auto oldHeight = std::max(lastRowWithText, oldCursorPos.y) + 1; + const auto newHeight = newBuffer.GetSize().Height(); + const auto newWidthU16 = gsl::narrow_cast(newWidth); - // 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 (row.WasWrapForced()) + // Copy oldBuffer into newBuffer until oldBuffer has been fully consumed. + for (; oldY < oldHeight && newY < newYLimit; ++oldY) + { + const auto& oldRow = oldBuffer.GetRowByOffset(oldY); + + // A pair of double height rows should optimally wrap as a union (i.e. after wrapping there should be 4 lines). + // But for this initial implementation I chose the alternative approach: Just truncate them. + if (oldRow.GetLineRendition() != LineRendition::SingleWidth) { - 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 (row.WasDoubleBytePadded()) + // Since rows with a non-standard line rendition should be truncated it's important + // that we pretend as if the previous row ended in a newline, even if it didn't. + // This is what this if does: It newlines. + if (newX) { - iRight--; + newX = 0; + newY++; } - } - // Loop through every character in the current row (up to - // the "right" boundary, which is one past the final valid - // character) - til::CoordType iOldCol = 0; - const auto copyRight = iRight; - for (; iOldCol < copyRight; iOldCol++) - { - if (iOldCol == cOldCursorPos.x && iOldRow == cOldCursorPos.y) + auto& newRow = newBuffer.GetMutableRowByOffset(newY); + + // See the comment marked with "REFLOW_RESET". + if (newY >= newHeight) { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; + newRow.Reset(newBuffer._initialAttributes); } - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto glyph = row.GlyphAt(iOldCol); - const auto dbcsAttr = row.DbcsAttrAt(iOldCol); - const auto textAttr = row.GetAttrByColumn(iOldCol); + newRow.CopyFrom(oldRow); + newRow.SetWrapForced(false); + + if (oldY == oldCursorPos.y) + { + newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x), newY }; + } + if (oldY >= mutableViewportTop) + { + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; + } + if (oldY >= visibleViewportTop) + { + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; + } - newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr); + newY++; + continue; } - // GH#32: Copy the attributes from the rest of the row into this new buffer. - // From where we are in the old buffer, to the end of the row, copy the - // remaining attributes. - // - if the old buffer is smaller than the new buffer, then just copy - // what we have, as it was. We already copied all _text_ with colors, - // but it's possible for someone to just put some color into the - // buffer to the right of that without any text (as just spaces). The - // buffer looks weird to the user when we resize and it starts losing - // those colors, so we need to copy them over too... as long as there - // is space. The last attr in the row will be extended to the end of - // the row in the new buffer. - // - if the old buffer is WIDER, than we might have wrapped onto a new - // line. Use the cursor's position's Y so that we know where the new - // row is, and start writing at the cursor position. Again, the attr - // in the last column of the old row will be extended to the end of the - // row that the text was flowed onto. - // - if the text in the old buffer didn't actually fill the whole - // line in the new buffer, then we didn't wrap. That's fine. just - // copy attributes from the old row till the end of the new row, and - // move on. - const auto newRowY = newCursor.GetPosition().y; - auto& newRow = newBuffer.GetMutableRowByOffset(newRowY); - auto newAttrColumn = newCursor.GetPosition().x; - const auto newWidth = newBuffer.GetLineWidth(newRowY); - // Stop when we get to the end of the buffer width, or the new position - // for inserting an attr would be past the right of the new buffer. - for (auto copyAttrCol = iOldCol; - copyAttrCol < cOldColsTotal && newAttrColumn < newWidth; - copyAttrCol++, newAttrColumn++) + // Rows don't store any information for what column the last written character is in. + // We simply truncate all trailing whitespace in this implementation. + auto oldRowLimit = oldRow.MeasureRight(); + if (oldY == oldCursorPos.y) { - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto textAttr = row.GetAttrByColumn(copyAttrCol); - newRow.SetAttrToEnd(newAttrColumn, textAttr); + // REFLOW_JANK_CURSOR_WRAP: + // Pretending as if there's always at least whitespace in front of the cursor has the benefit that + // * the cursor retains its distance from any preceding text. + // * when a client application starts writing on this new, empty line, + // enlarging the buffer unwraps the text onto the preceding line. + oldRowLimit = std::max(oldRowLimit, oldCursorPos.x + 1); } - // If we found the old row that the caller was interested in, set the - // out value of that parameter to the cursor's current Y position (the - // new location of the _end_ of that row in the buffer). - if (positionInfo.has_value()) + til::CoordType oldX = 0; + + // Copy oldRow into newBuffer until oldRow has been fully consumed. + // We use a do-while loop to ensure that line wrapping occurs and + // that attributes are copied over even for seemingly empty rows. + do { - if (!foundOldMutable) + // This if condition handles line wrapping. + // Only if we write past the last column we should wrap and as such this if + // condition is in front of the text insertion code instead of behind it. + // A SetWrapForced of false implies an explicit newline, which is the default. + if (newX >= newWidth) { - if (iOldRow >= positionInfo.value().get().mutableViewportTop) - { - positionInfo.value().get().mutableViewportTop = newCursor.GetPosition().y; - foundOldMutable = true; - } + newBuffer.GetMutableRowByOffset(newY).SetWrapForced(true); + newX = 0; + newY++; } - if (!foundOldVisible) + // REFLOW_RESET: + // If we shrink the buffer vertically, for instance from 100 rows to 90 rows, we will write 10 rows in the + // new buffer twice. We need to reset them before copying text, or otherwise we'll see the previous contents. + // We don't need to be smart about this. Reset() is fast and shrinking doesn't occur often. + if (newY >= newHeight && newX == 0) { - if (iOldRow >= positionInfo.value().get().visibleViewportTop) + // We need to ensure not to overwrite the row the cursor is on. + if (newY >= newYLimit) { - positionInfo.value().get().visibleViewportTop = newCursor.GetPosition().y; - foundOldVisible = true; + break; } + newBuffer.GetMutableRowByOffset(newY).Reset(newBuffer._initialAttributes); } - } - // If we didn't have a full row to copy, insert a new - // line into the new buffer. - // Only do so if we were not forced to wrap. If we did - // force a word wrap, then the existing line break was - // only because we ran out of space. - if (iRight < cOldColsTotal && !row.WasWrapForced()) - { - if (!fFoundCursorPos && (iRight == cOldCursorPos.x && iOldRow == cOldCursorPos.y)) + auto& newRow = newBuffer.GetMutableRowByOffset(newY); + + RowCopyTextFromState state{ + .source = oldRow, + .columnBegin = newX, + .columnLimit = til::CoordTypeMax, + .sourceColumnBegin = oldX, + .sourceColumnLimit = oldRowLimit, + }; + newRow.CopyTextFrom(state); + + const auto& oldAttr = oldRow.Attributes(); + auto& newAttr = newRow.Attributes(); + const auto attributes = oldAttr.slice(gsl::narrow_cast(oldX), oldAttr.size()); + newAttr.replace(gsl::narrow_cast(newX), newAttr.size(), attributes); + newAttr.resize_trailing_extent(newWidthU16); + + if (oldY == oldCursorPos.y && oldCursorPos.x >= oldX) { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; + // In theory AdjustToGlyphStart ensures we don't put the cursor on a trailing wide glyph. + // In practice I don't think that this can possibly happen. Better safe than sorry. + newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x - oldX + newX), newY }; + // If there's so much text past the old cursor position that it doesn't fit into new buffer, + // then the new cursor position will be "lost", because it's overwritten by unrelated text. + // We have two choices how can handle this: + // * If the new cursor is at an y < 0, just put the cursor at (0,0) + // * Stop writing into the new buffer before we overwrite the new cursor position + // This implements the second option. There's no fundamental reason why this is better. + newYLimit = newY + newHeight; } - // 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) + if (oldY >= mutableViewportTop) { - newBuffer.NewlineCursor(); + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; } - else + if (oldY >= visibleViewportTop) { - // If we are on the final line of the buffer, we have one more check. - // We got into this code path because we are at the right most column of a row in the old buffer - // that had a hard return (no wrap was forced). - // However, as we're inserting, the old row might have just barely fit into the new buffer and - // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below. - // We need to preserve the memory of the hard return at this point by inserting one additional - // hard newline, otherwise we've lost that information. - // We only do this when the cursor has just barely poured over onto the next line so the hard return - // isn't covered by the soft one. - // e.g. - // The old line was: - // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a. - // The cursor was here ^ - // And the new line will be: - // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end - // | | - // ^ and the cursor is now there. - // If we leave it like this, we've lost the newline information. - // So we insert one more newline so a continued reflow of this buffer by resizing larger will - // continue to look as the original output intended with the newline data. - // After this fix, it looks like this: - // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline) - // | | - // ^ and the cursor is now here. - const auto coordNewCursor = newCursor.GetPosition(); - if (coordNewCursor.x == 0 && coordNewCursor.y > 0) - { - if (newBuffer.GetRowByOffset(coordNewCursor.y - 1).WasWrapForced()) - { - newBuffer.NewlineCursor(); - } - } + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; } + + oldX = state.sourceColumnEnd; + newX = state.columnEnd; + } while (oldX < oldRowLimit); + + // If the row had an explicit newline we also need to newline. :) + if (!oldRow.WasWrapForced()) + { + newX = 0; + newY++; } } @@ -2617,85 +2575,40 @@ try // printable character. This is to fix the `color 2f` scenario, where you // change the buffer colors then resize and everything below the last // printable char gets reset. See GH #12567 - auto newRowY = newCursor.GetPosition().y + 1; - const auto newHeight = newBuffer.GetSize().Height(); - const auto oldHeight = oldBuffer._estimateOffsetOfLastCommittedRow() + 1; - for (; - iOldRow < oldHeight && newRowY < newHeight; - iOldRow++) + const auto initializedRowsEnd = oldBuffer._estimateOffsetOfLastCommittedRow() + 1; + for (; oldY < initializedRowsEnd && newY < newHeight; oldY++, newY++) { - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - - // Optimization: Since all these rows are below the last printable char, - // we can reasonably assume that they are filled with just spaces. - // That's convenient, we can just copy the attr row from the old buffer - // into the new one, and resize the row to match. We'll rely on the - // behavior of ATTR_ROW::Resize to trim down when narrower, or extend - // the last attr when wider. - auto& newRow = newBuffer.GetMutableRowByOffset(newRowY); - const auto newWidth = newBuffer.GetLineWidth(newRowY); - newRow.TransferAttributes(row.Attributes(), newWidth); - - newRowY++; + auto& oldRow = oldBuffer.GetRowByOffset(oldY); + auto& newRow = newBuffer.GetMutableRowByOffset(newY); + auto& newAttr = newRow.Attributes(); + newAttr = oldRow.Attributes(); + newAttr.resize_trailing_extent(newWidthU16); } - // Finish copying remaining parameters from the old text buffer to the new one - newBuffer.CopyProperties(oldBuffer); - newBuffer.CopyHyperlinkMaps(oldBuffer); - - // If we found where to put the cursor while placing characters into the buffer, - // just put the cursor there. Otherwise we have to advance manually. - if (fFoundCursorPos) + // Since we didn't use IncrementCircularBuffer() we need to compute the proper + // _firstRow offset now, in a way that replicates IncrementCircularBuffer(). + // We need to do the same for newCursorPos.y for basically the same reason. + if (newY > newHeight) { - newCursor.SetPosition(cNewCursorPos); + newBuffer._firstRow = newY % newHeight; + // _firstRow maps from API coordinates that always start at 0,0 in the top left corner of the + // terminal's scrollback, to the underlying buffer Y coordinate via `(y + _firstRow) % height`. + // Here, we need to un-map the `newCursorPos.y` from the underlying Y coordinate to the API coordinate + // and so we do `(y - _firstRow) % height`, but we add `+ newHeight` to avoid getting negative results. + newCursorPos.y = (newCursorPos.y - newBuffer._firstRow + newHeight) % newHeight; } - else - { - // Advance the cursor to the same offset as before - // get the number of newlines and spaces between the old end of text and the old cursor, - // then advance that many newlines and chars - auto iNewlines = cOldCursorPos.y - cOldLastChar.y; - const auto iIncrements = cOldCursorPos.x - cOldLastChar.x; - const auto cNewLastChar = newBuffer.GetLastNonSpaceCharacter(); - - // If the last row of the new buffer wrapped, there's going to be one less newline needed, - // because the cursor is already on the next line - if (newBuffer.GetRowByOffset(cNewLastChar.y).WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - else - { - // if this buffer didn't wrap, but the old one DID, then the d(columns) of the - // old buffer will be one more than in this buffer, so new need one LESS. - if (oldBuffer.GetRowByOffset(cOldLastChar.y).WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - } - for (auto r = 0; r < iNewlines; r++) - { - newBuffer.NewlineCursor(); - } - for (auto c = 0; c < iIncrements - 1; c++) - { - newBuffer.IncrementCursor(); - } - } - - // Save old cursor size before we delete it - const auto ulSize = oldCursor.GetSize(); + newBuffer.CopyProperties(oldBuffer); + newBuffer.CopyHyperlinkMaps(oldBuffer); - // Set size back to real size as it will be taking over the rendering duties. - newCursor.SetSize(ulSize); + assert(newCursorPos.x >= 0 && newCursorPos.x < newWidth); + assert(newCursorPos.y >= 0 && newCursorPos.y < newHeight); + newCursor.SetSize(oldCursor.GetSize()); + newCursor.SetPosition(newCursorPos); newBuffer._marks = oldBuffer._marks; newBuffer._trimMarksOutsideBuffer(); - - return S_OK; } -CATCH_RETURN() // Method Description: // - Adds or updates a hyperlink in our hyperlink table @@ -2916,14 +2829,10 @@ void TextBuffer::AddMark(const ScrollMark& m) void TextBuffer::_trimMarksOutsideBuffer() { - const auto height = GetSize().Height(); - _marks.erase(std::remove_if(_marks.begin(), - _marks.end(), - [height](const auto& m) { - return (m.start.y < 0) || - (m.start.y >= height); - }), - _marks.end()); + const til::CoordType height = _height; + std::erase_if(_marks, [height](const auto& m) { + return (m.start.y < 0) || (m.start.y >= height); + }); } std::wstring_view TextBuffer::CurrentCommand() const diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 360c74aab8c..a9d2e1714b6 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -163,7 +163,7 @@ class TextBuffer final // Scroll needs access to this to quickly rotate around the buffer. void IncrementCircularBuffer(const TextAttribute& fillAttributes = {}); - til::point GetLastNonSpaceCharacter(std::optional viewOptional = std::nullopt) const; + til::point GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport* viewOptional = nullptr) const; Cursor& GetCursor() noexcept; const Cursor& GetCursor() const noexcept; @@ -194,7 +194,7 @@ class TextBuffer final void Reset() noexcept; - [[nodiscard]] HRESULT ResizeTraditional(const til::size newSize) noexcept; + void ResizeTraditional(const til::size newSize); void SetAsActiveBuffer(const bool isActiveBuffer) noexcept; bool IsActiveBuffer() const noexcept; @@ -262,10 +262,7 @@ class TextBuffer final til::CoordType visibleViewportTop{ 0 }; }; - static HRESULT Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo); + static void Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Microsoft::Console::Types::Viewport* lastCharacterViewport = nullptr, PositionInformation* positionInfo = nullptr); std::vector SearchText(const std::wstring_view& needle, bool caseInsensitive) const; std::vector SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const; diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index 3c99a9fb583..d1fad4c407f 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -46,8 +46,6 @@ namespace std::vector buffers; }; - static constexpr auto true_due_to_exact_wrap_bug{ true }; - static const TestCase testCases[] = { TestCase{ L"No reflow required", @@ -61,7 +59,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -72,7 +70,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 4, 5 }, @@ -83,7 +81,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -99,40 +97,40 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. - { L" ", false }, + { L"F ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 0, 2 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original { - { L"ABCDEF", true_due_to_exact_wrap_bug }, + { L"ABCDEF", false }, { L"$ ", false }, { L" ", false }, { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original { - { L"ABCDEF$", true_due_to_exact_wrap_bug }, // EXACT WRAP BUG: $ should be alone on next line - { L" ", false }, + { L"ABCDEF ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, { L" ", false }, }, - { 6, 0 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -148,7 +146,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 1, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -159,7 +157,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original @@ -170,7 +168,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 1, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original @@ -181,7 +179,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -197,29 +195,29 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // reduce width by 1 { { L"AB $", true }, - { L" CD", true_due_to_exact_wrap_bug }, - { L" ", false }, + { L" CD", false }, // CD ends with a newline -> .wrap = false { L"EFG ", false }, { L" ", false }, + { L" ", false }, }, - { 6, 0 } // cursor on $ + { 6, 0 }, // cursor on $ }, TestBuffer{ { 8, 5 }, { { L"AB $ ", true }, - { L" CD ", false }, // Goes to false because we hit the end of ..CD - { L"EFG ", false }, // [BUG] EFG moves up due to exact wrap bug above + { L" CD ", false }, + { L"EFG ", false }, { L" ", false }, { L" ", false }, }, - { 6, 0 } // cursor on $ + { 6, 0 }, // cursor on $ }, }, }, @@ -236,19 +234,19 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { //--012345-- { L"カタ ", true }, // KA TA [FORCED SPACER] - { L"カナ$", true_due_to_exact_wrap_bug }, // KA NA + { L"カナ$", false }, // KA NA { L" ", false }, { L" ", false }, { L" ", false }, }, - { 4, 1 } // cursor on $ + { 4, 1 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original @@ -260,7 +258,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original (by one; no visible change!) @@ -272,7 +270,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 8, 5 }, // grow width enough to fit second DBCS @@ -284,7 +282,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -300,41 +298,29 @@ namespace { L"MNOPQR", false }, { L"STUVWX", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { - { L"F$ ", false }, - { L"GHIJK", true }, // [BUG] We should see GHIJK\n L\n MNOPQ\n R\n - { L"LMNOP", true }, // The wrapping here is irregular - { L"QRSTU", true }, - { L"VWX ", false }, + { L"$ ", false }, + { L"GHIJK", true }, + { L"L ", false }, + { L"MNOPQ", true }, + { L"R ", false }, }, - { 1, 1 } // [BUG] cursor moves to 1,1 instead of sticking with the $ + { 0, 0 }, }, TestBuffer{ { 6, 5 }, // going back to 6,5, the data lost has been destroyed { - //{ L"F$ ", false }, // [BUG] this line is erroneously destroyed too! - { L"GHIJKL", true }, - { L"MNOPQR", true }, - { L"STUVWX", true }, + { L"$ ", false }, + { L"GHIJKL", false }, + { L"MNOPQR", false }, + { L" ", false }, { L" ", false }, - { L" ", false }, // [BUG] this line is added - }, - { 1, 1 }, // [BUG] cursor does not follow [H], it sticks at 1,1 - }, - TestBuffer{ - { 7, 5 }, // a number of errors are carried forward from the previous buffer - { - { L"GHIJKLM", true }, - { L"NOPQRST", true }, - { L"UVWX ", false }, // [BUG] This line loses wrap for some reason - { L" ", false }, - { L" ", false }, }, - { 0, 1 }, // [BUG] The cursor moves to 0, 1 now, sticking with the [N] from before + { 0, 0 }, }, }, }, @@ -353,18 +339,18 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor *after* $ + { 1, 1 }, // cursor *after* $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. - { L" ", false }, + { L"F ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor follows space after $ to next line + { 1, 2 }, // cursor follows space after $ to next line }, }, }, @@ -380,7 +366,7 @@ namespace { L"STUVWX", true }, { L"YZ0 $ ", false }, }, - { 5, 4 } // cursor *after* $ + { 5, 4 }, // cursor *after* $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -391,7 +377,7 @@ namespace { L"UVWXY", true }, { L"Z0 $ ", false }, }, - { 4, 4 } // cursor follows space after $ to newly introduced bottom line + { 4, 4 }, // cursor follows space after $ to newly introduced bottom line }, }, }, @@ -402,40 +388,36 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", true }, // [BUG] This line is marked wrapped, and I do not know why - // v cursor - { L" ", false }, - // ^ cursor + { L"F ", false }, + // The reflow implementation marks a wrapped cursor as a forced row-wrap (= the row is padded with whitespace), so that when + // the buffer is enlarged again, we restore the original cursor position correctly. That's why it says .cursor={5,1} below. + { L"$ ", true }, { L" ", false }, { L" ", false }, }, - { 1, 2 } // cursor stays same linear distance from $ + { 0, 3 }, // $ is now at 0,2 and the cursor used to be 5 columns to the right. -> 0,3 }, TestBuffer{ { 6, 5 }, // grow back to original size { - { L"ABCDEF", true_due_to_exact_wrap_bug }, - // v cursor [BUG] cursor does not retain linear distance from $ + { L"ABCDEF", false }, { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 4, 1 } // cursor stays same linear distance from $ + { 5, 1 }, }, }, }, @@ -446,39 +428,37 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L"BLAH ", false }, { L"BLAH ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 5, 5 }, // reduce width by 1 { - { L"ABCDE", true }, - { L"F$ ", false }, - { L"BLAH ", false }, - { L"BLAH ", true }, // [BUG] this line wraps, no idea why - // v cursor [BUG] cursor erroneously moved to end of all content + { L"F ", false }, + // The reflow implementation pads the row with the cursor with whitespace. + // Search for "REFLOW_JANK_CURSOR_WRAP" to find the corresponding code. + { L"$ ", true }, { L" ", false }, - // ^ cursor + { L"BLAH ", false }, + { L"BLAH ", false }, }, - { 0, 4 } }, + { 0, 2 }, + }, TestBuffer{ { 6, 5 }, // grow back to original size { - { L"ABCDEF", true }, + { L"F ", false }, { L"$ ", false }, { L"BLAH ", false }, - // v cursor [BUG] cursor is pulled up to previous line because it was marked wrapped { L"BLAH ", false }, - // ^ cursor { L" ", false }, }, - { 5, 3 } }, + { 5, 1 }, + }, }, }, TestCase{ @@ -489,27 +469,24 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 2, 5 }, // reduce width aggressively { { L"CD", true }, - { L"EF", true }, + { L"EF", false }, { L"$ ", true }, { L" ", true }, - // v cursor { L" ", false }, - // ^ cursor }, - { 1, 4 } }, + { 1, 4 }, + }, }, }, TestCase{ @@ -519,27 +496,24 @@ namespace { 6, 5 }, { { L"ABCDEF", true }, - // v cursor { L"$ ", true }, - // ^ cursor { L" ", true }, { L" ", true }, { L" ", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively { - { L"EF", true }, - { L"$ ", true }, { L" ", true }, { L" ", true }, - // v cursor [BUG] cursor does not maintain linear distance from $ - { L" ", false }, - // ^ cursor + { L" ", true }, + { L" ", true }, + { L" ", true }, }, - { 1, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -549,14 +523,12 @@ namespace { 6, 5 }, { { L"ABCDEF", true }, - // v cursor { L"$ ", true }, - // ^ cursor { L" ", true }, { L" ", true }, { L" Q", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively @@ -564,12 +536,11 @@ namespace { L" ", true }, { L" ", true }, { L" ", true }, - { L" Q", true }, - // v cursor [BUG] cursor jumps to end of world - { L" ", false }, // POTENTIAL [BUG] a whole new blank line is added for the cursor - // ^ cursor + { L" ", true }, + { L" ", true }, }, - { 1, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -579,27 +550,24 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", true }, { L" Q", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively { + { L" ", false }, + { L" ", false }, { L" ", true }, { L" ", true }, { L" ", true }, - { L" Q", true }, - // v cursor [BUG] cursor jumps to different place than fully wrapped case - { L" ", false }, - // ^ cursor }, - { 0, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -614,24 +582,21 @@ namespace { L"$ ", false }, { L" Q", true }, { L" ", true }, - // v cursor { L" ", true }, - // ^ cursor }, - { 5, 4 } // cursor at end of buffer + { 5, 4 }, // cursor at end of buffer }, TestBuffer{ { 2, 5 }, // reduce width aggressively { + { L" ", false }, + { L" ", true }, + { L" ", true }, { L" ", true }, { L" ", true }, - { L" Q", true }, - { L" ", false }, - // v cursor [BUG] cursor loses linear distance from Q; is this important? - { L" ", false }, - // ^ cursor }, - { 0, 4 } }, + { 1, 0 }, + }, }, }, }; @@ -761,7 +726,7 @@ class ReflowTests static std::unique_ptr _textBufferByReflowingTextBuffer(TextBuffer& originalBuffer, const til::size newSize) { auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, renderer); - TextBuffer::Reflow(originalBuffer, *buffer, std::nullopt, std::nullopt); + TextBuffer::Reflow(originalBuffer, *buffer); return buffer; } diff --git a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj index 72a89ba1246..ad3b48c7cb5 100644 --- a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj +++ b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj @@ -14,7 +14,7 @@ - + Create @@ -30,7 +30,7 @@ - + diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index d72c96e1405..04c7960ce95 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -36,7 +36,8 @@ - + + diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp index 5f02757084a..b5139a5263f 100644 --- a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -74,6 +74,8 @@ namespace SettingsModelLocalTests TEST_METHOD(TestInheritedCommand); TEST_METHOD(LoadFragmentsWithMultipleUpdates); + TEST_METHOD(MigrateReloadEnvVars); + private: static winrt::com_ptr createSettings(const std::string_view& userJSON) { @@ -2020,4 +2022,40 @@ namespace SettingsModelLocalTests // GH#12520: Fragments should be able to override the name of builtin profiles. VERIFY_ARE_EQUAL(L"NewName", loader.userSettings.profiles[0]->Name()); } + + void DeserializationTests::MigrateReloadEnvVars() + { + static constexpr std::string_view settings1Json{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "compatibility.reloadEnvironmentVariables": false, + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader loader{ settings1Json, DefaultJson }; + loader.MergeInboxIntoUserSettings(); + loader.FinalizeLayering(); + + VERIFY_IS_TRUE(loader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + + const auto settings = winrt::make_self(std::move(loader)); + + Log::Comment(L"Ensure that the profile defaults have the new setting added"); + VERIFY_IS_TRUE(settings->ProfileDefaults().HasReloadEnvironmentVariables()); + VERIFY_IS_FALSE(settings->ProfileDefaults().ReloadEnvironmentVariables()); + } } diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp index ffc7dea07bd..2488f3f812d 100644 --- a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -42,6 +42,9 @@ namespace SettingsModelLocalTests TEST_METHOD(CascadiaSettings); TEST_METHOD(LegacyFontSettings); + TEST_METHOD(RoundtripReloadEnvVars); + TEST_METHOD(DontRoundtripNoReloadEnvVars); + private: // Method Description: // - deserializes and reserializes a json string representing a settings object model of type T @@ -513,4 +516,114 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(toString(jsonOutput), toString(result)); } + + void SerializationTests::RoundtripReloadEnvVars() + { + static constexpr std::string_view oldSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "compatibility.reloadEnvironmentVariables": false, + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + static constexpr std::string_view newSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": + { + "defaults": + { + "compatibility.reloadEnvironmentVariables": false + }, + "list": + [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ] + }, + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, DefaultJson }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_TRUE(oldLoader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + implementation::SettingsLoader newLoader{ newSettingsJson, DefaultJson }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + const auto newResult{ newSettings->ToJson() }; + + VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); + } + + void SerializationTests::DontRoundtripNoReloadEnvVars() + { + // Kinda like the above test, but confirming that _nothing_ happens if + // we don't have a setting to migrate. + + static constexpr std::string_view oldSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, DefaultJson }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + oldLoader.FixupUserSettings(); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + Log::Comment(L"Now, create a _new_ settings object from the re-serialization of the first"); + implementation::SettingsLoader newLoader{ toString(oldResult), DefaultJson }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + VERIFY_IS_FALSE(newSettings->ProfileDefaults().HasReloadEnvironmentVariables(), + L"Ensure that the new settings object didn't find a reloadEnvironmentVariables"); + } } diff --git a/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp index 4e3bdacc254..c7bf11326af 100644 --- a/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp @@ -60,7 +60,8 @@ namespace SettingsModelLocalTests "tabRow": { "background": "#FFFF8800", - "unfocusedBackground": "#FF8844" + "unfocusedBackground": "#FF8844", + "iconStyle": "default" }, "window": { diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h index 96727252489..b077abab8be 100644 --- a/src/cascadia/LocalTests_SettingsModel/pch.h +++ b/src/cascadia/LocalTests_SettingsModel/pch.h @@ -64,6 +64,7 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include // Common includes for most tests: #include "../../inc/conattrs.hpp" diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj deleted file mode 100644 index f8db236b21f..00000000000 --- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj +++ /dev/null @@ -1,64 +0,0 @@ - - - - 16.0 - {84848BFA-931D-42CE-9ADF-01EE54DE7890} - Win32Proj - PublicTerminalCore - PublicTerminalCore - DynamicLibrary - - - - - - Create - - - - - - - - - - - - {1cf55140-ef6a-4736-a403-957e4f7430bb} - - - {ca5cad1a-abcd-429c-b551-8562ec954746} - - - {18D09A24-8240-42D6-8CB6-236EEE820263} - - - {8222900C-8B6C-452A-91AC-BE95DB04B95F} - - - {af0a096a-8b3a-4949-81ef-7df8f0fee91f} - - - {48d21369-3d7b-4431-9967-24e81292cf62} - - - {48d21369-3d7b-4431-9967-24e81292cf63} - - - - - pch.h - - - - - - - - - delayimp.lib;Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies) - uiautomationcore.dll;%(DelayLoadDLLs) - - - diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters deleted file mode 100644 index 6865448d4a3..00000000000 --- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {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;ipp;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 - - - Header Files - - - \ No newline at end of file diff --git a/src/cascadia/PublicTerminalCore/pch.cpp b/src/cascadia/PublicTerminalCore/pch.cpp deleted file mode 100644 index 398a99f6653..00000000000 --- a/src/cascadia/PublicTerminalCore/pch.cpp +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" diff --git a/src/cascadia/PublicTerminalCore/pch.h b/src/cascadia/PublicTerminalCore/pch.h deleted file mode 100644 index 3834f83cdee..00000000000 --- a/src/cascadia/PublicTerminalCore/pch.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN // If this is not defined, windows.h includes commdlg.h which defines FindText globally and conflicts with UIAutomation ITextRangeProvider. -#define NOMCX -#define NOHELP -#define NOCOMM -#endif - -#include -#include \ No newline at end of file diff --git a/src/cascadia/Remoting/CommandlineArgs.h b/src/cascadia/Remoting/CommandlineArgs.h index 4d40c898a62..2f618e53efc 100644 --- a/src/cascadia/Remoting/CommandlineArgs.h +++ b/src/cascadia/Remoting/CommandlineArgs.h @@ -15,10 +15,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation CommandlineArgs(const winrt::array_view& args, winrt::hstring currentDirectory, - const uint32_t showWindowCommand) : + const uint32_t showWindowCommand, + winrt::hstring envString) : _args{ args.begin(), args.end() }, _cwd{ currentDirectory }, - _ShowWindowCommand{ showWindowCommand } + _ShowWindowCommand{ showWindowCommand }, + CurrentEnvironment{ envString } { } @@ -27,6 +29,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void Commandline(const winrt::array_view& value); winrt::com_array Commandline(); + til::property CurrentEnvironment; + WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); // SW_NORMAL is 1, 0 is SW_HIDE private: diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 25c2cd1f8d2..203119a70ce 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -50,7 +50,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _WindowName{ windowInfo.WindowName() }, _args{ command.Commandline() }, _CurrentDirectory{ command.CurrentDirectory() }, - _ShowWindowCommand{ command.ShowWindowCommand() } {}; + _ShowWindowCommand{ command.ShowWindowCommand() }, + _CurrentEnvironment{ command.CurrentEnvironment() } {}; WindowRequestedArgs(const winrt::hstring& window, const winrt::hstring& content, const Windows::Foundation::IReference& bounds) : _Id{ 0u }, @@ -68,6 +69,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation WINRT_PROPERTY(winrt::hstring, CurrentDirectory); WINRT_PROPERTY(winrt::hstring, Content); WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); + WINRT_PROPERTY(winrt::hstring, CurrentEnvironment); WINRT_PROPERTY(Windows::Foundation::IReference, InitialBounds); private: diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index cc2c45f5426..f57bd6b59b2 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -27,6 +27,7 @@ namespace Microsoft.Terminal.Remoting String[] Commandline { get; }; String CurrentDirectory { get; }; UInt32 ShowWindowCommand { get; }; + String CurrentEnvironment { get; }; String Content { get; }; Windows.Foundation.IReference InitialBounds { get; }; diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 64ff512a1b4..f02d22da9c4 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -7,11 +7,12 @@ namespace Microsoft.Terminal.Remoting runtimeclass CommandlineArgs { CommandlineArgs(); - CommandlineArgs(String[] args, String cwd, UInt32 showWindowCommand); + CommandlineArgs(String[] args, String cwd, UInt32 showWindowCommand, String env); String[] Commandline { get; set; }; String CurrentDirectory { get; }; UInt32 ShowWindowCommand { get; }; + String CurrentEnvironment { get; }; }; runtimeclass RenameRequestArgs diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 5572c6eee95..aa9f67b0b3f 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -349,7 +349,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // If the name wasn't specified, this will be an empty string. p->WindowName(args.WindowName()); - p->ExecuteCommandline(*winrt::make_self(args.Commandline(), args.CurrentDirectory(), args.ShowWindowCommand())); + p->ExecuteCommandline(*winrt::make_self(args.Commandline(), + args.CurrentDirectory(), + args.ShowWindowCommand(), + args.CurrentEnvironment())); _monarch.AddPeasant(*p); diff --git a/src/cascadia/Remoting/pch.h b/src/cascadia/Remoting/pch.h index 3e65fdf1038..4c4bcb1ac96 100644 --- a/src/cascadia/Remoting/pch.h +++ b/src/cascadia/Remoting/pch.h @@ -52,3 +52,4 @@ TRACELOGGING_DECLARE_PROVIDER(g_hRemotingProvider); #include "til.h" #include +#include diff --git a/src/cascadia/TerminalApp/AboutDialog.cpp b/src/cascadia/TerminalApp/AboutDialog.cpp index 3fc9715dfa2..c443b73e09f 100644 --- a/src/cascadia/TerminalApp/AboutDialog.cpp +++ b/src/cascadia/TerminalApp/AboutDialog.cpp @@ -86,14 +86,55 @@ namespace winrt::TerminalApp::implementation co_await wil::resume_foreground(strongThis->Dispatcher()); UpdatesAvailable(true); #else // release build, likely has a store context - if (auto storeContext{ winrt::Windows::Services::Store::StoreContext::GetDefault() }) + bool packageManagerAnswered{ false }; + + try + { + if (auto currentPackage{ winrt::Windows::ApplicationModel::Package::Current() }) + { + // We need to look up our package in the Package Manager; we cannot use Current + winrt::Windows::Management::Deployment::PackageManager pm; + if (auto lookedUpPackage{ pm.FindPackageForUser(winrt::hstring{}, currentPackage.Id().FullName()) }) + { + using winrt::Windows::ApplicationModel::PackageUpdateAvailability; + auto availabilityResult = co_await lookedUpPackage.CheckUpdateAvailabilityAsync(); + co_await wil::resume_foreground(strongThis->Dispatcher()); + auto availability = availabilityResult.Availability(); + switch (availability) + { + case PackageUpdateAvailability::Available: + case PackageUpdateAvailability::Required: + case PackageUpdateAvailability::NoUpdates: + UpdatesAvailable(availability != PackageUpdateAvailability::NoUpdates); + packageManagerAnswered = true; + break; + case PackageUpdateAvailability::Error: + case PackageUpdateAvailability::Unknown: + default: + // Do not set packageManagerAnswered, which will trigger the store check. + break; + } + } + } + } + catch (...) + { + } // Do nothing on failure + + if (!packageManagerAnswered) { - const auto updates = co_await storeContext.GetAppAndOptionalStorePackageUpdatesAsync(); - co_await wil::resume_foreground(strongThis->Dispatcher()); - const auto numUpdates = updates.Size(); - if (numUpdates > 0) + if (auto storeContext{ winrt::Windows::Services::Store::StoreContext::GetDefault() }) { - UpdatesAvailable(true); + const auto updates = co_await storeContext.GetAppAndOptionalStorePackageUpdatesAsync(); + co_await wil::resume_foreground(strongThis->Dispatcher()); + if (updates) + { + const auto numUpdates = updates.Size(); + if (numUpdates > 0) + { + UpdatesAvailable(true); + } + } } } #endif diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 220631df25f..23a421e7051 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1419,4 +1419,10 @@ namespace winrt::TerminalApp::implementation } args.Handled(true); } + void TerminalPage::_HandleOpenAbout(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + _ShowAboutDialog(); + args.Handled(true); + } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 2fa36a9711f..5060b4ff2f7 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -571,6 +571,11 @@ void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubc subcommand.appendCommandLineOption = subcommand.subcommand->add_flag("--appendCommandLine", _appendCommandLineOption, RS_A(L"CmdAppendCommandLineDesc")); + subcommand.inheritEnvOption = subcommand.subcommand->add_flag( + "--inheritEnvironment,!--reloadEnvironment", + _inheritEnvironment, + RS_A(L"CmdInheritEnvDesc")); + // Using positionals_at_end allows us to support "wt new-tab -d wsl -d Ubuntu" // without CLI11 thinking that we've specified -d twice. // There's an alternate construction where we make all subcommands "prefix commands", @@ -592,7 +597,8 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT { NewTerminalArgs args{}; - if (!_commandline.empty()) + const auto hasCommandline{ !_commandline.empty() }; + if (hasCommandline) { std::ostringstream cmdlineBuffer; @@ -662,6 +668,13 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT args.AppendCommandLine(_appendCommandLineOption); } + bool inheritEnv = hasCommandline; + if (*subcommand.inheritEnvOption) + { + inheritEnv = _inheritEnvironment; + } + args.ReloadEnvironmentVariables(!inheritEnv); + return args; } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 292a3500c42..9145697f453 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -68,6 +68,7 @@ class TerminalApp::AppCommandlineArgs final CLI::Option* suppressApplicationTitleOption; CLI::Option* colorSchemeOption; CLI::Option* appendCommandLineOption; + CLI::Option* inheritEnvOption; }; struct NewPaneSubcommand : public NewTerminalSubcommand @@ -100,6 +101,7 @@ class TerminalApp::AppCommandlineArgs final std::string _startingTabColor; std::string _startingColorScheme; bool _suppressApplicationTitle{ false }; + bool _inheritEnvironment{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl index 54442316b1c..7232de01e1c 100644 --- a/src/cascadia/TerminalApp/IPaneContent.idl +++ b/src/cascadia/TerminalApp/IPaneContent.idl @@ -32,6 +32,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler TaskbarProgressChanged; + event Windows.Foundation.TypedEventHandler ConnectionStateChanged; event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusRequested; }; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 975971834e5..d55611b33fd 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -962,6 +962,21 @@ Pane::PaneNeighborSearch Pane::_FindPaneAndNeighbor(const std::shared_ptr return { nullptr, nullptr, offset }; } +// Method Description: +// - Returns true if the connection state of this pane is closed. +// Arguments: +// - +// Return Value: +// - true if the connection state of this Pane is closed. +bool Pane::IsConnectionClosed() const +{ + if (const auto& control{ GetTerminalControl() }) + { + return control.ConnectionState() >= ConnectionState::Closed; + } + return false; +} + // Event Description: // - Called when our control gains focus. We'll use this to trigger our GotFocus // callback. The tab that's hosting us should have registered a callback which @@ -1098,7 +1113,7 @@ TermControl Pane::GetLastFocusedTerminalControl() // - // Return Value: // - nullptr if this Pane is a parent, otherwise the TermControl of this Pane. -TermControl Pane::GetTerminalControl() +TermControl Pane::GetTerminalControl() const { if (const auto& terminalPane{ _getTerminalContent() }) { @@ -2263,6 +2278,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // Move our control, guid, isDefTermSession into the first one. _firstChild = std::make_shared(_content); _content = nullptr; + _firstChild->_broadcastEnabled = _broadcastEnabled; } _splitState = actualSplitType; @@ -2949,6 +2965,12 @@ void Pane::EnableBroadcast(bool enabled) if (_IsLeaf()) { _broadcastEnabled = enabled; + if (const auto& termControl{ GetTerminalControl() }) + { + termControl.CursorVisibility(enabled ? + CursorDisplayState::Shown : + CursorDisplayState::Default); + } UpdateVisuals(); } else diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 428d0a55fbe..de9aad13616 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -73,8 +73,9 @@ class Pane : public std::enable_shared_from_this std::shared_ptr GetActivePane(); winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl(); - winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); + winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl() const; winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); + bool IsConnectionClosed() const; // Method Description: // - If this is a leaf pane, return its profile. diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index df28d046daf..3ad4bdb73b8 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -371,6 +371,10 @@ Open the tab with tabTitle overriding default title and suppressing title change messages from the application {Locked="\"tabTitle\""} + + Inherit the terminal's own environment variables when creating the new tab or pane, rather than creating a fresh environment block. This defaults to set when a "command" is passed. + {Locked="\"command\""} + Open the tab with the specified color scheme, instead of the profile's set "colorScheme" {Locked="\"colorScheme\""} @@ -878,7 +882,11 @@ Active pane moved to "{0}" tab in "{1}" window - {Locked="{0}"}{Locked="{1}"}This text is read out by screen readers upon a successful pane movement. {0} is the name of the tab the pane was moved to. {1} is the name of the window the pane was moved to. + {Locked="{0}"}{Locked="{1}"}This text is read out by screen readers upon a successful pane movement. {0} is the name of the tab the pane was moved to. {1} is the name of the window the pane was moved to. Replaced in 1.19 by TerminalPage_PaneMovedAnnouncement_ExistingWindow2 + + + Active pane moved to "{0}" window + {Locked="{0}"}This text is read out by screen readers upon a successful pane movement. {0} is the name of the window the pane was moved to. Active pane moved to new window @@ -891,4 +899,10 @@ If set, the command will be appended to the profile's default command instead of replacing it. + + Restart Connection + + + Restart the active pane connection + diff --git a/src/cascadia/TerminalApp/SettingsTab.cpp b/src/cascadia/TerminalApp/SettingsTab.cpp index 107bbb45732..1c9c4049f66 100644 --- a/src/cascadia/TerminalApp/SettingsTab.cpp +++ b/src/cascadia/TerminalApp/SettingsTab.cpp @@ -114,7 +114,7 @@ namespace winrt::TerminalApp::implementation // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... Icon(winrt::hstring{ glyph }); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(glyph)); + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(glyph, false)); } winrt::Windows::UI::Xaml::Media::Brush SettingsTab::_BackgroundBrush() diff --git a/src/cascadia/TerminalApp/TabHeaderControl.xaml b/src/cascadia/TerminalApp/TabHeaderControl.xaml index 5fde709ecbf..3c5c7f7f053 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.xaml +++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml @@ -43,6 +43,12 @@ FontSize="12" Glyph="" Visibility="{x:Bind TabStatus.IsReadOnlyActive, Mode=OneWay}" /> + UpdateIcon(profile.Icon()); + const auto theme = _settings.GlobalSettings().CurrentTheme(); + const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default; + newTabImpl->UpdateIcon(profile.Icon(), iconStyle); } } @@ -241,7 +243,9 @@ namespace winrt::TerminalApp::implementation { if (const auto profile = tab.GetFocusedProfile()) { - tab.UpdateIcon(profile.Icon()); + const auto theme = _settings.GlobalSettings().CurrentTheme(); + const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default; + tab.UpdateIcon(profile.Icon(), iconStyle); } } @@ -301,7 +305,12 @@ namespace winrt::TerminalApp::implementation // In the future, it may be preferable to just duplicate the // current control's live settings (which will include changes // made through VT). - _CreateNewTabFromPane(_MakePane(nullptr, tab, nullptr), tab.TabViewIndex() + 1); + uint32_t insertPosition = _tabs.Size(); + if (_settings.GlobalSettings().NewTabPosition() == NewTabPosition::AfterCurrentTab) + { + insertPosition = tab.TabViewIndex() + 1; + } + _CreateNewTabFromPane(_MakePane(nullptr, tab, nullptr), insertPosition); const auto runtimeTabText{ tab.GetTabText() }; if (!runtimeTabText.empty()) @@ -506,7 +515,7 @@ namespace winrt::TerminalApp::implementation // * B (tabIndex=1): We'll want to focus tab A (now in index 0) // * C (tabIndex=2): We'll want to focus tab B (now in index 1) // * D (tabIndex=3): We'll want to focus tab C (now in index 2) - const auto newSelectedIndex = std::clamp(tabIndex - 1, 0, _tabs.Size()); + const auto newSelectedIndex = std::clamp(tabIndex - 1, 0, _tabs.Size() - 1); // _UpdatedSelectedTab will do the work of setting up the new tab as // the focused one, and unfocusing all the others. auto newSelectedTab{ _tabs.GetAt(newSelectedIndex) }; @@ -673,7 +682,7 @@ namespace winrt::TerminalApp::implementation { uint32_t tabIndexFromControl{}; const auto items{ _tabView.TabItems() }; - if (items.IndexOf(tabViewItem, tabIndexFromControl)) + if (items.IndexOf(tabViewItem, tabIndexFromControl) && tabIndexFromControl < _tabs.Size()) { // If IndexOf returns true, we've actually got an index return _tabs.GetAt(tabIndexFromControl); @@ -1030,7 +1039,8 @@ namespace winrt::TerminalApp::implementation // - suggestedNewTabIndex: the new index of the tab, might get clamped to fit int the tabs row boundaries // Return Value: // - - void TerminalPage::_TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex) + void TerminalPage::_TryMoveTab(const uint32_t currentTabIndex, + const int32_t suggestedNewTabIndex) { auto newTabIndex = gsl::narrow_cast(std::clamp(suggestedNewTabIndex, 0, _tabs.Size() - 1)); if (currentTabIndex != newTabIndex) @@ -1072,16 +1082,21 @@ namespace winrt::TerminalApp::implementation if (from.has_value() && to.has_value() && to != from) { - auto& tabs{ _tabs }; - auto tab = tabs.GetAt(from.value()); - tabs.RemoveAt(from.value()); - tabs.InsertAt(to.value(), tab); - _UpdateTabIndices(); + try + { + auto& tabs{ _tabs }; + auto tab = tabs.GetAt(from.value()); + tabs.RemoveAt(from.value()); + tabs.InsertAt(to.value(), tab); + _UpdateTabIndices(); + } + CATCH_LOG(); } _rearranging = false; - if (to.has_value()) + if (to.has_value() && + *to < gsl::narrow_cast(TabRow().TabView().TabItems().Size())) { // Selecting the dropped tab TabRow().TabView().SelectedIndex(to.value()); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index d9e706a0d37..d2c129f072d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -556,7 +556,8 @@ namespace winrt::TerminalApp::implementation // - winrt::fire_and_forget TerminalPage::ProcessStartupActions(Windows::Foundation::Collections::IVector actions, const bool initial, - const winrt::hstring cwd) + const winrt::hstring cwd, + const winrt::hstring env) { auto weakThis{ get_weak() }; @@ -576,6 +577,12 @@ namespace winrt::TerminalApp::implementation _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd); }); + // Literally the same thing with env vars too + auto originalVirtualEnv{ _WindowProperties.VirtualEnvVars() }; + auto restoreEnv = wil::scope_exit([&originalVirtualEnv, this]() { + _WindowProperties.VirtualEnvVars(originalVirtualEnv); + }); + if (cwd.empty()) { // We didn't actually need to change the virtual CWD, so we don't @@ -587,6 +594,15 @@ namespace winrt::TerminalApp::implementation _WindowProperties.VirtualWorkingDirectory(cwd); } + if (env.empty()) + { + restoreEnv.release(); + } + else + { + _WindowProperties.VirtualEnvVars(env); + } + if (auto page{ weakThis.get() }) { for (const auto& action : actions) @@ -1213,6 +1229,8 @@ namespace winrt::TerminalApp::implementation auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(), L".", L"Azure", + false, + L"", nullptr, settings.InitialRows(), settings.InitialCols(), @@ -1253,6 +1271,8 @@ namespace winrt::TerminalApp::implementation auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), newWorkingDirectory, settings.StartingTitle(), + settings.ReloadEnvironmentVariables(), + _WindowProperties.VirtualEnvVars(), environment, settings.InitialRows(), settings.InitialCols(), @@ -1260,8 +1280,6 @@ namespace winrt::TerminalApp::implementation profile.Guid()); valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough())); - valueSet.Insert(L"reloadEnvironmentVariables", - Windows::Foundation::PropertyValue::CreateBoolean(_settings.GlobalSettings().ReloadEnvironmentVariables())); if (inheritCursor) { @@ -1665,9 +1683,19 @@ namespace winrt::TerminalApp::implementation { term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); } - - term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened }); - term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened }); + winrt::weak_ref weakTerm{ term }; + term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), false); + } + }); + term.SelectionContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); + } + }); } // Method Description: @@ -1711,6 +1739,11 @@ namespace winrt::TerminalApp::implementation hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); } + void TerminalPage::_RegisterPaneEvents(const TerminalApp::TerminalPaneContent& paneContent) + { + paneContent.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); + } + // Method Description: // - Helper to manually exit "zoom" when certain actions take place. // Anything that modifies the state of the pane tree should probably @@ -1955,6 +1988,11 @@ namespace winrt::TerminalApp::implementation _settings.GlobalSettings().ConfirmCloseAllTabs() && !_displayingCloseDialog) { + if (_newTabButton && _newTabButton.Flyout()) + { + _newTabButton.Flyout().Hide(); + } + _DismissTabContextMenus(); _displayingCloseDialog = true; auto warningResult = co_await _ShowCloseWarningDialog(); _displayingCloseDialog = false; @@ -2035,9 +2073,6 @@ namespace winrt::TerminalApp::implementation { if (const auto pane{ terminalTab->GetActivePane() }) { - // Get the tab title _before_ moving things around in case the tabIdx doesn't point to the right one after the move - const auto tabTitle = _tabs.GetAt(tabIdx).Title(); - auto startupActions = pane->BuildStartupActions(0, 1, true, true); _DetachPaneFromWindow(pane); _MoveContent(std::move(startupActions.args), windowId, tabIdx); @@ -2056,7 +2091,7 @@ namespace winrt::TerminalApp::implementation { autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - fmt::format(std::wstring_view{ RS_(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow") }, tabTitle, windowId), + fmt::format(std::wstring_view{ RS_(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow2") }, windowId), L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); } } @@ -2073,7 +2108,7 @@ namespace winrt::TerminalApp::implementation // Moving the pane from the current tab might close it, so get the next // tab before its index changes. - if (_tabs.Size() > tabIdx) + if (tabIdx < _tabs.Size()) { auto targetTab = _GetTerminalTabImpl(_tabs.GetAt(tabIdx)); // if the selected tab is not a host of terminals (e.g. settings) @@ -2353,7 +2388,17 @@ namespace winrt::TerminalApp::implementation } _UnZoomIfNeeded(); - activeTab->SplitPane(*realSplitType, splitSize, newPane); + auto [original, _] = activeTab->SplitPane(*realSplitType, splitSize, newPane); + + // When we split the pane, the Pane itself will create a _new_ Pane + // instance for the original content. We need to make sure we also + // re-add our event handler to that newly created pane. + // + // _MakePane will already call this for the newly created pane. + if (const auto& paneContent{ original->GetContent().try_as() }) + { + _RegisterPaneEvents(*paneContent); + } // After GH#6586, the control will no longer focus itself // automatically when it's finished being laid out. Manually focus @@ -2605,62 +2650,91 @@ namespace winrt::TerminalApp::implementation // - 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 TerminalPage::_PasteFromClipboardHandler(const IInspectable /*sender*/, - const PasteFromClipboardEventArgs eventArgs) + fire_and_forget TerminalPage::_PasteFromClipboardHandler(const IInspectable /*sender*/, const PasteFromClipboardEventArgs eventArgs) + try { - const auto data = Clipboard::GetContent(); + // The old Win32 clipboard API as used below is somewhere in the order of 300-1000x faster than + // the WinRT one on average, depending on CPU load. Don't use the WinRT clipboard API if you can. + const auto weakThis = get_weak(); + const auto dispatcher = Dispatcher(); + const auto globalSettings = _settings.GlobalSettings(); + winrt::hstring text; - // 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. + // GetClipboardData might block for up to 30s for delay-rendered contents. co_await winrt::resume_background(); - try { - hstring text = L""; - if (data.Contains(StandardDataFormats::Text())) - { - text = co_await data.GetTextAsync(); - } - // Windows Explorer's "Copy address" menu item stores a StorageItem in the clipboard, and no text. - else if (data.Contains(StandardDataFormats::StorageItems())) + // According to various reports on the internet, OpenClipboard might + // fail to acquire the internal lock, for instance due to rdpclip.exe. + for (int attempts = 1;;) { - auto items = co_await data.GetStorageItemsAsync(); - if (items.Size() > 0) + if (OpenClipboard(nullptr)) { - auto item = items.GetAt(0); - text = item.Path(); + break; } - } - if (_settings.GlobalSettings().TrimPaste()) - { - text = { Utils::TrimPaste(text) }; - if (text.empty()) + if (attempts > 5) { - // Text is all white space, nothing to paste co_return; } + + attempts++; + Sleep(10 * attempts); } - // If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste. - auto warnMultiLine = _settings.GlobalSettings().WarnAboutMultiLinePaste() && - !eventArgs.BracketedPasteEnabled(); - if (warnMultiLine) + const auto clipboardCleanup = wil::scope_exit([]() { + CloseClipboard(); + }); + + const auto data = GetClipboardData(CF_UNICODETEXT); + if (!data) { - const auto isNewLineLambda = [](auto c) { return c == L'\n' || c == L'\r'; }; - const auto hasNewLine = std::find_if(text.cbegin(), text.cend(), isNewLineLambda) != text.cend(); - warnMultiLine = hasNewLine; + co_return; } - constexpr const std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB - const auto warnLargeText = text.size() > minimumSizeForWarning && - _settings.GlobalSettings().WarnAboutLargePaste(); + const auto str = static_cast(GlobalLock(data)); + if (!str) + { + co_return; + } + + const auto dataCleanup = wil::scope_exit([&]() { + GlobalUnlock(data); + }); + + const auto maxLength = GlobalSize(data) / sizeof(wchar_t); + const auto length = wcsnlen(str, maxLength); + text = winrt::hstring{ str, gsl::narrow_cast(length) }; + } - if (warnMultiLine || warnLargeText) + if (globalSettings.TrimPaste()) + { + text = { Utils::TrimPaste(text) }; + if (text.empty()) { - co_await wil::resume_foreground(Dispatcher()); + // Text is all white space, nothing to paste + co_return; + } + } + // If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste. + auto warnMultiLine = globalSettings.WarnAboutMultiLinePaste() && !eventArgs.BracketedPasteEnabled(); + if (warnMultiLine) + { + const auto isNewLineLambda = [](auto c) { return c == L'\n' || c == L'\r'; }; + const auto hasNewLine = std::find_if(text.cbegin(), text.cend(), isNewLineLambda) != text.cend(); + warnMultiLine = hasNewLine; + } + + constexpr const std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB + const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); + + if (warnMultiLine || warnLargeText) + { + co_await wil::resume_foreground(dispatcher); + + if (const auto strongThis = weakThis.get()) + { // We have to initialize the dialog here to be able to change the text of the text block within it FindName(L"MultiLinePasteDialog").try_as(); ClipboardText().Text(text); @@ -2688,10 +2762,15 @@ namespace winrt::TerminalApp::implementation } } - eventArgs.HandleClipboardData(text); + co_await winrt::resume_background(); } - CATCH_LOG(); + + // This will end up calling ConptyConnection::WriteInput which calls WriteFile which may block for + // an indefinite amount of time. Avoid freezes and deadlocks by running this on a background thread. + assert(!dispatcher.HasThreadAccess()); + eventArgs.HandleClipboardData(std::move(text)); } + CATCH_LOG(); void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs) { @@ -3104,7 +3183,7 @@ namespace winrt::TerminalApp::implementation original->SetActive(); } - terminalPane.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); + _RegisterPaneEvents(terminalPane); return resultPane; } @@ -4706,6 +4785,21 @@ namespace winrt::TerminalApp::implementation // the settings, change active panes, etc. _activated = activated; _updateThemeColors(); + + if (const auto& tab{ _GetFocusedTabImpl() }) + { + if (tab->TabStatus().IsInputBroadcastActive()) + { + tab->GetRootPane()->WalkTree([activated](const auto& p) { + if (const auto& control{ p->GetTerminalControl() }) + { + control.CursorVisibility(activated ? + Microsoft::Terminal::Control::CursorDisplayState::Shown : + Microsoft::Terminal::Control::CursorDisplayState::Default); + } + }); + } + } } winrt::fire_and_forget TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, @@ -4788,26 +4882,14 @@ namespace winrt::TerminalApp::implementation characterSize.Height); } - void TerminalPage::_ContextMenuOpened(const IInspectable& sender, - const IInspectable& /*args*/) - { - _PopulateContextMenu(sender, false /*withSelection*/); - } - void TerminalPage::_SelectionMenuOpened(const IInspectable& sender, - const IInspectable& /*args*/) - { - _PopulateContextMenu(sender, true /*withSelection*/); - } - - void TerminalPage::_PopulateContextMenu(const IInspectable& sender, + void TerminalPage::_PopulateContextMenu(const TermControl& control, + const MUX::Controls::CommandBarFlyout& menu, const bool withSelection) { // withSelection can be used to add actions that only appear if there's - // selected text, like "search the web". In this initial draft, it's not - // actually augmented by the TerminalPage, so it's left commented out. + // selected text, like "search the web" - const auto& menu{ sender.try_as() }; - if (!menu) + if (!control || !menu) { return; } @@ -4857,6 +4939,11 @@ namespace winrt::TerminalApp::implementation makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }); } + if (control.ConnectionState() >= ConnectionState::Closed) + { + makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }); + } + if (withSelection) { makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index c9f60dd13f8..62ab115788d 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -154,7 +154,8 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget ProcessStartupActions(Windows::Foundation::Collections::IVector actions, const bool initial, - const winrt::hstring cwd = L""); + const winrt::hstring cwd = L"", + const winrt::hstring env = L""); TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; @@ -344,6 +345,7 @@ namespace winrt::TerminalApp::implementation void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); void _RegisterTabEvents(TerminalTab& hostingTab); + void _RegisterPaneEvents(const TerminalApp::TerminalPaneContent& paneContent); void _DismissTabContextMenus(); void _FocusCurrentTab(const bool focusAlways); @@ -535,9 +537,7 @@ namespace winrt::TerminalApp::implementation const std::optional& dragPoint = std::nullopt); void _sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); - void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args); - void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args); - void _PopulateContextMenu(const IInspectable& sender, const bool withSelection); + void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 4375c000738..76ee847fb80 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -52,6 +52,7 @@ namespace TerminalApp String WindowIdForDisplay { get; }; String VirtualWorkingDirectory { get; set; }; + String VirtualEnvVars { get; set; }; Boolean IsQuakeWindow(); }; diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index 949c5e32707..3863a377824 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -160,24 +160,45 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void TerminalPaneContent::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + winrt::fire_and_forget TerminalPaneContent::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args) { - const auto newConnectionState = _control.ConnectionState(); - const auto previousConnectionState = std::exchange(_connectionState, newConnectionState); + ConnectionStateChanged.raise(sender, args); + auto newConnectionState = ConnectionState::Closed; + if (const auto coreState = sender.try_as()) + { + newConnectionState = coreState.ConnectionState(); + } + const auto previousConnectionState = std::exchange(_connectionState, newConnectionState); if (newConnectionState < ConnectionState::Closed) { // Pane doesn't care if the connection isn't entering a terminal state. - return; + co_return; + } + + const auto weakThis = get_weak(); + co_await wil::resume_foreground(_control.Dispatcher()); + const auto strongThis = weakThis.get(); + if (!strongThis) + { + co_return; } + // It's possible that this event handler started being executed, scheduled + // on the UI thread, another child got created. So our control is + // actually no longer _our_ control, and instead could be a descendant. + // + // When the control's new Pane takes ownership of the control, the new + // parent will register its own event handler. That event handler will get + // fired after this handler returns, and will properly cleanup state. + if (previousConnectionState < ConnectionState::Connected && newConnectionState >= ConnectionState::Failed) { // A failure to complete the connection (before it has _connected_) is not covered by "closeOnExit". // This is to prevent a misconfiguration (closeOnExit: always, startingDirectory: garbage) resulting // in Terminal flashing open and immediately closed. - return; + co_return; } if (_profile) diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.h b/src/cascadia/TerminalApp/TerminalPaneContent.h index 3ff9f893f7d..953b3d8879d 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.h +++ b/src/cascadia/TerminalApp/TerminalPaneContent.h @@ -45,6 +45,7 @@ namespace winrt::TerminalApp::implementation til::typed_event<> TitleChanged; til::typed_event<> TabColorChanged; til::typed_event<> TaskbarProgressChanged; + til::typed_event<> ConnectionStateChanged; til::typed_event<> ReadOnlyChanged; til::typed_event<> FocusRequested; @@ -76,7 +77,7 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget _playBellSound(winrt::Windows::Foundation::Uri uri); - void _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); + winrt::fire_and_forget _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); void _ControlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e); void _ControlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index ed748befbbc..843e8672f75 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -15,6 +15,7 @@ using namespace winrt; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::TerminalConnection; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::UI::Xaml::Controls; using namespace winrt::Windows::System; @@ -35,6 +36,7 @@ namespace winrt::TerminalApp::implementation _activePane = nullptr; _closePaneMenuItem.Visibility(WUX::Visibility::Collapsed); + _restartConnectionMenuItem.Visibility(WUX::Visibility::Collapsed); auto firstId = _nextPaneId; @@ -283,17 +285,17 @@ namespace winrt::TerminalApp::implementation // - iconPath: The new path string to use as the IconPath for our TabViewItem // Return Value: // - - void TerminalTab::UpdateIcon(const winrt::hstring iconPath) + void TerminalTab::UpdateIcon(const winrt::hstring iconPath, const winrt::Microsoft::Terminal::Settings::Model::IconStyle iconStyle) { ASSERT_UI_THREAD(); - // Don't reload our icon if it hasn't changed. - if (iconPath == _lastIconPath) + // Don't reload our icon and iconStyle hasn't changed. + if (iconPath == _lastIconPath && iconStyle == _lastIconStyle) { return; } - _lastIconPath = iconPath; + _lastIconStyle = iconStyle; // If the icon is currently hidden, just return here (but only after setting _lastIconPath to the new path // for when we show the icon again) @@ -302,9 +304,18 @@ namespace winrt::TerminalApp::implementation return; } - // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... - Icon(_lastIconPath); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); + if (iconStyle == IconStyle::Hidden) + { + // The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX... + Icon({}); + TabViewItem().IconSource(IconSource{ nullptr }); + } + else + { + Icon(_lastIconPath); + bool isMonochrome = iconStyle == IconStyle::Monochrome; + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath, isMonochrome)); + } } // Method Description: @@ -326,7 +337,7 @@ namespace winrt::TerminalApp::implementation else { Icon(_lastIconPath); - TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath)); + TabViewItem().IconSource(IconPathConverter::IconSourceMUX(_lastIconPath, _lastIconStyle == IconStyle::Monochrome)); } _iconHidden = hide; } @@ -500,9 +511,9 @@ namespace winrt::TerminalApp::implementation // could itself be a parent pane/the root node of a tree of panes // Return Value: // - - void TerminalTab::SplitPane(SplitDirection splitType, - const float splitSize, - std::shared_ptr pane) + std::pair, std::shared_ptr> TerminalTab::SplitPane(SplitDirection splitType, + const float splitSize, + std::shared_ptr pane) { ASSERT_UI_THREAD(); @@ -550,6 +561,8 @@ namespace winrt::TerminalApp::implementation // possible that the focus events won't propagate immediately. Updating // the focus here will give the same effect though. _UpdateActivePane(newPane); + + return { original, newPane }; } // Method Description: @@ -957,6 +970,16 @@ namespace winrt::TerminalApp::implementation } }); + events.ConnectionStateChanged = content.ConnectionStateChanged( + winrt::auto_revoke, + [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { + co_await wil::resume_foreground(dispatcher); + if (auto tab{ weakThis.get() }) + { + tab->_UpdateConnectionClosedState(); + } + }); + events.ReadOnlyChanged = content.ReadOnlyChanged( winrt::auto_revoke, [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget { @@ -1069,6 +1092,40 @@ namespace winrt::TerminalApp::implementation _TaskbarProgressChangedHandlers(nullptr, nullptr); } + // Method Description: + // - Set an indicator on the tab if any pane is in a closed connection state. + // - Show/hide the Restart Connection context menu entry depending on active pane's state. + // Arguments: + // - + // Return Value: + // - + void TerminalTab::_UpdateConnectionClosedState() + { + ASSERT_UI_THREAD(); + + if (_rootPane) + { + const bool isClosed = _rootPane->WalkTree([&](const auto& p) { + return p->IsConnectionClosed(); + }); + + _tabStatus.IsConnectionClosed(isClosed); + } + + if (_activePane) + { + _restartConnectionMenuItem.Visibility(_activePane->IsConnectionClosed() ? + WUX::Visibility::Visible : + WUX::Visibility::Collapsed); + } + } + + void TerminalTab::_RestartActivePaneConnection() + { + ActionAndArgs restartConnection{ ShortcutAction::RestartConnection, nullptr }; + _dispatch.DoAction(*this, restartConnection); + } + // Method Description: // - Mark the given pane as the active pane in this tab. All other panes // will be marked as inactive. We'll also update our own UI state to @@ -1087,6 +1144,7 @@ namespace winrt::TerminalApp::implementation // Update our own title text to match the newly-active pane. UpdateTitle(); _UpdateProgressState(); + _UpdateConnectionClosedState(); // We need to move the pane to the top of our mru list // If its already somewhere in the list, remove it first @@ -1380,6 +1438,28 @@ namespace winrt::TerminalApp::implementation Automation::AutomationProperties::SetHelpText(findMenuItem, findToolTip); } + Controls::MenuFlyoutItem restartConnectionMenuItem = _restartConnectionMenuItem; + { + // "Restart Connection" + Controls::FontIcon restartConnectionSymbol; + restartConnectionSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + restartConnectionSymbol.Glyph(L"\xE72C"); + + restartConnectionMenuItem.Click([weakThis](auto&&, auto&&) { + if (auto tab{ weakThis.get() }) + { + tab->_RestartActivePaneConnection(); + } + }); + restartConnectionMenuItem.Text(RS_(L"RestartConnectionText")); + restartConnectionMenuItem.Icon(restartConnectionSymbol); + + const auto restartConnectionToolTip = RS_(L"RestartConnectionToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(restartConnectionMenuItem, box_value(restartConnectionToolTip)); + Automation::AutomationProperties::SetHelpText(restartConnectionMenuItem, restartConnectionToolTip); + } + // Build the menu Controls::MenuFlyout contextMenuFlyout; Controls::MenuFlyoutSeparator menuSeparator; @@ -1390,6 +1470,7 @@ namespace winrt::TerminalApp::implementation contextMenuFlyout.Items().Append(moveTabToNewWindowMenuItem); contextMenuFlyout.Items().Append(exportTabMenuItem); contextMenuFlyout.Items().Append(findMenuItem); + contextMenuFlyout.Items().Append(restartConnectionMenuItem); contextMenuFlyout.Items().Append(menuSeparator); // GH#5750 - When the context menu is dismissed with ESC, toss the focus diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index ab24b8a6c46..fbbd48f1158 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -37,12 +37,12 @@ namespace winrt::TerminalApp::implementation void AttachColorPicker(winrt::TerminalApp::ColorPickupFlyout& colorPicker); - void SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, - const float splitSize, - std::shared_ptr newPane); + std::pair, std::shared_ptr> SplitPane(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, + const float splitSize, + std::shared_ptr newPane); void ToggleSplitOrientation(); - void UpdateIcon(const winrt::hstring iconPath); + void UpdateIcon(const winrt::hstring iconPath, const winrt::Microsoft::Terminal::Settings::Model::IconStyle iconStyle); void HideIcon(const bool hide); void ShowBellIndicator(const bool show); @@ -110,7 +110,9 @@ namespace winrt::TerminalApp::implementation std::shared_ptr _zoomedPane{ nullptr }; Windows::UI::Xaml::Controls::MenuFlyoutItem _closePaneMenuItem; + Windows::UI::Xaml::Controls::MenuFlyoutItem _restartConnectionMenuItem; + winrt::Microsoft::Terminal::Settings::Model::IconStyle _lastIconStyle; winrt::hstring _lastIconPath{}; std::optional _runtimeTabColor{}; winrt::TerminalApp::TabHeaderControl _headerControl{}; @@ -127,6 +129,7 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::IPaneContent::TitleChanged_revoker TitleChanged; winrt::TerminalApp::IPaneContent::TabColorChanged_revoker TabColorChanged; winrt::TerminalApp::IPaneContent::TaskbarProgressChanged_revoker TaskbarProgressChanged; + winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged; winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged; winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested; @@ -174,6 +177,9 @@ namespace winrt::TerminalApp::implementation void _UpdateProgressState(); + void _UpdateConnectionClosedState(); + void _RestartActivePaneConnection(); + void _DuplicateTab(); virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override; diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.h b/src/cascadia/TerminalApp/TerminalTabStatus.h index 95080935a38..838a909df22 100644 --- a/src/cascadia/TerminalApp/TerminalTabStatus.h +++ b/src/cascadia/TerminalApp/TerminalTabStatus.h @@ -12,6 +12,7 @@ namespace winrt::TerminalApp::implementation TerminalTabStatus() = default; WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(bool, IsConnectionClosed, _PropertyChangedHandlers); WINRT_OBSERVABLE_PROPERTY(bool, IsPaneZoomed, _PropertyChangedHandlers); WINRT_OBSERVABLE_PROPERTY(bool, IsProgressRingActive, _PropertyChangedHandlers); WINRT_OBSERVABLE_PROPERTY(bool, IsProgressRingIndeterminate, _PropertyChangedHandlers); diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.idl b/src/cascadia/TerminalApp/TerminalTabStatus.idl index 57f4e05fa1a..5dc69ffe59b 100644 --- a/src/cascadia/TerminalApp/TerminalTabStatus.idl +++ b/src/cascadia/TerminalApp/TerminalTabStatus.idl @@ -7,6 +7,7 @@ namespace TerminalApp { TerminalTabStatus(); + Boolean IsConnectionClosed { get; set; }; Boolean IsPaneZoomed { get; set; }; Boolean IsProgressRingActive { get; set; }; Boolean IsProgressRingIndeterminate { get; set; }; diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 7b60611db9b..66db1eb70b3 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -204,39 +204,19 @@ namespace winrt::TerminalApp::implementation // Pay attention, that even if some command line arguments were parsed (like launch mode), // we will not use the startup actions from settings. // While this simplifies the logic, we might want to reconsider this behavior in the future. - if (!_hasCommandLineArguments && _gotSettingsStartupActions) + // + // Obviously, don't use the `startupActions` from the settings in the + // case of a tear-out / reattach. GH#16050 + if (!_hasCommandLineArguments && + _initialContentArgs.empty() && + _gotSettingsStartupActions) { _root->SetStartupActions(_settingsStartupArgs); } _root->SetSettings(_settings, false); // We're on our UI thread right now, so this is safe _root->Loaded({ get_weak(), &TerminalWindow::_OnLoaded }); - - _root->Initialized([this](auto&&, auto&&) { - // GH#288 - When we finish initialization, if the user wanted us - // launched _fullscreen_, toggle fullscreen mode. This will make sure - // that the window size is _first_ set up as something sensible, so - // leaving fullscreen returns to a reasonable size. - const auto launchMode = this->GetLaunchMode(); - if (_WindowProperties->IsQuakeWindow() || WI_IsFlagSet(launchMode, LaunchMode::FocusMode)) - { - _root->SetFocusMode(true); - } - - // The IslandWindow handles (creating) the maximized state - // we just want to record it here on the page as well. - if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) - { - _root->Maximized(true); - } - - if (WI_IsFlagSet(launchMode, LaunchMode::FullscreenMode) && !_WindowProperties->IsQuakeWindow()) - { - _root->SetFullscreen(true); - } - - AppLogic::Current()->NotifyRootInitialized(); - }); + _root->Initialized({ get_weak(), &TerminalWindow::_pageInitialized }); _root->Create(); AppLogic::Current()->SettingsChanged({ get_weak(), &TerminalWindow::UpdateSettingsHandler }); @@ -255,6 +235,34 @@ namespace winrt::TerminalApp::implementation TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); } + + void TerminalWindow::_pageInitialized(const IInspectable&, const IInspectable&) + { + // GH#288 - When we finish initialization, if the user wanted us + // launched _fullscreen_, toggle fullscreen mode. This will make sure + // that the window size is _first_ set up as something sensible, so + // leaving fullscreen returns to a reasonable size. + const auto launchMode = this->GetLaunchMode(); + if (_WindowProperties->IsQuakeWindow() || WI_IsFlagSet(launchMode, LaunchMode::FocusMode)) + { + _root->SetFocusMode(true); + } + + // The IslandWindow handles (creating) the maximized state + // we just want to record it here on the page as well. + if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) + { + _root->Maximized(true); + } + + if (WI_IsFlagSet(launchMode, LaunchMode::FullscreenMode) && !_WindowProperties->IsQuakeWindow()) + { + _root->SetFullscreen(true); + } + + AppLogic::Current()->NotifyRootInitialized(); + } + void TerminalWindow::Quit() { if (_root) @@ -775,6 +783,10 @@ namespace winrt::TerminalApp::implementation { _ShowLoadWarningsDialog(args.Warnings()); } + else if (args.Result() == S_OK) + { + DismissDialog(); + } _RefreshThemeRoutine(); } } @@ -1024,9 +1036,12 @@ namespace winrt::TerminalApp::implementation // Return Value: // - the result of the first command who's parsing returned a non-zero code, // or 0. (see TerminalWindow::_ParseArgs) - int32_t TerminalWindow::SetStartupCommandline(array_view args, winrt::hstring cwd) + int32_t TerminalWindow::SetStartupCommandline(array_view args, + winrt::hstring cwd, + winrt::hstring env) { _WindowProperties->SetInitialCwd(std::move(cwd)); + _WindowProperties->VirtualEnvVars(std::move(env)); // This is called in AppHost::ctor(), before we've created the window // (or called TerminalWindow::Initialize) @@ -1081,7 +1096,8 @@ namespace winrt::TerminalApp::implementation // - the result of the first command who's parsing returned a non-zero code, // or 0. (see TerminalWindow::_ParseArgs) int32_t TerminalWindow::ExecuteCommandline(array_view args, - const winrt::hstring& cwd) + const winrt::hstring& cwd, + const winrt::hstring& env) { ::TerminalApp::AppCommandlineArgs appArgs; auto result = appArgs.ParseArgs(args); @@ -1089,7 +1105,7 @@ namespace winrt::TerminalApp::implementation { auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); - _root->ProcessStartupActions(actions, false, cwd); + _root->ProcessStartupActions(actions, false, cwd, env); if (appArgs.IsHandoffListener()) { diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index a26b7c98bfb..8eff22ef455 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -56,6 +56,8 @@ namespace winrt::TerminalApp::implementation // Used for setting the initial CWD, before we have XAML set up for property change notifications. void SetInitialCwd(winrt::hstring cwd) { _VirtualWorkingDirectory = std::move(cwd); }; + til::property VirtualEnvVars; + private: winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; @@ -77,9 +79,9 @@ namespace winrt::TerminalApp::implementation bool HasCommandlineArguments() const noexcept; - int32_t SetStartupCommandline(array_view actions, winrt::hstring cwd); + int32_t SetStartupCommandline(array_view actions, winrt::hstring cwd, winrt::hstring env); void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference& contentBounds); - int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); + int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd, const winrt::hstring& env); void SetSettingsStartupArgs(const std::vector& actions); winrt::hstring ParseCommandlineMessage(); bool ShouldExitEarly(); @@ -200,6 +202,7 @@ namespace winrt::TerminalApp::implementation void _RefreshThemeRoutine(); void _OnLoaded(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _pageInitialized(const IInspectable& sender, const IInspectable& eventArgs); void _OpenSettingsUI(); winrt::Windows::Foundation::Collections::IVector _contentStringToActions(const winrt::hstring& content, diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index 2ca487be544..a41cfb96c8e 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -53,9 +53,9 @@ namespace TerminalApp Boolean HasCommandlineArguments(); - Int32 SetStartupCommandline(String[] commands, String cwd); + Int32 SetStartupCommandline(String[] commands, String cwd, String env); void SetStartupContent(String json, Windows.Foundation.IReference bounds); - Int32 ExecuteCommandline(String[] commands, String cwd); + Int32 ExecuteCommandline(String[] commands, String cwd, String env); String ParseCommandlineMessage { get; }; Boolean ShouldExitEarly { get; }; diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index 86c32776e8d..c4a75ff4350 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index cecb5c7d759..e086c9b2cfc 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include "CTerminalHandoff.h" @@ -90,14 +89,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation }); // Populate the environment map with the current environment. - if (_reloadEnvironmentVariables) - { - environment.regenerate(); - } - else - { - environment = til::env::from_current_environment(); - } + environment = _initialEnv; { // Convert connection Guid to string and ignore the enclosing '{}'. @@ -235,7 +227,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation Windows::Foundation::Collections::ValueSet ConptyConnection::CreateSettings(const winrt::hstring& cmdline, const winrt::hstring& startingDirectory, const winrt::hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, + bool reloadEnvironmentVariables, + const winrt::hstring& initialEnvironment, + const Windows::Foundation::Collections::IMapView& environmentOverrides, uint32_t rows, uint32_t columns, const winrt::guid& guid, @@ -246,23 +240,35 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation vs.Insert(L"commandline", Windows::Foundation::PropertyValue::CreateString(cmdline)); vs.Insert(L"startingDirectory", Windows::Foundation::PropertyValue::CreateString(startingDirectory)); vs.Insert(L"startingTitle", Windows::Foundation::PropertyValue::CreateString(startingTitle)); + vs.Insert(L"reloadEnvironmentVariables", Windows::Foundation::PropertyValue::CreateBoolean(reloadEnvironmentVariables)); vs.Insert(L"initialRows", Windows::Foundation::PropertyValue::CreateUInt32(rows)); vs.Insert(L"initialCols", Windows::Foundation::PropertyValue::CreateUInt32(columns)); vs.Insert(L"guid", Windows::Foundation::PropertyValue::CreateGuid(guid)); vs.Insert(L"profileGuid", Windows::Foundation::PropertyValue::CreateGuid(profileGuid)); - if (environment) + if (environmentOverrides) { Windows::Foundation::Collections::ValueSet env{}; - for (const auto& [k, v] : environment) + for (const auto& [k, v] : environmentOverrides) { env.Insert(k, Windows::Foundation::PropertyValue::CreateString(v)); } vs.Insert(L"environment", env); } + + if (!initialEnvironment.empty()) + { + vs.Insert(L"initialEnvironment", Windows::Foundation::PropertyValue::CreateString(initialEnvironment)); + } return vs; } + template + T unbox_prop_or(const Windows::Foundation::Collections::ValueSet& blob, std::wstring_view key, T defaultValue) + { + return winrt::unbox_value_or(blob.TryLookup(key).try_as(), defaultValue); + } + void ConptyConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings) { if (settings) @@ -271,26 +277,47 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // auto bad = unbox_value_or(settings.TryLookup(L"foo").try_as(), nullptr); // It'll just return null - _commandline = winrt::unbox_value_or(settings.TryLookup(L"commandline").try_as(), _commandline); - _startingDirectory = winrt::unbox_value_or(settings.TryLookup(L"startingDirectory").try_as(), _startingDirectory); - _startingTitle = winrt::unbox_value_or(settings.TryLookup(L"startingTitle").try_as(), _startingTitle); - _rows = winrt::unbox_value_or(settings.TryLookup(L"initialRows").try_as(), _rows); - _cols = winrt::unbox_value_or(settings.TryLookup(L"initialCols").try_as(), _cols); - _guid = winrt::unbox_value_or(settings.TryLookup(L"guid").try_as(), _guid); + _commandline = unbox_prop_or(settings, L"commandline", _commandline); + _startingDirectory = unbox_prop_or(settings, L"startingDirectory", _startingDirectory); + _startingTitle = unbox_prop_or(settings, L"startingTitle", _startingTitle); + _rows = unbox_prop_or(settings, L"initialRows", _rows); + _cols = unbox_prop_or(settings, L"initialCols", _cols); + _guid = unbox_prop_or(settings, L"guid", _guid); _environment = settings.TryLookup(L"environment").try_as(); if constexpr (Feature_VtPassthroughMode::IsEnabled()) { - _passthroughMode = winrt::unbox_value_or(settings.TryLookup(L"passthroughMode").try_as(), _passthroughMode); + _passthroughMode = unbox_prop_or(settings, L"passthroughMode", _passthroughMode); } - _inheritCursor = winrt::unbox_value_or(settings.TryLookup(L"inheritCursor").try_as(), _inheritCursor); - _reloadEnvironmentVariables = winrt::unbox_value_or(settings.TryLookup(L"reloadEnvironmentVariables").try_as(), - _reloadEnvironmentVariables); - _profileGuid = winrt::unbox_value_or(settings.TryLookup(L"profileGuid").try_as(), _profileGuid); - } + _inheritCursor = unbox_prop_or(settings, L"inheritCursor", _inheritCursor); + _profileGuid = unbox_prop_or(settings, L"profileGuid", _profileGuid); - if (_guid == guid{}) - { - _guid = Utils::CreateGuid(); + const auto& initialEnvironment{ unbox_prop_or(settings, L"initialEnvironment", L"") }; + + const bool reloadEnvironmentVariables = unbox_prop_or(settings, L"reloadEnvironmentVariables", false); + + if (reloadEnvironmentVariables) + { + _initialEnv.regenerate(); + } + else + { + if (!initialEnvironment.empty()) + { + _initialEnv = til::env{ initialEnvironment.c_str() }; + } + else + { + // If we were not explicitly provided an "initial" env block to + // treat as our original one, then just use our actual current + // env block. + _initialEnv = til::env::from_current_environment(); + } + } + + if (_guid == guid{}) + { + _guid = Utils::CreateGuid(); + } } } @@ -734,5 +761,4 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation co_await winrt::resume_background(); // move to background connection.reset(); // explicitly destruct } - } diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index 747434f6dba..0105104c130 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -7,6 +7,7 @@ #include "ConnectionStateHolder.h" #include "ITerminalHandoff.h" +#include namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { @@ -49,7 +50,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation static Windows::Foundation::Collections::ValueSet CreateSettings(const winrt::hstring& cmdline, const winrt::hstring& startingDirectory, const winrt::hstring& startingTitle, - const Windows::Foundation::Collections::IMapView& environment, + bool reloadEnvironmentVariables, + const winrt::hstring& initialEnvironment, + const Windows::Foundation::Collections::IMapView& environmentOverrides, uint32_t rows, uint32_t columns, const winrt::guid& guid, @@ -91,7 +94,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation std::array _buffer{}; bool _passthroughMode{}; bool _inheritCursor{ false }; - bool _reloadEnvironmentVariables{}; + + til::env _initialEnv{}; guid _profileGuid{}; struct StartupInfoFromDefTerm diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index c9928d5c425..5cc0fb6f1d8 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -28,7 +28,9 @@ namespace Microsoft.Terminal.TerminalConnection static Windows.Foundation.Collections.ValueSet CreateSettings(String cmdline, String startingDirectory, String startingTitle, - IMapView environment, + Boolean reloadEnvironmentVariables, + String initialEnvironment, + IMapView environmentOverrides, UInt32 rows, UInt32 columns, Guid guid, diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 52dc7d4087f..a7609d29747 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -86,6 +86,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _settings = winrt::make_self(settings, unfocusedAppearance); _terminal = std::make_shared<::Microsoft::Terminal::Core::Terminal>(); + const auto lock = _terminal->LockForWriting(); _setupDispatcherAndCallbacks(); @@ -197,7 +198,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation [weakTerminal = std::weak_ptr{ _terminal }]() { if (const auto t = weakTerminal.lock()) { - auto lock = t->LockForWriting(); + const auto lock = t->LockForWriting(); t->UpdatePatternsUnderLock(); } }); @@ -282,6 +283,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Get our current size in rows/cols, and hook them up to // this connection too. { + const auto lock = _terminal->LockForReading(); const auto vp = _terminal->GetViewport(); const auto width = vp.Width(); const auto height = vp.Height(); @@ -318,7 +320,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _compositionScale = compositionScale; { // scope for terminalLock - auto terminalLock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); if (_initializedTerminal.load(std::memory_order_relaxed)) { @@ -427,6 +429,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (_initializedTerminal.load(std::memory_order_relaxed)) { + const auto lock = _terminal->LockForWriting(); _renderer->EnablePainting(); } } @@ -489,6 +492,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _handleControlC(); } + const auto lock = _terminal->LockForWriting(); return _terminal->SendCharEvent(ch, scanCode, modifiers); } @@ -517,18 +521,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation // modifier key. We'll wait for a real keystroke to dismiss the // GH #7395 - don't update selection when taking PrintScreen // selection. - return HasSelection() && ::Microsoft::Terminal::Core::Terminal::IsInputKey(vkey); + return _terminal->IsSelectionActive() && ::Microsoft::Terminal::Core::Terminal::IsInputKey(vkey); } bool ControlCore::TryMarkModeKeybinding(const WORD vkey, const ::Microsoft::Terminal::Core::ControlKeyStates mods) { + auto lock = _terminal->LockForWriting(); + if (_shouldTryUpdateSelection(vkey) && _terminal->SelectionMode() == ::Terminal::SelectionInteractionMode::Mark) { if (vkey == 'A' && !mods.IsAltPressed() && !mods.IsShiftPressed() && mods.IsCtrlPressed()) { // Ctrl + A --> Select all - auto lock = _terminal->LockForWriting(); _terminal->SelectAll(); _updateSelectionUI(); return true; @@ -536,7 +541,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation else if (vkey == VK_TAB && !mods.IsAltPressed() && !mods.IsCtrlPressed() && _settings->DetectURLs()) { // [Shift +] Tab --> next/previous hyperlink - auto lock = _terminal->LockForWriting(); const auto direction = mods.IsShiftPressed() ? ::Terminal::SearchDirection::Backward : ::Terminal::SearchDirection::Forward; _terminal->SelectHyperlink(direction); _updateSelectionUI(); @@ -545,14 +549,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation else if (vkey == VK_RETURN && mods.IsCtrlPressed() && !mods.IsAltPressed() && !mods.IsShiftPressed()) { // Ctrl + Enter --> Open URL - auto lock = _terminal->LockForReading(); if (const auto uri = _terminal->GetHyperlinkAtBufferPosition(_terminal->GetSelectionAnchor()); !uri.empty()) { + lock.unlock(); _OpenHyperlinkHandlers(*this, winrt::make(winrt::hstring{ uri })); } else { const auto selectedText = _terminal->GetTextBuffer().GetPlainText(_terminal->GetSelectionAnchor(), _terminal->GetSelectionEnd()); + lock.unlock(); _OpenHyperlinkHandlers(*this, winrt::make(winrt::hstring{ selectedText })); } return true; @@ -560,7 +565,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed()) { // [Shift +] Enter --> copy text - // Don't lock here! CopySelectionToClipboard already locks for you! CopySelectionToClipboard(mods.IsShiftPressed(), nullptr); _terminal->ClearSelection(); _updateSelectionUI(); @@ -575,7 +579,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation else if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(mods, vkey) }) { // try to update the selection - auto lock = _terminal->LockForWriting(); _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, mods); _updateSelectionUI(); return true; @@ -599,6 +602,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const ControlKeyStates modifiers, const bool keyDown) { + const auto lock = _terminal->LockForWriting(); + // Update the selection, if it's present // GH#8522, GH#3758 - Only modify the selection on key _down_. If we // modify on key up, then there's chance that we'll immediately dismiss @@ -608,7 +613,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // try to update the selection if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) }) { - auto lock = _terminal->LockForWriting(); _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers); _updateSelectionUI(); return true; @@ -646,17 +650,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation const short wheelDelta, const TerminalInput::MouseButtonState state) { + const auto lock = _terminal->LockForWriting(); return _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state); } void ControlCore::UserScrollViewport(const int viewTop) { - // Clear the regex pattern tree so the renderer does not try to render them while scrolling - _terminal->ClearPatternTree(); - - // This is a scroll event that wasn't initiated by the terminal - // itself - it was initiated by the mouse wheel, or the scrollbar. - _terminal->UserScrollViewport(viewTop); + { + // This is a scroll event that wasn't initiated by the terminal + // itself - it was initiated by the mouse wheel, or the scrollbar. + const auto lock = _terminal->LockForWriting(); + _terminal->UserScrollViewport(viewTop); + } const auto shared = _shared.lock_shared(); if (shared->updatePatternLocations) @@ -696,6 +701,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // cleartype -> grayscale if the BG is transparent / acrylic. if (_renderEngine) { + const auto lock = _terminal->LockForWriting(); _renderEngine->EnableTransparentBackground(_isBackgroundTransparent()); _renderer->NotifyPaintFrame(); } @@ -707,7 +713,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::ToggleShaderEffects() { const auto path = _settings->PixelShaderPath(); - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); // Originally, this action could be used to enable the retro effects // even when they're set to `false` in the settings. If the user didn't // specify a custom pixel shader, manually enable the legacy retro @@ -755,7 +761,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation decltype(_terminal->GetHyperlinkIntervalFromViewportPosition({})) newInterval{ std::nullopt }; if (terminalPosition.has_value()) { - auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. + const auto lock = _terminal->LockForReading(); newId = _terminal->GetHyperlinkIdAtViewportPosition(*terminalPosition); newInterval = _terminal->GetHyperlinkIntervalFromViewportPosition(*terminalPosition); } @@ -770,7 +776,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // wouldn't be able to ask us about the hyperlink text/position // without deadlocking us. { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _lastHoveredId = newId; _lastHoveredInterval = newInterval; @@ -785,16 +791,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::hstring ControlCore::GetHyperlink(const Core::Point pos) const { - // Lock for the duration of our reads. - auto lock = _terminal->LockForReading(); + const auto lock = _terminal->LockForReading(); return winrt::hstring{ _terminal->GetHyperlinkAtViewportPosition(til::point{ pos }) }; } winrt::hstring ControlCore::HoveredUriText() const { - auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. if (_lastHoveredCell.has_value()) { + const auto lock = _terminal->LockForReading(); auto uri{ _terminal->GetHyperlinkAtViewportPosition(*_lastHoveredCell) }; uri.resize(std::min(1024u, uri.size())); // Truncate for display return winrt::hstring{ uri }; @@ -814,7 +819,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _settings = winrt::make_self(settings, newAppearance); - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _cellWidth = CSSLengthPercentage::FromString(_settings->CellWidth().c_str()); _cellHeight = CSSLengthPercentage::FromString(_settings->CellHeight().c_str()); @@ -856,7 +861,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal. void ControlCore::ApplyAppearance(const bool& focused) { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); const auto& newAppearance{ focused ? _settings->FocusedAppearance() : _settings->UnfocusedAppearance() }; // Update the terminal core with its new Core settings _terminal->UpdateAppearance(*newAppearance); @@ -1101,7 +1106,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _panelHeight = height; _compositionScale = scale; - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); if (scaleChanged) { // _updateFont relies on the new _compositionScale set above @@ -1112,7 +1117,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::SetSelectionAnchor(const til::point position) { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _terminal->SetSelectionAnchor(position); } @@ -1123,7 +1128,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // to throw it all in a struct and pass it along. Control::SelectionData ControlCore::SelectionInfo() const { - auto lock = _terminal->LockForReading(); + const auto lock = _terminal->LockForReading(); Control::SelectionData info; const auto start{ _terminal->SelectionStartForRendering() }; @@ -1146,15 +1151,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - position: the point in terminal coordinates (in cells, not pixels) void ControlCore::SetEndSelectionPoint(const til::point position) { + const auto lock = _terminal->LockForWriting(); + if (!_terminal->IsSelectionActive()) { return; } - // Have to take the lock because the renderer will not draw correctly if - // you move its endpoints while it is generating a frame. - auto lock = _terminal->LockForWriting(); - til::point terminalPosition{ std::clamp(position.x, 0, _terminal->GetViewport().Width() - 1), std::clamp(position.y, 0, _terminal->GetViewport().Height() - 1) @@ -1182,6 +1185,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ControlCore::CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference& formats) { + const auto lock = _terminal->LockForWriting(); + // no selection --> nothing to copy if (!_terminal->IsSelectionActive()) { @@ -1231,21 +1236,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::SelectAll() { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _terminal->SelectAll(); _updateSelectionUI(); } void ControlCore::ClearSelection() { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _terminal->ClearSelection(); _updateSelectionUI(); } bool ControlCore::ToggleBlockSelection() { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); if (_terminal->IsSelectionActive()) { _terminal->SetBlockSelection(!_terminal->IsBlockSelection()); @@ -1260,7 +1265,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::ToggleMarkMode() { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _terminal->ToggleMarkMode(); _updateSelectionUI(); } @@ -1272,6 +1277,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ControlCore::SwitchSelectionEndpoint() { + const auto lock = _terminal->LockForWriting(); if (_terminal->IsSelectionActive()) { _terminal->SwitchSelectionEndpoint(); @@ -1283,6 +1289,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ControlCore::ExpandSelectionToWord() { + const auto lock = _terminal->LockForWriting(); if (_terminal->IsSelectionActive()) { _terminal->ExpandSelectionToWord(); @@ -1297,6 +1304,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // before sending it over the terminal's connection. void ControlCore::PasteText(const winrt::hstring& hstr) { + const auto lock = _terminal->LockForWriting(); _terminal->WritePastedText(hstr); _terminal->ClearSelection(); _updateSelectionUI(); @@ -1346,21 +1354,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring ControlCore::Title() { + const auto lock = _terminal->LockForReading(); return hstring{ _terminal->GetConsoleTitle() }; } hstring ControlCore::WorkingDirectory() const { + const auto lock = _terminal->LockForReading(); return hstring{ _terminal->GetWorkingDirectory() }; } bool ControlCore::BracketedPasteEnabled() const noexcept { + const auto lock = _terminal->LockForReading(); return _terminal->IsXtermBracketedPasteModeEnabled(); } Windows::Foundation::IReference ControlCore::TabColor() noexcept { + const auto lock = _terminal->LockForReading(); auto coreColor = _terminal->GetTabColor(); return coreColor.has_value() ? Windows::Foundation::IReference{ static_cast(coreColor.value()) } : nullptr; @@ -1368,11 +1380,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::color ControlCore::ForegroundColor() const { + const auto lock = _terminal->LockForReading(); return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultForeground); } til::color ControlCore::BackgroundColor() const { + const auto lock = _terminal->LockForReading(); return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultBackground); } @@ -1382,6 +1396,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The taskbar state of this control const size_t ControlCore::TaskbarState() const noexcept { + const auto lock = _terminal->LockForReading(); return _terminal->GetTaskbarState(); } @@ -1391,11 +1406,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The taskbar progress of this control const size_t ControlCore::TaskbarProgress() const noexcept { + const auto lock = _terminal->LockForReading(); return _terminal->GetTaskbarProgress(); } int ControlCore::ScrollOffset() { + const auto lock = _terminal->LockForReading(); return _terminal->GetScrollOffset(); } @@ -1406,6 +1423,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The height of the terminal in lines of text int ControlCore::ViewHeight() const { + const auto lock = _terminal->LockForReading(); return _terminal->GetViewport().Height(); } @@ -1416,6 +1434,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The height of the terminal in lines of text int ControlCore::BufferHeight() const { + const auto lock = _terminal->LockForReading(); return _terminal->GetBufferHeight(); } @@ -1463,12 +1482,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - // Clear the regex pattern tree so the renderer does not try to render them while scrolling - // We're **NOT** taking the lock here unlike _scrollbarChangeHandler because - // we are already under lock (since this usually happens as a result of writing). - // TODO GH#9617: refine locking around pattern tree - _terminal->ClearPatternTree(); - // Start the throttled update of our scrollbar. auto update{ winrt::make(viewTop, viewHeight, @@ -1527,6 +1540,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ControlCore::HasSelection() const { + const auto lock = _terminal->LockForReading(); return _terminal->IsSelectionActive(); } @@ -1538,6 +1552,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::Collections::IVector ControlCore::SelectedText(bool trimTrailingWhitespace) const { // RetrieveSelectedTextFromBuffer will lock while it's reading + const auto lock = _terminal->LockForReading(); const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text }; auto result = winrt::single_threaded_vector(); @@ -1565,11 +1580,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void ControlCore::Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive) { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive)) { - _searcher.MovePastCurrentSelection(); + _searcher.MoveToCurrentSelection(); + _cachedSearchResultRows = {}; } else { @@ -1600,12 +1616,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() { - auto lock = _terminal->LockForWriting(); - if (_searcher.ResetIfStale(*GetRenderData())) + const auto lock = _terminal->LockForReading(); + + if (!_cachedSearchResultRows) { auto results = std::vector(); - auto lastRow = til::CoordTypeMin; + for (const auto& match : _searcher.Results()) { const auto row{ match.start.y }; @@ -1615,6 +1632,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation lastRow = row; } } + _cachedSearchResultRows = winrt::single_threaded_vector(std::move(results)); } @@ -1690,7 +1708,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::BlinkAttributeTick() { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); auto& renderSettings = _terminal->GetRenderSettings(); renderSettings.ToggleBlinkRendition(*_renderer); @@ -1698,13 +1716,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::BlinkCursor() { - if (!_terminal->IsCursorBlinkingAllowed() && - _terminal->IsCursorVisible()) - { - return; - } - // SetCursorOn will take the write lock for you. - _terminal->SetCursorOn(!_terminal->IsCursorOn()); + const auto lock = _terminal->LockForWriting(); + _terminal->BlinkCursor(); } bool ControlCore::CursorOn() const @@ -1714,22 +1727,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::CursorOn(const bool isCursorOn) { + const auto lock = _terminal->LockForWriting(); _terminal->SetCursorOn(isCursorOn); } void ControlCore::ResumeRendering() { + const auto lock = _terminal->LockForWriting(); _renderer->ResetErrorStateAndResume(); } bool ControlCore::IsVtMouseModeEnabled() const { - return _terminal != nullptr && _terminal->IsTrackingMouseInput(); + const auto lock = _terminal->LockForWriting(); + return _terminal->IsTrackingMouseInput(); } bool ControlCore::ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const { - return _terminal != nullptr && _terminal->ShouldSendAlternateScroll(uiButton, delta); + const auto lock = _terminal->LockForWriting(); + return _terminal->ShouldSendAlternateScroll(uiButton, delta); } Core::Point ControlCore::CursorPosition() const @@ -1740,7 +1757,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return { 0, 0 }; } - auto lock = _terminal->LockForReading(); + const auto lock = _terminal->LockForReading(); return _terminal->GetViewportRelativeCursorPosition().to_core_point(); } @@ -1756,7 +1773,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool isOnOriginalPosition, bool& selectionNeedsToBeCopied) { - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); // handle ALT key _terminal->SetBlockSelection(altEnabled); @@ -1782,14 +1799,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // the selection (we need to reset selection on double-click or // triple-click, so it captures the word or the line, rather than // extending the selection) - if (HasSelection() && (!shiftEnabled || isOnOriginalPosition)) + if (_terminal->IsSelectionActive() && (!shiftEnabled || isOnOriginalPosition)) { // Reset the selection _terminal->ClearSelection(); selectionNeedsToBeCopied = false; // there's no selection, so there's nothing to update } - if (shiftEnabled && HasSelection()) + if (shiftEnabled && _terminal->IsSelectionActive()) { // If shift is pressed and there is a selection we extend it using // the selection mode (expand the "end" selection point) @@ -1878,6 +1895,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Updates the renderer's representation of the selection as well as the selection marker overlay in TermControl void ControlCore::_updateSelectionUI() { + const auto lock = _terminal->LockForWriting(); _renderer->TriggerSelection(); // only show the markers if we're doing a keyboard selection or in mark mode const bool showMarkers{ _terminal->SelectionMode() >= ::Microsoft::Terminal::Core::Terminal::SelectionInteractionMode::Keyboard }; @@ -1887,10 +1905,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) { // _renderer will always exist since it's introduced in the ctor + const auto lock = _terminal->LockForWriting(); _renderer->AddRenderEngine(pEngine); } void ControlCore::DetachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) { + const auto lock = _terminal->LockForWriting(); _renderer->RemoveRenderEngine(pEngine); } @@ -1918,7 +1938,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation { try { - _terminal->Write(hstr); + { + const auto lock = _terminal->LockForWriting(); + _terminal->Write(hstr); + } // Start the throttled update of where our hyperlinks are. const auto shared = _shared.lock_shared(); @@ -1959,6 +1982,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (clearType == Control::ClearBufferType::Scrollback || clearType == Control::ClearBufferType::All) { + const auto lock = _terminal->LockForWriting(); _terminal->EraseScrollback(); } @@ -1976,7 +2000,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring ControlCore::ReadEntireBuffer() const { - auto terminalLock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); const auto& textBuffer = _terminal->GetTextBuffer(); @@ -2004,7 +2028,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Get all of our recent commands. This will only really work if the user has enabled shell integration. Control::CommandHistoryContext ControlCore::CommandHistory() const { - auto terminalLock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); const auto& textBuffer = _terminal->GetTextBuffer(); std::vector commands; @@ -2089,6 +2113,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else { + const auto lock = _terminal->LockForReading(); s = _terminal->GetColorScheme(); } @@ -2112,8 +2137,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void ControlCore::ColorScheme(const Core::Scheme& scheme) { - auto l{ _terminal->LockForWriting() }; - _settings->FocusedAppearance()->DefaultForeground(scheme.Foreground); _settings->FocusedAppearance()->DefaultBackground(scheme.Background); _settings->FocusedAppearance()->CursorColor(scheme.CursorColor); @@ -2136,10 +2159,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _settings->FocusedAppearance()->SetColorTableEntry(14, scheme.BrightCyan); _settings->FocusedAppearance()->SetColorTableEntry(15, scheme.BrightWhite); + const auto lock = _terminal->LockForWriting(); _terminal->ApplyScheme(scheme); - _renderEngine->SetSelectionBackground(til::color{ _settings->SelectionBackground() }); - _renderer->TriggerRedrawAll(true); } @@ -2211,6 +2233,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // focused. const auto previous = std::exchange(_isReadOnly, false); const auto restore = wil::scope_exit([&]() { _isReadOnly = previous; }); + const auto lock = _terminal->LockForWriting(); _terminal->FocusChanged(focused); } @@ -2245,31 +2268,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::Collections::IVector ControlCore::ScrollMarks() const { - auto internalMarks{ _terminal->GetScrollMarks() }; - auto v = winrt::single_threaded_observable_vector(); - for (const auto& mark : internalMarks) - { - Control::ScrollMark m{}; + const auto lock = _terminal->LockForReading(); + const auto& internalMarks = _terminal->GetScrollMarks(); + std::vector v; - // sneaky: always evaluate the color of the mark to a real value - // before shoving it into the optional. If the mark doesn't have a - // specific color set, we'll use the value from the color table - // that's appropriate for this category of mark. If we do have a - // color set, then great we'll use that. The TermControl can then - // always use the value in the Mark regardless if it was actually - // set or not. - m.Color = OptionalFromColor(_terminal->GetColorForMark(mark)); - m.Start = mark.start.to_core_point(); - m.End = mark.end.to_core_point(); + v.reserve(internalMarks.size()); - v.Append(m); + for (const auto& mark : internalMarks) + { + v.emplace_back( + mark.start.to_core_point(), + mark.end.to_core_point(), + OptionalFromColor(_terminal->GetColorForMark(mark))); } - return v; + return winrt::single_threaded_vector(std::move(v)); } void ControlCore::AddMark(const Control::ScrollMark& mark) { + const auto lock = _terminal->LockForReading(); ::ScrollMark m{}; if (mark.Color.HasValue) @@ -2277,7 +2295,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation m.color = til::color{ mark.Color.Color }; } - if (HasSelection()) + if (_terminal->IsSelectionActive()) { m.start = til::point{ _terminal->GetSelectionAnchor() }; m.end = til::point{ _terminal->GetSelectionEnd() }; @@ -2291,11 +2309,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation // set the start & end to the cursor position. _terminal->AddMark(m, m.start, m.end, true); } - void ControlCore::ClearMark() { _terminal->ClearMark(); } - void ControlCore::ClearAllMarks() { _terminal->ClearAllMarks(); } + + void ControlCore::ClearMark() + { + const auto lock = _terminal->LockForWriting(); + _terminal->ClearMark(); + } + + void ControlCore::ClearAllMarks() + { + const auto lock = _terminal->LockForWriting(); + _terminal->ClearAllMarks(); + } void ControlCore::ScrollToMark(const Control::ScrollToMarkDirection& direction) { + const auto lock = _terminal->LockForWriting(); const auto currentOffset = ScrollOffset(); const auto& marks{ _terminal->GetScrollMarks() }; @@ -2402,15 +2431,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto bufferSize{ _terminal->GetTextBuffer().GetSize() }; bufferSize.DecrementInBounds(s.end); - auto lock = _terminal->LockForWriting(); _terminal->SelectNewRegion(s.start, s.end); _renderer->TriggerSelection(); } void ControlCore::SelectCommand(const bool goUp) { - const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) : - _terminal->GetTextBuffer().GetCursor().GetPosition(); + const auto lock = _terminal->LockForWriting(); + + const til::point start = _terminal->IsSelectionActive() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) : + _terminal->GetTextBuffer().GetCursor().GetPosition(); std::optional<::ScrollMark> nearest{ std::nullopt }; const auto& marks{ _terminal->GetScrollMarks() }; @@ -2449,8 +2479,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::SelectOutput(const bool goUp) { - const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) : - _terminal->GetTextBuffer().GetCursor().GetPosition(); + const auto lock = _terminal->LockForWriting(); + + const til::point start = _terminal->IsSelectionActive() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) : + _terminal->GetTextBuffer().GetCursor().GetPosition(); std::optional<::ScrollMark> nearest{ std::nullopt }; const auto& marks{ _terminal->GetScrollMarks() }; @@ -2483,7 +2515,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode) { - if (HasSelection()) + const auto lock = _terminal->LockForWriting(); + + if (_terminal->IsSelectionActive()) { const auto pForeground = winrt::get_self(fg); const auto pBackground = winrt::get_self(bg); @@ -2521,6 +2555,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // viewportRelativeCharacterPosition is relative to the current // viewport, so adjust for that: + const auto lock = _terminal->LockForReading(); _contextMenuBufferPosition = _terminal->GetViewport().Origin() + viewportRelativeCharacterPosition; } @@ -2529,6 +2564,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool (*filter)(const ::ScrollMark&), til::point_span (*getSpan)(const ::ScrollMark&)) { + const auto lock = _terminal->LockForWriting(); + // Do nothing if the caller didn't give us a way to get the span to select for this mark. if (!getSpan) { @@ -2575,6 +2612,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const til::point& pos, bool (*filter)(const ::ScrollMark&)) { + const auto lock = _terminal->LockForWriting(); + // Don't show this if the click was on the selection if (_terminal->IsSelectionActive() && _terminal->GetSelectionAnchor() <= pos && diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index d0b74edfaa2..a97a700162a 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -224,22 +224,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Initiate a paste operation. void ControlInteractivity::RequestPasteTextFromClipboard() { - // attach ControlInteractivity::_sendPastedTextToConnection() as the - // clipboardDataHandler. This is called when the clipboard data is - // loaded. - auto clipboardDataHandler = std::bind(&ControlInteractivity::_sendPastedTextToConnection, this, std::placeholders::_1); - auto pasteArgs = winrt::make_self(clipboardDataHandler, _core->BracketedPasteEnabled()); + auto args = winrt::make( + [core = _core](const winrt::hstring& wstr) { + core->PasteText(wstr); + }, + _core->BracketedPasteEnabled()); // send paste event up to TermApp - _PasteFromClipboardHandlers(*this, *pasteArgs); - } - - // Method Description: - // - Pre-process text pasted (presumably from the clipboard) - // before sending it over the terminal's connection. - void ControlInteractivity::_sendPastedTextToConnection(std::wstring_view wstr) - { - _core->PasteText(winrt::hstring{ wstr }); + _PasteFromClipboardHandlers(*this, std::move(args)); } void ControlInteractivity::PointerPressed(Control::MouseButtonState buttonState, diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index dee2fe9beed..cafd8972c63 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -155,7 +155,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers); bool _shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta); - void _sendPastedTextToConnection(std::wstring_view wstr); til::point _getTerminalPosition(const til::point pixelPosition); bool _sendMouseEventHelper(const til::point terminalPosition, diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 12632871fdb..1c135cdf4f6 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -86,7 +86,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation struct PasteFromClipboardEventArgs : public PasteFromClipboardEventArgsT { public: - PasteFromClipboardEventArgs(std::function clipboardDataHandler, bool bracketedPasteEnabled) : + PasteFromClipboardEventArgs(std::function clipboardDataHandler, bool bracketedPasteEnabled) : m_clipboardDataHandler(clipboardDataHandler), _BracketedPasteEnabled{ bracketedPasteEnabled } {} @@ -98,7 +98,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(bool, BracketedPasteEnabled, false); private: - std::function m_clipboardDataHandler; + std::function m_clipboardDataHandler; }; struct OpenHyperlinkEventArgs : public OpenHyperlinkEventArgsT diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/TerminalControl/HwndTerminal.cpp similarity index 85% rename from src/cascadia/PublicTerminalCore/HwndTerminal.cpp rename to src/cascadia/TerminalControl/HwndTerminal.cpp index ad4e753c9a6..a13ac5b3eda 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/TerminalControl/HwndTerminal.cpp @@ -43,13 +43,13 @@ try return DefWindowProc(hwnd, WM_NCCREATE, wParam, lParam); } #pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast - auto terminal = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + const auto publicTerminal = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - if (terminal) + if (publicTerminal) { if (_IsMouseMessage(uMsg)) { - if (terminal->_CanSendVTMouseInput() && terminal->_SendMouseEvent(uMsg, wParam, lParam)) + if (publicTerminal->_CanSendVTMouseInput() && publicTerminal->_SendMouseEvent(uMsg, wParam, lParam)) { // GH#6401: Capturing the mouse ensures that we get drag/release events // even if the user moves outside the window. @@ -81,14 +81,14 @@ try case WM_GETOBJECT: if (lParam == UiaRootObjectId) { - return UiaReturnRawElementProvider(hwnd, wParam, lParam, terminal->_GetUiaProvider()); + return UiaReturnRawElementProvider(hwnd, wParam, lParam, publicTerminal->_GetUiaProvider()); } break; case WM_LBUTTONDOWN: - LOG_IF_FAILED(terminal->_StartSelection(lParam)); + LOG_IF_FAILED(publicTerminal->_StartSelection(lParam)); return 0; case WM_LBUTTONUP: - terminal->_singleClickTouchdownPos = std::nullopt; + publicTerminal->_singleClickTouchdownPos = std::nullopt; [[fallthrough]]; case WM_MBUTTONUP: case WM_RBUTTONUP: @@ -97,30 +97,31 @@ try case WM_MOUSEMOVE: if (WI_IsFlagSet(wParam, MK_LBUTTON)) { - LOG_IF_FAILED(terminal->_MoveSelection(lParam)); + LOG_IF_FAILED(publicTerminal->_MoveSelection(lParam)); return 0; } break; case WM_RBUTTONDOWN: - if (const auto& termCore{ terminal->_terminal }; termCore && termCore->IsSelectionActive()) + if (publicTerminal->_terminal && publicTerminal->_terminal->IsSelectionActive()) { try { - const auto bufferData = termCore->RetrieveSelectedTextFromBuffer(false); - LOG_IF_FAILED(terminal->_CopyTextToSystemClipboard(bufferData, true)); - TerminalClearSelection(terminal); + const auto lock = publicTerminal->_terminal->LockForWriting(); + const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false); + LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData, true)); + publicTerminal->_ClearSelection(); } CATCH_LOG(); } else { - terminal->_PasteTextFromClipboard(); + publicTerminal->_PasteTextFromClipboard(); } return 0; case WM_DESTROY: // Release Terminal's hwnd so Teardown doesn't try to destroy it again - terminal->_hwnd.release(); - terminal->Teardown(); + publicTerminal->_hwnd.release(); + publicTerminal->Teardown(); return 0; default: break; @@ -195,6 +196,8 @@ HwndTerminal::~HwndTerminal() HRESULT HwndTerminal::Initialize() { _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); + const auto lock = _terminal->LockForWriting(); + auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); auto* const localPointerToThread = renderThread.get(); auto& renderSettings = _terminal->GetRenderSettings(); @@ -304,7 +307,6 @@ void HwndTerminal::_UpdateFont(int newDpi) return; } _currentDpi = newDpi; - auto lock = _terminal->LockForWriting(); // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. @@ -323,10 +325,10 @@ IRawElementProviderSimple* HwndTerminal::_GetUiaProvider() noexcept { return nullptr; } - auto lock = _terminal->LockForWriting(); LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize(&_uiaProvider, this->GetRenderData(), this)); _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(_uiaProvider.Get()); LOG_IF_FAILED(_uiaEngine->Enable()); + const auto lock = _terminal->LockForWriting(); _renderer->AddRenderEngine(_uiaEngine.get()); } catch (...) @@ -344,7 +346,7 @@ HRESULT HwndTerminal::Refresh(const til::size windowSize, _Out_ til::size* dimen RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal); RETURN_HR_IF_NULL(E_INVALIDARG, dimensions); - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); _terminal->ClearSelection(); @@ -381,37 +383,45 @@ void HwndTerminal::SendOutput(std::wstring_view data) { return; } + const auto lock = _terminal->LockForWriting(); _terminal->Write(data); } HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal) { - auto _terminal = std::make_unique(parentHwnd); - RETURN_IF_FAILED(_terminal->Initialize()); + auto publicTerminal = std::make_unique(parentHwnd); + + RETURN_IF_FAILED(publicTerminal->Initialize()); - *hwnd = _terminal->GetHwnd(); - *terminal = _terminal.release(); + *hwnd = publicTerminal->GetHwnd(); + *terminal = publicTerminal.release(); return S_OK; } void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int)) +try { - auto publicTerminal = static_cast(terminal); + const auto publicTerminal = static_cast(terminal); publicTerminal->RegisterScrollCallback(callback); } +CATCH_LOG() void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*)) +try { const auto publicTerminal = static_cast(terminal); publicTerminal->RegisterWriteCallback(callback); } +CATCH_LOG() void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data) +try { const auto publicTerminal = static_cast(terminal); publicTerminal->SendOutput(data); } +CATCH_LOG() /// /// Triggers a terminal resize using the new width and height in pixel. @@ -446,13 +456,18 @@ HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ til::CoordType /// Out parameter with the new size of the renderer. /// HRESULT of the attempted resize. HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ til::size dimensionsInCharacters, _Out_ til::size* dimensionsInPixels) +try { RETURN_HR_IF_NULL(E_INVALIDARG, dimensionsInPixels); const auto publicTerminal = static_cast(terminal); - const auto viewInCharacters = Viewport::FromDimensions(dimensionsInCharacters); - const auto viewInPixels = publicTerminal->_renderEngine->GetViewportInPixels(viewInCharacters); + Viewport viewInPixels; + { + const auto viewInCharacters = Viewport::FromDimensions(dimensionsInCharacters); + const auto lock = publicTerminal->_terminal->LockForReading(); + viewInPixels = publicTerminal->_renderEngine->GetViewportInPixels(viewInCharacters); + } dimensionsInPixels->width = viewInPixels.Width(); dimensionsInPixels->height = viewInPixels.Height(); @@ -461,6 +476,7 @@ HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ ti return TerminalTriggerResize(terminal, viewInPixels.Width(), viewInPixels.Height(), &unused); } +CATCH_RETURN() /// /// Calculates the amount of rows and columns that fit in the provided width and height. @@ -471,10 +487,12 @@ HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ ti /// Out parameter containing the columns and rows that fit the new size. /// HRESULT of the calculation. HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ til::CoordType width, _In_ til::CoordType height, _Out_ til::size* dimensions) +try { const auto publicTerminal = static_cast(terminal); const auto viewInPixels = Viewport::FromDimensions({ width, height }); + const auto lock = publicTerminal->_terminal->LockForReading(); const auto viewInCharacters = publicTerminal->_renderEngine->GetViewportInCharacters(viewInPixels); dimensions->width = viewInCharacters.Width(); @@ -482,20 +500,27 @@ HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ til::CoordTyp return S_OK; } +CATCH_RETURN() void _stdcall TerminalDpiChanged(void* terminal, int newDpi) +try { const auto publicTerminal = static_cast(terminal); + const auto lock = publicTerminal->_terminal->LockForWriting(); publicTerminal->_UpdateFont(newDpi); } +CATCH_LOG() void _stdcall TerminalUserScroll(void* terminal, int viewTop) +try { if (const auto publicTerminal = static_cast(terminal); publicTerminal && publicTerminal->_terminal) { + const auto lock = publicTerminal->_terminal->LockForWriting(); publicTerminal->_terminal->UserScrollViewport(viewTop); } } +CATCH_LOG() const unsigned int HwndTerminal::_NumberOfClicks(til::point point, std::chrono::steady_clock::time_point timestamp) noexcept { @@ -522,7 +547,7 @@ try GET_Y_LPARAM(lParam), }; - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); const auto altPressed = GetKeyState(VK_MENU) < 0; const til::size fontSize{ this->_actualFont.GetSize() }; @@ -566,7 +591,7 @@ try GET_Y_LPARAM(lParam), }; - auto lock = _terminal->LockForWriting(); + const auto lock = _terminal->LockForWriting(); const til::size fontSize{ this->_actualFont.GetSize() }; RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.area() == 0); // either dimension = 0, area == 0 @@ -596,45 +621,57 @@ try } CATCH_RETURN(); -void HwndTerminal::_ClearSelection() noexcept -try +void HwndTerminal::_ClearSelection() { if (!_terminal) { return; } - auto lock{ _terminal->LockForWriting() }; _terminal->ClearSelection(); _renderer->TriggerSelection(); } -CATCH_LOG(); void _stdcall TerminalClearSelection(void* terminal) +try { - auto publicTerminal = static_cast(terminal); + const auto publicTerminal = static_cast(terminal); + const auto lock = publicTerminal->_terminal->LockForWriting(); publicTerminal->_ClearSelection(); } +CATCH_LOG() bool _stdcall TerminalIsSelectionActive(void* terminal) +try { if (const auto publicTerminal = static_cast(terminal); publicTerminal && publicTerminal->_terminal) { + const auto lock = publicTerminal->_terminal->LockForReading(); return publicTerminal->_terminal->IsSelectionActive(); } return false; } +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + return false; +} // Returns the selected text in the terminal. const wchar_t* _stdcall TerminalGetSelection(void* terminal) +try { - auto publicTerminal = static_cast(terminal); + const auto publicTerminal = static_cast(terminal); if (!publicTerminal || !publicTerminal->_terminal) { return nullptr; } - const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false); - publicTerminal->_ClearSelection(); + TextBuffer::TextAndColor bufferData; + { + const auto lock = publicTerminal->_terminal->LockForWriting(); + bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false); + publicTerminal->_ClearSelection(); + } // convert text: vector --> string std::wstring selectedText; @@ -646,6 +683,11 @@ const wchar_t* _stdcall TerminalGetSelection(void* terminal) auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str()); return returnText.release(); } +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + return nullptr; +} static ControlKeyStates getControlKeyState() noexcept { @@ -683,6 +725,7 @@ bool HwndTerminal::_CanSendVTMouseInput() const noexcept { // Only allow the transit of mouse events if shift isn't pressed. const auto shiftPressed = GetKeyState(VK_SHIFT) < 0; + const auto lock = _terminal->LockForReading(); return !shiftPressed && _focused && _terminal && _terminal->IsTrackingMouseInput(); } @@ -715,6 +758,7 @@ try WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed) }; + const auto lock = _terminal->LockForWriting(); return _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta, state); } catch (...) @@ -740,6 +784,7 @@ try { _uiaProvider->RecordKeyEvent(vkey); } + const auto lock = _terminal->LockForWriting(); _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown); } CATCH_LOG(); @@ -751,7 +796,10 @@ try { return; } - else if (_terminal->IsSelectionActive()) + + const auto lock = _terminal->LockForWriting(); + + if (_terminal->IsSelectionActive()) { _ClearSelection(); if (ch == UNICODE_ESC) @@ -778,16 +826,20 @@ try CATCH_LOG(); void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown) +try { const auto publicTerminal = static_cast(terminal); publicTerminal->_SendKeyEvent(vkey, scanCode, flags, keyDown); } +CATCH_LOG() void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode, WORD flags) +try { const auto publicTerminal = static_cast(terminal); publicTerminal->_SendCharEvent(ch, scanCode, flags); } +CATCH_LOG() void _stdcall DestroyTerminal(void* terminal) { @@ -803,8 +855,9 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font { return; } + { - auto lock = publicTerminal->_terminal->LockForWriting(); + const auto lock = publicTerminal->_terminal->LockForWriting(); auto& renderSettings = publicTerminal->_terminal->GetRenderSettings(); renderSettings.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, theme.DefaultForeground); @@ -818,12 +871,12 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font // It's using gsl::at to check the index is in bounds, but the analyzer still calls this array-to-pointer-decay [[gsl::suppress(bounds .3)]] renderSettings.SetColorTableEntry(tableIndex, gsl::at(theme.ColorTable, tableIndex)); } - } - publicTerminal->_terminal->SetCursorStyle(static_cast(theme.CursorStyle)); + publicTerminal->_terminal->SetCursorStyle(static_cast(theme.CursorStyle)); - publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, static_cast(fontSize), CP_UTF8 }; - publicTerminal->_UpdateFont(newDpi); + publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, static_cast(fontSize), CP_UTF8 }; + publicTerminal->_UpdateFont(newDpi); + } // When the font changes the terminal dimensions need to be recalculated since the available row and column // space will have changed. @@ -836,29 +889,35 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font } void _stdcall TerminalBlinkCursor(void* terminal) +try { const auto publicTerminal = static_cast(terminal); - if (!publicTerminal || !publicTerminal->_terminal || (!publicTerminal->_terminal->IsCursorBlinkingAllowed() && publicTerminal->_terminal->IsCursorVisible())) + if (!publicTerminal || !publicTerminal->_terminal) { return; } - publicTerminal->_terminal->SetCursorOn(!publicTerminal->_terminal->IsCursorOn()); + const auto lock = publicTerminal->_terminal->LockForWriting(); + publicTerminal->_terminal->BlinkCursor(); } +CATCH_LOG() void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible) +try { const auto publicTerminal = static_cast(terminal); if (!publicTerminal || !publicTerminal->_terminal) { return; } + const auto lock = publicTerminal->_terminal->LockForWriting(); publicTerminal->_terminal->SetCursorOn(visible); } +CATCH_LOG() void __stdcall TerminalSetFocus(void* terminal) { - auto publicTerminal = static_cast(terminal); + const auto publicTerminal = static_cast(terminal); publicTerminal->_focused = true; if (auto uiaEngine = publicTerminal->_uiaEngine.get()) { @@ -868,7 +927,7 @@ void __stdcall TerminalSetFocus(void* terminal) void __stdcall TerminalKillFocus(void* terminal) { - auto publicTerminal = static_cast(terminal); + const auto publicTerminal = static_cast(terminal); publicTerminal->_focused = false; if (auto uiaEngine = publicTerminal->_uiaEngine.get()) { @@ -923,7 +982,11 @@ try { const auto& fontData = _actualFont; const int iFontHeightPoints = fontData.GetUnscaledSize().height; // this renderer uses points already - const auto bgColor = _terminal->GetAttributeColors({}).second; + COLORREF bgColor; + { + const auto lock = _terminal->LockForReading(); + bgColor = _terminal->GetAttributeColors({}).second; + } auto HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor); _CopyToSystemClipboard(HTMLToPlaceOnClip, L"HTML Format"); @@ -993,30 +1056,16 @@ void HwndTerminal::_PasteTextFromClipboard() noexcept return; } - auto pwstr = static_cast(GlobalLock(ClipboardDataHandle)); - - _StringPaste(pwstr); + if (const auto pwstr = static_cast(GlobalLock(ClipboardDataHandle))) + { + _WriteTextToConnection(pwstr); + } GlobalUnlock(ClipboardDataHandle); CloseClipboard(); } -void HwndTerminal::_StringPaste(const wchar_t* const pData) noexcept -{ - if (pData == nullptr) - { - return; - } - - try - { - std::wstring text(pData); - _WriteTextToConnection(text); - } - CATCH_LOG(); -} - til::size HwndTerminal::GetFontSize() const noexcept { return _actualFont.GetSize(); @@ -1045,6 +1094,7 @@ void HwndTerminal::ChangeViewport(const til::inclusive_rect& NewWindow) { return; } + const auto lock = _terminal->LockForWriting(); _terminal->UserScrollViewport(NewWindow.top); } diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/TerminalControl/HwndTerminal.hpp similarity index 97% rename from src/cascadia/PublicTerminalCore/HwndTerminal.hpp rename to src/cascadia/TerminalControl/HwndTerminal.hpp index ab3da289587..b88acd03504 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/TerminalControl/HwndTerminal.hpp @@ -112,14 +112,13 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo HRESULT _CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, const bool fAlsoCopyFormatting); HRESULT _CopyToSystemClipboard(std::string stringToCopy, LPCWSTR lpszFormat); void _PasteTextFromClipboard() noexcept; - void _StringPaste(const wchar_t* const pData) noexcept; const unsigned int _NumberOfClicks(til::point clickPos, std::chrono::steady_clock::time_point clickTime) noexcept; HRESULT _StartSelection(LPARAM lParam) noexcept; HRESULT _MoveSelection(LPARAM lParam) noexcept; IRawElementProviderSimple* _GetUiaProvider() noexcept; - void _ClearSelection() noexcept; + void _ClearSelection(); bool _CanSendVTMouseInput() const noexcept; bool _SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept; diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp b/src/cascadia/TerminalControl/HwndTerminalAutomationPeer.cpp similarity index 100% rename from src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp rename to src/cascadia/TerminalControl/HwndTerminalAutomationPeer.cpp diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp b/src/cascadia/TerminalControl/HwndTerminalAutomationPeer.hpp similarity index 100% rename from src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp rename to src/cascadia/TerminalControl/HwndTerminalAutomationPeer.hpp diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d26adcaead6..8a1c9018809 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -4,11 +4,9 @@ #include "pch.h" #include "TermControl.h" -#include #include #include "TermControlAutomationPeer.h" -#include "../../types/inc/GlyphWidth.hpp" #include "../../renderer/atlas/AtlasEngine.h" #include "TermControl.g.cpp" @@ -275,6 +273,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_throttledUpdateScrollbar(const ScrollBarUpdate& update) { + if (!_initializedTerminal) + { + return; + } + // Assumptions: // * we're already not closing // * caller already checked weak ptr to make sure we're still alive @@ -296,56 +299,102 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_showMarksInScrollbar) { - // Update scrollbar marks - ScrollBarCanvas().Children().Clear(); - const auto marks{ _core.ScrollMarks() }; - const auto fullHeight{ ScrollBarCanvas().ActualHeight() }; - const auto totalBufferRows{ update.newMaximum + update.newViewportSize }; - - auto drawPip = [&](const auto row, const auto rightAlign, const auto& brush) { - Windows::UI::Xaml::Shapes::Rectangle r; - r.Fill(brush); - r.Width(16.0f / 3.0f); // pip width - 1/3rd of the scrollbar width. - r.Height(2); - const auto fractionalHeight = row / totalBufferRows; - const auto relativePos = fractionalHeight * fullHeight; - ScrollBarCanvas().Children().Append(r); - Windows::UI::Xaml::Controls::Canvas::SetTop(r, relativePos); - if (rightAlign) + const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); + const auto scrollBarWidthInDIP = scrollBar.ActualWidth(); + const auto scrollBarHeightInDIP = scrollBar.ActualHeight(); + const auto scrollBarWidthInPx = gsl::narrow_cast(lrint(scrollBarWidthInDIP * scaleFactor)); + const auto scrollBarHeightInPx = gsl::narrow_cast(lrint(scrollBarHeightInDIP * scaleFactor)); + + const auto canvas = FindName(L"ScrollBarCanvas").as(); + auto source = canvas.Source().try_as(); + + if (!source || scrollBarWidthInPx != source.PixelWidth() || scrollBarHeightInPx != source.PixelHeight()) + { + source = Media::Imaging::WriteableBitmap{ scrollBarWidthInPx, scrollBarHeightInPx }; + canvas.Source(source); + canvas.Width(scrollBarWidthInDIP); + canvas.Height(scrollBarHeightInDIP); + } + + const auto buffer = source.PixelBuffer(); + const auto data = buffer.data(); + const auto stride = scrollBarWidthInPx * sizeof(til::color); + + // The bitmap has the size of the entire scrollbar, but we want the marks to only show in the range the "thumb" + // (the scroll indicator) can move. That's why we need to add an offset to the start of the drawable bitmap area + // (to offset the decrease button) and subtract twice that (to offset the increase button as well). + // + // The WinUI standard scrollbar defines a Margin="2,0,2,0" for the "VerticalPanningThumb" and a Padding="0,4,0,0" + // for the "VerticalDecrementTemplate" (and similar for the increment), but it seems neither of those is correct, + // because a padding for 3 DIPs seem to be the exact right amount to add. + const auto increaseDecreaseButtonHeight = scrollBarWidthInPx + lround(3 * scaleFactor); + const auto drawableDataStart = data + stride * increaseDecreaseButtonHeight; + const auto drawableRange = scrollBarHeightInPx - 2 * increaseDecreaseButtonHeight; + + // Protect the remaining code against negative offsets. This normally can't happen + // and this code just exists so it doesn't crash if I'm ever wrong about this. + // (The window has a min. size that ensures that there's always a scrollbar thumb.) + if (drawableRange < 0) + { + assert(false); + return; + } + + // The scrollbar bitmap is divided into 3 evenly sized stripes: + // Left: Regular marks + // Center: nothing + // Right: Search marks + const auto pipWidth = (scrollBarWidthInPx + 1) / 3; + const auto pipHeight = lround(1 * scaleFactor); + + const auto maxOffsetY = drawableRange - pipHeight; + const auto offsetScale = maxOffsetY / gsl::narrow_cast(update.newMaximum + update.newViewportSize); + // A helper to turn a TextBuffer row offset into a bitmap offset. + const auto dataAt = [&](til::CoordType row) [[msvc::forceinline]] { + const auto y = std::clamp(lrintf(row * offsetScale), 0, maxOffsetY); + return drawableDataStart + stride * y; + }; + // A helper to draw a single pip (mark) at the given location. + const auto drawPip = [&](uint8_t* beg, til::color color) [[msvc::forceinline]] { + const auto end = beg + pipHeight * stride; + for (; beg < end; beg += stride) { - Windows::UI::Xaml::Controls::Canvas::SetLeft(r, 16.0f * .66f); + // Coincidentally a til::color has the same RGBA format as the bitmap. +#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). + std::fill_n(reinterpret_cast(beg), pipWidth, color); } }; - for (const auto m : marks) + memset(data, 0, buffer.Length()); + + if (const auto marks = _core.ScrollMarks()) { - Windows::UI::Xaml::Shapes::Rectangle r; - Media::SolidColorBrush brush{}; - // Sneaky: technically, a mark doesn't need to have a color set, - // it might want to just use the color from the palette for that - // kind of mark. Fortunately, ControlCore is kind enough to - // pre-evaluate that for us, and shove the real value into the - // Color member, regardless if the mark has a literal value set. - brush.Color(static_cast(m.Color.Color)); - drawPip(m.Start.Y, false, brush); + for (const auto& m : marks) + { + const auto row = m.Start.Y; + const til::color color{ m.Color.Color }; + const auto base = dataAt(row); + drawPip(base, color); + } } - if (_searchBox) + if (_searchBox && _searchBox->Visibility() == Visibility::Visible) { - const auto searchMatches{ _core.SearchResultRows() }; - if (searchMatches && - searchMatches.Size() > 0 && - _searchBox->Visibility() == Visibility::Visible) + if (const auto searchMatches = _core.SearchResultRows()) { - const til::color fgColor{ _core.ForegroundColor() }; - Media::SolidColorBrush searchMarkBrush{}; - searchMarkBrush.Color(fgColor); - for (const auto m : searchMatches) + const til::color color{ _core.ForegroundColor() }; + const auto rightAlignedOffset = (scrollBarWidthInPx - pipWidth) * sizeof(til::color); + + for (const auto row : searchMatches) { - drawPip(m, true, searchMarkBrush); + const auto base = dataAt(row) + rightAlignedOffset; + drawPip(base, color); } } } + + source.Invalidate(); + canvas.Visibility(Visibility::Visible); } } @@ -620,14 +669,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // achieve the intended effect. ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None); ScrollBar().Visibility(Visibility::Collapsed); - ScrollMarksGrid().Visibility(Visibility::Collapsed); } else // (default or Visible) { // Default behavior ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); ScrollBar().Visibility(Visibility::Visible); - ScrollMarksGrid().Visibility(Visibility::Visible); } _interactivity.UpdateSettings(); @@ -640,8 +687,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation } _showMarksInScrollbar = settings.ShowMarks(); - // Clear out all the current marks - ScrollBarCanvas().Children().Clear(); + // Hide all scrollbar marks since they might be disabled now. + if (const auto canvas = ScrollBarCanvas()) + { + canvas.Visibility(Visibility::Collapsed); + } // When we hot reload the settings, the core will send us a scrollbar // update. If we enabled scrollbar marks, then great, when we handle // that message, we'll redraw them. @@ -733,17 +783,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (acrylic == nullptr) { acrylic = Media::AcrylicBrush{}; - - if (_core.Settings().EnableUnfocusedAcrylic()) - { - acrylic.BackgroundSource(Media::AcrylicBackgroundSource::Backdrop); - } - else - { - acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); - } } + const auto backdropStyle = + _core.Settings().EnableUnfocusedAcrylic() ? Media::AcrylicBackgroundSource::Backdrop : Media::AcrylicBackgroundSource::HostBackdrop; + acrylic.BackgroundSource(backdropStyle); + // see GH#1082: Initialize background color so we don't get a // fade/flash when _BackgroundColorChanged is called acrylic.FallbackColor(bgColor); @@ -1056,8 +1101,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // _cursorTimer doesn't exist, and it would never turn on the // cursor. To mitigate, we'll initialize the cursor's 'on' state // with `_focused` here. - _core.CursorOn(_focused || DisplayCursorWhileBlurred); - if (DisplayCursorWhileBlurred) + _core.CursorOn(_focused || _displayCursorWhileBlurred()); + if (_displayCursorWhileBlurred()) { _cursorTimer->Start(); } @@ -1973,7 +2018,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TSFInputControl().NotifyFocusLeave(); } - if (_cursorTimer && !DisplayCursorWhileBlurred) + if (_cursorTimer && !_displayCursorWhileBlurred()) { _cursorTimer->Stop(); _core.CursorOn(false); @@ -3161,51 +3206,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ClearHoveredCell(); } - // Attackers abuse Unicode characters that happen to look similar to ASCII characters. Cyrillic for instance has - // its own glyphs for а, с, е, о, р, х, and у that look practically identical to their ASCII counterparts. - // This is called an "IDN homoglyph attack". - // - // But outright showing Punycode URIs only is similarly flawed as they can end up looking similar to valid ASCII URIs. - // xn--cnn.com for instance looks confusingly similar to cnn.com, but actually represents U+407E. - // - // An optimal solution would detect any URI that contains homoglyphs and show them in their Punycode form. - // Such a detector however is not quite trivial and requires constant maintenance, which this project's - // maintainers aren't currently well equipped to handle. As such we do the next best thing and show the - // Punycode encoding side-by-side with the Unicode string for any IDN. - static winrt::hstring sanitizeURI(winrt::hstring uri) - { - if (uri.empty()) - { - return uri; - } - - wchar_t punycodeBuffer[256]; - wchar_t unicodeBuffer[256]; - - // These functions return int, but are documented to only return positive numbers. - // Better make sure though. It allows us to pass punycodeLength right into IdnToUnicode. - const auto punycodeLength = std::max(0, IdnToAscii(0, uri.data(), gsl::narrow(uri.size()), &punycodeBuffer[0], 256)); - const auto unicodeLength = std::max(0, IdnToUnicode(0, &punycodeBuffer[0], punycodeLength, &unicodeBuffer[0], 256)); - - if (punycodeLength <= 0 || unicodeLength <= 0) - { - return RS_(L"InvalidUri"); - } - - const std::wstring_view punycode{ &punycodeBuffer[0], gsl::narrow_cast(punycodeLength) }; - const std::wstring_view unicode{ &unicodeBuffer[0], gsl::narrow_cast(unicodeLength) }; - - // IdnToAscii/IdnToUnicode return the input string as is if it's all - // plain ASCII. But we don't know if the input URI is Punycode or not. - // --> It's non-Punycode and ASCII if it round-trips. - if (uri == punycode && uri == unicode) - { - return uri; - } - - return winrt::hstring{ fmt::format(FMT_COMPILE(L"{}\n({})"), punycode, unicode) }; - } - void TermControl::_hoveredHyperlinkChanged(const IInspectable& /*sender*/, const IInspectable& /*args*/) { const auto lastHoveredCell = _core.HoveredCell(); @@ -3214,12 +3214,48 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - const auto uriText = sanitizeURI(_core.HoveredUriText()); + auto uriText = _core.HoveredUriText(); if (uriText.empty()) { return; } + // Attackers abuse Unicode characters that happen to look similar to ASCII characters. Cyrillic for instance has + // its own glyphs for а, с, е, о, р, х, and у that look practically identical to their ASCII counterparts. + // This is called an "IDN homoglyph attack". + // + // But outright showing Punycode URIs only is similarly flawed as they can end up looking similar to valid ASCII URIs. + // xn--cnn.com for instance looks confusingly similar to cnn.com, but actually represents U+407E. + // + // An optimal solution would detect any URI that contains homoglyphs and show them in their Punycode form. + // Such a detector however is not quite trivial and requires constant maintenance, which this project's + // maintainers aren't currently well equipped to handle. As such we do the next best thing and show the + // Punycode encoding side-by-side with the Unicode string for any IDN. + try + { + // DisplayUri/Iri drop authentication credentials, which is probably great, but AbsoluteCanonicalUri() + // is the only getter that returns a punycode encoding of the URL. AbsoluteUri() is the only possible + // counterpart, but as the name indicates, we'll end up hitting the != below for any non-canonical URL. + // + // This issue can be fixed by using the IUrl API from urlmon.h directly, which the WinRT API simply wraps. + // IUrl is a very complex system with a ton of useful functionality, but we don't rely on it (neither WinRT), + // so we could alternatively use its underlying API in wininet.h (InternetCrackUrlW, etc.). + // That API however is rather difficult to use for such seldom executed code. + const Windows::Foundation::Uri uri{ uriText }; + const auto unicode = uri.AbsoluteUri(); + const auto punycode = uri.AbsoluteCanonicalUri(); + + if (punycode != unicode) + { + const auto text = fmt::format(FMT_COMPILE(L"{}\n({})"), punycode, unicode); + uriText = winrt::hstring{ text }; + } + } + catch (...) + { + uriText = RS_(L"InvalidUri"); + } + const auto panel = SwapChainPanel(); const auto scale = panel.CompositionScaleX(); const auto offset = panel.ActualOffset(); @@ -3665,4 +3701,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation SelectionContextMenu().Hide(); _core.ContextMenuSelectOutput(); } + + // Should the text cursor be displayed, even when the control isn't focused? + // n.b. "blur" is the opposite of "focus". + bool TermControl::_displayCursorWhileBlurred() const noexcept + { + return CursorVisibility() == Control::CursorDisplayState::Shown; + } + Control::CursorDisplayState TermControl::CursorVisibility() const noexcept + { + return _cursorVisibility; + } + void TermControl::CursorVisibility(Control::CursorDisplayState cursorVisibility) + { + _cursorVisibility = cursorVisibility; + if (!_initializedTerminal) + { + return; + } + + if (_displayCursorWhileBlurred()) + { + // If we should be ALWAYS displaying the cursor, turn it on and start blinking. + _core.CursorOn(true); + if (_cursorTimer.has_value()) + { + _cursorTimer->Start(); + } + } + else + { + // Otherwise, if we're unfocused, then turn the cursor off and stop + // blinking. (if we're focused, then we're already doing the right + // thing) + const auto focused = FocusState() != FocusState::Unfocused; + if (!focused && _cursorTimer.has_value()) + { + _cursorTimer->Stop(); + } + _core.CursorOn(focused); + } + } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index f2f76971ade..441ca5f5763 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -160,6 +160,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ITerminalConnection Connection(); void Connection(const TerminalConnection::ITerminalConnection& connection); + Control::CursorDisplayState CursorVisibility() const noexcept; + void CursorVisibility(Control::CursorDisplayState cursorVisibility); + // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); @@ -194,9 +197,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr); - public: - til::property DisplayCursorWhileBlurred{ false }; - private: friend struct TermControlT; // friend our parent so it can bind private event handlers @@ -258,6 +258,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::Collections::IObservableVector _originalSelectedPrimaryElements{ nullptr }; Windows::Foundation::Collections::IObservableVector _originalSelectedSecondaryElements{ nullptr }; + Control::CursorDisplayState _cursorVisibility{ Control::CursorDisplayState::Default }; + inline bool _IsClosing() const noexcept { #ifndef NDEBUG @@ -376,6 +378,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _SelectCommandHandler(const IInspectable& sender, const IInspectable& args); void _SelectOutputHandler(const IInspectable& sender, const IInspectable& args); + bool _displayCursorWhileBlurred() const noexcept; struct Revokers { diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index f01a11104ef..66727690082 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -12,6 +12,12 @@ import "ControlCore.idl"; namespace Microsoft.Terminal.Control { + enum CursorDisplayState + { + Default, + Shown + }; + [default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IDirectKeyListener, IMouseWheelListener, @@ -121,7 +127,7 @@ namespace Microsoft.Terminal.Control // opacity set by the settings should call this instead. Double BackgroundOpacity { get; }; - Boolean DisplayCursorWhileBlurred; + CursorDisplayState CursorVisibility; Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 9f1c9dc83a5..57bc4fa9308 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -1322,28 +1322,14 @@ ValueChanged="_ScrollbarChangeHandler" ViewportSize="10" /> - - - - - - - - - - - - + diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj index d4560ea14a6..151fe35932d 100644 --- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj +++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj @@ -69,6 +69,8 @@ TSFInputControl.xaml + + @@ -111,6 +113,8 @@ InteractivityAutomationPeer.idl + + diff --git a/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def b/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def index ba15818ddb1..8500c457d68 100644 --- a/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def +++ b/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def @@ -1,3 +1,26 @@ EXPORTS -DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE -DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE + ; WinRT ABI + DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE + DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE + + ; Flat C ABI + CreateTerminal + DestroyTerminal + TerminalBlinkCursor + TerminalCalculateResize + TerminalClearSelection + TerminalDpiChanged + TerminalGetSelection + TerminalIsSelectionActive + TerminalKillFocus + TerminalRegisterScrollCallback + TerminalRegisterWriteCallback + TerminalSendCharEvent + TerminalSendKeyEvent + TerminalSendOutput + TerminalSetCursorVisible + TerminalSetFocus + TerminalSetTheme + TerminalTriggerResize + TerminalTriggerResizeWithDimension + TerminalUserScroll diff --git a/src/cascadia/TerminalControl/dll/TerminalControl.vcxproj b/src/cascadia/TerminalControl/dll/TerminalControl.vcxproj index 82ef3b91658..4116d986ea5 100644 --- a/src/cascadia/TerminalControl/dll/TerminalControl.vcxproj +++ b/src/cascadia/TerminalControl/dll/TerminalControl.vcxproj @@ -89,7 +89,8 @@ - dwrite.lib;dxgi.lib;d2d1.lib;d3d11.lib;shcore.lib;winmm.lib;pathcch.lib;propsys.lib;uiautomationcore.lib;Shlwapi.lib;ntdll.lib;user32.lib;shell32.lib;kernel32.lib;%(AdditionalDependencies) + delayimp.lib;Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies) + uiautomationcore.dll;%(DelayLoadDLLs) + + + diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp index d045bc3cf20..aa6585e6a14 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp @@ -41,7 +41,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _previewControl = Control::TermControl(settings, settings, *_previewConnection); _previewControl.IsEnabled(false); _previewControl.AllowFocusWhenDisabled(false); - _previewControl.DisplayCursorWhileBlurred(true); + _previewControl.CursorVisibility(Microsoft::Terminal::Control::CursorDisplayState::Shown); ControlPreview().Child(_previewControl); } diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 85a19334d4f..630bcd87dd0 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1142,6 +1142,14 @@ Use the new text renderer ("AtlasEngine") {Locked="AtlasEngine"} + + Launch this application with a new environment block + "environment variables" are user-definable values that can affect the way running processes will behave on a computer + + + When enabled, the Terminal will generate a new environment block when creating new tabs or panes with this profile. When disabled, the tab/pane will instead inherit the variables the Terminal was started with. + A description for what the "Reload environment variables" setting does. Presented near "Profile_ReloadEnvVars". + Enable experimental virtual terminal passthrough An option to enable experimental virtual terminal passthrough connectivity option with the underlying ConPTY diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index ae81d5b498f..e90a2b8dcfa 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -96,6 +96,7 @@ static constexpr std::string_view ShowContextMenuKey{ "showContextMenu" }; static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWord" }; static constexpr std::string_view RestartConnectionKey{ "restartConnection" }; static constexpr std::string_view ToggleBroadcastInputKey{ "toggleBroadcastInput" }; +static constexpr std::string_view OpenAboutKey{ "openAbout" }; static constexpr std::string_view ActionKey{ "action" }; @@ -430,6 +431,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") }, { ShortcutAction::RestartConnection, RS_(L"RestartConnectionKey") }, { ShortcutAction::ToggleBroadcastInput, RS_(L"ToggleBroadcastInputCommandKey") }, + { ShortcutAction::OpenAbout, RS_(L"OpenAboutCommandKey") }, }; }(); @@ -459,7 +461,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto data = winrt::to_string(content); std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) { diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index b2247321d5f..a276ebeff2b 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -814,10 +814,25 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring SearchForTextArgs::GenerateName() const { - return winrt::hstring{ - fmt::format(std::wstring_view(RS_(L"SearchForTextCommandKey")), - Windows::Foundation::Uri(QueryUrl()).Domain().c_str()) - }; + if (QueryUrl().empty()) + { + // Return the default command name, because we'll just use the + // default search engine for this. + return RS_(L"SearchWebCommandKey"); + } + + try + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"SearchForTextCommandKey")), + Windows::Foundation::Uri(QueryUrl()).Domain().c_str()) + }; + } + CATCH_LOG(); + + // We couldn't parse a URL out of this. Return no string at all, so that + // we don't even put this into the command palette. + return L""; } winrt::hstring GlobalSummonArgs::GenerateName() const diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 5633c039469..6d83964cbbc 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -313,6 +313,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARG(Windows::Foundation::IReference, SuppressApplicationTitle, nullptr); ACTION_ARG(winrt::hstring, ColorScheme); ACTION_ARG(Windows::Foundation::IReference, Elevate, nullptr); + ACTION_ARG(Windows::Foundation::IReference, ReloadEnvironmentVariables, nullptr); ACTION_ARG(uint64_t, ContentId); static constexpr std::string_view CommandlineKey{ "commandline" }; @@ -325,6 +326,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static constexpr std::string_view SuppressApplicationTitleKey{ "suppressApplicationTitle" }; static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; static constexpr std::string_view ElevateKey{ "elevate" }; + static constexpr std::string_view ReloadEnvironmentVariablesKey{ "reloadEnvironmentVariables" }; static constexpr std::string_view ContentKey{ "__content" }; public: @@ -346,6 +348,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation otherAsUs->_SuppressApplicationTitle == _SuppressApplicationTitle && otherAsUs->_ColorScheme == _ColorScheme && otherAsUs->_Elevate == _Elevate && + otherAsUs->_ReloadEnvironmentVariables == _ReloadEnvironmentVariables && otherAsUs->_ContentId == _ContentId; } return false; @@ -363,6 +366,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::GetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::GetValueForKey(json, ColorSchemeKey, args->_ColorScheme); JsonUtils::GetValueForKey(json, ElevateKey, args->_Elevate); + JsonUtils::GetValueForKey(json, ReloadEnvironmentVariablesKey, args->_ReloadEnvironmentVariables); JsonUtils::GetValueForKey(json, ContentKey, args->_ContentId); return *args; } @@ -383,6 +387,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::SetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::SetValueForKey(json, ColorSchemeKey, args->_ColorScheme); JsonUtils::SetValueForKey(json, ElevateKey, args->_Elevate); + JsonUtils::SetValueForKey(json, ReloadEnvironmentVariablesKey, args->_ReloadEnvironmentVariables); JsonUtils::SetValueForKey(json, ContentKey, args->_ContentId); return json; } @@ -398,6 +403,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation copy->_SuppressApplicationTitle = _SuppressApplicationTitle; copy->_ColorScheme = _ColorScheme; copy->_Elevate = _Elevate; + copy->_ReloadEnvironmentVariables = _ReloadEnvironmentVariables; copy->_ContentId = _ContentId; return *copy; } @@ -418,6 +424,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation h.write(SuppressApplicationTitle()); h.write(ColorScheme()); h.write(Elevate()); + h.write(ReloadEnvironmentVariables()); h.write(ContentId()); } }; diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 271c9d979d8..32f9005fdee 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -147,6 +147,8 @@ namespace Microsoft.Terminal.Settings.Model // This needs to be an optional so that the default value (null) does // not modify whatever the profile's value is (either true or false) Windows.Foundation.IReference Elevate; + // Similarly with ReloadEnvironmentVariables + Windows.Foundation.IReference ReloadEnvironmentVariables; UInt64 ContentId{ get; set; }; diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index df008b58220..f9d934e36e0 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -109,7 +109,8 @@ ON_ALL_ACTIONS(ExpandSelectionToWord) \ ON_ALL_ACTIONS(CloseOtherPanes) \ ON_ALL_ACTIONS(RestartConnection) \ - ON_ALL_ACTIONS(ToggleBroadcastInput) + ON_ALL_ACTIONS(ToggleBroadcastInput) \ + ON_ALL_ACTIONS(OpenAbout) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \ diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 5925271fadc..8964169f207 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -450,6 +450,17 @@ bool SettingsLoader::FixupUserSettings() } } + // Terminal 1.19: Migrate the global + // `compatibility.reloadEnvironmentVariables` to being a per-profile + // setting. If the user had it disabled in 1.18, then set the + // profiles.defaults value to false to match. + if (!userSettings.globals->LegacyReloadEnvironmentVariables()) + { + // migrate the user's opt-out to the profiles.defaults + userSettings.baseLayerProfile->ReloadEnvironmentVariables(false); + fixedUp = true; + } + return fixedUp; } @@ -1228,7 +1239,7 @@ void CascadiaSettings::WriteSettingsToDisk() } #ifndef NDEBUG -static [[maybe_unused]] std::string _getDevPathToSchema() +[[maybe_unused]] static std::string _getDevPathToSchema() { std::filesystem::path filePath{ __FILE__ }; auto schemaPath = filePath.parent_path().parent_path().parent_path().parent_path() / "doc" / "cascadia" / "profiles.schema.json"; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index dff11656140..1caef96b2cf 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -20,6 +20,7 @@ static constexpr std::string_view ActionsKey{ "actions" }; static constexpr std::string_view ThemeKey{ "theme" }; static constexpr std::string_view DefaultProfileKey{ "defaultProfile" }; static constexpr std::string_view LegacyUseTabSwitcherModeKey{ "useTabSwitcher" }; +static constexpr std::string_view LegacyReloadEnvironmentVariablesKey{ "compatibility.reloadEnvironmentVariables" }; // Method Description: // - Copies any extraneous data from the parent before completing a CreateChild call @@ -157,6 +158,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); } } + + JsonUtils::GetValueForKey(json, LegacyReloadEnvironmentVariablesKey, _legacyReloadEnvironmentVariables); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 8ef1a8a2f72..3ccc0972bea 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -67,6 +67,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, const Windows::Foundation::Collections::IMapView& schemes); + bool LegacyReloadEnvironmentVariables() const noexcept { return _legacyReloadEnvironmentVariables; } + INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L""); #define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \ @@ -82,6 +84,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation #endif winrt::guid _defaultProfile; + bool _legacyReloadEnvironmentVariables{ true }; winrt::com_ptr _actionMap{ winrt::make_self() }; std::vector _keybindingsWarnings; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index a1847408fdf..20cb1d90528 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -79,7 +79,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, ForceFullRepaintRendering); INHERITABLE_SETTING(Boolean, SoftwareRendering); INHERITABLE_SETTING(Boolean, UseBackgroundImageForWindow); - INHERITABLE_SETTING(Boolean, ReloadEnvironmentVariables); INHERITABLE_SETTING(Boolean, ForceVTInput); INHERITABLE_SETTING(Boolean, DebugFeaturesEnabled); INHERITABLE_SETTING(Boolean, StartOnUserLogin); diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp b/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp index 4b2239a9ad7..a51ead61899 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp @@ -72,7 +72,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Return Value: // - An IconElement with its IconSource set, if possible. template - TIconSource _getColoredBitmapIcon(const winrt::hstring& path) + TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome) { // FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters. // To skip throwing on Uri construction, we can quickly check if the first character is ASCII. @@ -85,7 +85,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // 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.ShowAsMonochrome(monochrome); iconSource.UriSource(iconUri); return iconSource; } @@ -121,14 +121,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Return Value: // - An IconElement with its IconSource set, if possible. template - TIconSource _getIconSource(const winrt::hstring& iconPath) + TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome) { TIconSource iconSource{ nullptr }; if (iconPath.size() != 0) { const auto expandedIconPath{ _expandIconPath(iconPath) }; - iconSource = _getColoredBitmapIcon(expandedIconPath); + iconSource = _getColoredBitmapIcon(expandedIconPath, monochrome); // If we fail to set the icon source using the "icon" as a path, // let's try it as a symbol/emoji. @@ -197,7 +197,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const hstring& /* language */) { const auto& iconPath = winrt::unbox_value_or(value, L""); - return _getIconSource(iconPath); + return _getIconSource(iconPath, false); } // unused for one-way bindings @@ -211,12 +211,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::UI::Xaml::Controls::IconSource _IconSourceWUX(hstring path) { - return _getIconSource(path); + return _getIconSource(path, false); } - Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(hstring path) + Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(hstring path, bool monochrome) { - return _getIconSource(path); + return _getIconSource(path, monochrome); } SoftwareBitmap _convertToSoftwareBitmap(HICON hicon, @@ -329,13 +329,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return bitmapSource; } - MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath) + MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath, + const bool monochrome) { std::wstring_view iconPathWithoutIndex; const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); if (!indexOpt.has_value()) { - return _IconSourceMUX(iconPath); + return _IconSourceMUX(iconPath, monochrome); } const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value()); diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.h b/src/cascadia/TerminalSettingsModel/IconPathConverter.h index a9eee104b94..f91fdcad7bd 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.h +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.h @@ -19,7 +19,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const hstring& language); static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath); - static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath); + static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, const bool convertToGrayscale); }; } diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.idl b/src/cascadia/TerminalSettingsModel/IconPathConverter.idl index bbe1e055b8c..b52367b6c34 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.idl +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.idl @@ -16,7 +16,7 @@ namespace Microsoft.Terminal.Settings.Model IconPathConverter(); static Windows.UI.Xaml.Controls.IconElement IconWUX(String path); - static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path); + static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale); }; } diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp index 32681150692..c4a5a0cfec0 100644 --- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp @@ -123,6 +123,11 @@ static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring // - a newly constructed KeyChord static KeyChord _fromString(std::wstring_view wstr) { + if (wstr.empty()) + { + return nullptr; + } + using nameToVkeyPair = std::pair; static constinit til::static_map nameToVkey{ // The above VKEY_NAME_PAIRS macro contains a list of key-binding names for each virtual key. diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 6c7b08649f9..15e91a1d7ae 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -27,7 +27,6 @@ Author(s): X(bool, ForceFullRepaintRendering, "experimental.rendering.forceFullRepaint", false) \ X(bool, SoftwareRendering, "experimental.rendering.software", false) \ X(bool, UseBackgroundImageForWindow, "experimental.useBackgroundImageForWindow", false) \ - X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ X(bool, ForceVTInput, "experimental.input.forceVT", false) \ X(bool, TrimBlockSelection, "trimBlockSelection", true) \ X(bool, DetectURLs, "experimental.detectURLs", true) \ @@ -98,7 +97,8 @@ Author(s): X(bool, VtPassthrough, "experimental.connection.passthroughMode", false) \ X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false) \ X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) \ - X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) + X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ + X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) // Intentionally omitted Profile settings: // * Name @@ -151,7 +151,8 @@ Author(s): X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) -#define MTSM_THEME_TAB_SETTINGS(X) \ - X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ - X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \ +#define MTSM_THEME_TAB_SETTINGS(X) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::IconStyle, IconStyle, "iconStyle", winrt::Microsoft::Terminal::Settings::Model::IconStyle::Default) \ X(winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility, ShowCloseButton, "showCloseButton", winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always) diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 86bce9beeb4..4aa9af3422a 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -94,5 +94,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, RightClickContextMenu); INHERITABLE_PROFILE_SETTING(Boolean, RepositionCursorWithMouse); + + INHERITABLE_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables); + } } diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 74035df755d..d6d6d9565f9 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -720,4 +720,8 @@ Search the web for selected text This will open a web browser to search for some user-selected text - \ No newline at end of file + + Open about dialog + This will open the "about" dialog, to display version info and other documentation + + diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 35881c8c715..617692691ee 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -185,6 +185,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { defaultSettings.Elevate(newTerminalArgs.Elevate().Value()); } + + if (newTerminalArgs.ReloadEnvironmentVariables()) + { + defaultSettings.ReloadEnvironmentVariables(newTerminalArgs.ReloadEnvironmentVariables().Value()); + } } return settingsPair; @@ -334,6 +339,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _RightClickContextMenu = profile.RightClickContextMenu(); _RepositionCursorWithMouse = profile.RepositionCursorWithMouse(); + + _ReloadEnvironmentVariables = profile.ReloadEnvironmentVariables(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index daa895518e8..60c9b5fb2c2 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -167,6 +167,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, bool, RightClickContextMenu, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false); + INHERITABLE_SETTING(Model::TerminalSettings, bool, ReloadEnvironmentVariables, true); + private: std::optional> _ColorTable; std::span _getColorTableImpl(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl index 3bc2c72ed0e..23c305ee4e0 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl @@ -45,5 +45,6 @@ namespace Microsoft.Terminal.Settings.Model String StartingDirectory { set; }; Boolean Elevate; + Boolean ReloadEnvironmentVariables; }; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 0dc91e30b11..f5431aabf3d 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -652,6 +652,15 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVi }; }; +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::IconStyle) +{ + JSON_MAPPINGS(3) = { + pair_type{ "default", ValueType::Default }, + pair_type{ "hidden", ValueType::Hidden }, + pair_type{ "monochrome", ValueType::Monochrome }, + }; +}; + // Possible ScrollToMarkDirection values JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection) { diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index f7f041a9dda..830bafef2a2 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -4,6 +4,13 @@ namespace Microsoft.Terminal.Settings.Model { + enum IconStyle + { + Default, + Hidden, + Monochrome + }; + enum ThemeColorType { Accent, @@ -63,6 +70,7 @@ namespace Microsoft.Terminal.Settings.Model ThemeColor Background { get; }; ThemeColor UnfocusedBackground { get; }; TabCloseButtonVisibility ShowCloseButton { get; }; + IconStyle IconStyle { get; }; } [default_interface] runtimeclass Theme : Windows.Foundation.IStringable { diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 117abb5c892..cdd5d6e78dd 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -373,7 +373,8 @@ { "command": "quakeMode", "keys":"win+sc(41)" }, { "command": "openSystemMenu", "keys": "alt+space" }, { "command": "quit" }, - { "command": "restoreLastClosed"}, + { "command": "restoreLastClosed" }, + { "command": "openAbout" }, // Tab Management // "command": "closeTab" is unbound by default. @@ -436,6 +437,7 @@ { "command": { "action": "swapPane", "direction": "previousInOrder"} }, { "command": { "action": "swapPane", "direction": "nextInOrder"} }, { "command": { "action": "swapPane", "direction": "first" } }, + { "command": "toggleBroadcastInput" }, { "command": "togglePaneZoom" }, { "command": "toggleSplitOrientation" }, { "command": "toggleReadOnlyMode" }, diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h index 75b789ed581..133786e5dae 100644 --- a/src/cascadia/TerminalSettingsModel/pch.h +++ b/src/cascadia/TerminalSettingsModel/pch.h @@ -58,6 +58,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider); // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include #include #include diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 0b8a07ddfe1..7dec3c052e6 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -431,7 +431,7 @@ namespace RemotingUnitTests m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); std::vector args{}; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -468,7 +468,7 @@ namespace RemotingUnitTests }); std::vector args{ L"1", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -489,7 +489,7 @@ namespace RemotingUnitTests { std::vector args{ L"-1" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -497,7 +497,7 @@ namespace RemotingUnitTests } { std::vector args{ L"-2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -534,7 +534,7 @@ namespace RemotingUnitTests winrt::clock().now() }; p1->ActivateWindow(activatedArgs); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -559,7 +559,7 @@ namespace RemotingUnitTests p2->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -572,7 +572,7 @@ namespace RemotingUnitTests p1->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p1 again"); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -593,7 +593,7 @@ namespace RemotingUnitTests { std::vector args{ L"2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -602,7 +602,7 @@ namespace RemotingUnitTests } { std::vector args{ L"10" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -648,7 +648,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to p2, who is still alive. We won't create a new window."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -656,7 +656,7 @@ namespace RemotingUnitTests } { Log::Comment(L"Send a commandline to p1, who is dead. We will create a new window."); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -1359,7 +1359,7 @@ namespace RemotingUnitTests std::vector p2Args{ L"two", L"this is for p2" }; { - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1368,7 +1368,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1380,7 +1380,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", who is now dead."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -2392,7 +2392,7 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); std::vector commandlineArgs{ L"0", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" }, SW_NORMAL }; + Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" }, SW_NORMAL, L"" }; Log::Comment(L"When we attempt to send a commandline to the MRU window," L" we should find peasant 1 (who's name is \"one\"), not 2" @@ -2577,7 +2577,7 @@ namespace RemotingUnitTests auto m0 = make_private(monarch0PID); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL, L"" }; const auto result = m0->ProposeCommandline(args); auto shouldCreateWindow = result.ShouldCreateWindow(); VERIFY_IS_TRUE(shouldCreateWindow); @@ -2585,7 +2585,7 @@ namespace RemotingUnitTests auto m1 = make_self(); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL, L"" }; try { diff --git a/src/cascadia/UnitTests_Remoting/pch.h b/src/cascadia/UnitTests_Remoting/pch.h index 4ad6732bf6b..523f6305c3b 100644 --- a/src/cascadia/UnitTests_Remoting/pch.h +++ b/src/cascadia/UnitTests_Remoting/pch.h @@ -49,3 +49,4 @@ Licensed under the MIT license. #include "../../inc/DefaultSettings.h" #include +#include diff --git a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj index 853fc1eec42..959daaddf8f 100644 --- a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj +++ b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj @@ -9,6 +9,7 @@ StaticLibrary Console true + false true diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 1de5441ab94..36d47211b79 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -158,7 +158,7 @@ void AppHost::_HandleCommandlineArgs(const Remoting::WindowRequestedArgs& window } else if (args) { - const auto result = _windowLogic.SetStartupCommandline(args.Commandline(), args.CurrentDirectory()); + const auto result = _windowLogic.SetStartupCommandline(args.Commandline(), args.CurrentDirectory(), args.CurrentEnvironment()); const auto message = _windowLogic.ParseCommandlineMessage(); if (!message.empty()) { @@ -912,7 +912,7 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send // Summon the window whenever we dispatch a commandline to it. This will // make it obvious when a new tab/pane is created in a window. _HandleSummon(sender, summonArgs); - _windowLogic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); + _windowLogic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory(), args.CurrentEnvironment()); } void AppHost::_WindowActivated(bool activated) @@ -931,8 +931,17 @@ winrt::fire_and_forget AppHost::_peasantNotifyActivateWindow() const auto peasant = _peasant; const auto hwnd = _window->GetHandle(); + auto weakThis{ weak_from_this() }; + co_await winrt::resume_background(); + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + GUID currentDesktopGuid{}; if (FAILED_LOG(desktopManager->GetWindowDesktopId(hwnd, ¤tDesktopGuid))) { @@ -963,8 +972,18 @@ winrt::fire_and_forget AppHost::_peasantNotifyActivateWindow() winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() { winrt::hstring layoutJson = L""; + + auto weakThis{ weak_from_this() }; + // Use the main thread since we are accessing controls. co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); + + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return layoutJson; + } + try { const auto pos = _GetWindowLaunchPosition(); @@ -1021,10 +1040,19 @@ void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*se winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable /*sender*/, const winrt::Windows::Foundation::IInspectable /*args*/) { + auto weakThis{ weak_from_this() }; + // We'll be raising an event that may result in a RPC call to the monarch - // make sure we're on the background thread, or this will silently fail co_await winrt::resume_background(); + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + if (_peasant) { _peasant.RequestIdentifyWindows(); @@ -1234,9 +1262,16 @@ void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectab winrt::fire_and_forget AppHost::_QuitRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { + auto weakThis{ weak_from_this() }; // Need to be on the main thread to close out all of the tabs. co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + _windowLogic.Quit(); } @@ -1387,12 +1422,20 @@ winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows:: nCmdShow = SW_MAXIMIZE; } + auto weakThis{ weak_from_this() }; // For inexplicable reasons, again, hop to the BG thread, then back to the // UI thread. This is shockingly load bearing - without this, then // sometimes, we'll _still_ show the HWND before the XAML island actually // paints. co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher(), winrt::Windows::UI::Core::CoreDispatcherPriority::Low); + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + ShowWindow(_window->GetHandle(), nCmdShow); // If we didn't start the window hidden (in one way or another), then try to diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index dabe517394b..fd912aee67d 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -6,7 +6,7 @@ #include "NotificationIcon.h" #include -class AppHost +class AppHost : public std::enable_shared_from_this { public: AppHost(const winrt::TerminalApp::AppLogic& logic, diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 87ec8d5dbce..9bea05acfe9 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -199,6 +199,8 @@ void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexce UpdateWindow(_window.get()); UpdateWindowIconForActiveMetrics(_window.get()); + + _currentSystemThemeIsDark = Theme::IsSystemInDarkTheme(); } // Method Description: @@ -745,7 +747,16 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize // themes, color schemes that might depend on the OS theme if (param == L"ImmersiveColorSet") { - _UpdateSettingsRequestedHandlers(); + // GH#15732: Don't update the settings, unless the theme + // _actually_ changed. ImmersiveColorSet gets sent more often + // than just on a theme change. It notably gets sent when the PC + // is locked, or the UAC prompt opens. + auto isCurrentlyDark = Theme::IsSystemInDarkTheme(); + if (isCurrentlyDark != _currentSystemThemeIsDark) + { + _currentSystemThemeIsDark = isCurrentlyDark; + _UpdateSettingsRequestedHandlers(); + } } } break; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index c0abd343e1b..eee013c529a 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -117,6 +117,7 @@ class IslandWindow : RECT _rcWindowBeforeFullscreen{}; RECT _rcWorkBeforeFullscreen{}; UINT _dpiBeforeFullscreen{ 96 }; + bool _currentSystemThemeIsDark{ true }; void _coldInitialize(); void _warmInitialize(); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 23aba3d683e..47a11bba38e 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -12,6 +12,7 @@ #include "resource.h" #include "NotificationIcon.h" +#include using namespace winrt; using namespace winrt::Microsoft::Terminal; @@ -105,7 +106,9 @@ bool WindowEmperor::HandleCommandlineArgs() GetStartupInfoW(&si); const uint32_t showWindow = WI_IsFlagSet(si.dwFlags, STARTF_USESHOWWINDOW) ? si.wShowWindow : SW_SHOW; - Remoting::CommandlineArgs eventArgs{ { args }, { cwd }, showWindow }; + const auto currentEnv{ til::env::from_current_environment() }; + + Remoting::CommandlineArgs eventArgs{ { args }, { cwd }, showWindow, winrt::hstring{ currentEnv.to_string() } }; const auto isolatedMode{ _app.Logic().IsolatedMode() }; diff --git a/src/cascadia/WindowsTerminal/WindowThread.cpp b/src/cascadia/WindowsTerminal/WindowThread.cpp index 25fb94d5b07..2c70741ad1c 100644 --- a/src/cascadia/WindowsTerminal/WindowThread.cpp +++ b/src/cascadia/WindowsTerminal/WindowThread.cpp @@ -24,7 +24,7 @@ void WindowThread::CreateHost() assert(_warmWindow == nullptr); // Start the AppHost HERE, on the actual thread we want XAML to run on - _host = std::make_unique<::AppHost>(_appLogic, + _host = std::make_shared<::AppHost>(_appLogic, _args, _manager, _peasant); @@ -164,7 +164,7 @@ void WindowThread::Microwave( _peasant = std::move(peasant); _args = std::move(args); - _host = std::make_unique<::AppHost>(_appLogic, + _host = std::make_shared<::AppHost>(_appLogic, _args, _manager, _peasant, diff --git a/src/cascadia/WindowsTerminal/WindowThread.h b/src/cascadia/WindowsTerminal/WindowThread.h index 0e8bc5429d1..a1af2db6500 100644 --- a/src/cascadia/WindowsTerminal/WindowThread.h +++ b/src/cascadia/WindowsTerminal/WindowThread.h @@ -35,7 +35,11 @@ class WindowThread : public std::enable_shared_from_this winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs _args{ nullptr }; winrt::Microsoft::Terminal::Remoting::WindowManager _manager{ nullptr }; - std::unique_ptr<::AppHost> _host{ nullptr }; + // This is a "shared_ptr", but it should be treated as a unique, owning ptr. + // It's shared, because there are edge cases in refrigeration where internal + // co_awaits inside AppHost might resume after we've dtor'd it, and there's + // no other way for us to let the AppHost know it has passed on. + std::shared_ptr<::AppHost> _host{ nullptr }; winrt::event_token _UpdateSettingsRequestedToken; std::unique_ptr<::IslandWindow> _warmWindow{ nullptr }; diff --git a/src/cascadia/WindowsTerminal_UIATests/Init.cs b/src/cascadia/WindowsTerminal_UIATests/Init.cs index dec248167a2..ad46a8b642a 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Init.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Init.cs @@ -22,7 +22,12 @@ class Init 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"); + string winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver", "WinAppDriver.exe"); + + if (!File.Exists(winAppDriver)) + { + winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver.exe"); + } Log.Comment($"Attempting to launch WinAppDriver at: {winAppDriver}"); Log.Comment($"Working directory: {Environment.CurrentDirectory}"); diff --git a/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj b/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj index 589cb6f3af4..4bc9246e8fd 100644 --- a/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj +++ b/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj @@ -126,7 +126,7 @@ - copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\" + mkdir "$(OutDir)\WinAppDriver" 2>nul & copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\WinAppDriver\" - \ No newline at end of file + diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index e22f40bdf0b..96898df5dc7 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -179,72 +179,66 @@ public enum SetWindowPosFlags : uint SWP_SHOWWINDOW = 0x0040, } - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint CreateTerminal(IntPtr parent, out IntPtr hwnd, out IntPtr terminal); + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = false)] + public static extern void CreateTerminal(IntPtr parent, out IntPtr hwnd, out IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSendOutput(IntPtr terminal, string lpdata); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalTriggerResize(IntPtr terminal, int width, int height, out TilSize dimensions); + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = false)] + public static extern void TerminalTriggerResize(IntPtr terminal, int width, int height, out TilSize dimensions); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalTriggerResizeWithDimension(IntPtr terminal, TilSize dimensions, out TilSize dimensionsInPixels); + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = false)] + public static extern void TerminalTriggerResizeWithDimension(IntPtr terminal, TilSize dimensions, out TilSize dimensionsInPixels); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalCalculateResize(IntPtr terminal, int width, int height, out TilSize dimensions); + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = false)] + public static extern void TerminalCalculateResize(IntPtr terminal, int width, int height, out TilSize dimensions); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalDpiChanged(IntPtr terminal, int newDpi); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalRegisterScrollCallback(IntPtr terminal, [MarshalAs(UnmanagedType.FunctionPtr)] ScrollCallback callback); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalRegisterWriteCallback(IntPtr terminal, [MarshalAs(UnmanagedType.FunctionPtr)] WriteCallback callback); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalUserScroll(IntPtr terminal, int viewTop); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalStartSelection(IntPtr terminal, TilPoint cursorPosition, bool altPressed); - - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalMoveSelection(IntPtr terminal, TilPoint cursorPosition); - - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalClearSelection(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] [return: MarshalAs(UnmanagedType.LPWStr)] public static extern string TerminalGetSelection(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool TerminalIsSelectionActive(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void DestroyTerminal(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode, ushort flags, bool keyDown); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode, ushort flags); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSetTheme(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] TerminalTheme theme, string fontFamily, short fontSize, int newDpi); - [DllImport("PublicTerminalCore.dll", CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalBlinkCursor(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSetCursorVisible(IntPtr terminal, bool visible); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSetFocus(IntPtr terminal); - [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalKillFocus(IntPtr terminal); [DllImport("user32.dll", SetLastError = true)] diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index 206005a0924..967d5b4f7d1 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -443,35 +443,6 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam return IntPtr.Zero; } - private void LeftClickHandler(int lParam) - { - var altPressed = NativeMethods.GetKeyState((int)NativeMethods.VirtualKey.VK_MENU) < 0; - var x = lParam & 0xffff; - var y = lParam >> 16; - var cursorPosition = new NativeMethods.TilPoint - { - X = x, - Y = y, - }; - - NativeMethods.TerminalStartSelection(this.terminal, cursorPosition, altPressed); - } - - private void MouseMoveHandler(int wParam, int lParam) - { - if ((wParam & 0x0001) == 1) - { - var x = lParam & 0xffff; - var y = lParam >> 16; - var cursorPosition = new NativeMethods.TilPoint - { - X = x, - Y = y, - }; - NativeMethods.TerminalMoveSelection(this.terminal, cursorPosition); - } - } - private void Connection_TerminalOutput(object sender, TerminalOutputEventArgs e) { if (this.terminal == IntPtr.Zero || string.IsNullOrEmpty(e.Data)) diff --git a/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj b/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj index dc3eebe8749..6581f1478b2 100644 --- a/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj +++ b/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj @@ -34,28 +34,28 @@ - + true runtimes\win-x86\native\ - + true runtimes\win-x64\native\ - + true runtimes\win-arm64\native\ - + true runtimes\win-x86\native\ - + true runtimes\win-x64\native\ - + true runtimes\win-arm64\native\ diff --git a/src/cascadia/WpfTerminalTestNetCore/WpfTerminalTestNetCore.csproj b/src/cascadia/WpfTerminalTestNetCore/WpfTerminalTestNetCore.csproj index e7fb1479ed5..b3b839c4855 100644 --- a/src/cascadia/WpfTerminalTestNetCore/WpfTerminalTestNetCore.csproj +++ b/src/cascadia/WpfTerminalTestNetCore/WpfTerminalTestNetCore.csproj @@ -9,7 +9,7 @@ - + false @@ -21,7 +21,7 @@ app.manifest - + PreserveNewest diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index 7ca1943d4b4..17c720614b1 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -17,6 +17,15 @@ Revision History: #pragma once +template<> +struct fmt::formatter : fmt::formatter +{ + auto format(const winrt::hstring& str, auto& ctx) + { + return fmt::formatter::format({ str.data(), str.size() }, ctx); + } +}; + // This is a helper macro for both declaring the signature of an event, and // defining the body. Winrt events need a method for adding a callback to the // event and removing the callback. This macro will both declare the method diff --git a/src/common.build.pre.props b/src/common.build.pre.props index 1cf31b37e44..9404c24c4bd 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -101,6 +101,13 @@ x64 true false + + true diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 2a3d5cc062e..1c152920d22 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -62,7 +62,7 @@ - + @@ -93,7 +93,7 @@ - + diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 5d33e607931..dd8b635efd9 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -255,7 +255,6 @@ bool VtIo::IsUsingVt() const { g.pRender->AddRenderEngine(_pVtRenderEngine.get()); g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get()); - g.getConsoleInformation().GetActiveInputBuffer()->SetTerminalConnection(_pVtRenderEngine.get()); // Force the whole window to be put together first. // We don't really need the handle, we just want to leverage the setup steps. @@ -463,37 +462,15 @@ void VtIo::SendCloseEvent() } } -// Method Description: -// - Tell the vt renderer to begin a resize operation. During a resize -// operation, the vt renderer should _not_ request to be repainted during a -// text buffer circling event. Any callers of this method should make sure to -// call EndResize to make sure the renderer returns to normal behavior. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtIo::BeginResize() -{ - if (_pVtRenderEngine) - { - _pVtRenderEngine->BeginResizeRequest(); - } -} - -// Method Description: -// - Tell the vt renderer to end a resize operation. -// See BeginResize for more details. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtIo::EndResize() +// The name of this method is an analogy to TCP_CORK. It instructs +// the VT renderer to stop flushing its buffer to the output pipe. +// Don't forget to uncork it! +void VtIo::CorkRenderer(bool corked) const noexcept { - if (_pVtRenderEngine) + _pVtRenderEngine->Cork(corked); + if (!corked) { - _pVtRenderEngine->EndResizeRequest(); + LOG_IF_FAILED(ServiceLocator::LocateGlobals().pRender->PaintFrame()); } } @@ -547,3 +524,12 @@ bool VtIo::IsResizeQuirkEnabled() const } return S_OK; } + +[[nodiscard]] HRESULT VtIo::RequestMouseMode(bool enable) const noexcept +{ + if (_pVtRenderEngine) + { + return _pVtRenderEngine->RequestMouseMode(enable); + } + return S_OK; +} diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index a15653ad084..2ecc0d51754 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -40,8 +40,7 @@ namespace Microsoft::Console::VirtualTerminal void CloseInput(); void CloseOutput(); - void BeginResize(); - void EndResize(); + void CorkRenderer(bool corked) const noexcept; #ifdef UNIT_TESTING void EnableConptyModeForTests(std::unique_ptr vtRenderEngine); @@ -50,6 +49,7 @@ namespace Microsoft::Console::VirtualTerminal bool IsResizeQuirkEnabled() const; [[nodiscard]] HRESULT ManuallyClearScrollback() const noexcept; + [[nodiscard]] HRESULT RequestMouseMode(bool enable) const noexcept; void CreatePseudoWindow(); void SetWindowVisibility(bool showOrHide) noexcept; diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index d9858c7e003..bf9e32ed38b 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -334,17 +334,24 @@ try return CONSOLE_STATUS_WAIT; } - auto restoreVtQuirk{ - wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); }) - }; - + const auto vtIo = ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo(); + const auto restoreVtQuirk = wil::scope_exit([&]() { + if (requiresVtQuirk) + { + screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); + } + if (vtIo->IsUsingVt()) + { + vtIo->CorkRenderer(false); + } + }); if (requiresVtQuirk) { screenInfo.SetIgnoreLegacyEquivalentVTAttributes(); } - else + if (vtIo->IsUsingVt()) { - restoreVtQuirk.release(); + vtIo->CorkRenderer(true); } const std::wstring_view str{ pwchBuffer, *pcbBuffer / sizeof(WCHAR) }; diff --git a/src/host/ft_uia/Host.Tests.UIA.csproj b/src/host/ft_uia/Host.Tests.UIA.csproj index 0a3c8dd13aa..222a3274d2d 100644 --- a/src/host/ft_uia/Host.Tests.UIA.csproj +++ b/src/host/ft_uia/Host.Tests.UIA.csproj @@ -149,7 +149,7 @@ - copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\" + mkdir "$(OutDir)\WinAppDriver" 2>nul & copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\WinAppDriver\" - \ No newline at end of file + diff --git a/src/host/ft_uia/Init.cs b/src/host/ft_uia/Init.cs index e6c7fae56c7..eb8b76cc923 100644 --- a/src/host/ft_uia/Init.cs +++ b/src/host/ft_uia/Init.cs @@ -28,7 +28,12 @@ class Init 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"); + string winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver", "WinAppDriver.exe"); + + if (!File.Exists(winAppDriver)) + { + winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver.exe"); + } Log.Comment($"Attempting to launch WinAppDriver at: {winAppDriver}"); Log.Comment($"Working directory: {Environment.CurrentDirectory}"); diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 5c2954f4fa4..eec9f720db8 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -368,17 +368,19 @@ void ApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept WI_ClearFlag(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS); } - const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) }; - - // Mouse input should be received when mouse mode is on and quick edit mode is off - // (for more information regarding the quirks of mouse mode and why/how it relates - // to quick edit mode, see GH#9970) - const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) }; - const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) }; - - if (oldMouseMode != newMouseMode) + if (gci.IsInVtIoMode()) { - gci.GetActiveInputBuffer()->PassThroughWin32MouseRequest(newMouseMode); + // Mouse input should be received when mouse mode is on and quick edit mode is off + // (for more information regarding the quirks of mouse mode and why/how it relates + // to quick edit mode, see GH#9970) + const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) }; + const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) }; + const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) }; + + if (oldMouseMode != newMouseMode) + { + LOG_IF_FAILED(gci.GetVtIo()->RequestMouseMode(newMouseMode)); + } } context.InputMode = mode; diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp index a4ed5000a89..3a4e556e440 100644 --- a/src/host/inputBuffer.cpp +++ b/src/host/inputBuffer.cpp @@ -25,8 +25,7 @@ using namespace Microsoft::Console; // Return Value: // - A new instance of InputBuffer InputBuffer::InputBuffer() : - InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE }, - _pTtyConnection(nullptr) + InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE } { // initialize buffer header fInComposition = false; @@ -342,26 +341,6 @@ void InputBuffer::FlushAllButKeys() _storage.erase(newEnd, _storage.end()); } -void InputBuffer::SetTerminalConnection(_In_ Render::VtEngine* const pTtyConnection) -{ - this->_pTtyConnection = pTtyConnection; -} - -void InputBuffer::PassThroughWin32MouseRequest(bool enable) -{ - if (_pTtyConnection) - { - if (enable) - { - LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006h")); - } - else - { - LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006l")); - } - } -} - // 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 diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp index 0044cb89d27..ec7cd75b082 100644 --- a/src/host/inputBuffer.hpp +++ b/src/host/inputBuffer.hpp @@ -63,8 +63,6 @@ class InputBuffer final : public ConsoleObjectHeader bool IsInVirtualTerminalInputMode() const; Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput(); - void SetTerminalConnection(_In_ Microsoft::Console::Render::VtEngine* const pTtyConnection); - void PassThroughWin32MouseRequest(bool enable); private: enum class ReadingMode : uint8_t @@ -86,7 +84,6 @@ class InputBuffer final : public ConsoleObjectHeader INPUT_RECORD _writePartialByteSequence{}; bool _writePartialByteSequenceAvailable = false; Microsoft::Console::VirtualTerminal::TerminalInput _termInput; - Microsoft::Console::Render::VtEngine* _pTtyConnection; // This flag is used in _HandleTerminalInputCallback // If the InputBuffer leads to a _HandleTerminalInputCallback call, diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 222868aad7a..a48e189f00d 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -95,9 +95,11 @@ til::rect ConhostInternalGetSet::GetViewport() const void ConhostInternalGetSet::SetViewportPosition(const til::point position) { auto& info = _io.GetActiveOutputBuffer(); - const auto dimensions = info.GetViewport().Dimensions(); - const auto windowRect = til::rect{ position, dimensions }.to_inclusive_rect(); - THROW_IF_FAILED(ServiceLocator::LocateGlobals().api->SetConsoleWindowInfoImpl(info, true, windowRect)); + THROW_IF_FAILED(info.SetViewportOrigin(true, position, false)); + // SetViewportOrigin() only updates the virtual bottom (the bottom coordinate of the area + // in the text buffer a VT client writes its output into) when it's moving downwards. + // But this function is meant to truly move the viewport no matter what. Otherwise `tput reset` breaks. + info.UpdateBottom(); } // Method Description: diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index a5859d6766f..d05502dc0d0 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -204,18 +204,19 @@ bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& contr { controlKeyState = 0; - const auto done = _readCharInputLoop(); + _readCharInputLoop(); // NOTE: Don't call _flushBuffer in a wil::scope_exit/defer. // It may throw and throwing during an ongoing exception is a bad idea. _flushBuffer(); - if (done) + if (_state == State::Accumulating) { - _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + return false; } - return done; + _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + return true; } // Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted. @@ -308,17 +309,10 @@ size_t COOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t positi return position; } -const std::wstring_view& COOKED_READ_DATA::_newlineSuffix() const noexcept -{ - static constexpr std::wstring_view cr{ L"\r" }; - static constexpr std::wstring_view crlf{ L"\r\n" }; - return WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr; -} - // Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents. -bool COOKED_READ_DATA::_readCharInputLoop() +void COOKED_READ_DATA::_readCharInputLoop() { - for (;;) + while (_state == State::Accumulating) { const auto hasPopup = !_popups.empty(); auto charOrVkey = UNICODE_NULL; @@ -331,7 +325,7 @@ bool COOKED_READ_DATA::_readCharInputLoop() const auto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers); if (status == CONSOLE_STATUS_WAIT) { - return false; + break; } THROW_IF_NTSTATUS_FAILED(status); @@ -339,10 +333,7 @@ bool COOKED_READ_DATA::_readCharInputLoop() { const auto wch = static_cast(popupKeys ? 0 : charOrVkey); const auto vkey = static_cast(popupKeys ? charOrVkey : 0); - if (_popupHandleInput(wch, vkey, modifiers)) - { - return true; - } + _popupHandleInput(wch, vkey, modifiers); } else { @@ -350,16 +341,16 @@ bool COOKED_READ_DATA::_readCharInputLoop() { _handleVkey(charOrVkey, modifiers); } - else if (_handleChar(charOrVkey, modifiers)) + else { - return true; + _handleChar(charOrVkey, modifiers); } } } } // Handles character input for _readCharInputLoop() when no popups exist. -bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) +void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) { // All paths in this function modify the buffer. @@ -376,17 +367,19 @@ bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) _bufferCursor++; _controlKeyState = modifiers; - return true; + _transitionState(State::DoneWithWakeupMask); + return; } switch (wch) { case UNICODE_CARRIAGERETURN: { - _buffer.append(_newlineSuffix()); + // NOTE: Don't append newlines to the buffer just yet! See _handlePostCharInputLoop for more information. _bufferCursor = _buffer.size(); _markAsDirty(); - return true; + _transitionState(State::DoneWithCarriageReturn); + return; } case EXTKEY_ERASE_PREV_WORD: // Ctrl+Backspace case UNICODE_BACKSPACE: @@ -415,7 +408,7 @@ bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); } } - return false; + return; } // If processed mode is disabled, control characters like backspace are treated like any other character. break; @@ -437,7 +430,6 @@ bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) _bufferCursor++; _markAsDirty(); - return false; } // Handles non-character input for _readCharInputLoop() when no popups exist. @@ -656,15 +648,34 @@ void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& nu std::wstring_view input{ _buffer }; size_t lineCount = 1; - if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) + if (_state == State::DoneWithCarriageReturn) { - // The last characters in line-read are a \r or \r\n unless _ctrlWakeupMask was used. - // Neither History nor s_MatchAndCopyAlias want to know about them. - const auto& suffix = _newlineSuffix(); - if (input.ends_with(suffix)) - { - input.remove_suffix(suffix.size()); + static constexpr std::wstring_view cr{ L"\r" }; + static constexpr std::wstring_view crlf{ L"\r\n" }; + const auto newlineSuffix = WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr; + std::wstring alias; + // Here's why we can't easily use _flushBuffer() to handle newlines: + // + // A carriage return (enter key) will increase the _distanceEnd by up to viewport-width many columns, + // since it increases the Y distance between the start and end by 1 (it's a newline after all). + // This will make _flushBuffer() think that the new _buffer is way longer than the old one and so + // _erase() ends up not erasing the tail end of the prompt, even if the new prompt is actually shorter. + // + // If you were to break this (remove this code and then append \r\n in _handleChar()) + // you can reproduce the issue easily if you do this: + // * Run cmd.exe + // * Write "echo hello" and press Enter + // * Write "foobar foo bar" (don't press Enter) + // * Press F7, select "echo hello" and press Enter + // + // It'll print "hello" but the previous prompt will say "echo hello bar" because the _distanceEnd + // ended up being well over 14 leading it to believe that "bar" got overwritten during WriteCharsLegacy(). + + WriteCharsLegacy(_screenInfo, newlineSuffix, true, nullptr); + + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) + { if (_history) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -672,24 +683,27 @@ void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& nu } Tracing::s_TraceCookedRead(_processHandle, input); + alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount); + } - const auto alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount); - if (!alias.empty()) - { - _buffer = alias; - } + if (!alias.empty()) + { + _buffer = std::move(alias); + } + else + { + _buffer.append(newlineSuffix); + } - // NOTE: Even if there's no alias we should restore the trailing \r\n that we removed above. - input = std::wstring_view{ _buffer }; + input = std::wstring_view{ _buffer }; - // doskey aliases may result in multiple lines of output (for instance `doskey test=echo foo$Techo bar$Techo baz`). - // We need to emit them as multiple cooked reads as well, so that each read completes at a \r\n. - if (lineCount > 1) - { - // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast. - const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1; - input = input.substr(0, std::min(input.size(), firstLineEnd)); - } + // doskey aliases may result in multiple lines of output (for instance `doskey test=echo foo$Techo bar$Techo baz`). + // We need to emit them as multiple cooked reads as well, so that each read completes at a \r\n. + if (lineCount > 1) + { + // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast. + const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1; + input = input.substr(0, std::min(input.size(), firstLineEnd)); } } @@ -722,6 +736,12 @@ void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& nu controlKeyState = _controlKeyState; } +void COOKED_READ_DATA::_transitionState(State state) noexcept +{ + assert(_state == State::Accumulating); + _state = state; +} + // Signals to _flushBuffer() that the contents of _buffer are stale and need to be redrawn. // ALL _buffer and _bufferCursor changes must be flagged with _markAsDirty(). // @@ -733,6 +753,7 @@ void COOKED_READ_DATA::_markAsDirty() } // Draws the contents of _buffer onto the screen. +// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information. void COOKED_READ_DATA::_flushBuffer() { // _flushBuffer() is called often and is a good place to assert() that our _bufferCursor is still in bounds. @@ -1043,12 +1064,12 @@ void COOKED_READ_DATA::_popupsDone() _screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(false); } -bool COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) +void COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) { if (_popups.empty()) { assert(false); // Don't call this function. - return false; + return; } auto& popup = _popups.back(); @@ -1056,17 +1077,18 @@ bool COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modif { case PopupKind::CopyToChar: _popupHandleCopyToCharInput(popup, wch, vkey, modifiers); - return false; + break; case PopupKind::CopyFromChar: _popupHandleCopyFromCharInput(popup, wch, vkey, modifiers); - return false; + break; case PopupKind::CommandNumber: _popupHandleCommandNumberInput(popup, wch, vkey, modifiers); - return false; + break; case PopupKind::CommandList: - return _popupHandleCommandListInput(popup, wch, vkey, modifiers); + _popupHandleCommandListInput(popup, wch, vkey, modifiers); + break; default: - return false; + break; } } @@ -1166,7 +1188,7 @@ void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_ } } -bool COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers) +void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers) { auto& cl = popup.commandList; @@ -1174,30 +1196,31 @@ bool COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t { _buffer.assign(_history->RetrieveNth(cl.selected)); _popupsDone(); - return _handleChar(UNICODE_CARRIAGERETURN, modifiers); + _handleChar(UNICODE_CARRIAGERETURN, modifiers); + return; } switch (vkey) { case VK_ESCAPE: _popupsDone(); - return false; + return; case VK_F9: _popupPush(PopupKind::CommandNumber); - return false; + return; case VK_DELETE: _history->Remove(cl.selected); if (_history->GetNumberOfCommands() <= 0) { _popupsDone(); - return false; + return; } break; case VK_LEFT: case VK_RIGHT: _replaceBuffer(_history->RetrieveNth(cl.selected)); _popupsDone(); - return false; + return; case VK_UP: if (WI_IsFlagSet(modifiers, SHIFT_PRESSED)) { @@ -1230,11 +1253,11 @@ bool COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t cl.selected += popup.contentRect.height(); break; default: - return false; + return; } _popupDrawCommandList(popup); - return false; + return; } void COOKED_READ_DATA::_popupDrawPrompt(const Popup& popup, const UINT id) const diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 8f1e5849e81..803724b6f52 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -41,6 +41,13 @@ class COOKED_READ_DATA final : public ReadData private: static constexpr uint8_t CommandNumberMaxInputLength = 5; + enum class State : uint8_t + { + Accumulating = 0, + DoneWithWakeupMask, + DoneWithCarriageReturn, + }; + enum class PopupKind { // Copies text from the previous command between the current cursor position and the first instance @@ -106,11 +113,11 @@ class COOKED_READ_DATA final : public ReadData static size_t _wordPrev(const std::wstring_view& chars, size_t position); static size_t _wordNext(const std::wstring_view& chars, size_t position); - const std::wstring_view& _newlineSuffix() const noexcept; - bool _readCharInputLoop(); - bool _handleChar(wchar_t wch, DWORD modifiers); + void _readCharInputLoop(); + void _handleChar(wchar_t wch, DWORD modifiers); void _handleVkey(uint16_t vkey, DWORD modifiers); void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); + void _transitionState(State state) noexcept; void _markAsDirty(); void _flushBuffer(); void _erase(ptrdiff_t distance) const; @@ -124,8 +131,8 @@ class COOKED_READ_DATA final : public ReadData void _popupHandleCopyToCharInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleCopyFromCharInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleCommandNumberInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); - bool _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); - bool _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); + void _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + void _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); void _popupDrawPrompt(const Popup& popup, UINT id) const; void _popupDrawCommandList(Popup& popup) const; @@ -140,10 +147,15 @@ class COOKED_READ_DATA final : public ReadData std::wstring _buffer; size_t _bufferCursor = 0; + // _distanceCursor is the distance between the start of the prompt and the + // current cursor location in columns (including wide glyph padding columns). ptrdiff_t _distanceCursor = 0; + // _distanceEnd is the distance between the start of the prompt and its last + // glyph at the end in columns (including wide glyph padding columns). ptrdiff_t _distanceEnd = 0; bool _bufferDirty = false; bool _insertMode = false; + State _state = State::Accumulating; std::vector _popups; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 58ac0812d52..d02bbaf3d67 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1387,6 +1387,7 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeWithReflow(const til::size coordNewScreenSize) +try { if ((USHORT)coordNewScreenSize.width >= SHORT_MAX || (USHORT)coordNewScreenSize.height >= SHORT_MAX) { @@ -1394,26 +1395,14 @@ bool SCREEN_INFORMATION::IsMaximizedY() const return STATUS_INVALID_PARAMETER; } - // First allocate a new text buffer to take the place of the current one. - std::unique_ptr newTextBuffer; - - // GH#3848 - Stash away the current attributes the old text buffer is using. - // We'll initialize the new buffer with the default attributes, but after - // the resize, we'll want to make sure that the new buffer's current + // GH#3848 - We'll initialize the new buffer with the default attributes, + // but after the resize, we'll want to make sure that the new buffer's current // attributes (the ones used for printing new text) match the old buffer's. - const auto oldPrimaryAttributes = _textBuffer->GetCurrentAttributes(); - try - { - newTextBuffer = std::make_unique(coordNewScreenSize, - TextAttribute{}, - 0, // temporarily set size to 0 so it won't render. - _textBuffer->IsActiveBuffer(), - _textBuffer->GetRenderer()); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } + auto newTextBuffer = std::make_unique(coordNewScreenSize, + TextAttribute{}, + 0, // temporarily set size to 0 so it won't render. + _textBuffer->IsActiveBuffer(), + _textBuffer->GetRenderer()); // Save cursor's relative height versus the viewport const auto sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().y - _viewport.Top(); @@ -1426,38 +1415,35 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // we're capturing _textBuffer by reference here because when we exit, we want to EndDefer on the current active buffer. auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - auto hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), std::nullopt, std::nullopt); + TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get()); - if (SUCCEEDED(hr)) - { - // Since the reflow doesn't preserve the virtual bottom, we try and - // estimate where it ought to be by making it the same distance from - // the cursor row as it was before the resize. However, we also need - // to make sure it is far enough down to include the last non-space - // row, and it shouldn't be less than the height of the viewport, - // otherwise the top of the virtual viewport would end up negative. - const auto cursorRow = newTextBuffer->GetCursor().GetPosition().y; - const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().y; - const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; - const auto viewportBottom = _viewport.Height() - 1; - _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); + // Since the reflow doesn't preserve the virtual bottom, we try and + // estimate where it ought to be by making it the same distance from + // the cursor row as it was before the resize. However, we also need + // to make sure it is far enough down to include the last non-space + // row, and it shouldn't be less than the height of the viewport, + // otherwise the top of the virtual viewport would end up negative. + const auto cursorRow = newTextBuffer->GetCursor().GetPosition().y; + const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().y; + const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; + const auto viewportBottom = _viewport.Height() - 1; + _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); - // We can't let it extend past the bottom of the buffer either. - _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); + // We can't let it extend past the bottom of the buffer either. + _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); - // Adjust the viewport so the cursor doesn't wildly fly off up or down. - const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); - til::point coordCursorHeightDiff; - coordCursorHeightDiff.y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; - LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); + // Adjust the viewport so the cursor doesn't wildly fly off up or down. + const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); + til::point coordCursorHeightDiff; + coordCursorHeightDiff.y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; + LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); - newTextBuffer->SetCurrentAttributes(oldPrimaryAttributes); + newTextBuffer->SetCurrentAttributes(_textBuffer->GetCurrentAttributes()); - _textBuffer.swap(newTextBuffer); - } - - return NTSTATUS_FROM_HRESULT(hr); + _textBuffer = std::move(newTextBuffer); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: @@ -1467,11 +1453,14 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const til::size coordNewScreenSize) +try { _textBuffer->GetCursor().StartDeferDrawing(); auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - return NTSTATUS_FROM_HRESULT(_textBuffer->ResizeTraditional(coordNewScreenSize)); + _textBuffer->ResizeTraditional(coordNewScreenSize); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: @@ -1493,19 +1482,6 @@ bool SCREEN_INFORMATION::IsMaximizedY() const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto status = STATUS_SUCCESS; - // If we're in conpty mode, suppress any immediate painting we might do - // during the resize. - if (gci.IsInVtIoMode()) - { - gci.GetVtIo()->BeginResize(); - } - auto endResize = wil::scope_exit([&] { - if (gci.IsInVtIoMode()) - { - gci.GetVtIo()->EndResize(); - } - }); - // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions Selection::Instance().ClearSelection(); diff --git a/src/host/ut_host/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp index 90d3fd1044b..611eb55f14c 100644 --- a/src/host/ut_host/ApiRoutinesTests.cpp +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -607,7 +607,7 @@ class ApiRoutinesTests auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever."); + si.GetTextBuffer().ResizeTraditional({ 5, 5 }); // Tests are run both with and without the DECSTBM margins set. This should not alter // the results, since ScrollConsoleScreenBuffer should not be affected by VT margins. diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 334d770d0b8..b7ea13e6dc6 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -2307,7 +2307,7 @@ void ScreenBufferTests::GetWordBoundary() // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); @@ -2383,7 +2383,7 @@ void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on) // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index d52dfc5ae69..d1c7395635f 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -1755,7 +1755,7 @@ void TextBufferTests::ResizeTraditional() auto expectedSpace = UNICODE_SPACE; std::wstring_view expectedSpaceView(&expectedSpace, 1); - VERIFY_SUCCEEDED(buffer.ResizeTraditional(newSize)); + 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."); { @@ -1821,7 +1821,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() _buffer->_SetFirstRowIndex(pos.y); // Perform resize to rotate the rows around - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(bufferSize)); + _buffer->ResizeTraditional(bufferSize); // Retrieve the text at the old and new positions. const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); @@ -1893,7 +1893,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() // Perform resize to trim off the row of the buffer that included the emoji til::size trimmedBufferSize{ bufferSize.width, bufferSize.height - 1 }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + _buffer->ResizeTraditional(trimmedBufferSize); } // This tests that columns removed from the buffer while resizing traditionally will also drop the high unicode @@ -1923,7 +1923,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() // Perform resize to trim off the column of the buffer that included the emoji til::size trimmedBufferSize{ bufferSize.width - 1, bufferSize.height }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + _buffer->ResizeTraditional(trimmedBufferSize); } void TextBufferTests::TestBurrito() diff --git a/src/inc/til/env.h b/src/inc/til/env.h index 36b682cfbd3..4d95f9f2f98 100644 --- a/src/inc/til/env.h +++ b/src/inc/til/env.h @@ -559,7 +559,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" get_vars_from_registry(HKEY_CURRENT_USER, fmt::format(til::details::vars::reg::user_volatile_session_env_var_root_pattern, NtCurrentTeb()->ProcessEnvironmentBlock->SessionId)); } - std::wstring to_string() + std::wstring to_string() const { std::wstring result; for (const auto& [name, value] : _envMap) diff --git a/src/inc/til/rle.h b/src/inc/til/rle.h index d94c3a744e6..34b6c929a0b 100644 --- a/src/inc/til/rle.h +++ b/src/inc/til/rle.h @@ -426,7 +426,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Returns the range [start_index, end_index) as a new vector. // It works just like std::string::substr(), but with absolute indices. - [[nodiscard]] basic_rle slice(size_type start_index, size_type end_index) const noexcept + [[nodiscard]] basic_rle slice(size_type start_index, size_type end_index) const { if (end_index > _total_length) { @@ -446,14 +446,14 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // --> It's safe to subtract 1 from end_index rle_scanner scanner(_runs.begin(), _runs.end()); - auto [begin_run, start_run_pos] = scanner.scan(start_index); - auto [end_run, end_run_pos] = scanner.scan(end_index - 1); + const auto [begin_run, start_run_pos] = scanner.scan(start_index); + const auto [end_run, end_run_pos] = scanner.scan(end_index - 1); container slice{ begin_run, end_run + 1 }; slice.back().length = end_run_pos + 1; slice.front().length -= start_run_pos; - return { std::move(slice), static_cast(end_index - start_index) }; + return { std::move(slice), gsl::narrow_cast(end_index - start_index) }; } // Replace the range [start_index, end_index) with the given value. @@ -463,7 +463,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { _check_indices(start_index, end_index); - const rle_type replacement{ value, static_cast(end_index - start_index) }; + const rle_type replacement{ value, gsl::narrow_cast(end_index - start_index) }; _replace_unchecked(start_index, end_index, { &replacement, 1 }); } @@ -651,7 +651,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" size_type total = 0; }; - basic_rle(container&& runs, size_type size) : + basic_rle(container&& runs, size_type size) noexcept : _runs(std::forward(runs)), _total_length(size) { diff --git a/src/interactivity/win32/find.cpp b/src/interactivity/win32/find.cpp index 93c4d55a8b4..4e7cf36a589 100644 --- a/src/interactivity/win32/find.cpp +++ b/src/interactivity/win32/find.cpp @@ -54,7 +54,7 @@ INT_PTR CALLBACK FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM l if (searcher.ResetIfStale(gci.renderData, lastFindString, reverse, caseInsensitive)) { - searcher.MovePastCurrentSelection(); + searcher.MoveToCurrentSelection(); } else { diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index b4ea4767010..376e93fe031 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -735,7 +735,7 @@ using namespace Microsoft::Console::Types; } break; } -#endif DBG +#endif // DBG case EVENT_CONSOLE_CARET: case EVENT_CONSOLE_UPDATE_REGION: diff --git a/src/project.inc b/src/project.inc index b604f50ea6d..b34fb10f2e3 100644 --- a/src/project.inc +++ b/src/project.inc @@ -3,6 +3,9 @@ # - Common Project Configuration # ------------------------------------- +# Pull our dependencies from vcpkg. This includes gsl - if you run into errors +# from a missing gsl, make sure you go build onecore/window/vcpkg first. + !include $(PROJECT_ROOT)\vcpkg\consume.inc # ------------------------------------- diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 2b8f0c0a905..4459c97a8e5 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -163,8 +163,9 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2& out) noexcept [[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept { const auto softFont = _api.s.write()->font.write(); - softFont->softFontPattern = std::vector(bitPattern.begin(), bitPattern.end()); - softFont->softFontCellSize = cellSize; + softFont->softFontPattern.assign(bitPattern.begin(), bitPattern.end()); + softFont->softFontCellSize.width = std::max(0, cellSize.width); + softFont->softFontCellSize.height = std::max(0, cellSize.height); return S_OK; } diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 478705b542e..e46bb250835 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -633,13 +633,12 @@ void AtlasEngine::_flushBufferLine() auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y]; - wil::com_ptr mappedFontFace; - #pragma warning(suppress : 26494) // Variable 'mappedEnd' is uninitialized. Always initialize an object (type.5). for (u32 idx = 0, mappedEnd; idx < _api.bufferLine.size(); idx = mappedEnd) { u32 mappedLength = 0; - _mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast(_api.bufferLine.size()) - idx, &mappedLength, mappedFontFace.put()); + wil::com_ptr mappedFontFace; + _mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast(_api.bufferLine.size()) - idx, &mappedLength, mappedFontFace.addressof()); mappedEnd = idx + mappedLength; if (!mappedFontFace) @@ -945,10 +944,6 @@ void AtlasEngine::_mapReplacementCharacter(u32 from, u32 to, ShapedRow& row) return; } - static constexpr auto isSoftFontChar = [](wchar_t ch) noexcept { - return ch >= 0xEF20 && ch < 0xEF80; - }; - auto pos1 = from; auto pos2 = pos1; size_t col1 = _api.bufferLineColumn[from]; diff --git a/src/renderer/atlas/Backend.h b/src/renderer/atlas/Backend.h index 5da33f6e304..0ca325a5383 100644 --- a/src/renderer/atlas/Backend.h +++ b/src/renderer/atlas/Backend.h @@ -69,11 +69,16 @@ namespace Microsoft::Console::Render::Atlas // std::clamp(T, T, T, Predicate) with std::less{} as the argument, // which introduces branching. While not perfect, this is still better than std::clamp. template - constexpr T clamp(T val, T min, T max) + constexpr T clamp(T val, T min, T max) noexcept { return val < min ? min : (max < val ? max : val); } + constexpr bool isSoftFontChar(wchar_t ch) noexcept + { + return ch >= 0xEF20 && ch < 0xEF80; + } + inline constexpr D2D1_RECT_F GlyphRunEmptyBounds{ 1e38f, 1e38f, -1e38f, -1e38f }; void GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds); diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 5598ce8d7c2..846567ca613 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -1498,7 +1498,7 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa if (!_softFontBitmap) { // Allocating such a tiny texture is very wasteful (min. texture size on GPUs - // right now is 64kB), but this is a seldomly used feature so it's fine... + // right now is 64kB), but this is a seldom used feature so it's fine... const D2D1_SIZE_U size{ static_cast(p.s->font->softFontCellSize.width), static_cast(p.s->font->softFontCellSize.height), @@ -1511,30 +1511,6 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, nullptr, 0, &bitmapProperties, _softFontBitmap.addressof())); } - { - const auto width = static_cast(p.s->font->softFontCellSize.width); - const auto height = static_cast(p.s->font->softFontCellSize.height); - - auto bitmapData = Buffer{ width * height }; - const auto glyphIndex = glyphEntry.glyphIndex - 0xEF20u; - auto src = p.s->font->softFontPattern.begin() + height * glyphIndex; - auto dst = bitmapData.begin(); - - for (size_t y = 0; y < height; y++) - { - auto srcBits = *src++; - for (size_t x = 0; x < width; x++) - { - const auto srcBitIsSet = (srcBits & 0x8000) != 0; - *dst++ = srcBitIsSet ? 0xffffffff : 0x00000000; - srcBits <<= 1; - } - } - - const auto pitch = static_cast(width * sizeof(u32)); - THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch)); - } - const auto interpolation = p.s->font->antialiasingMode == AntialiasingMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; const D2D1_RECT_F dest{ static_cast(rect.x), @@ -1544,6 +1520,7 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa }; _d2dBeginDrawing(); + _drawSoftFontGlyphInBitmap(p, glyphEntry); _d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr); glyphEntry.data.shadingType = static_cast(ShadingType::TextGrayscale); @@ -1563,6 +1540,51 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa return true; } +void BackendD3D::_drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const +{ + if (!isSoftFontChar(glyphEntry.glyphIndex)) + { + // AtlasEngine::_mapReplacementCharacter should have filtered inputs with isSoftFontChar already. + assert(false); + return; + } + + const auto width = static_cast(p.s->font->softFontCellSize.width); + const auto height = static_cast(p.s->font->softFontCellSize.height); + const auto& softFontPattern = p.s->font->softFontPattern; + + // The isSoftFontChar() range is [0xEF20,0xEF80). + const auto offset = glyphEntry.glyphIndex - 0xEF20u; + const auto offsetBeg = offset * height; + const auto offsetEnd = offsetBeg + height; + + if (offsetEnd > softFontPattern.size()) + { + // Out of range values should not occur, but they do and it's unknown why: GH#15553 + assert(false); + return; + } + + Buffer bitmapData{ width * height }; + auto dst = bitmapData.begin(); + auto it = softFontPattern.begin() + offsetBeg; + const auto end = softFontPattern.begin() + offsetEnd; + + while (it != end) + { + auto srcBits = *it++; + for (size_t x = 0; x < width; x++) + { + const auto srcBitIsSet = (srcBits & 0x8000) != 0; + *dst++ = srcBitIsSet ? 0xffffffff : 0x00000000; + srcBits <<= 1; + } + } + + const auto pitch = static_cast(width * sizeof(u32)); + THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch)); +} + void BackendD3D::_drawGlyphPrepareRetry(const RenderingPayload& p) { THROW_HR_IF_MSG(E_UNEXPECTED, _glyphAtlasMap.empty(), "BackendD3D::_drawGlyph deadlock"); diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index 266f070a350..14f4ca2943e 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -218,8 +218,9 @@ namespace Microsoft::Console::Render::Atlas void _drawText(RenderingPayload& p); ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y); ATLAS_ATTR_COLD static void _initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry); - ATLAS_ATTR_COLD [[nodiscard]] bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); + [[nodiscard]] ATLAS_ATTR_COLD bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); + void _drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const; void _drawGlyphPrepareRetry(const RenderingPayload& p); void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); void _drawGridlines(const RenderingPayload& p, u16 y); diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index c1a0d838852..14822351585 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -555,7 +555,8 @@ CATCH_RETURN(); { RETURN_IF_FAILED(_Write("\x1b[2t")); } - return _Flush(); + _Flush(); + return S_OK; } // Method Description: diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 1c7f3fe498f..7ef59bc03b0 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -106,26 +106,13 @@ CATCH_RETURN(); // - S_OK [[nodiscard]] HRESULT VtEngine::InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept { - // If we're in the middle of a resize request, don't try to immediately start a frame. - if (_inResizeRequest) - { - *pForcePaint = false; - } - else - { - *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 = circled; - } + *pForcePaint = true; - // If we flushed for any reason other than circling (i.e, a sequence that we - // didn't understand), we don't need to push the buffer out on EndPaint. - _noFlushOnEnd = !circled; + // Keep track of the fact that we circled, we'll need to do some work on + // end paint to specifically handle this. + _circled = circled; _trace.TraceTriggerCircling(*pForcePaint); - return S_OK; } diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index d261b1c1624..801390085c0 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -94,22 +94,7 @@ using namespace Microsoft::Console::Types; RETURN_IF_FAILED(_MoveCursor(_deferredCursorPos)); } - // If this frame was triggered because we encountered a VT sequence which - // required the buffered state to get printed, we don't want to flush this - // frame to the pipe. That might result in us rendering half the output of a - // particular frame (as emitted by the client). - // - // Instead, we'll leave this frame in _buffer, and just keep appending to - // it as needed. - if (_noFlushOnEnd) [[unlikely]] - { - _noFlushOnEnd = false; - } - else - { - RETURN_IF_FAILED(_Flush()); - } - + _Flush(); return S_OK; } diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index d5db02afba9..8408ee3aeb6 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -46,11 +46,9 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _circled(false), _firstPaint(true), _skipCursor(false), - _exitResult{ S_OK }, _terminalOwner{ nullptr }, _newBottomLine{ false }, _deferredCursorPos{ INVALID_COORDS }, - _inResizeRequest{ false }, _trace{}, _bufferLine{}, _buffer{}, @@ -136,25 +134,39 @@ CATCH_RETURN(); CATCH_RETURN(); } -[[nodiscard]] HRESULT VtEngine::_Flush() noexcept +void VtEngine::_Flush() noexcept +{ + if (!_corked && !_buffer.empty()) + { + _flushImpl(); + } +} + +// _corked is often true and separating _flushImpl() out allows _flush() to be inlined. +void VtEngine::_flushImpl() noexcept { if (_hFile) { - auto fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); + const auto fSuccess = WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); _buffer.clear(); if (!fSuccess) { - _exitResult = HRESULT_FROM_WIN32(GetLastError()); + LOG_LAST_ERROR(); _hFile.reset(); if (_terminalOwner) { _terminalOwner->CloseOutput(); } - return _exitResult; } } +} - return S_OK; +// The name of this method is an analogy to TCP_CORK. It instructs +// the VT renderer to stop flushing its buffer to the output pipe. +// Don't forget to uncork it! +void VtEngine::Cork(bool corked) noexcept +{ + _corked = corked; } // Method Description: @@ -425,7 +437,7 @@ void VtEngine::SetTerminalOwner(Microsoft::Console::VirtualTerminal::VtIo* const HRESULT VtEngine::RequestCursor() noexcept { RETURN_IF_FAILED(_RequestCursor()); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } @@ -446,34 +458,6 @@ HRESULT VtEngine::RequestCursor() noexcept return S_OK; } -// Method Description: -// - Tell the vt renderer to begin a resize operation. During a resize -// operation, the vt renderer should _not_ request to be repainted during a -// text buffer circling event. Any callers of this method should make sure to -// call EndResize to make sure the renderer returns to normal behavior. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtEngine::BeginResizeRequest() -{ - _inResizeRequest = true; -} - -// Method Description: -// - Tell the vt renderer to end a resize operation. -// See BeginResize for more details. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtEngine::EndResizeRequest() -{ - _inResizeRequest = false; -} - // Method Description: // - Configure the renderer for the resize quirk. This changes the behavior of // conpty to _not_ InvalidateAll the entire viewport on a resize operation. @@ -546,13 +530,20 @@ HRESULT VtEngine::RequestWin32Input() noexcept // in the connected terminal after passing through an RIS sequence. RETURN_IF_FAILED(_RequestWin32Input()); RETURN_IF_FAILED(_RequestFocusEventMode()); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } HRESULT VtEngine::SwitchScreenBuffer(const bool useAltBuffer) noexcept { RETURN_IF_FAILED(_SwitchScreenBuffer(useAltBuffer)); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } + +HRESULT VtEngine::RequestMouseMode(const bool enable) noexcept +{ + const auto status = _WriteFormatted(FMT_COMPILE("\x1b[?1003;1006{}"), enable ? 'h' : 'l'); + _Flush(); + return status; +} diff --git a/src/renderer/vt/tracing.cpp b/src/renderer/vt/tracing.cpp index 99a8b85ddaa..21f2bc67094 100644 --- a/src/renderer/vt/tracing.cpp +++ b/src/renderer/vt/tracing.cpp @@ -18,14 +18,14 @@ RenderTracing::RenderTracing() { #ifndef UNIT_TESTING TraceLoggingRegister(g_hConsoleVtRendererTraceProvider); -#endif UNIT_TESTING +#endif // UNIT_TESTING } RenderTracing::~RenderTracing() { #ifndef UNIT_TESTING TraceLoggingUnregister(g_hConsoleVtRendererTraceProvider); -#endif UNIT_TESTING +#endif // UNIT_TESTING } // Function Description: @@ -79,7 +79,7 @@ void RenderTracing::TraceStringFill(const size_t n, const char c) const #else UNREFERENCED_PARAMETER(n); UNREFERENCED_PARAMETER(c); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceString(const std::string_view& instr) const { @@ -96,7 +96,7 @@ void RenderTracing::TraceString(const std::string_view& instr) const } #else UNREFERENCED_PARAMETER(instr); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidate(const til::rect& invalidRect) const @@ -114,7 +114,7 @@ void RenderTracing::TraceInvalidate(const til::rect& invalidRect) const } #else UNREFERENCED_PARAMETER(invalidRect); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidateAll(const til::rect& viewport) const @@ -132,7 +132,7 @@ void RenderTracing::TraceInvalidateAll(const til::rect& viewport) const } #else UNREFERENCED_PARAMETER(viewport); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceTriggerCircling(const bool newFrame) const @@ -145,7 +145,7 @@ void RenderTracing::TraceTriggerCircling(const bool newFrame) const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); #else UNREFERENCED_PARAMETER(newFrame); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidateScroll(const til::point scroll) const @@ -215,7 +215,7 @@ void RenderTracing::TraceStartPaint(const bool quickReturn, UNREFERENCED_PARAMETER(scrollDelt); UNREFERENCED_PARAMETER(cursorMoved); UNREFERENCED_PARAMETER(wrappedRow); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceEndPaint() const @@ -226,7 +226,7 @@ void RenderTracing::TraceEndPaint() const TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceLastText(const til::point lastTextPos) const @@ -244,7 +244,7 @@ void RenderTracing::TraceLastText(const til::point lastTextPos) const } #else UNREFERENCED_PARAMETER(lastTextPos); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceScrollFrame(const til::point scrollDeltaPos) const @@ -262,7 +262,7 @@ void RenderTracing::TraceScrollFrame(const til::point scrollDeltaPos) const } #else UNREFERENCED_PARAMETER(scrollDeltaPos); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceMoveCursor(const til::point lastTextPos, const til::point cursor) const @@ -286,7 +286,7 @@ void RenderTracing::TraceMoveCursor(const til::point lastTextPos, const til::poi #else UNREFERENCED_PARAMETER(lastTextPos); UNREFERENCED_PARAMETER(cursor); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceWrapped() const @@ -302,7 +302,7 @@ void RenderTracing::TraceWrapped() const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceSetWrapped(const til::CoordType wrappedRow) const @@ -318,7 +318,7 @@ void RenderTracing::TraceSetWrapped(const til::CoordType wrappedRow) const } #else UNREFERENCED_PARAMETER(wrappedRow); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceClearWrapped() const @@ -334,7 +334,7 @@ void RenderTracing::TraceClearWrapped() const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TracePaintCursor(const til::point coordCursor) const @@ -352,5 +352,5 @@ void RenderTracing::TracePaintCursor(const til::point coordCursor) const } #else UNREFERENCED_PARAMETER(coordCursor); -#endif UNIT_TESTING +#endif // UNIT_TESTING } diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index a9f2c76cf93..15de6e0e760 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -90,6 +90,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT RequestWin32Input() noexcept; [[nodiscard]] virtual HRESULT SetWindowVisibility(const bool showOrHide) noexcept = 0; [[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer) noexcept; + [[nodiscard]] HRESULT RequestMouseMode(bool enable) noexcept; + void Cork(bool corked) noexcept; protected: wil::unique_hfile _hFile; @@ -127,11 +129,9 @@ namespace Microsoft::Console::Render bool _newBottomLine; til::point _deferredCursorPos; - HRESULT _exitResult; Microsoft::Console::VirtualTerminal::VtIo* _terminalOwner; Microsoft::Console::VirtualTerminal::RenderTracing _trace; - bool _inResizeRequest{ false }; std::optional _wrappedRow{ std::nullopt }; @@ -139,12 +139,13 @@ namespace Microsoft::Console::Render bool _resizeQuirk{ false }; bool _passthrough{ false }; - bool _noFlushOnEnd{ false }; + bool _corked{ false }; std::optional _newBottomLineBG{ std::nullopt }; [[nodiscard]] HRESULT _WriteFill(const size_t n, const char c) noexcept; [[nodiscard]] HRESULT _Write(std::string_view const str) noexcept; - [[nodiscard]] HRESULT _Flush() noexcept; + void _Flush() noexcept; + void _flushImpl() noexcept; template [[nodiscard]] HRESULT _WriteFormatted(S&& format, Args&&... args) diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 3c2e1ba4d76..c3192462052 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -2106,26 +2106,26 @@ class AdapterTest auto& stateMachine = *_testGetSet->_stateMachine; Log::Comment(L"Default tabs stops in 80-column mode"); - VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 })); + textBuffer.ResizeTraditional({ 80, 600 }); _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73\033\\"); Log::Comment(L"Default tabs stops in 132-column mode"); - VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 132, 600 })); + textBuffer.ResizeTraditional({ 132, 600 }); _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73/81/89/97/105/113/121/129\033\\"); Log::Comment(L"Custom tab stops in 80 columns"); - VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 })); + textBuffer.ResizeTraditional({ 80, 600 }); _testGetSet->_stateMachine->ProcessString(L"\033P2$t30/60/120/240\033\\"); _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); _testGetSet->ValidateInputEvent(L"\033P2$u30/60\033\\"); Log::Comment(L"After expanding width to 132 columns"); - VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 132, 600 })); + textBuffer.ResizeTraditional({ 132, 600 }); _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); _testGetSet->ValidateInputEvent(L"\033P2$u30/60/120\033\\"); - VERIFY_SUCCEEDED(textBuffer.ResizeTraditional({ 80, 600 })); + textBuffer.ResizeTraditional({ 80, 600 }); Log::Comment(L"Out of order tab stops"); stateMachine.ProcessString(L"\033P2$t44/22/66\033\\"); diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 820d22f8a90..1bb49cca7b2 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -18,8 +18,8 @@ namespace Microsoft::Console::VirtualTerminal bool isRightButtonDown; }; - static [[nodiscard]] OutputType MakeUnhandled() noexcept; - static [[nodiscard]] OutputType MakeOutput(const std::wstring_view& str); + [[nodiscard]] static OutputType MakeUnhandled() noexcept; + [[nodiscard]] static OutputType MakeOutput(const std::wstring_view& str); [[nodiscard]] OutputType HandleKey(const INPUT_RECORD& pInEvent); [[nodiscard]] OutputType HandleFocus(bool focused) const; [[nodiscard]] OutputType HandleMouse(til::point position, unsigned int button, short modifierKeyState, short delta, MouseButtonState state); @@ -74,9 +74,9 @@ namespace Microsoft::Console::VirtualTerminal bool _forceDisableWin32InputMode{ false }; [[nodiscard]] OutputType _makeCharOutput(wchar_t ch); - static [[nodiscard]] OutputType _makeEscapedOutput(wchar_t wch); - static [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key); - static [[nodiscard]] OutputType _searchWithModifier(const KEY_EVENT_RECORD& keyEvent); + [[nodiscard]] static OutputType _makeEscapedOutput(wchar_t wch); + [[nodiscard]] static OutputType _makeWin32Output(const KEY_EVENT_RECORD& key); + [[nodiscard]] static OutputType _searchWithModifier(const KEY_EVENT_RECORD& keyEvent); #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp @@ -92,9 +92,9 @@ namespace Microsoft::Console::VirtualTerminal #pragma endregion #pragma region MouseInput - static [[nodiscard]] OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); - static [[nodiscard]] OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); - static [[nodiscard]] OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta); + [[nodiscard]] static OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + [[nodiscard]] static OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + [[nodiscard]] static OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta); [[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const; diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 1f56c04b535..af5ebb431ea 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -1335,7 +1335,7 @@ til::point UiaTextRangeBase::_getDocumentEnd() const { const auto optimizedBufferSize{ _getOptimizedBufferSize() }; const auto& buffer{ _pData->GetTextBuffer() }; - const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(optimizedBufferSize) }; + const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(&optimizedBufferSize) }; const auto cursorPos{ buffer.GetCursor().GetPosition() }; return { optimizedBufferSize.Left(), std::max(lastCharPos.y, cursorPos.y) + 1 }; }