From 1a40ff3746ba927a11b0f0ab9db3621f7cc89bf5 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 25 Jul 2023 15:19:41 -0500 Subject: [PATCH 01/59] Switch away from the WinDev agent pools (#15755) Using our own pools like this gives us a lot of freedom in the tooling that's installed, the OS versions it targets, and when we take on Visual Studio updates. As part of this effort, I've also stood up a "small" agent pool. At the time of this PR, that pool is using D2ads-v5 SKU VMs (2 vcore 8 GiB) versus the "large" agent pool's D8as-v5 (8 vcore 32 GiB). Smaller build tasks have been moved over to the small pool. Compilation's the hard part, so it gets to stay on the large pool. --- build/pipelines/release.yml | 7 +++++-- build/pipelines/templates/build-console-audit-job.yml | 6 +++--- build/pipelines/templates/build-console-ci.yml | 6 +++--- build/pipelines/templates/build-console-fuzzing.yml | 6 +++--- build/pipelines/templates/build-console-pgo.yml | 6 +++--- build/pipelines/templates/test-console-ci.yml | 6 +++--- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 8429f0abe0f..cea5c7d0fc5 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -3,8 +3,8 @@ trigger: none pr: none pool: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-S # By default, send jobs to the small agent pool. + demands: ImageOverride -equals SHINE-VS17-Latest parameters: - name: branding @@ -91,6 +91,9 @@ resources: ref: main jobs: - job: Build + pool: + name: SHINE-INT-L # Run the compilation on the large agent pool, rather than the default small one. + demands: ImageOverride -equals SHINE-VS17-Latest strategy: matrix: ${{ each config in parameters.buildConfigurations }}: diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml index 1ed556eff8f..69b0b05ce31 100644 --- a/build/pipelines/templates/build-console-audit-job.yml +++ b/build/pipelines/templates/build-console-audit-job.yml @@ -10,10 +10,10 @@ jobs: BuildPlatform: ${{ parameters.platform }} pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPoolOSS-L + name: SHINE-OSS-L ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-L + demands: ImageOverride -equals SHINE-VS17-Latest steps: - checkout: self diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml index 0ccf90f10f6..48c27052e29 100644 --- a/build/pipelines/templates/build-console-ci.yml +++ b/build/pipelines/templates/build-console-ci.yml @@ -14,10 +14,10 @@ jobs: EnableRichCodeNavigation: true pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPoolOSS-L + name: SHINE-OSS-L ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-L + demands: ImageOverride -equals SHINE-VS17-Latest steps: - template: build-console-steps.yml diff --git a/build/pipelines/templates/build-console-fuzzing.yml b/build/pipelines/templates/build-console-fuzzing.yml index e115fbb3751..8277ab9dc3a 100644 --- a/build/pipelines/templates/build-console-fuzzing.yml +++ b/build/pipelines/templates/build-console-fuzzing.yml @@ -11,10 +11,10 @@ jobs: BuildPlatform: ${{ parameters.platform }} pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPoolOSS-L + name: SHINE-OSS-L ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-L + demands: ImageOverride -equals SHINE-VS17-Latest steps: - checkout: self diff --git a/build/pipelines/templates/build-console-pgo.yml b/build/pipelines/templates/build-console-pgo.yml index 1187146ccf2..b1b4c501633 100644 --- a/build/pipelines/templates/build-console-pgo.yml +++ b/build/pipelines/templates/build-console-pgo.yml @@ -14,10 +14,10 @@ jobs: PGOBuildMode: 'Instrument' pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPoolOSS-L + name: SHINE-OSS-L ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-L + demands: ImageOverride -equals SHINE-VS17-Latest steps: - template: build-console-steps.yml diff --git a/build/pipelines/templates/test-console-ci.yml b/build/pipelines/templates/test-console-ci.yml index b2eea094f13..66357e102bb 100644 --- a/build/pipelines/templates/test-console-ci.yml +++ b/build/pipelines/templates/test-console-ci.yml @@ -13,10 +13,10 @@ jobs: BuildPlatform: ${{ parameters.platform }} pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPoolOSS-L + name: SHINE-OSS-L ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest + name: SHINE-INT-L + demands: ImageOverride -equals SHINE-VS17-Latest steps: - checkout: self From d70794a24607e02313f101d727c01fa1974bdcc6 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Tue, 25 Jul 2023 13:42:47 -0700 Subject: [PATCH 02/59] Add UIA element grouping to SettingsContainer (#15756) Adds an `AutomationProperty.Name` to the main grid in the `SettingContainer`. Doing so makes it so that the group of elements is considered a "group \". Now, when navigating with a screen reader, when you enter the group of elements, the "group \" will be presented. Thus, if the user navigates to the "reset" button, it'll be prefaced with a "group \" announcement first. If the user navigates to it from the other direction (the setting control), this announcement isn't made, but the user already has an understanding of what group of settings they're in, which is standard practice. Closes #15158 --- src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml index 5efeceeaece..9803220c6b3 100644 --- a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml +++ b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml @@ -106,7 +106,8 @@ - + From cbe915c554f24da406f12bbcc8b65baa17d7ed92 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 26 Jul 2023 18:22:18 -0500 Subject: [PATCH 03/59] Add support for ARM64 testing, reintroduce it in CI (#15761) This pull request introduces the arm64 testing agents and a few build phases to use them. In addition to running the ARM64 tests in CI, it makes the following changes: - The x64 tests now run on equivalent x64 testing agents - We now run ARM64 builds (and tests!) on all pull requests - I've deduplicated a lot of the build and test stages - New queue-time parameters have been added to control various phases, for quick pipeline testing - A bunch of conditions have been promoted to compile-time checks to control the existence of stages and steps more tightly --- build/pipelines/ci.yml | 126 +++++++++--------- build/pipelines/templates/test-console-ci.yml | 36 ++--- 2 files changed, 82 insertions(+), 80 deletions(-) diff --git a/build/pipelines/ci.yml b/build/pipelines/ci.yml index 7e225d7cb89..3b833d8e718 100644 --- a/build/pipelines/ci.yml +++ b/build/pipelines/ci.yml @@ -29,64 +29,36 @@ variables: # 0.0.1904.0900 name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) -stages: - - stage: Audit_x64 - displayName: Audit Mode - dependsOn: [] - condition: succeeded() - jobs: - - template: ./templates/build-console-audit-job.yml - parameters: - platform: x64 - - - stage: Build_x64 - displayName: Build x64 - dependsOn: [] - condition: succeeded() - jobs: - - template: ./templates/build-console-ci.yml - parameters: - platform: x64 - - stage: Build_x86 - displayName: Build x86 - dependsOn: [] - jobs: - - template: ./templates/build-console-ci.yml - parameters: - platform: x86 - - stage: Build_ARM64 - displayName: Build ARM64 - dependsOn: [] - condition: not(eq(variables['Build.Reason'], 'PullRequest')) - jobs: - - template: ./templates/build-console-ci.yml - parameters: - platform: ARM64 - - - stage: Test_x64 - displayName: Test x64 - dependsOn: [Build_x64] - condition: succeeded() - jobs: - - template: ./templates/test-console-ci.yml - parameters: - platform: x64 - - stage: Test_x86 - displayName: Test x86 - dependsOn: [Build_x86] - jobs: - - template: ./templates/test-console-ci.yml - parameters: - platform: x86 +parameters: + - name: auditMode + displayName: "Build in Audit Mode (x64)" + type: boolean + default: true + - name: runTests + displayName: "Run Unit and Feature Tests" + type: boolean + default: true + - name: submitHelix + displayName: "Submit to Helix for testing and PGO" + type: boolean + default: true + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 - - stage: Helix_x64 - displayName: Helix x64 - dependsOn: [Build_x64] - condition: and(succeeded(), not(eq(variables['Build.Reason'], 'PullRequest'))) - jobs: - - template: ./templates/console-ci-helix-job.yml - parameters: - platform: x64 +stages: + - ${{ if eq(parameters.auditMode, true) }}: + - stage: Audit_x64 + displayName: Audit Mode + dependsOn: [] + condition: succeeded() + jobs: + - template: ./templates/build-console-audit-job.yml + parameters: + platform: x64 - stage: Scripts displayName: Code Health Scripts @@ -95,10 +67,38 @@ stages: jobs: - template: ./templates/check-formatting.yml + - ${{ each platform in parameters.buildPlatforms }}: + - stage: Build_${{ platform }} + displayName: Build ${{ platform }} + dependsOn: [] + condition: succeeded() + jobs: + - template: ./templates/build-console-ci.yml + parameters: + platform: ${{ platform }} + - ${{ if eq(parameters.runTests, true) }}: + - stage: Test_${{ platform }} + displayName: Test ${{ platform }} + dependsOn: + - Build_${{ platform }} + condition: succeeded() + jobs: + - template: ./templates/test-console-ci.yml + parameters: + platform: ${{ platform }} - - stage: CodeIndexer - displayName: Github CodeNav Indexer - dependsOn: [Build_x64] - condition: and(succeeded(), not(eq(variables['Build.Reason'], 'PullRequest'))) - jobs: - - template: ./templates/codenav-indexer.yml + - ${{ if and(containsValue(parameters.buildPlatforms, 'x64'), eq(parameters.submitHelix, true), ne(variables['Build.Reason'], 'PullRequest')) }}: + - stage: Helix_x64 + displayName: Helix x64 + dependsOn: [Build_x64] + jobs: + - template: ./templates/console-ci-helix-job.yml + parameters: + platform: x64 + + - ${{ if and(containsValue(parameters.buildPlatforms, 'x64'), ne(variables['Build.Reason'], 'PullRequest')) }}: + - stage: CodeIndexer + displayName: Github CodeNav Indexer + dependsOn: [Build_x64] + jobs: + - template: ./templates/codenav-indexer.yml diff --git a/build/pipelines/templates/test-console-ci.yml b/build/pipelines/templates/test-console-ci.yml index 66357e102bb..5a7db6475bd 100644 --- a/build/pipelines/templates/test-console-ci.yml +++ b/build/pipelines/templates/test-console-ci.yml @@ -13,14 +13,19 @@ jobs: BuildPlatform: ${{ parameters.platform }} pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-OSS-L + ${{ if ne(parameters.platform, 'ARM64') }}: + name: SHINE-OSS-Testing-x64 + ${{ else }}: + name: SHINE-OSS-Testing-arm64 ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-INT-L - demands: ImageOverride -equals SHINE-VS17-Latest + ${{ if ne(parameters.platform, 'ARM64') }}: + name: SHINE-INT-Testing-x64 + ${{ else }}: + name: SHINE-INT-Testing-arm64 steps: - checkout: self - submodules: true + submodules: false clean: true fetchDepth: 1 @@ -43,15 +48,16 @@ jobs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(System.ArtifactsDirectory)\\${{ parameters.artifactName }}\\$(BuildConfiguration)\\$(BuildPlatform)\\test" - condition: and(and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - - task: PowerShell@2 - displayName: 'Run Feature Tests (x64 only)' - inputs: - targetType: filePath - filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(System.ArtifactsDirectory)\\${{ parameters.artifactName }}\\$(BuildConfiguration)\\$(BuildPlatform)\\test" - condition: and(and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')), eq(variables['BuildPlatform'], 'x64')) + - ${{ if or(eq(parameters.platform, 'x64'), eq(parameters.platform, 'arm64')) }}: + - task: PowerShell@2 + displayName: 'Run Feature Tests' + inputs: + targetType: filePath + filePath: build\scripts\Run-Tests.ps1 + arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(System.ArtifactsDirectory)\\${{ parameters.artifactName }}\\$(BuildConfiguration)\\$(BuildPlatform)\\test" + condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - task: PowerShell@2 displayName: 'Convert Test Logs from WTL to xUnit format' @@ -59,7 +65,7 @@ jobs: targetType: filePath filePath: build\Helix\ConvertWttLogToXUnit.ps1 arguments: -WttInputPath '${{ parameters.testLogPath }}' -WttSingleRerunInputPath 'unused.wtl' -WttMultipleRerunInputPath 'unused2.wtl' -XUnitOutputPath 'onBuildMachineResults.xml' -TestNamePrefix '$(BuildConfiguration).$(BuildPlatform)' - condition: and(ne(variables['PGOBuildMode'], 'Instrument'),or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + condition: ne(variables['PGOBuildMode'], 'Instrument') - task: PublishTestResults@2 displayName: 'Upload converted test logs' @@ -67,13 +73,9 @@ jobs: inputs: testResultsFormat: 'xUnit' # Options: JUnit, NUnit, VSTest, xUnit, cTest testResultsFiles: '**/onBuildMachineResults.xml' - #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional - #mergeTestResults: false # Optional - #failTaskOnFailedTests: false # Optional testRunTitle: 'On Build Machine Tests' # Optional buildPlatform: $(BuildPlatform) # Optional buildConfiguration: $(BuildConfiguration) # Optional - #publishRunAttachments: true # Optional - task: CopyFiles@2 displayName: 'Copy result logs to Artifacts' From 1f9426b051badd2ad872ca764643fbffc7861545 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 26 Jul 2023 16:25:19 -0700 Subject: [PATCH 04/59] Fix Default Terminal and Color Scheme ComboBoxes cropping at 200% Text Size (#15762) When the OS' "text size" setting gets set to 200% and the display resolution is reduced quite a bit, we get some cropped text in the SUI's Default Terminal ComboBox. Turns out, we have a height set on the items. I went ahead and removed that so we don't crop the text. Everything looks good still! A similar issue occurs in the Profile > Appearance > Color Scheme ComboBox. I went ahead and fixed that too by removing the height restriction. Other minor changes: - fixed the comments - changed "author and version" row to "auto" instead of "*" (star sizing is great for proportional sizing, so we're not really taking advantage of it) Closes #15149 --- .../TerminalSettingsEditor/Appearances.xaml | 3 +-- src/cascadia/TerminalSettingsEditor/Launch.xaml | 17 +++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index ec20d9e418b..e37cdd2e1bf 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -59,8 +59,7 @@ - - - + + + - + - - + + - + Date: Wed, 26 Jul 2023 17:13:49 -0700 Subject: [PATCH 05/59] Fix matches of multiple schemas on "colorScheme" (#15748) Adds proper `type` for `SchemePair` definition to avoid warnings about matches of multiple schemas. Same fix as https://github.com/microsoft/terminal/pull/4045 ## Validation Steps Performed - Pointed $schema to local file instead of https://aka.ms/terminal-profiles-schema - Confirmed warning goes away when using a string - Confirmed using the light/dark object format still passes validation - Confirmed values like `"colorScheme": 3` no longer incorrectly pass validation whereas they would before --- doc/cascadia/profiles.schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index d3ccfa8d122..c681ef29267 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -260,7 +260,8 @@ "description": "Name of the scheme to use when the app is using dark theme", "type": "string" } - } + }, + "type": "object" }, "FontConfig": { "properties": { From a4340af1e79914c11cd3f268b34650f050557a2b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 1 Aug 2023 02:00:57 +0200 Subject: [PATCH 06/59] Add text based cursor movement helpers (#15779) `COOKED_READ_DATA` is a little special and requires cursor navigation based on the raw (buffered) text contents instead of what's in the text buffer. This requires the introduction of new helper functions to implement such cursor navigation. They're made part of `TextBuffer` as these helpers will get support graphemes in the future. It also helps keeping it close to `TextBuffer` as the cursor navigation should optimally behave identical between the two. Part of #8000. --- src/buffer/out/textBuffer.cpp | 17 ++++++++++--- src/buffer/out/textBuffer.hpp | 4 ++- src/inc/til/unicode.h | 34 ++++++++++++++------------ src/terminal/adapter/adaptDispatch.cpp | 2 +- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index c9269f08c43..8ab633361fd 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -441,11 +441,20 @@ void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute } } -void TextBuffer::ConsumeGrapheme(std::wstring_view& chars) noexcept +// Given the character offset `position` in the `chars` string, this function returns the starting position of the next grapheme. +// For instance, given a `chars` of L"x\uD83D\uDE42y" and a `position` of 1 it'll return 3. +// GraphemePrev would do the exact inverse of this operation. +// In the future, these functions are expected to also deliver information about how many columns a grapheme occupies. +// (I know that mere UTF-16 code point iteration doesn't handle graphemes, but that's what we're working towards.) +size_t TextBuffer::GraphemeNext(const std::wstring_view& chars, size_t position) noexcept { - // This function is supposed to mirror the behavior of ROW::Write, when it reads characters off of `chars`. - // (I know that a UTF-16 code point is not a grapheme, but that's what we're working towards.) - chars = til::utf16_pop(chars); + return til::utf16_iterate_next(chars, position); +} + +// It's the counterpart to GraphemeNext. See GraphemeNext. +size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) noexcept +{ + return til::utf16_iterate_prev(chars, position); } // This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row. diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 49c8bc5f29e..855aaca7ff3 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -131,8 +131,10 @@ class TextBuffer final TextBufferTextIterator GetTextLineDataAt(const til::point at) const; TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const; + static size_t GraphemeNext(const std::wstring_view& chars, size_t position) noexcept; + static size_t GraphemePrev(const std::wstring_view& chars, size_t position) noexcept; + // Text insertion functions - static void ConsumeGrapheme(std::wstring_view& chars) noexcept; void Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state); void FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes); diff --git a/src/inc/til/unicode.h b/src/inc/til/unicode.h index 5191e236c31..9f703dcb813 100644 --- a/src/inc/til/unicode.h +++ b/src/inc/til/unicode.h @@ -59,28 +59,30 @@ namespace til return { ptr, len }; } - // Removes the first code point off of `wstr` and returns the rest. - constexpr std::wstring_view utf16_pop(std::wstring_view wstr) noexcept + // Returns the index of the next codepoint in the given wstr (i.e. after the codepoint that idx points at). + constexpr size_t utf16_iterate_next(const std::wstring_view& wstr, size_t idx) noexcept { - auto it = wstr.begin(); - const auto end = wstr.end(); - - if (it != end) + if (idx < wstr.size() && is_leading_surrogate(til::at(wstr, idx++))) { - const auto wch = *it; - ++it; - - if (is_surrogate(wch)) + if (idx < wstr.size() && is_trailing_surrogate(til::at(wstr, idx))) { - const auto wch2 = it != end ? *it : wchar_t{}; - if (is_leading_surrogate(wch) && is_trailing_surrogate(wch2)) - { - ++it; - } + ++idx; } } + return idx; + } - return { it, end }; + // Returns the index of the preceding codepoint in the given wstr (i.e. in front of the codepoint that idx points at). + constexpr size_t utf16_iterate_prev(const std::wstring_view& wstr, size_t idx) noexcept + { + if (idx > 0 && is_trailing_surrogate(til::at(wstr, --idx))) + { + if (idx > 0 && is_leading_surrogate(til::at(wstr, idx - 1))) + { + --idx; + } + } + return idx; } // Splits a UTF16 string into codepoints, yielding `wstring_view`s of UTF16 text. Use it as: diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 3282a2eae90..04ce77e46cd 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -176,7 +176,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // we tried writing a wide glyph into the last column which can't work. if (textPositionBefore == textPositionAfter && (state.columnBegin == 0 || !wrapAtEOL)) { - textBuffer.ConsumeGrapheme(state.text); + state.text = state.text.substr(textBuffer.GraphemeNext(state.text, 0)); } if (wrapAtEOL) From 07566fac4c2fe2ae49ad9831befedc15628b575c Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 1 Aug 2023 18:13:51 +0200 Subject: [PATCH 07/59] Merge ReadConsoleA/W API routines (#15780) This is a minor cleanup to deduplicate the two ReadConsole methods and will help with making changes to how `COOKED_READ_DATA` is called. It additionally changes the initial data payload from a `string_view` to a `wstring_view` as it is guaranteed to be `wchar_t`. This matches the current `COOKED_READ_DATA` implementation which blindly assumes that the initial data consists of `wchar_t`. Closes #5618 --- .../ConptyRoundtripTests.cpp | 3 +- src/host/ApiRoutines.h | 32 +++----- src/host/VtApiRoutines.cpp | 49 +++--------- src/host/VtApiRoutines.h | 32 +++----- src/host/readDataCooked.cpp | 8 +- src/host/readDataCooked.hpp | 2 +- src/host/stream.cpp | 79 ++++++------------- src/inc/test/CommonState.hpp | 2 +- src/server/ApiDispatchers.cpp | 33 +++----- src/server/IApiRoutines.h | 32 +++----- 10 files changed, 87 insertions(+), 185 deletions(-) diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 9f267746511..76065f93c95 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -2891,8 +2891,7 @@ void ConptyRoundtripTests::TestResizeWithCookedRead() m_state->PrepareReadHandle(); // TODO GH#5618: This string will get mangled, but we don't really care // about the buffer contents in this test, so it doesn't really matter. - const std::string_view cookedReadContents{ "This is some cooked read data" }; - m_state->PrepareCookedReadData(cookedReadContents); + m_state->PrepareCookedReadData(L"This is some cooked read data"); Log::Comment(L"Painting the frame"); VERIFY_SUCCEEDED(renderer.PaintFrame()); diff --git a/src/host/ApiRoutines.h b/src/host/ApiRoutines.h index 3492ae234a8..a46f1397ec6 100644 --- a/src/host/ApiRoutines.h +++ b/src/host/ApiRoutines.h @@ -57,27 +57,17 @@ class ApiRoutines : public IApiRoutines const bool IsPeek, std::unique_ptr& waiter) noexcept override; - [[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; + [[nodiscard]] HRESULT ReadConsoleImpl(IConsoleInputObject& context, + std::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::wstring_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept override; [[nodiscard]] HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, diff --git a/src/host/VtApiRoutines.cpp b/src/host/VtApiRoutines.cpp index ff71f8c3a5b..fe7d0b7060e 100644 --- a/src/host/VtApiRoutines.cpp +++ b/src/host/VtApiRoutines.cpp @@ -119,42 +119,19 @@ void VtApiRoutines::_SynchronizeCursor(std::unique_ptr& waiter) no return hr; } -[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - const auto hr = m_pUsualRoutines->ReadConsoleAImpl(context, buffer, written, waiter, initialData, exeName, readHandleState, clientHandle, controlWakeupMask, controlKeyState); - // If we're about to tell the caller to wait, let's synchronize the cursor we have with what - // the terminal is presenting in case there's a cooked read going on. - // TODO GH10001: we only need to do this in cooked read mode. - if (clientHandle) - { - m_listeningForDSR = true; - (void)m_pVtEngine->_ListenForDSR(); - (void)m_pVtEngine->RequestCursor(); - } - return hr; -} - -[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleWImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - const auto hr = m_pUsualRoutines->ReadConsoleWImpl(context, buffer, written, waiter, initialData, exeName, readHandleState, clientHandle, controlWakeupMask, controlKeyState); +[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleImpl(IConsoleInputObject& context, + std::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::wstring_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept +{ + const auto hr = m_pUsualRoutines->ReadConsoleImpl(context, buffer, written, waiter, initialData, exeName, readHandleState, IsUnicode, clientHandle, controlWakeupMask, controlKeyState); // If we're about to tell the caller to wait, let's synchronize the cursor we have with what // the terminal is presenting in case there's a cooked read going on. // TODO GH10001: we only need to do this in cooked read mode. diff --git a/src/host/VtApiRoutines.h b/src/host/VtApiRoutines.h index 8c3cbf917ef..7dc77f78b09 100644 --- a/src/host/VtApiRoutines.h +++ b/src/host/VtApiRoutines.h @@ -60,27 +60,17 @@ class VtApiRoutines : public IApiRoutines const bool IsPeek, std::unique_ptr& waiter) noexcept override; - [[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; + [[nodiscard]] HRESULT ReadConsoleImpl(IConsoleInputObject& context, + std::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::wstring_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept override; [[nodiscard]] HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 6da7ed899ab..75e57b55963 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -50,7 +50,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, _In_ char* UserBuffer, _In_ ULONG CtrlWakeupMask, _In_ const std::wstring_view exeName, - _In_ const std::string_view initialData, + _In_ const std::wstring_view initialData, _In_ ConsoleProcessHandle* const pClientProcess) : ReadData(pInputBuffer, pInputReadHandleData), _screenInfo{ screenInfo }, @@ -97,11 +97,11 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, if (!initialData.empty()) { - memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size()); + memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size() * 2); - _bytesRead += initialData.size(); + _bytesRead += initialData.size() * 2; - const auto cchInitialData = initialData.size() / sizeof(wchar_t); + const auto cchInitialData = initialData.size(); VisibleCharCount() = cchInitialData; _bufPtr += cchInitialData; _currentPosition = cchInitialData; diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index a263303e45d..be2e8645bc1 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -40,7 +40,7 @@ class COOKED_READ_DATA final : public ReadData _In_ char* UserBuffer, _In_ ULONG CtrlWakeupMask, _In_ const std::wstring_view exeName, - _In_ const std::string_view initialData, + _In_ const std::wstring_view initialData, _In_ ConsoleProcessHandle* const pClientProcess); ~COOKED_READ_DATA() override; diff --git a/src/host/stream.cpp b/src/host/stream.cpp index 9ecccd2fadb..c98ef9eaf08 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -354,7 +354,7 @@ NT_CATCH_RETURN() std::span buffer, size_t& bytesRead, DWORD& controlKeyState, - const std::string_view initialData, + const std::wstring_view initialData, const DWORD ctrlWakeupMask, INPUT_READ_HANDLE_DATA& readHandleState, const std::wstring_view exeName, @@ -492,7 +492,7 @@ NT_CATCH_RETURN() std::span buffer, size_t& bytesRead, ULONG& controlKeyState, - const std::string_view initialData, + const std::wstring_view initialData, const DWORD ctrlWakeupMask, INPUT_READ_HANDLE_DATA& readHandleState, const std::wstring_view exeName, @@ -552,60 +552,29 @@ NT_CATCH_RETURN() CATCH_RETURN(); } -[[nodiscard]] HRESULT ApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept +[[nodiscard]] HRESULT ApiRoutines::ReadConsoleImpl(IConsoleInputObject& context, + std::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::wstring_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept { - try - { - return HRESULT_FROM_NT(DoReadConsole(context, - clientHandle, - buffer, - written, - controlKeyState, - initialData, - controlWakeupMask, - readHandleState, - exeName, - false, - waiter)); - } - CATCH_RETURN(); -} - -[[nodiscard]] HRESULT ApiRoutines::ReadConsoleWImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - try - { - return HRESULT_FROM_NT(DoReadConsole(context, - clientHandle, - buffer, - written, - controlKeyState, - initialData, - controlWakeupMask, - readHandleState, - exeName, - true, - waiter)); - } - CATCH_RETURN(); + return HRESULT_FROM_NT(DoReadConsole(context, + clientHandle, + buffer, + written, + controlKeyState, + initialData, + controlWakeupMask, + readHandleState, + exeName, + IsUnicode, + waiter)); } void UnblockWriteConsole(const DWORD dwReason) diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp index 942d0c6007e..9a0e1272b7d 100644 --- a/src/inc/test/CommonState.hpp +++ b/src/inc/test/CommonState.hpp @@ -144,7 +144,7 @@ class CommonState delete gci.pInputBuffer; } - void PrepareCookedReadData(const std::string_view initialData = {}) + void PrepareCookedReadData(const std::wstring_view initialData = {}) { CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); auto* readData = new COOKED_READ_DATA(gci.pInputBuffer, diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index ce3b81b8212..01f4bcac8df 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -276,17 +276,23 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) const std::wstring_view exeView(pwsExeName.get(), cchExeName); // 2. Existing data in the buffer that was passed in. - const auto cbInitialData = a->InitialNumBytes; std::unique_ptr pbInitialData; + std::wstring_view initialData; try { + const auto cbInitialData = a->InitialNumBytes; if (cbInitialData > 0) { + // InitialNumBytes is only supported for ReadConsoleW (via CONSOLE_READCONSOLE_CONTROL::nInitialChars). + RETURN_HR_IF(E_INVALIDARG, !a->Unicode); + pbInitialData = std::make_unique(cbInitialData); // This parameter starts immediately after the exe name so skip by that many bytes. RETURN_IF_FAILED(m->ReadMessageInput(cbExeName, pbInitialData.get(), cbInitialData)); + + initialData = { reinterpret_cast(pbInitialData.get()), cbInitialData / sizeof(wchar_t) }; } } CATCH_RETURN(); @@ -301,37 +307,18 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) std::unique_ptr waiter; size_t cbWritten; - HRESULT hr; - if (a->Unicode) - { - const std::string_view initialData(pbInitialData.get(), cbInitialData); - const std::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); - hr = m->_pApiRoutines->ReadConsoleWImpl(*pInputBuffer, - outputBuffer, - cbWritten, // We must set the reply length in bytes. - waiter, - initialData, - exeView, - *pInputReadHandleData, - hConsoleClient, - a->CtrlWakeupMask, - a->ControlKeyState); - } - else - { - const std::string_view initialData(pbInitialData.get(), cbInitialData); - const std::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); - hr = m->_pApiRoutines->ReadConsoleAImpl(*pInputBuffer, + const std::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); + auto hr = m->_pApiRoutines->ReadConsoleImpl(*pInputBuffer, outputBuffer, cbWritten, // We must set the reply length in bytes. waiter, initialData, exeView, *pInputReadHandleData, + a->Unicode, hConsoleClient, a->CtrlWakeupMask, a->ControlKeyState); - } LOG_IF_FAILED(SizeTToULong(cbWritten, &a->NumBytes)); diff --git a/src/server/IApiRoutines.h b/src/server/IApiRoutines.h index a5d691bf149..87830795c6c 100644 --- a/src/server/IApiRoutines.h +++ b/src/server/IApiRoutines.h @@ -74,27 +74,17 @@ class IApiRoutines const bool IsPeek, std::unique_ptr& waiter) noexcept = 0; - [[nodiscard]] virtual HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - std::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept = 0; + [[nodiscard]] virtual HRESULT ReadConsoleImpl(IConsoleInputObject& context, + std::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::wstring_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, From 2d7066f5c60a51618649eb5a73a7f48901041714 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 1 Aug 2023 21:27:08 +0200 Subject: [PATCH 08/59] Remove IsValidStringBuffer helper (#15781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change is a fairly subjective one. It was done because `IsValidStringBuffer` will very soon be the only function left in `cmdline.cpp`. Removing it allows removing `cmdline.cpp`. While the code that replaces it is somewhat tricky, it's also much more straightforward, as the `IsValidStringBuffer` function didn't just check if the string buffer is valid - it also retrieved the pointers to each of the strings contained in the buffer. ## Validation Steps Performed Exhaustively covered by conhost feature tests ✅ --- src/host/cmdline.cpp | 45 ------------------ src/host/cmdline.h | 57 +---------------------- src/host/conddkrefs.h | 2 - src/server/ApiDispatchers.cpp | 86 ++++++++++++++++++----------------- 4 files changed, 46 insertions(+), 144 deletions(-) diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp index 4b48f4bd24b..b39040c7cd4 100644 --- a/src/host/cmdline.cpp +++ b/src/host/cmdline.cpp @@ -27,51 +27,6 @@ #pragma hdrstop using Microsoft::Console::Interactivity::ServiceLocator; -// Routine Description: -// - This routine validates a string buffer and returns the pointers of where the strings start within the buffer. -// Arguments: -// - Unicode - Supplies a boolean that is TRUE if the buffer contains Unicode strings, FALSE otherwise. -// - Buffer - Supplies the buffer to be validated. -// - Size - Supplies the size, in bytes, of the buffer to be validated. -// - Count - Supplies the expected number of strings in the buffer. -// ... - Supplies a pair of arguments per expected string. The first one is the expected size, in bytes, of the string -// and the second one receives a pointer to where the string starts. -// Return Value: -// - TRUE if the buffer is valid, FALSE otherwise. -bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...) -{ - va_list Marker; - va_start(Marker, Count); - - while (Count > 0) - { - const auto StringSize = va_arg(Marker, ULONG); - const auto StringStart = va_arg(Marker, PVOID*); - - // Make sure the string fits in the supplied buffer and that it is properly aligned. - if (StringSize > Size) - { - break; - } - - if (Unicode && (StringSize % sizeof(WCHAR)) != 0) - { - break; - } - - *StringStart = Buffer; - - // Go to the next string. - Buffer = RtlOffsetToPointer(Buffer, StringSize); - Size -= StringSize; - Count -= 1; - } - - va_end(Marker); - - return Count == 0; -} - // Routine Description: // - Detects Word delimiters bool IsWordDelim(const wchar_t wch) diff --git a/src/host/cmdline.h b/src/host/cmdline.h index a8669edadde..34286d270eb 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -1,56 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- cmdline.h - -Abstract: -- This file contains the internal structures and definitions used by command line input and editing. - -Author: -- Therese Stowell (ThereseS) 15-Nov-1991 - -Revision History: -- Mike Griese (migrie) Jan 2018: - Refactored the history and alias functionality into their own files. -- Michael Niksa (miniksa) May 2018: - Split apart popup information. Started encapsulating command line things. Removed 0 length buffers. -Notes: - The input model for the command line editing popups is complex. - Here is the relevant pseudocode: - - CookedReadWaitRoutine - if (CookedRead->Popup) - Status = (*CookedRead->Popup->Callback)(); - if (Status == CONSOLE_STATUS_READ_COMPLETE) - return STATUS_SUCCESS; - return Status; - - CookedRead - if (Command Line Editing Key) - ProcessCommandLine - else - process regular key - - ProcessCommandLine - if F7 - return Popup - - Popup - draw popup - return ProcessCommandListInput - - ProcessCommandListInput - while (TRUE) - GetChar - if (wait) - return wait - switch (char) - . - . - . ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -138,6 +87,4 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); bool IsWordDelim(const wchar_t wch); bool IsWordDelim(const std::wstring_view charData); -bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...); - void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index); diff --git a/src/host/conddkrefs.h b/src/host/conddkrefs.h index 486825ed8ab..854d03e8081 100644 --- a/src/host/conddkrefs.h +++ b/src/host/conddkrefs.h @@ -139,8 +139,6 @@ typedef struct _FILE_FS_DEVICE_INFORMATION #pragma region ntifs.h(public DDK) -#define RtlOffsetToPointer(B, O) ((PCHAR)(((PCHAR)(B)) + ((ULONG_PTR)(O)))) - __kernel_entry NTSYSCALLAPI NTSTATUS NTAPI diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index 01f4bcac8df..63e321d6c8a 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -1257,38 +1257,38 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) ULONG cbBufferSize; RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize)); - PVOID pvInputTarget; - const auto cbInputTarget = a->TargetLength; - PVOID pvInputExeName; - const auto cbInputExeName = a->ExeLength; - PVOID pvInputSource; - const auto cbInputSource = a->SourceLength; - // clang-format off - RETURN_HR_IF(E_INVALIDARG, !IsValidStringBuffer(a->Unicode, - pvBuffer, - cbBufferSize, - 3, - cbInputExeName, - &pvInputExeName, - cbInputSource, - &pvInputSource, - cbInputTarget, - &pvInputTarget)); - // clang-format on + // There are 3 strings stored back-to-back within the message payload. + // First we verify that their size and alignment are alright and then we extract them. + const ULONG cbInputExeName = a->ExeLength; + const ULONG cbInputSource = a->SourceLength; + const ULONG cbInputTarget = a->TargetLength; + + const auto alignment = a->Unicode ? alignof(wchar_t) : alignof(char); + // ExeLength, SourceLength and TargetLength are USHORT and summing them up will not overflow a ULONG. + const auto badLength = cbInputTarget + cbInputExeName + cbInputSource > cbBufferSize; + // Since (any) alignment is a power of 2, we can use bit tricks to test if the alignment is right: + // a) Combining the values with OR works, because we're only interested whether the lowest bits are 0 (= aligned). + // b) x % y can be replaced with x & (y - 1) if y is a power of 2. + const auto badAlignment = ((cbInputExeName | cbInputSource | cbInputTarget) & (alignment - 1)) != 0; + RETURN_HR_IF(E_INVALIDARG, badLength || badAlignment); + + const auto pvInputExeName = static_cast(pvBuffer); + const auto pvInputSource = pvInputExeName + cbInputExeName; + const auto pvInputTarget = pvInputSource + cbInputSource; if (a->Unicode) { + const std::wstring_view inputExeName(reinterpret_cast(pvInputExeName), cbInputExeName / sizeof(wchar_t)); const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); const std::wstring_view inputTarget(reinterpret_cast(pvInputTarget), cbInputTarget / sizeof(wchar_t)); - const std::wstring_view inputExeName(reinterpret_cast(pvInputExeName), cbInputExeName / sizeof(wchar_t)); return m->_pApiRoutines->AddConsoleAliasWImpl(inputSource, inputTarget, inputExeName); } else { - const std::string_view inputSource(reinterpret_cast(pvInputSource), cbInputSource); - const std::string_view inputTarget(reinterpret_cast(pvInputTarget), cbInputTarget); - const std::string_view inputExeName(reinterpret_cast(pvInputExeName), cbInputExeName); + const std::string_view inputExeName(pvInputExeName, cbInputExeName); + const std::string_view inputSource(pvInputSource, cbInputSource); + const std::string_view inputTarget(pvInputTarget, cbInputTarget); return m->_pApiRoutines->AddConsoleAliasAImpl(inputSource, inputTarget, inputExeName); } @@ -1304,20 +1304,22 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) ULONG cbInputBufferSize; RETURN_IF_FAILED(m->GetInputBuffer(&pvInputBuffer, &cbInputBufferSize)); - PVOID pvInputExe; - const auto cbInputExe = a->ExeLength; - PVOID pvInputSource; - const auto cbInputSource = a->SourceLength; - // clang-format off - RETURN_HR_IF(E_INVALIDARG, !IsValidStringBuffer(a->Unicode, - pvInputBuffer, - cbInputBufferSize, - 2, - cbInputExe, - &pvInputExe, - cbInputSource, - &pvInputSource)); - // clang-format on + // There are 2 strings stored back-to-back within the message payload. + // First we verify that their size and alignment are alright and then we extract them. + const ULONG cbInputExeName = a->ExeLength; + const ULONG cbInputSource = a->SourceLength; + + const auto alignment = a->Unicode ? alignof(wchar_t) : alignof(char); + // ExeLength and SourceLength are USHORT and summing them up will not overflow a ULONG. + const auto badLength = cbInputExeName + cbInputSource > cbInputBufferSize; + // Since (any) alignment is a power of 2, we can use bit tricks to test if the alignment is right: + // a) Combining the values with OR works, because we're only interested whether the lowest bits are 0 (= aligned). + // b) x % y can be replaced with x & (y - 1) if y is a power of 2. + const auto badAlignment = ((cbInputExeName | cbInputSource) & (alignment - 1)) != 0; + RETURN_HR_IF(E_INVALIDARG, badLength || badAlignment); + + const auto pvInputExeName = static_cast(pvInputBuffer); + const auto pvInputSource = pvInputExeName + cbInputExeName; PVOID pvOutputBuffer; ULONG cbOutputBufferSize; @@ -1327,9 +1329,9 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) size_t cbWritten; if (a->Unicode) { - const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); - const std::wstring_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe / sizeof(wchar_t)); - std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize / sizeof(wchar_t)); + const std::wstring_view inputExeName{ reinterpret_cast(pvInputExeName), cbInputExeName / sizeof(wchar_t) }; + const std::wstring_view inputSource{ reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t) }; + const std::span outputBuffer{ static_cast(pvOutputBuffer), cbOutputBufferSize / sizeof(wchar_t) }; size_t cchWritten; hr = m->_pApiRoutines->GetConsoleAliasWImpl(inputSource, outputBuffer, cchWritten, inputExeName); @@ -1339,9 +1341,9 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) } else { - const std::string_view inputSource(reinterpret_cast(pvInputSource), cbInputSource); - const std::string_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe); - std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize); + const std::string_view inputExeName{ pvInputExeName, cbInputExeName }; + const std::string_view inputSource{ pvInputSource, cbInputSource }; + const std::span outputBuffer{ static_cast(pvOutputBuffer), cbOutputBufferSize }; size_t cchWritten; hr = m->_pApiRoutines->GetConsoleAliasAImpl(inputSource, outputBuffer, cchWritten, inputExeName); From c6e5f791152f0591cb617518fe1c9e3c15a6c721 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 2 Aug 2023 00:46:40 +0200 Subject: [PATCH 09/59] Modernize CommandHistory and switch to int32 (#15782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit slightly modernizes `CommandHistory` by leaning more heavily on the STL container functionalities. For one, it uses for-range iterations to loop through `_commands` instead of using `GetNth` on every iteration. Another major improvement however is that the code previously copied entire `CommandHistory` instances out of the linked list `s_historyLists`, then removed the slot and copied (not moved!) that instance into the front again. Now it uses the `splice` function from `std::list` to do it in `O(1)` and virtually cost-free. Another major improvement (and the one I'm personally interested in) is the switch from `SHORT` to `int32_t`. This will greatly simplify the implementation of the future `COOKED_READ_DATA` class, as the larger integer type will remove worries about over/underflow. For instance, we can then just blindly increment/decrement the history position and then only later clamp it to the expected range. ## Validation Steps Performed * Existing history tests ✅ * History cycling with F8 ✅ * Navigating history with F7 ✅ --- src/host/CommandListPopup.cpp | 58 +++--- src/host/CommandListPopup.hpp | 8 +- src/host/CommandNumberPopup.cpp | 3 +- src/host/cmdline.cpp | 6 +- src/host/cmdline.h | 2 +- src/host/history.cpp | 192 +++++++------------ src/host/history.h | 51 +++-- src/host/ut_host/CommandLineTests.cpp | 2 +- src/host/ut_host/CommandListPopupTests.cpp | 2 +- src/host/ut_host/CommandNumberPopupTests.cpp | 2 +- src/host/ut_host/HistoryTests.cpp | 22 +-- src/host/ut_host/PopupTestHelper.hpp | 4 +- 12 files changed, 150 insertions(+), 202 deletions(-) diff --git a/src/host/CommandListPopup.cpp b/src/host/CommandListPopup.cpp index 62abadb379d..b530ca2760d 100644 --- a/src/host/CommandListPopup.cpp +++ b/src/host/CommandListPopup.cpp @@ -34,9 +34,9 @@ static til::size calculatePopupSize(const CommandHistory& history) // find the widest command history item and use it for the width size_t width = minSize.width; - for (size_t i = 0; i < history.GetNumberOfCommands(); ++i) + for (CommandHistory::Index i = 0; i < history.GetNumberOfCommands(); ++i) { - const auto& historyItem = history.GetNth(gsl::narrow(i)); + const auto& historyItem = history.GetNth(i); width = std::max(width, historyItem.size() + padding); } if (width > SHRT_MAX) @@ -53,7 +53,7 @@ static til::size calculatePopupSize(const CommandHistory& history) CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) : Popup(screenInfo, calculatePopupSize(history)), _history{ history }, - _currentCommand{ std::min(history.LastDisplayed, static_cast(history.GetNumberOfCommands() - 1)) } + _currentCommand{ std::min(history.LastDisplayed, history.GetNumberOfCommands() - 1) } { FAIL_FAST_IF(_currentCommand < 0); _setBottomIndex(); @@ -65,7 +65,7 @@ CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const Command { try { - short Index = 0; + CommandHistory::Index Index = 0; const auto shiftPressed = WI_IsFlagSet(modifiers, SHIFT_PRESSED); switch (wch) { @@ -107,17 +107,17 @@ CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const Command break; case VK_END: // Move waaay forward, UpdateCommandListPopup() can handle it. - _update((SHORT)(cookedReadData.History().GetNumberOfCommands())); + _update(cookedReadData.History().GetNumberOfCommands()); break; case VK_HOME: // Move waaay back, UpdateCommandListPopup() can handle it. - _update(-(SHORT)(cookedReadData.History().GetNumberOfCommands())); + _update(-cookedReadData.History().GetNumberOfCommands()); break; case VK_PRIOR: - _update(-(SHORT)Height()); + _update(-Height()); break; case VK_NEXT: - _update((SHORT)Height()); + _update(Height()); break; case VK_DELETE: return _deleteSelection(cookedReadData); @@ -125,7 +125,7 @@ CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const Command case VK_RIGHT: Index = _currentCommand; CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, (SHORT)Index); + SetCurrentCommandLine(cookedReadData, Index); return CONSOLE_STATUS_WAIT_NO_BLOCK; default: break; @@ -137,13 +137,13 @@ CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const Command void CommandListPopup::_setBottomIndex() { - if (_currentCommand < (SHORT)(_history.GetNumberOfCommands() - Height())) + if (_currentCommand < _history.GetNumberOfCommands() - Height()) { - _bottomIndex = std::max(_currentCommand, gsl::narrow(Height() - 1)); + _bottomIndex = std::max(_currentCommand, Height() - 1); } else { - _bottomIndex = (SHORT)(_history.GetNumberOfCommands() - 1); + _bottomIndex = _history.GetNumberOfCommands() - 1; } } @@ -152,7 +152,7 @@ void CommandListPopup::_setBottomIndex() try { auto& history = cookedReadData.History(); - history.Remove(static_cast(_currentCommand)); + history.Remove(_currentCommand); _setBottomIndex(); if (history.GetNumberOfCommands() == 0) @@ -160,9 +160,9 @@ void CommandListPopup::_setBottomIndex() // close the popup return CONSOLE_STATUS_READ_COMPLETE; } - else if (_currentCommand >= static_cast(history.GetNumberOfCommands())) + else if (_currentCommand >= history.GetNumberOfCommands()) { - _currentCommand = static_cast(history.GetNumberOfCommands() - 1); + _currentCommand = history.GetNumberOfCommands() - 1; _bottomIndex = _currentCommand; } @@ -204,7 +204,7 @@ void CommandListPopup::_setBottomIndex() { auto& history = cookedReadData.History(); - if (history.GetNumberOfCommands() <= 1 || _currentCommand == gsl::narrow(history.GetNumberOfCommands()) - 1) + if (history.GetNumberOfCommands() <= 1 || _currentCommand == history.GetNumberOfCommands() - 1) { return STATUS_SUCCESS; } @@ -218,12 +218,12 @@ void CommandListPopup::_setBottomIndex() void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) { - short Index = 0; + CommandHistory::Index Index = 0; auto Status = STATUS_SUCCESS; DWORD LineCount = 1; Index = _currentCommand; CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, (SHORT)Index); + SetCurrentCommandLine(cookedReadData, Index); cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status); // complete read if (cookedReadData.IsEchoInput()) @@ -268,13 +268,13 @@ void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) void CommandListPopup::_cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch) { - short Index = 0; + CommandHistory::Index Index = 0; if (cookedReadData.History().FindMatchingCommand({ &wch, 1 }, _currentCommand, Index, CommandHistory::MatchOptions::JustLooking)) { - _update((SHORT)(Index - _currentCommand), true); + _update(Index - _currentCommand, true); } } @@ -345,7 +345,7 @@ void CommandListPopup::_drawList() auto api = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().api; WriteCoord.y = _region.top + 1; - auto i = gsl::narrow(std::max(_bottomIndex - Height() + 1, 0)); + auto i = std::max(_bottomIndex - Height() + 1, 0); for (; i <= _bottomIndex; i++) { CHAR CommandNumber[COMMAND_NUMBER_SIZE]; @@ -447,7 +447,7 @@ void CommandListPopup::_drawList() // Arguments: // - originalDelta - The number of lines to move up or down // - wrap - Down past the bottom or up past the top should wrap the command list -void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) +void CommandListPopup::_update(const CommandHistory::Index originalDelta, const bool wrap) { auto delta = originalDelta; if (delta == 0) @@ -457,7 +457,7 @@ void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) const auto Size = Height(); auto CurCmdNum = _currentCommand; - SHORT NewCmdNum = CurCmdNum + delta; + CommandHistory::Index NewCmdNum = CurCmdNum + delta; if (wrap) { @@ -466,9 +466,9 @@ void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) } else { - if (NewCmdNum >= gsl::narrow(_history.GetNumberOfCommands())) + if (NewCmdNum >= _history.GetNumberOfCommands()) { - NewCmdNum = gsl::narrow(_history.GetNumberOfCommands()) - 1; + NewCmdNum = _history.GetNumberOfCommands() - 1; } else if (NewCmdNum < 0) { @@ -484,16 +484,16 @@ void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) _bottomIndex += delta; if (_bottomIndex < Size - 1) { - _bottomIndex = gsl::narrow(Size - 1); + _bottomIndex = Size - 1; } Scroll = true; } else if (NewCmdNum > _bottomIndex) { _bottomIndex += delta; - if (_bottomIndex >= gsl::narrow(_history.GetNumberOfCommands())) + if (_bottomIndex >= _history.GetNumberOfCommands()) { - _bottomIndex = gsl::narrow(_history.GetNumberOfCommands()) - 1; + _bottomIndex = _history.GetNumberOfCommands() - 1; } Scroll = true; } @@ -516,7 +516,7 @@ void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) // Arguments: // - OldCurrentCommand - The previous command highlighted // - NewCurrentCommand - The new command to be highlighted. -void CommandListPopup::_updateHighlight(const SHORT OldCurrentCommand, const SHORT NewCurrentCommand) +void CommandListPopup::_updateHighlight(const CommandHistory::Index OldCurrentCommand, const CommandHistory::Index NewCurrentCommand) { til::CoordType TopIndex; if (_bottomIndex < Height()) diff --git a/src/host/CommandListPopup.hpp b/src/host/CommandListPopup.hpp index a893b905554..59bef4c3946 100644 --- a/src/host/CommandListPopup.hpp +++ b/src/host/CommandListPopup.hpp @@ -29,8 +29,8 @@ class CommandListPopup : public Popup private: void _drawList(); - void _update(const SHORT delta, const bool wrap = false); - void _updateHighlight(const SHORT oldCommand, const SHORT newCommand); + void _update(const CommandHistory::Index delta, const bool wrap = false); + void _updateHighlight(const CommandHistory::Index oldCommand, const CommandHistory::Index newCommand); void _handleReturn(COOKED_READ_DATA& cookedReadData); void _cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch); @@ -40,8 +40,8 @@ class CommandListPopup : public Popup [[nodiscard]] NTSTATUS _swapUp(COOKED_READ_DATA& cookedReadData) noexcept; [[nodiscard]] NTSTATUS _swapDown(COOKED_READ_DATA& cookedReadData) noexcept; - SHORT _currentCommand; - SHORT _bottomIndex; // number of command displayed on last line of popup + CommandHistory::Index _currentCommand; + CommandHistory::Index _bottomIndex; // number of command displayed on last line of popup const CommandHistory& _history; #ifdef UNIT_TESTING diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp index 4ccce4d6d75..6e990439d70 100644 --- a/src/host/CommandNumberPopup.cpp +++ b/src/host/CommandNumberPopup.cpp @@ -101,8 +101,7 @@ void CommandNumberPopup::_handleEscape(COOKED_READ_DATA& cookedReadData) noexcep // - cookedReadData - read data to operate on void CommandNumberPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) noexcept { - const auto commandNumber = gsl::narrow(std::min(static_cast(_parse()), - cookedReadData.History().GetNumberOfCommands() - 1)); + const auto commandNumber = gsl::narrow(std::min(_parse(), cookedReadData.History().GetNumberOfCommands() - 1)); CommandLine::Instance().EndAllPopups(); SetCurrentCommandLine(cookedReadData, commandNumber); diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp index b39040c7cd4..1e1587e37fc 100644 --- a/src/host/cmdline.cpp +++ b/src/host/cmdline.cpp @@ -229,7 +229,7 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) // Routine Description: // - This routine copies the commandline specified by Index into the cooked read buffer -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) // index, not command number +void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index) // index, not command number { DeleteCommandLine(cookedReadData, TRUE); FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, @@ -938,7 +938,7 @@ til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& c auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (cookedReadData.HasHistory()) { - SHORT index; + CommandHistory::Index index; if (cookedReadData.History().FindMatchingCommand({ cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() }, cookedReadData.History().LastDisplayed, index, @@ -948,7 +948,7 @@ til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& c const auto CurrentPos = cookedReadData.InsertionPoint(); DeleteCommandLine(cookedReadData, true); - THROW_IF_FAILED(cookedReadData.History().RetrieveNth((SHORT)index, + THROW_IF_FAILED(cookedReadData.History().RetrieveNth(index, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); diff --git a/src/host/cmdline.h b/src/host/cmdline.h index 34286d270eb..a8a5d19ca5b 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -87,4 +87,4 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); bool IsWordDelim(const wchar_t wch); bool IsWordDelim(const std::wstring_view charData); -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index); +void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index); diff --git a/src/host/history.cpp b/src/host/history.cpp index 501b33e0ee2..dfe1624987a 100644 --- a/src/host/history.cpp +++ b/src/host/history.cpp @@ -62,13 +62,14 @@ void CommandHistory::s_Free(const HANDLE processHandle) void CommandHistory::s_ResizeAll(const size_t commands) { + const auto size = gsl::narrow(commands); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - FAIL_FAST_IF(commands > SHORT_MAX); gci.SetHistoryBufferSize(gsl::narrow(commands)); for (auto& historyList : s_historyLists) { - historyList.Realloc(commands); + historyList.Realloc(size); } } @@ -81,7 +82,7 @@ bool CommandHistory::IsAppNameMatch(const std::wstring_view other) const // - This routine is called when escape is entered or a command is added. void CommandHistory::_Reset() { - LastDisplayed = gsl::narrow(_commands.size()) - 1; + LastDisplayed = GetNumberOfCommands() - 1; WI_SetFlag(Flags, CLE_RESET); } @@ -109,7 +110,7 @@ void CommandHistory::_Reset() if (suppressDuplicates) { - SHORT index; + Index index; if (FindMatchingCommand(newCommand, LastDisplayed, index, CommandHistory::MatchOptions::ExactMatch)) { reuse = Remove(index); @@ -117,7 +118,7 @@ void CommandHistory::_Reset() } // find free record. if all records are used, free the lru one. - if ((SHORT)_commands.size() == _maxCommands) + if (GetNumberOfCommands() == _maxCommands) { _commands.erase(_commands.cbegin()); // move LastDisplayed back one in order to stay synced with the @@ -152,27 +153,28 @@ void CommandHistory::_Reset() return S_OK; } -std::wstring_view CommandHistory::GetNth(const SHORT index) const +std::wstring_view CommandHistory::GetNth(Index index) const { - try + if (index >= 0 && index < GetNumberOfCommands()) { return _commands.at(index); } - CATCH_LOG(); - return {}; } -[[nodiscard]] HRESULT CommandHistory::RetrieveNth(const SHORT index, - std::span buffer, - size_t& commandSize) +const std::vector& CommandHistory::GetCommands() const noexcept +{ + return _commands; +} + +[[nodiscard]] HRESULT CommandHistory::RetrieveNth(const Index index, std::span buffer, size_t& commandSize) { LastDisplayed = index; try { const auto& cmd = _commands.at(index); - if (cmd.size() > (size_t)buffer.size()) + if (cmd.size() > buffer.size()) { commandSize = buffer.size(); // room for CRLF? } @@ -229,16 +231,7 @@ std::wstring_view CommandHistory::GetNth(const SHORT index) const std::wstring_view CommandHistory::GetLastCommand() const { - if (_commands.size() != 0) - { - try - { - return _commands.at(LastDisplayed); - } - CATCH_LOG(); - } - - return {}; + return GetNth(LastDisplayed); } void CommandHistory::Empty() @@ -255,54 +248,44 @@ bool CommandHistory::AtFirstCommand() const return FALSE; } - auto i = (SHORT)(LastDisplayed - 1); + auto i = LastDisplayed - 1; if (i == -1) { - i = ((SHORT)_commands.size()) - 1i16; + i = GetNumberOfCommands() - 1; } - return (i == ((SHORT)_commands.size()) - 1i16); + return (i == GetNumberOfCommands() - 1); } bool CommandHistory::AtLastCommand() const { - return LastDisplayed == ((SHORT)_commands.size()) - 1i16; + return LastDisplayed == GetNumberOfCommands() - 1; } -void CommandHistory::Realloc(const size_t commands) +void CommandHistory::Realloc(const Index commands) { - // To protect ourselves from overflow and general arithmetic errors, a limit of SHORT_MAX is put on the size of the command history. - if (_maxCommands == (SHORT)commands || commands > SHORT_MAX) + if (_maxCommands == commands) { return; } - const auto oldCommands = _commands; - const auto newNumberOfCommands = gsl::narrow(std::min(_commands.size(), commands)); - - _commands.clear(); - for (SHORT i = 0; i < newNumberOfCommands; i++) - { - _commands.emplace_back(oldCommands[i]); - } + _commands.resize(std::min(_commands.size(), gsl::narrow_cast(std::max(0, commands)))); WI_SetFlag(Flags, CLE_RESET); - LastDisplayed = gsl::narrow(_commands.size()) - 1; - _maxCommands = (SHORT)commands; + LastDisplayed = GetNumberOfCommands() - 1; + _maxCommands = commands; } void CommandHistory::s_ReallocExeToFront(const std::wstring_view appName, const size_t commands) { - for (auto it = s_historyLists.begin(); it != s_historyLists.end(); it++) + const auto size = gsl::narrow(commands); + + for (auto it = s_historyLists.begin(), end = s_historyLists.end(); it != end; ++it) { if (WI_IsFlagSet(it->Flags, CLE_ALLOCATED) && it->IsAppNameMatch(appName)) { - auto backup = *it; - backup.Realloc(commands); - - s_historyLists.erase(it); - s_historyLists.push_front(backup); - + it->Realloc(size); + s_historyLists.splice(s_historyLists.begin(), s_historyLists, it); return; } } @@ -336,19 +319,20 @@ CommandHistory* CommandHistory::s_Allocate(const std::wstring_view appName, cons auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // Reuse a history buffer. The buffer must be !CLE_ALLOCATED. // If possible, the buffer should have the same app name. - std::optional BestCandidate; + const auto beg = s_historyLists.begin(); + const auto end = s_historyLists.end(); + auto BestCandidate = end; auto SameApp = false; - for (auto it = s_historyLists.cbegin(); it != s_historyLists.cend(); it++) + for (auto it = beg; it != end; ++it) { if (WI_IsFlagClear(it->Flags, CLE_ALLOCATED)) { // use MRU history buffer with same app name if (it->IsAppNameMatch(appName)) { - BestCandidate = *it; + BestCandidate = it; SameApp = true; - s_historyLists.erase(it); break; } } @@ -363,7 +347,7 @@ CommandHistory* CommandHistory::s_Allocate(const std::wstring_view appName, cons History._appName = appName; History.Flags = CLE_ALLOCATED; History.LastDisplayed = -1; - History._maxCommands = gsl::narrow(gci.GetHistoryBufferSize()); + History._maxCommands = gsl::narrow(gci.GetHistoryBufferSize()); History._processHandle = processHandle; return &s_historyLists.emplace_front(History); } @@ -371,28 +355,22 @@ CommandHistory* CommandHistory::s_Allocate(const std::wstring_view appName, cons // If we have no candidate already and we need one, // take the LRU (which is the back/last one) which isn't allocated // and if possible the one with empty commands list. - if (!BestCandidate.has_value()) + if (BestCandidate == end) { - auto BestCandidateIt = s_historyLists.cend(); - for (auto it = s_historyLists.cbegin(); it != s_historyLists.cend(); it++) + for (auto it = beg; it != end; ++it) { if (WI_IsFlagClear(it->Flags, CLE_ALLOCATED)) { - if (it->_commands.empty() || BestCandidateIt == s_historyLists.cend() || !BestCandidateIt->_commands.empty()) + if (it->_commands.empty() || BestCandidate == end || !BestCandidate->_commands.empty()) { - BestCandidateIt = it; + BestCandidate = it; } } } - if (BestCandidateIt != s_historyLists.cend()) - { - BestCandidate = *BestCandidateIt; - s_historyLists.erase(BestCandidateIt); - } } // If the app name doesn't match, copy in the new app name and free the old commands. - if (BestCandidate.has_value()) + if (BestCandidate != end) { if (!SameApp) { @@ -404,36 +382,38 @@ CommandHistory* CommandHistory::s_Allocate(const std::wstring_view appName, cons BestCandidate->_processHandle = processHandle; WI_SetFlag(BestCandidate->Flags, CLE_ALLOCATED); - return &s_historyLists.emplace_front(BestCandidate.value()); + // move to the front of the list + s_historyLists.splice(beg, s_historyLists, BestCandidate); + return &*BestCandidate; } return nullptr; } -size_t CommandHistory::GetNumberOfCommands() const +CommandHistory::Index CommandHistory::GetNumberOfCommands() const { - return _commands.size(); + return gsl::narrow_cast(_commands.size()); } -void CommandHistory::_Prev(SHORT& ind) const +void CommandHistory::_Prev(Index& ind) const { if (ind <= 0) { - ind = gsl::narrow(_commands.size()); + ind = GetNumberOfCommands(); } ind--; } -void CommandHistory::_Next(SHORT& ind) const +void CommandHistory::_Next(Index& ind) const { ++ind; - if (ind >= (SHORT)_commands.size()) + if (ind >= GetNumberOfCommands()) { ind = 0; } } -void CommandHistory::_Dec(SHORT& ind) const +void CommandHistory::_Dec(Index& ind) const { if (ind <= 0) { @@ -442,7 +422,7 @@ void CommandHistory::_Dec(SHORT& ind) const ind--; } -void CommandHistory::_Inc(SHORT& ind) const +void CommandHistory::_Inc(Index& ind) const { ++ind; if (ind >= _maxCommands) @@ -451,64 +431,33 @@ void CommandHistory::_Inc(SHORT& ind) const } } -std::wstring CommandHistory::Remove(const SHORT iDel) +std::wstring CommandHistory::Remove(const Index iDel) { - SHORT iFirst = 0; - auto iLast = gsl::narrow(_commands.size() - 1); - auto iDisp = LastDisplayed; - - if (_commands.size() == 0) + if (iDel < 0 || iDel >= GetNumberOfCommands()) { return {}; } - const auto nDel = iDel; - if ((nDel < iFirst) || (nDel > iLast)) - { - return {}; - } + const auto str = std::move(_commands.at(iDel)); + _commands.erase(_commands.begin() + iDel); - if (iDisp == iDel) + if (LastDisplayed == iDel) { LastDisplayed = -1; } - - try + else if (LastDisplayed > iDel) { - const auto str = _commands.at(iDel); - - if (iDel < iLast) - { - _commands.erase(_commands.cbegin() + iDel); - if ((iDisp > iDel) && (iDisp <= iLast)) - { - _Dec(iDisp); - } - _Dec(iLast); - } - else if (iFirst <= iDel) - { - _commands.erase(_commands.cbegin() + iDel); - if ((iDisp >= iFirst) && (iDisp < iDel)) - { - _Inc(iDisp); - } - _Inc(iFirst); - } - - LastDisplayed = iDisp; - return str; + _Dec(LastDisplayed); } - CATCH_LOG(); - return {}; + return str; } // Routine Description: // - this routine finds the most recent command that starts with the letters already in the current command. it returns the array index (no mod needed). [[nodiscard]] bool CommandHistory::FindMatchingCommand(const std::wstring_view givenCommand, - const SHORT startingIndex, - SHORT& indexFound, + const Index startingIndex, + Index& indexFound, const MatchOptions options) { indexFound = startingIndex; @@ -565,9 +514,15 @@ void CommandHistory::s_ClearHistoryListStorage() // Arguments: // - indexA - index of one history item to swap // - indexB - index of one history item to swap -void CommandHistory::Swap(const short indexA, const short indexB) +void CommandHistory::Swap(const Index indexA, const Index indexB) { - std::swap(_commands.at(indexA), _commands.at(indexB)); + const auto num = GetNumberOfCommands(); + if (indexA != indexB && + indexA >= 0 && indexA < num && + indexB >= 0 && indexB < num) + { + std::swap(_commands.at(indexA), _commands.at(indexB)); + } } // Routine Description: @@ -689,9 +644,8 @@ HRESULT GetConsoleCommandHistoryLengthImplHelper(const std::wstring_view exeName // Every command history item is made of a string length followed by 1 null character. const size_t cchNull = 1; - for (SHORT i = 0; i < gsl::narrow(pCommandHistory->GetNumberOfCommands()); i++) + for (const auto& command : pCommandHistory->GetCommands()) { - const auto command = pCommandHistory->GetNth(i); auto cchCommand = command.size(); // If we're counting how much multibyte space will be needed, trial convert the command string before we add. @@ -796,10 +750,8 @@ HRESULT GetConsoleCommandHistoryWImplHelper(const std::wstring_view exeName, const size_t cchNull = 1; - for (SHORT i = 0; i < gsl::narrow(CommandHistory->GetNumberOfCommands()); i++) + for (const auto& command : CommandHistory->GetCommands()) { - const auto command = CommandHistory->GetNth(i); - const auto cchCommand = command.size(); size_t cchNeeded; diff --git a/src/host/history.h b/src/host/history.h index 9abe6841a95..943f9475e21 100644 --- a/src/host/history.h +++ b/src/host/history.h @@ -1,20 +1,14 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- history.h - -Abstract: -- Encapsulates the cmdline functions and structures specifically related to - command history functionality. ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once class CommandHistory { public: + using Index = int32_t; + static constexpr Index IndexMax = INT32_MAX; + // CommandHistory Flags static constexpr int CLE_ALLOCATED = 0x00000001; static constexpr int CLE_RESET = 0x00000002; @@ -41,8 +35,8 @@ class CommandHistory }; bool FindMatchingCommand(const std::wstring_view command, - const SHORT startingIndex, - SHORT& indexFound, + const Index startingIndex, + Index& indexFound, const MatchOptions options); bool IsAppNameMatch(const std::wstring_view other) const; @@ -53,24 +47,25 @@ class CommandHistory const std::span buffer, size_t& commandSize); - [[nodiscard]] HRESULT RetrieveNth(const SHORT index, + [[nodiscard]] HRESULT RetrieveNth(const Index index, const std::span buffer, size_t& commandSize); - size_t GetNumberOfCommands() const; - std::wstring_view GetNth(const SHORT index) const; + Index GetNumberOfCommands() const; + std::wstring_view GetNth(Index index) const; + const std::vector& GetCommands() const noexcept; - void Realloc(const size_t commands); + void Realloc(Index commands); void Empty(); - std::wstring Remove(const SHORT iDel); + std::wstring Remove(const Index iDel); bool AtFirstCommand() const; bool AtLastCommand() const; std::wstring_view GetLastCommand() const; - void Swap(const short indexA, const short indexB); + void Swap(const Index indexA, const Index indexB); private: void _Reset(); @@ -78,22 +73,24 @@ class CommandHistory // _Next and _Prev go to the next and prev command // _Inc and _Dec go to the next and prev slots // Don't get the two confused - it matters when the cmd history is not full! - void _Prev(SHORT& ind) const; - void _Next(SHORT& ind) const; - void _Dec(SHORT& ind) const; - void _Inc(SHORT& ind) const; + void _Prev(Index& ind) const; + void _Next(Index& ind) const; + void _Dec(Index& ind) const; + void _Inc(Index& ind) const; + // NOTE: In conhost v1 this used to be a circular buffer because removal at the + // start is a very common operation. It seems this was lost in the C++ refactor. std::vector _commands; - SHORT _maxCommands; + Index _maxCommands = 0; std::wstring _appName; - HANDLE _processHandle; + HANDLE _processHandle = nullptr; static std::list s_historyLists; public: - DWORD Flags; - SHORT LastDisplayed; + DWORD Flags = 0; + Index LastDisplayed = 0; #ifdef UNIT_TESTING static void s_ClearHistoryListStorage(); diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp index b003c37b5f2..de09dd2e241 100644 --- a/src/host/ut_host/CommandLineTests.cpp +++ b/src/host/ut_host/CommandLineTests.cpp @@ -420,7 +420,7 @@ class CommandLineTests auto& commandLine = CommandLine::Instance(); commandLine._deleteCommandHistory(cookedReadData); - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), 0u); + VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), 0); } TEST_METHOD(CanFillPromptWithPreviousCommandFragment) diff --git a/src/host/ut_host/CommandListPopupTests.cpp b/src/host/ut_host/CommandListPopupTests.cpp index d47f3bbfa31..342248702db 100644 --- a/src/host/ut_host/CommandListPopupTests.cpp +++ b/src/host/ut_host/CommandListPopupTests.cpp @@ -237,7 +237,7 @@ class CommandListPopupTests VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); // selection should have moved to the bottom line - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands() - 1, gsl::narrow(popup._currentCommand)); + VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands() - 1, popup._currentCommand); } TEST_METHOD(HomeMovesSelectionToStart) diff --git a/src/host/ut_host/CommandNumberPopupTests.cpp b/src/host/ut_host/CommandNumberPopupTests.cpp index 8c4460ec8a0..006c36207bd 100644 --- a/src/host/ut_host/CommandNumberPopupTests.cpp +++ b/src/host/ut_host/CommandNumberPopupTests.cpp @@ -174,7 +174,7 @@ class CommandNumberPopupTests TEST_METHOD(CanSelectHistoryItem) { PopupTestHelper::InitHistory(*m_pHistory); - for (unsigned int historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) + for (CommandHistory::Index historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) { Popup::UserInputFunction fn = [historyIndex](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, diff --git a/src/host/ut_host/HistoryTests.cpp b/src/host/ut_host/HistoryTests.cpp index 4861fb5dd6c..dbbe6f1c82d 100644 --- a/src/host/ut_host/HistoryTests.cpp +++ b/src/host/ut_host/HistoryTests.cpp @@ -145,15 +145,15 @@ class HistoryTests Log::Comment(L"Retrieve items/order."); std::vector commandsStored; - for (SHORT i = 0; i < (SHORT)history->GetNumberOfCommands(); i++) + for (CommandHistory::Index i = 0; i < history->GetNumberOfCommands(); i++) { commandsStored.emplace_back(history->GetNth(i)); } Log::Comment(L"Reallocate larger and ensure items and order are preserved."); - history->Realloc(_manyHistoryItems.size()); + history->Realloc((CommandHistory::Index)_manyHistoryItems.size()); VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands()); - for (SHORT i = 0; i < (SHORT)commandsStored.size(); i++) + for (CommandHistory::Index i = 0; i < (CommandHistory::Index)commandsStored.size(); i++) { VERIFY_ARE_EQUAL(String(commandsStored[i].data()), String(history->GetNth(i).data())); } @@ -163,7 +163,7 @@ class HistoryTests { VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[j], false)); } - VERIFY_ARE_EQUAL(_manyHistoryItems.size(), history->GetNumberOfCommands()); + VERIFY_ARE_EQUAL((CommandHistory::Index)_manyHistoryItems.size(), history->GetNumberOfCommands()); } TEST_METHOD(ReallocDown) @@ -179,14 +179,14 @@ class HistoryTests Log::Comment(L"Retrieve items/order."); std::vector commandsStored; - for (SHORT i = 0; i < (SHORT)history->GetNumberOfCommands(); i++) + for (CommandHistory::Index i = 0; i < history->GetNumberOfCommands(); i++) { commandsStored.emplace_back(history->GetNth(i)); } Log::Comment(L"Reallocate smaller and ensure items and order are preserved. Items at end of list should be trimmed."); history->Realloc(5); - for (SHORT i = 0; i < 5; i++) + for (CommandHistory::Index i = 0; i < 5; i++) { VERIFY_ARE_EQUAL(String(commandsStored[i].data()), String(history->GetNth(i).data())); } @@ -201,7 +201,7 @@ class HistoryTests VERIFY_SUCCEEDED(history->Add(L"dir", false)); VERIFY_SUCCEEDED(history->Add(L"dir", false)); - VERIFY_ARE_EQUAL(1ul, history->GetNumberOfCommands()); + VERIFY_ARE_EQUAL(1, history->GetNumberOfCommands()); } TEST_METHOD(AddSequentialNoDuplicates) @@ -213,7 +213,7 @@ class HistoryTests VERIFY_SUCCEEDED(history->Add(L"dir", true)); VERIFY_SUCCEEDED(history->Add(L"dir", true)); - VERIFY_ARE_EQUAL(1ul, history->GetNumberOfCommands()); + VERIFY_ARE_EQUAL(1, history->GetNumberOfCommands()); } TEST_METHOD(AddNonsequentialDuplicates) @@ -226,7 +226,7 @@ class HistoryTests VERIFY_SUCCEEDED(history->Add(L"cd", false)); VERIFY_SUCCEEDED(history->Add(L"dir", false)); - VERIFY_ARE_EQUAL(3ul, history->GetNumberOfCommands()); + VERIFY_ARE_EQUAL(3, history->GetNumberOfCommands()); } TEST_METHOD(AddNonsequentialNoDuplicates) @@ -239,7 +239,7 @@ class HistoryTests VERIFY_SUCCEEDED(history->Add(L"cd", false)); VERIFY_SUCCEEDED(history->Add(L"dir", true)); - VERIFY_ARE_EQUAL(2ul, history->GetNumberOfCommands()); + VERIFY_ARE_EQUAL(2, history->GetNumberOfCommands()); } private: @@ -267,7 +267,7 @@ class HistoryTests }; static constexpr UINT s_NumberOfBuffers = 4; - static constexpr UINT s_BufferSize = 10; + static constexpr CommandHistory::Index s_BufferSize = 10; HANDLE _MakeHandle(size_t index) { diff --git a/src/host/ut_host/PopupTestHelper.hpp b/src/host/ut_host/PopupTestHelper.hpp index 146949781e7..e427cd9624b 100644 --- a/src/host/ut_host/PopupTestHelper.hpp +++ b/src/host/ut_host/PopupTestHelper.hpp @@ -44,7 +44,7 @@ class PopupTestHelper final VERIFY_SUCCEEDED(history.Add(L"hear me shout", false)); VERIFY_SUCCEEDED(history.Add(L"here is my handle", false)); VERIFY_SUCCEEDED(history.Add(L"here is my spout", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 4u); + VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 4); } static void InitLongHistory(CommandHistory& history) noexcept @@ -79,6 +79,6 @@ class PopupTestHelper final VERIFY_SUCCEEDED(history.Add(L"Since then - 'tis Centuries - and yet", false)); VERIFY_SUCCEEDED(history.Add(L"Feels shorter than the Day", false)); VERIFY_SUCCEEDED(history.Add(L"~ Emily Dickinson", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 28u); + VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 28); } }; From 9b70a40bf9efb7541981106e094009b79627ca86 Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Tue, 8 Aug 2023 21:28:34 +0530 Subject: [PATCH 10/59] ConHost: Ignore WM_[SYS]KEYDOWN/WM_[SYS]KEYUP events with an invalid virtual keycode and a scan code of 0 (#15753) ## Summary Applications like PowerToys, with their keyboard remapping features frequently (i.e whenever a remapped shortcut is triggerred) send `KeyEvent` with out-of-range virtual keycode values (E.g. 0xFF). This is fixed for WT in #7145, we just needed it in our good ol' `conhost`. After this PR, Key events with an invalid virtual keycode and scancode==0 are ignored, and are not added to the `InputBuffer`. Incase, only virtual keycode is valid but not scancode, we will try to infer the correct scancode using the virtual keycode mapping. ## References and Relevant Issues #7145 #7064 ## Validation Steps Performed - Triggered a remapped shortcut and verified that `showkey -a` doesn't output `^@` unexpectedly. - Key events with an Invalid virtual Keycode and Scancode == 0 are ignored. - This PR doesn't include any changes for `WM_[SYS][DEAD]CHAR` messages, they are left unchanged. --- src/host/ft_host/Message_KeyPressTests.cpp | 98 +++++++++++++++++++++- src/interactivity/win32/windowio.cpp | 29 +++++-- 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/host/ft_host/Message_KeyPressTests.cpp b/src/host/ft_host/Message_KeyPressTests.cpp index 53d8b9dade8..b11f53ed0bf 100644 --- a/src/host/ft_host/Message_KeyPressTests.cpp +++ b/src/host/ft_host/Message_KeyPressTests.cpp @@ -103,6 +103,100 @@ class KeyPressTests TEST_METHOD_PROPERTY(L"Ignore[default]", L"true") END_TEST_METHOD() + TEST_METHOD(TestInvalidKeyPressIsIgnored) + { + if (!OneCoreDelay::IsSendMessageWPresent()) + { + Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + Log::Comment(L"Testing that key events with an invalid virtual keycode and an invalid scan code are properly ignored, and not put into the input buffer"); + BOOL successBool; + auto hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + auto inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_ARE_EQUAL(events, 0u); + + WPARAM vKey = 0xFF; + BYTE scanCode = 0; + WORD repeatCount = 1; + + LPARAM lParam = (scanCode << 16) | repeatCount; + + // Send the keypress + SendMessage(hwnd, WM_KEYDOWN, vKey, lParam); + + // make sure the keypress got ignored + events = 0; + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_ARE_EQUAL(events, 0u); + auto inputBuffer = std::make_unique(1); + PeekConsoleInput(inputHandle, + inputBuffer.get(), + 1, + &events); + VERIFY_ARE_EQUAL(events, 0u); + } + + TEST_METHOD(TestKeyPressWithScanCodeZero) + { + if (!OneCoreDelay::IsSendMessageWPresent()) + { + Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + Log::Comment(L"Testing that key events with a valid keycode and an invalid scancode (0) are properly processed."); + BOOL successBool; + auto hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + auto inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_ARE_EQUAL(events, 0u); + + WPARAM vKey = VK_LWIN; + BYTE scanCode = 0; // Conhost should convert this to the correct scan code + WORD repeatCount = 1; + + LPARAM lParam = (scanCode << 16) | repeatCount; + + // Send the keypress + SendMessage(hwnd, WM_KEYDOWN, vKey, lParam); + + // Make sure the keypress got processed. + events = 0; + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_ARE_EQUAL(events, 1u); + auto inputBuffer = std::make_unique(1); + PeekConsoleInput(inputHandle, + inputBuffer.get(), + 1, + &events); + VERIFY_ARE_EQUAL(events, 1u); + VERIFY_ARE_EQUAL(inputBuffer[0].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(inputBuffer[0].Event.KeyEvent.wRepeatCount, 1, NoThrowString().Format(L"%d", inputBuffer[0].Event.KeyEvent.wRepeatCount)); + // Scan code should be set to the correct value. + VERIFY_ARE_EQUAL(inputBuffer[0].Event.KeyEvent.wVirtualScanCode, VK_LWIN); + // 'VK_LWIN' is an enhanced key, so the ENHANCED_KEY bit should be set. + VERIFY_IS_TRUE(inputBuffer[0].Event.KeyEvent.dwControlKeyState & ENHANCED_KEY); + } + TEST_METHOD(TestCoalesceSameKeyPress) { if (!OneCoreDelay::IsSendMessageWPresent()) @@ -125,7 +219,7 @@ class KeyPressTests VERIFY_IS_TRUE(!!successBool); VERIFY_ARE_EQUAL(events, 0u); - // send a bunch of 'a' keypresses to the console + // send a bunch of 'a' keypresses to the console. DWORD repeatCount = 1; const unsigned int messageSendCount = 1000; for (unsigned int i = 0; i < messageSendCount; ++i) @@ -133,7 +227,7 @@ class KeyPressTests SendMessage(hwnd, WM_CHAR, 0x41, - repeatCount); + repeatCount); // WM_CHAR doesn't use scan code } // make sure the keypresses got processed and coalesced diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 5b882feeeef..c5a77b0e095 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -127,12 +127,13 @@ void HandleKeyEvent(const HWND hWnd, { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // BOGUS for WM_CHAR/WM_DEADCHAR, in which LOWORD(lParam) is a character + // BOGUS for WM_CHAR/WM_DEADCHAR, in which LOWORD(wParam) is a character auto VirtualKeyCode = LOWORD(wParam); WORD VirtualScanCode = LOBYTE(HIWORD(lParam)); const auto RepeatCount = LOWORD(lParam); - const auto ControlKeyState = GetControlKeyState(lParam); + auto ControlKeyState = GetControlKeyState(lParam); const BOOL bKeyDown = WI_IsFlagClear(lParam, KEY_TRANSITION_UP); + const bool IsCharacterMessage = (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR); if (bKeyDown) { @@ -146,7 +147,7 @@ void HandleKeyEvent(const HWND hWnd, // Make sure we retrieve the key info first, or we could chew up // unneeded space in the key info table if we bail out early. - if (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR) + if (IsCharacterMessage) { // --- START LOAD BEARING CODE --- // NOTE: We MUST match up the original data from the WM_KEYDOWN stroke (handled at some inexact moment in the @@ -167,9 +168,27 @@ void HandleKeyEvent(const HWND hWnd, // --- END LOAD BEARING CODE --- } + // Simulated key events (using `SendInput` or `SendMessage`) can have invalid + // virtual key code, and invalid scan code. We need to filter such events out, + // as some applications (e.g. WSL) treat those events as valid key events and + // translate them to an ascii NULL character. GH#15753 + if (VirtualScanCode == 0 && !IsCharacterMessage) // `WM_[SYS][DEAD]CHAR` messages don't have this issue + { + // We try to infer the correct scan code from the virtual key code. If the + // virtual key code is invalid or we couldn't map it to a scan code, + // MapVirtualKeyEx will return 0. + auto FullVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VirtualKeyCode, MAPVK_VK_TO_VSC_EX)); + VirtualScanCode = LOBYTE(FullVirtualScanCode); + ControlKeyState |= (HIBYTE(FullVirtualScanCode) == 0xE0) ? ENHANCED_KEY : 0; + if (VirtualScanCode == 0) + { + return; + } + } + KeyEvent keyEvent{ !!bKeyDown, RepeatCount, VirtualKeyCode, VirtualScanCode, UNICODE_NULL, 0 }; - if (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR) + if (IsCharacterMessage) { // If this is a fake character, zero the scancode. if (lParam & 0x02000000) @@ -409,7 +428,7 @@ void HandleKeyEvent(const HWND hWnd, // ignore key strokes that will generate CHAR messages. this is only necessary while a dialog box is up. if (ServiceLocator::LocateGlobals().uiDialogBoxCount != 0) { - if (Message != WM_CHAR && Message != WM_SYSCHAR && Message != WM_DEADCHAR && Message != WM_SYSDEADCHAR) + if (!IsCharacterMessage) { WCHAR awch[MAX_CHARS_FROM_1_KEYSTROKE]; BYTE KeyState[256]; From 1ed57cc5c95c2b8c073559d0953fda09a9e5ee1b Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 8 Aug 2023 11:14:47 -0500 Subject: [PATCH 11/59] Add a note about how to run the UIA tests locally (#15806) Carlos and I spent an hour rediscovering how to make this work. I figured it'd be best to leave notes behind for future archeologists. --- .../WindowsTerminal_UIATests/Elements/TerminalApp.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs index 291c608e087..ac944525e98 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs @@ -49,7 +49,15 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe") { this.context = context; - // If running locally, set WTPath to where we can find a loose deployment of Windows Terminal + // If running locally, set WTPath to where we can find a loose + // deployment of Windows Terminal. That means you'll need to build + // the Terminal appx, then use + // New-UnpackagedTerminalDistribution.ps1 to build an unpackaged + // layout that can successfully launch. Then, point the tests at + // that WindowsTerminal.exe like so: + // + // te.exe WindowsTerminal.UIA.Tests.dll /p:WTPath=C:\the\path\to\the\unpackaged\layout\WindowsTerminal.exe + // // On the build machines, the scripts lay it out at the terminal-0.0.1.0\ subfolder of the test deployment directory string path = Path.GetFullPath(Path.Combine(context.TestDeploymentDir, @"terminal-0.0.1.0\WindowsTerminal.exe")); if (context.Properties.Contains("WTPath")) From 6a29ca2adae50e3e99fe4326e277094af151cac2 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Wed, 9 Aug 2023 00:15:16 +0800 Subject: [PATCH 12/59] README: Fix GitHub built-in note highlight styles (#15807) ## Summary of the Pull Request I updated the note prefix for blockquotes with GitHub latest built-in styles. ## References and Relevant Issues - https://github.com/orgs/community/discussions/16925 ## Detailed Description of the Pull Request / Additional comments This PR fixed the broken style due to their format change. ## Related former Pull Request - https://github.com/microsoft/terminal/pull/13615 --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3d88994521a..f43556bb03d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Related repositories include: ## Installing and running Windows Terminal -> **Note**: Windows Terminal requires Windows 10 2004 (build 19041) or later +> **Note**\ +> Windows Terminal requires Windows 10 2004 (build 19041) or later ### Microsoft Store [Recommended] @@ -52,9 +53,10 @@ fails for any reason, you can try the following command at a PowerShell prompt: Add-AppxPackage Microsoft.WindowsTerminal_.msixbundle ``` -> **Note**: If you install Terminal manually: +> **Note**\ +> If you install Terminal manually: > -> * You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). +> * You may need to install the [VC++ v14 Desktop Framework Package](https://docs.microsoft.com/troubleshoot/cpp/c-runtime-packages-desktop-bridge#how-to-install-and-update-desktop-framework-packages). > This should only be necessary on older builds of Windows 10 and only if you get an error about missing framework packages. > * Terminal will not auto-update when new builds are released so you will need > to regularly install the latest Terminal release to receive all the latest @@ -70,7 +72,8 @@ package: winget install --id Microsoft.WindowsTerminal -e ``` -> **Note** Due to a dependency issue, Terminal's current versions cannot be installed via the Windows Package Manager CLI. To install the stable release 1.17 or later, or the Preview release 1.18 or later, please use an alternative installation method. +> **Note**\ +> Due to a dependency issue, Terminal's current versions cannot be installed via the Windows Package Manager CLI. To install the stable release 1.17 or later, or the Preview release 1.18 or later, please use an alternative installation method. #### Via Chocolatey (unofficial) @@ -237,7 +240,8 @@ Cause: You're launching the incorrect solution in Visual Studio. Solution: Make sure you're building & deploying the `CascadiaPackage` project in Visual Studio. -> **Note**: `OpenConsole.exe` is just a locally-built `conhost.exe`, the classic +> **Note**\ +> `OpenConsole.exe` is just a locally-built `conhost.exe`, the classic > Windows Console that hosts Windows' command-line infrastructure. OpenConsole > is used by Windows Terminal to connect to and communicate with command-line > applications (via From 256d46ad6becba858f05f375a6ae9ae9a7c75179 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 10 Aug 2023 17:13:36 -0500 Subject: [PATCH 13/59] Fix the TilWinRT tests (#15820) It's unknown how this ever worked, or why it stopped working recently. We removed our desire to directly compare property against T at some point. --- .../TilWinRtHelpersTests.cpp | 20 +++++++++---------- src/inc/til/winrt.h | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp b/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp index 091f42b1c3e..89c2856b8e0 100644 --- a/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp @@ -81,23 +81,23 @@ void TilWinRtHelpersTests::TestTruthiness() til::property FullString{ L"Full" }; VERIFY_IS_FALSE(Foo()); - VERIFY_IS_FALSE(Foo); + VERIFY_IS_FALSE((bool)Foo); VERIFY_IS_FALSE(Bar()); - VERIFY_IS_FALSE(Bar); + VERIFY_IS_FALSE((bool)Bar); - VERIFY_IS_FALSE(EmptyString); + VERIFY_IS_FALSE((bool)EmptyString); VERIFY_IS_FALSE(!EmptyString().empty()); Foo(true); VERIFY_IS_TRUE(Foo()); - VERIFY_IS_TRUE(Foo); + VERIFY_IS_TRUE((bool)Foo); Bar(11); VERIFY_IS_TRUE(Bar()); - VERIFY_IS_TRUE(Bar); + VERIFY_IS_TRUE((bool)Bar); - VERIFY_IS_TRUE(FullString); + VERIFY_IS_TRUE((bool)FullString); VERIFY_IS_TRUE(!FullString().empty()); } @@ -177,13 +177,13 @@ void TilWinRtHelpersTests::TestComposedConstProperties() const struct Helper noTouching; VERIFY_ARE_EQUAL(0, changeMe.Foo()); - VERIFY_ARE_EQUAL(3, changeMe.Composed().first); - VERIFY_ARE_EQUAL(2, changeMe.Composed().second); + VERIFY_ARE_EQUAL(3, changeMe.Composed().first()); + VERIFY_ARE_EQUAL(2, changeMe.Composed().second()); VERIFY_ARE_EQUAL(L"", changeMe.MyString()); VERIFY_ARE_EQUAL(0, noTouching.Foo()); - VERIFY_ARE_EQUAL(3, noTouching.Composed().first); - VERIFY_ARE_EQUAL(2, noTouching.Composed().second); + VERIFY_ARE_EQUAL(3, noTouching.Composed().first()); + VERIFY_ARE_EQUAL(2, noTouching.Composed().second()); VERIFY_ARE_EQUAL(L"", noTouching.MyString()); changeMe.Foo(42); diff --git a/src/inc/til/winrt.h b/src/inc/til/winrt.h index 28bcefdf354..ed815d17671 100644 --- a/src/inc/til/winrt.h +++ b/src/inc/til/winrt.h @@ -21,7 +21,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { _value = std::forward(arg); } - operator bool() const noexcept + explicit operator bool() const noexcept { if constexpr (std::is_same_v) { From e9c8391fd5901f0f84ecc44cdd1c6408669b4d8d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 11 Aug 2023 15:17:18 +0200 Subject: [PATCH 14/59] Fix compilation with Visual Studio 17.8 (#15819) This broke with https://github.com/microsoft/STL/pull/3721 It's a minor issue and a minor fix. :) --- src/buffer/out/Row.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index b31976b8467..760f8cf501f 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -4,6 +4,7 @@ #include "precomp.h" #include "Row.hpp" +#include #include #include "textBuffer.hpp" From 5b44476048c3bf9d0838b468e4fc0bcade9596da Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 11 Aug 2023 16:06:08 +0200 Subject: [PATCH 15/59] Replace IInputEvent with INPUT_RECORD (#15673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `IInputEvent` makes adding Unicode support to `InputBuffer` more difficult than necessary as the abstract class makes downcasting as well as copying quite verbose. I found that using `INPUT_RECORD`s directly leads to a significantly simplified implementation. In addition, this commit fixes at least one bug: The previous approach to detect the null key via `DoActiveModifierKeysMatch` didn't work. As it compared the modifier keys as a bitset with `==` it failed to match whenever the numpad key was set, which it usually is. ## Validation Steps Performed * Unit and feature tests are ✅ --- src/cascadia/TerminalControl/ControlCore.cpp | 2 +- src/cascadia/TerminalCore/Terminal.cpp | 10 +- src/cascadia/TerminalCore/Terminal.hpp | 16 + src/host/cmdline.h | 2 + src/host/conimeinfo.cpp | 13 +- src/host/directio.cpp | 15 +- src/host/ft_host/CJK_DbcsTests.cpp | 6 +- src/host/ft_host/Message_KeyPressTests.cpp | 1 - src/host/input.cpp | 39 +- src/host/input.h | 2 +- src/host/inputBuffer.cpp | 191 ++---- src/host/inputBuffer.hpp | 56 +- src/host/output.cpp | 2 +- src/host/outputStream.cpp | 11 +- src/host/readDataDirect.cpp | 6 +- src/host/readDataDirect.hpp | 2 +- src/host/stream.cpp | 182 ++++-- src/host/ut_host/ClipboardTests.cpp | 157 ++--- src/host/ut_host/Host.UnitTests.vcxproj | 1 - .../ut_host/Host.UnitTests.vcxproj.filters | 3 - src/host/ut_host/InputBufferTests.cpp | 163 +++-- src/inc/til/small_vector.h | 6 + src/interactivity/base/EventSynthesis.cpp | 203 ++---- src/interactivity/inc/EventSynthesis.hpp | 12 +- src/interactivity/onecore/ConIoSrvComm.cpp | 4 +- src/interactivity/win32/Clipboard.cpp | 20 +- src/interactivity/win32/clipboard.hpp | 6 +- src/interactivity/win32/windowio.cpp | 41 +- src/server/ApiDispatchers.cpp | 14 +- src/server/WaitBlock.cpp | 12 +- src/terminal/adapter/IInteractDispatch.hpp | 4 +- src/terminal/adapter/InteractDispatch.cpp | 18 +- src/terminal/adapter/InteractDispatch.hpp | 4 +- .../adapter/ut_adapter/MouseInputTest.cpp | 3 +- src/terminal/adapter/ut_adapter/inputTest.cpp | 96 +-- src/terminal/input/mouseInput.cpp | 1 + src/terminal/input/terminalInput.cpp | 127 ++-- src/terminal/input/terminalInput.hpp | 24 +- .../parser/InputStateMachineEngine.cpp | 93 +-- .../parser/InputStateMachineEngine.hpp | 6 +- .../parser/ut_parser/InputEngineTest.cpp | 164 +++-- src/types/FocusEvent.cpp | 38 -- src/types/IInputEvent.cpp | 68 -- src/types/IInputEventStreams.cpp | 113 ---- src/types/KeyEvent.cpp | 189 ------ src/types/MenuEvent.cpp | 25 - src/types/ModifierKeyState.cpp | 137 ---- src/types/MouseEvent.cpp | 43 -- src/types/WindowBufferSizeEvent.cpp | 26 - src/types/inc/IInputEvent.hpp | 602 ++---------------- src/types/lib/types.vcxproj | 9 +- src/types/lib/types.vcxproj.filters | 45 -- src/types/sources.inc | 6 - tools/ConsoleTypes.natvis | 25 +- 54 files changed, 786 insertions(+), 2278 deletions(-) delete mode 100644 src/types/FocusEvent.cpp delete mode 100644 src/types/IInputEvent.cpp delete mode 100644 src/types/IInputEventStreams.cpp delete mode 100644 src/types/KeyEvent.cpp delete mode 100644 src/types/MenuEvent.cpp delete mode 100644 src/types/ModifierKeyState.cpp delete mode 100644 src/types/MouseEvent.cpp delete mode 100644 src/types/WindowBufferSizeEvent.cpp diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 870b8f9fed5..7c460666a9d 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -511,7 +511,7 @@ 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() && !KeyEvent::IsModifierKey(vkey) && vkey != VK_SNAPSHOT; + return HasSelection() && ::Microsoft::Terminal::Core::Terminal::IsInputKey(vkey); } bool ControlCore::TryMarkModeKeybinding(const WORD vkey, diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index d475966350e..de33990e365 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -661,7 +661,7 @@ bool Terminal::SendKeyEvent(const WORD vkey, // modifier key. We'll wait for a real keystroke to snap to the bottom. // GH#6481 - Additionally, make sure the key was actually pressed. This // check will make sure we behave the same as before GH#6309 - if (!KeyEvent::IsModifierKey(vkey) && keyDown) + if (IsInputKey(vkey) && keyDown) { TrySnapOnInput(); } @@ -714,8 +714,8 @@ bool Terminal::SendKeyEvent(const WORD vkey, return false; } - const KeyEvent keyEv{ keyDown, 1, vkey, sc, ch, states.Value() }; - return _handleTerminalInputResult(_terminalInput.HandleKey(&keyEv)); + const auto keyEv = SynthesizeKeyEvent(keyDown, 1, vkey, sc, ch, states.Value()); + return _handleTerminalInputResult(_terminalInput.HandleKey(keyEv)); } // Method Description: @@ -791,8 +791,8 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro MarkOutputStart(); } - const KeyEvent keyDown{ true, 1, vkey, scanCode, ch, states.Value() }; - return _handleTerminalInputResult(_terminalInput.HandleKey(&keyDown)); + const auto keyDown = SynthesizeKeyEvent(true, 1, vkey, scanCode, ch, states.Value()); + return _handleTerminalInputResult(_terminalInput.HandleKey(keyDown)); } // Method Description: diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index fc392bc7e0c..faa6e12b1a3 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -60,6 +60,22 @@ class Microsoft::Terminal::Core::Terminal final : using RenderSettings = Microsoft::Console::Render::RenderSettings; public: + static constexpr bool IsInputKey(WORD vkey) + { + return vkey != VK_CONTROL && + vkey != VK_LCONTROL && + vkey != VK_RCONTROL && + vkey != VK_MENU && + vkey != VK_LMENU && + vkey != VK_RMENU && + vkey != VK_SHIFT && + vkey != VK_LSHIFT && + vkey != VK_RSHIFT && + vkey != VK_LWIN && + vkey != VK_RWIN && + vkey != VK_SNAPSHOT; + } + Terminal(); void Create(til::size viewportSize, diff --git a/src/host/cmdline.h b/src/host/cmdline.h index a8a5d19ca5b..e3e9511ff29 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -87,4 +87,6 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); bool IsWordDelim(const wchar_t wch); bool IsWordDelim(const std::wstring_view charData); +bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...); + void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index); diff --git a/src/host/conimeinfo.cpp b/src/host/conimeinfo.cpp index 9ab47a80029..fc2ce5ae74c 100644 --- a/src/host/conimeinfo.cpp +++ b/src/host/conimeinfo.cpp @@ -462,18 +462,13 @@ void ConsoleImeInfo::_InsertConvertedString(const std::wstring_view text) } const auto dwControlKeyState = GetControlKeyState(0); - std::deque> inEvents; - KeyEvent keyEvent{ TRUE, // keydown - 1, // repeatCount - 0, // virtualKeyCode - 0, // virtualScanCode - 0, // charData - dwControlKeyState }; // activeModifierKeys + InputEventQueue inEvents; + auto keyEvent = SynthesizeKeyEvent(true, 1, 0, 0, 0, dwControlKeyState); for (const auto& ch : text) { - keyEvent.SetCharData(ch); - inEvents.push_back(std::make_unique(keyEvent)); + keyEvent.Event.KeyEvent.uChar.UnicodeChar = ch; + inEvents.push_back(keyEvent); } gci.pInputBuffer->Write(inEvents); diff --git a/src/host/directio.cpp b/src/host/directio.cpp index b5ababfe4b4..1187bd4faae 100644 --- a/src/host/directio.cpp +++ b/src/host/directio.cpp @@ -101,7 +101,7 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Return Value: // - HRESULT indicating success or failure [[nodiscard]] static HRESULT _WriteConsoleInputWImplHelper(InputBuffer& context, - std::deque>& events, + const std::span& events, size_t& written, const bool append) noexcept { @@ -151,7 +151,7 @@ try auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - til::small_vector events; + InputEventQueue events; auto it = buffer.begin(); const auto end = buffer.end(); @@ -160,7 +160,7 @@ try // the next call to WriteConsoleInputAImpl to join it with the now available trailing DBCS. if (context.IsWritePartialByteSequenceAvailable()) { - auto lead = context.FetchWritePartialByteSequence(false)->ToInputRecord(); + auto lead = context.FetchWritePartialByteSequence(); const auto& trail = *it; if (trail.EventType == KEY_EVENT) @@ -200,7 +200,7 @@ try if (it == end) { // Missing trailing DBCS -> Store the lead for the next call to WriteConsoleInputAImpl. - context.StoreWritePartialByteSequence(IInputEvent::Create(lead)); + context.StoreWritePartialByteSequence(lead); break; } @@ -225,8 +225,7 @@ try } } - auto result = IInputEvent::Create(std::span{ events.data(), events.size() }); - return _WriteConsoleInputWImplHelper(context, result, written, append); + return _WriteConsoleInputWImplHelper(context, events, written, append); } CATCH_RETURN(); @@ -252,9 +251,7 @@ CATCH_RETURN(); try { - auto events = IInputEvent::Create(buffer); - - return _WriteConsoleInputWImplHelper(context, events, written, append); + return _WriteConsoleInputWImplHelper(context, buffer, written, append); } CATCH_RETURN(); } diff --git a/src/host/ft_host/CJK_DbcsTests.cpp b/src/host/ft_host/CJK_DbcsTests.cpp index 6e29b8a815c..68798b700fd 100644 --- a/src/host/ft_host/CJK_DbcsTests.cpp +++ b/src/host/ft_host/CJK_DbcsTests.cpp @@ -1919,11 +1919,11 @@ void DbcsTests::TestMultibyteInputCoalescing() DWORD count; { - const auto record = KeyEvent{ true, 1, 123, 456, 0x82, 789 }.ToInputRecord(); + const auto record = SynthesizeKeyEvent(true, 1, 123, 456, 0x82, 789); VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); } { - const auto record = KeyEvent{ true, 1, 234, 567, 0xA2, 890 }.ToInputRecord(); + const auto record = SynthesizeKeyEvent(true, 1, 234, 567, 0xA2, 890); VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); } @@ -1933,7 +1933,7 @@ void DbcsTests::TestMultibyteInputCoalescing() VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(in, &actual[0], 2, &count)); VERIFY_ARE_EQUAL(1u, count); - const auto expected = KeyEvent{ true, 1, 123, 456, L'い', 789 }.ToInputRecord(); + const auto expected = SynthesizeKeyEvent(true, 1, 123, 456, L'い', 789); VERIFY_ARE_EQUAL(expected, actual[0]); } diff --git a/src/host/ft_host/Message_KeyPressTests.cpp b/src/host/ft_host/Message_KeyPressTests.cpp index b11f53ed0bf..07ce3ec9268 100644 --- a/src/host/ft_host/Message_KeyPressTests.cpp +++ b/src/host/ft_host/Message_KeyPressTests.cpp @@ -86,7 +86,6 @@ class KeyPressTests expectedRecord.Event.KeyEvent.uChar.UnicodeChar = 0x0; expectedRecord.Event.KeyEvent.bKeyDown = true; expectedRecord.Event.KeyEvent.dwControlKeyState = ENHANCED_KEY; - expectedRecord.Event.KeyEvent.dwControlKeyState |= (GetKeyState(VK_NUMLOCK) & KEY_STATE_TOGGLED) ? NUMLOCK_ON : 0; expectedRecord.Event.KeyEvent.wRepeatCount = SINGLE_KEY_REPEAT; expectedRecord.Event.KeyEvent.wVirtualKeyCode = VK_APPS; expectedRecord.Event.KeyEvent.wVirtualScanCode = (WORD)scanCode; diff --git a/src/host/input.cpp b/src/host/input.cpp index 94e58d1407a..d48192daabb 100644 --- a/src/host/input.cpp +++ b/src/host/input.cpp @@ -105,17 +105,18 @@ bool ShouldTakeOverKeyboardShortcuts() // Routine Description: // - handles key events without reference to Win32 elements. -void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak) +void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak) { + auto& keyEvent = event.Event.KeyEvent; const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto ContinueProcessing = true; - if (keyEvent.IsCtrlPressed() && - !keyEvent.IsAltPressed() && - keyEvent.IsKeyDown()) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED) && + WI_AreAllFlagsClear(keyEvent.dwControlKeyState, ALT_PRESSED) && + keyEvent.bKeyDown) { // check for ctrl-c, if in line input mode. - if (keyEvent.GetVirtualKeyCode() == 'C' && IsInProcessedInputMode()) + if (keyEvent.wVirtualKeyCode == 'C' && IsInProcessedInputMode()) { HandleCtrlEvent(CTRL_C_EVENT); if (gci.PopupCount == 0) @@ -130,7 +131,7 @@ void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak) } // Check for ctrl-break. - else if (keyEvent.GetVirtualKeyCode() == VK_CANCEL) + else if (keyEvent.wVirtualKeyCode == VK_CANCEL) { gci.pInputBuffer->Flush(); HandleCtrlEvent(CTRL_BREAK_EVENT); @@ -146,33 +147,25 @@ void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak) } // don't write ctrl-esc to the input buffer - else if (keyEvent.GetVirtualKeyCode() == VK_ESCAPE) + else if (keyEvent.wVirtualKeyCode == VK_ESCAPE) { ContinueProcessing = false; } } - else if (keyEvent.IsAltPressed() && - keyEvent.IsKeyDown() && - keyEvent.GetVirtualKeyCode() == VK_ESCAPE) + else if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && + keyEvent.bKeyDown && + keyEvent.wVirtualKeyCode == VK_ESCAPE) { ContinueProcessing = false; } if (ContinueProcessing) { - size_t EventsWritten = 0; - try + gci.pInputBuffer->Write(event); + if (generateBreak) { - EventsWritten = gci.pInputBuffer->Write(std::make_unique(keyEvent)); - if (EventsWritten && generateBreak) - { - keyEvent.SetKeyDown(false); - EventsWritten = gci.pInputBuffer->Write(std::make_unique(keyEvent)); - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); + keyEvent.bKeyDown = false; + gci.pInputBuffer->Write(event); } } } @@ -210,7 +203,7 @@ void HandleMenuEvent(const DWORD wParam) size_t EventsWritten = 0; try { - EventsWritten = gci.pInputBuffer->Write(std::make_unique(wParam)); + EventsWritten = gci.pInputBuffer->Write(SynthesizeMenuEvent(wParam)); if (EventsWritten != 1) { RIPMSG0(RIP_WARNING, "PutInputInBuffer: EventsWritten != 1, 1 expected"); diff --git a/src/host/input.h b/src/host/input.h index 4b7667aa063..7e4509f6a1f 100644 --- a/src/host/input.h +++ b/src/host/input.h @@ -78,7 +78,7 @@ bool ShouldTakeOverKeyboardShortcuts(); void HandleMenuEvent(const DWORD wParam); void HandleFocusEvent(const BOOL fSetFocus); void HandleCtrlEvent(const DWORD EventType); -void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak); +void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak); void ProcessCtrlEvents(); diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp index de272bc1dc2..a4ed5000a89 100644 --- a/src/host/inputBuffer.cpp +++ b/src/host/inputBuffer.cpp @@ -183,7 +183,7 @@ size_t InputBuffer::PeekCached(bool isUnicode, size_t count, InputEventQueue& ta break; } - target.push_back(IInputEvent::Create(e->ToInputRecord())); + target.push_back(e); i++; } @@ -222,7 +222,7 @@ void InputBuffer::_switchReadingModeSlowPath(ReadingMode mode) _cachedTextW = std::wstring{}; _cachedTextReaderW = {}; - _cachedInputEvents = std::deque>{}; + _cachedInputEvents = std::deque{}; _readingMode = mode; } @@ -234,9 +234,9 @@ void InputBuffer::_switchReadingModeSlowPath(ReadingMode mode) // - None // Return Value: // - true if partial char data is available, false otherwise -bool InputBuffer::IsWritePartialByteSequenceAvailable() +bool InputBuffer::IsWritePartialByteSequenceAvailable() const noexcept { - return _writePartialByteSequence.get() != nullptr; + return _writePartialByteSequenceAvailable; } // Routine Description: @@ -245,23 +245,10 @@ bool InputBuffer::IsWritePartialByteSequenceAvailable() // - peek - if true, data will not be removed after being fetched // Return Value: // - the partial char data. may be nullptr if no data is available -std::unique_ptr InputBuffer::FetchWritePartialByteSequence(_In_ bool peek) +const INPUT_RECORD& InputBuffer::FetchWritePartialByteSequence() noexcept { - if (!IsWritePartialByteSequenceAvailable()) - { - return std::unique_ptr(); - } - - if (peek) - { - return IInputEvent::Create(_writePartialByteSequence->ToInputRecord()); - } - else - { - std::unique_ptr outEvent; - outEvent.swap(_writePartialByteSequence); - return outEvent; - } + _writePartialByteSequenceAvailable = false; + return _writePartialByteSequence; } // Routine Description: @@ -271,9 +258,10 @@ std::unique_ptr InputBuffer::FetchWritePartialByteSequence(_In_ boo // - event - The event to store // Return Value: // - None -void InputBuffer::StoreWritePartialByteSequence(std::unique_ptr event) +void InputBuffer::StoreWritePartialByteSequence(const INPUT_RECORD& event) noexcept { - _writePartialByteSequence.swap(event); + _writePartialByteSequenceAvailable = true; + _writePartialByteSequence = event; } // Routine Description: @@ -348,8 +336,8 @@ void InputBuffer::Flush() // - The console lock must be held when calling this routine. void InputBuffer::FlushAllButKeys() { - auto newEnd = std::remove_if(_storage.begin(), _storage.end(), [](const std::unique_ptr& event) { - return event->EventType() != InputEventType::KeyEvent; + auto newEnd = std::remove_if(_storage.begin(), _storage.end(), [](const INPUT_RECORD& event) { + return event.EventType != KEY_EVENT; }); _storage.erase(newEnd, _storage.end()); } @@ -391,7 +379,7 @@ void InputBuffer::PassThroughWin32MouseRequest(bool enable) // - STATUS_SUCCESS if records were read into the client buffer and everything is OK. // - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed) // - otherwise a suitable memory/math/string error in NTSTATUS form. -[[nodiscard]] NTSTATUS InputBuffer::Read(_Out_ std::deque>& OutEvents, +[[nodiscard]] NTSTATUS InputBuffer::Read(_Out_ InputEventQueue& OutEvents, const size_t AmountToRead, const bool Peek, const bool WaitForData, @@ -417,31 +405,29 @@ try while (it != end && OutEvents.size() < AmountToRead) { - auto event = IInputEvent::Create((*it)->ToInputRecord()); - - if (event->EventType() == InputEventType::KeyEvent) + if (it->EventType == KEY_EVENT) { - const auto keyEvent = static_cast(event.get()); + auto event = *it; WORD repeat = 1; // for stream reads we need to split any key events that have been coalesced if (Stream) { - repeat = keyEvent->GetRepeatCount(); - keyEvent->SetRepeatCount(1); + repeat = std::max(1, event.Event.KeyEvent.wRepeatCount); + event.Event.KeyEvent.wRepeatCount = 1; } if (Unicode) { do { - OutEvents.push_back(std::make_unique(*keyEvent)); + OutEvents.push_back(event); repeat--; } while (repeat > 0 && OutEvents.size() < AmountToRead); } else { - const auto wch = keyEvent->GetCharData(); + const auto wch = event.Event.KeyEvent.uChar.UnicodeChar; char buffer[8]; const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr); @@ -453,9 +439,10 @@ try { for (const auto& ch : str) { - auto tempEvent = std::make_unique(*keyEvent); - tempEvent->SetCharData(ch); - OutEvents.push_back(std::move(tempEvent)); + // char is signed and assigning it to UnicodeChar would cause sign-extension. + // unsigned char doesn't have this problem. + event.Event.KeyEvent.uChar.UnicodeChar = til::bit_cast(ch); + OutEvents.push_back(event); } repeat--; } while (repeat > 0 && OutEvents.size() < AmountToRead); @@ -463,14 +450,13 @@ try if (repeat && !Peek) { - const auto originalKeyEvent = static_cast((*it).get()); - originalKeyEvent->SetRepeatCount(repeat); + it->Event.KeyEvent.wRepeatCount = repeat; break; } } else { - OutEvents.push_back(std::move(event)); + OutEvents.push_back(*it); } ++it; @@ -498,51 +484,6 @@ catch (...) return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } -// Routine Description: -// - This routine reads a single event from the input buffer. -// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition -// if there isn't enough data in the buffer, and it can be set to not remove records as it reads them out. -// Note: -// - The console lock must be held when calling this routine. -// Arguments: -// - outEvent - where the read event is stored -// - Peek - If true, copy events to pInputRecord but don't remove them from the input buffer. -// - WaitForData - if true, wait until an event is input (if there aren't enough to fill client buffer). if false, return immediately -// - Unicode - true if the data in key events should be treated as unicode. false if they should be converted by the current input CP. -// - Stream - true if read should unpack KeyEvents that have a >1 repeat count. -// Return Value: -// - STATUS_SUCCESS if records were read into the client buffer and everything is OK. -// - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed) -// - otherwise a suitable memory/math/string error in NTSTATUS form. -[[nodiscard]] NTSTATUS InputBuffer::Read(_Out_ std::unique_ptr& outEvent, - const bool Peek, - const bool WaitForData, - const bool Unicode, - const bool Stream) -{ - NTSTATUS Status; - try - { - std::deque> outEvents; - Status = Read(outEvents, - 1, - Peek, - WaitForData, - Unicode, - Stream); - if (!outEvents.empty()) - { - outEvent.swap(outEvents.front()); - } - } - catch (...) - { - Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - - return Status; -} - // Routine Description: // - Writes events to the beginning of the input buffer. // Arguments: @@ -552,7 +493,7 @@ catch (...) // S_OK if successful. // Note: // - The console lock must be held when calling this routine. -size_t InputBuffer::Prepend(_Inout_ std::deque>& inEvents) +size_t InputBuffer::Prepend(const std::span& inEvents) { try { @@ -569,7 +510,7 @@ size_t InputBuffer::Prepend(_Inout_ std::deque>& in // this way to handle any coalescing that might occur. // get all of the existing records, "emptying" the buffer - std::deque> existingStorage; + std::deque existingStorage; existingStorage.swap(_storage); // We will need this variable to pass to _WriteBuffer so it can attempt to determine wait status. @@ -583,10 +524,10 @@ size_t InputBuffer::Prepend(_Inout_ std::deque>& in _WriteBuffer(inEvents, prependEventsWritten, unusedWaitStatus); FAIL_FAST_IF(!(unusedWaitStatus)); - // write all previously existing records - size_t existingEventsWritten; - _WriteBuffer(existingStorage, existingEventsWritten, unusedWaitStatus); - FAIL_FAST_IF(!(!unusedWaitStatus)); + for (const auto& event : existingStorage) + { + _storage.push_back(event); + } // We need to set the wait event if there were 0 events in the // input queue when we started. @@ -621,19 +562,9 @@ size_t InputBuffer::Prepend(_Inout_ std::deque>& in // - The console lock must be held when calling this routine. // - any outside references to inEvent will ben invalidated after // calling this method. -size_t InputBuffer::Write(_Inout_ std::unique_ptr inEvent) +size_t InputBuffer::Write(const INPUT_RECORD& inEvent) { - try - { - std::deque> inEvents; - inEvents.push_back(std::move(inEvent)); - return Write(inEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - return 0; - } + return Write(std::span{ &inEvent, 1 }); } // Routine Description: @@ -645,7 +576,7 @@ size_t InputBuffer::Write(_Inout_ std::unique_ptr inEvent) // - The number of events that were written to input buffer. // Note: // - The console lock must be held when calling this routine. -size_t InputBuffer::Write(_Inout_ std::deque>& inEvents) +size_t InputBuffer::Write(const std::span& inEvents) { try { @@ -694,7 +625,7 @@ void InputBuffer::WriteFocusEvent(bool focused) noexcept { // This is a mini-version of Write(). const auto wasEmpty = _storage.empty(); - _storage.push_back(std::make_unique(focused)); + _storage.push_back(SynthesizeFocusEvent(focused)); if (wasEmpty) { ServiceLocator::LocateGlobals().hInputEvent.SetEvent(); @@ -760,9 +691,7 @@ static bool IsPauseKey(const KEY_EVENT_RECORD& event) // Note: // - The console lock must be held when calling this routine. // - will throw on failure -void InputBuffer::_WriteBuffer(_Inout_ std::deque>& inEvents, - _Out_ size_t& eventsWritten, - _Out_ bool& setWaitEvent) +void InputBuffer::_WriteBuffer(const std::span& inEvents, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -772,18 +701,18 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& const auto initialInEventsSize = inEvents.size(); const auto vtInputMode = IsInVirtualTerminalInputMode(); - for (auto& inEvent : inEvents) + for (const auto& inEvent : inEvents) { - if (inEvent->EventType() == InputEventType::KeyEvent && static_cast(inEvent.get())->IsKeyDown()) + if (inEvent.EventType == KEY_EVENT && inEvent.Event.KeyEvent.bKeyDown) { // if output is suspended, any keyboard input releases it. - if (WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED) && !IsSystemKey(static_cast(inEvent.get())->GetVirtualKeyCode())) + if (WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED) && !IsSystemKey(inEvent.Event.KeyEvent.wVirtualKeyCode)) { UnblockWriteConsole(CONSOLE_OUTPUT_SUSPENDED); continue; } // intercept control-s - if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && IsPauseKey(inEvent->ToInputRecord().Event.KeyEvent)) + if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && IsPauseKey(inEvent.Event.KeyEvent)) { WI_SetFlag(gci.Flags, CONSOLE_SUSPENDED); continue; @@ -797,7 +726,7 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& if (vtInputMode) { // GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly - if (const auto out = _termInput.HandleKey(inEvent.get())) + if (const auto out = _termInput.HandleKey(inEvent)) { _HandleTerminalInputCallback(*out); eventsWritten++; @@ -816,7 +745,7 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& } // At this point, the event was neither coalesced, nor processed by VT. - _storage.push_back(std::move(inEvent)); + _storage.push_back(inEvent); ++eventsWritten; } if (initiallyEmptyQueue && !_storage.empty()) @@ -839,39 +768,39 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& // the buffer with updated values from an incoming event, instead of // storing the incoming event (which would make the original one // redundant/out of date with the most current state). -bool InputBuffer::_CoalesceEvent(const std::unique_ptr& inEvent) const noexcept +bool InputBuffer::_CoalesceEvent(const INPUT_RECORD& inEvent) noexcept { auto& lastEvent = _storage.back(); - if (lastEvent->EventType() == InputEventType::MouseEvent && inEvent->EventType() == InputEventType::MouseEvent) + if (lastEvent.EventType == MOUSE_EVENT && inEvent.EventType == MOUSE_EVENT) { - const auto& inMouse = *static_cast(inEvent.get()); - auto& lastMouse = *static_cast(lastEvent.get()); + const auto& inMouse = inEvent.Event.MouseEvent; + auto& lastMouse = lastEvent.Event.MouseEvent; - if (lastMouse.IsMouseMoveEvent() && inMouse.IsMouseMoveEvent()) + if (lastMouse.dwEventFlags == MOUSE_MOVED && inMouse.dwEventFlags == MOUSE_MOVED) { - lastMouse.SetPosition(inMouse.GetPosition()); + lastMouse.dwMousePosition = inMouse.dwMousePosition; return true; } } - else if (lastEvent->EventType() == InputEventType::KeyEvent && inEvent->EventType() == InputEventType::KeyEvent) + else if (lastEvent.EventType == KEY_EVENT && inEvent.EventType == KEY_EVENT) { - const auto& inKey = *static_cast(inEvent.get()); - auto& lastKey = *static_cast(lastEvent.get()); - - if (lastKey.IsKeyDown() && inKey.IsKeyDown() && - (lastKey.GetVirtualScanCode() == inKey.GetVirtualScanCode() || WI_IsFlagSet(inKey.GetActiveModifierKeys(), NLS_IME_CONVERSION)) && - lastKey.GetCharData() == inKey.GetCharData() && - lastKey.GetActiveModifierKeys() == inKey.GetActiveModifierKeys() && - // TODO: This behavior is an import from old conhost v1 and has been broken for decades. + const auto& inKey = inEvent.Event.KeyEvent; + auto& lastKey = lastEvent.Event.KeyEvent; + + if (lastKey.bKeyDown && inKey.bKeyDown && + (lastKey.wVirtualScanCode == inKey.wVirtualScanCode || WI_IsFlagSet(inKey.dwControlKeyState, NLS_IME_CONVERSION)) && + lastKey.uChar.UnicodeChar == inKey.uChar.UnicodeChar && + lastKey.dwControlKeyState == inKey.dwControlKeyState && + // TODO:GH#8000 This behavior is an import from old conhost v1 and has been broken for decades. // This is probably the outdated idea that any wide glyph is being represented by 2 characters (DBCS) and likely // resulted from conhost originally being split into a ASCII/OEM and a DBCS variant with preprocessor flags. // You can't update the repeat count of such a A,B pair, because they're stored as A,A,B,B (down-down, up-up). // I believe the proper approach is to store pairs of characters as pairs, update their combined // repeat count and only when they're being read de-coalesce them into their alternating form. - !IsGlyphFullWidth(inKey.GetCharData())) + !IsGlyphFullWidth(inKey.uChar.UnicodeChar)) { - lastKey.SetRepeatCount(lastKey.GetRepeatCount() + inKey.GetRepeatCount()); + lastKey.wRepeatCount += inKey.wRepeatCount; return true; } } @@ -908,7 +837,7 @@ void InputBuffer::_HandleTerminalInputCallback(const TerminalInput::StringType& for (const auto& wch : text) { - _storage.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); + _storage.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0)); } if (!_vtInputShouldSuppress) diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp index 5fde60431f9..0044cb89d27 100644 --- a/src/host/inputBuffer.hpp +++ b/src/host/inputBuffer.hpp @@ -1,20 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- inputBuffer.hpp - -Abstract: -- storage area for incoming input events. - -Author: -- Therese Stowell (Thereses) 12-Nov-1990. Adapted from OS/2 subsystem server\srvpipe.c - -Revision History: -- Moved from input.h/input.cpp. (AustDi, 2017) -- Refactored to class, added stl container usage (AustDi, 2017) ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -52,9 +37,9 @@ class InputBuffer final : public ConsoleObjectHeader void Cache(bool isUnicode, InputEventQueue& source, size_t expectedSourceSize); // storage API for partial dbcs bytes being written to the buffer - bool IsWritePartialByteSequenceAvailable(); - std::unique_ptr FetchWritePartialByteSequence(_In_ bool peek); - void StoreWritePartialByteSequence(std::unique_ptr event); + bool IsWritePartialByteSequenceAvailable() const noexcept; + const INPUT_RECORD& FetchWritePartialByteSequence() noexcept; + void StoreWritePartialByteSequence(const INPUT_RECORD& event) noexcept; void ReinitializeInputBuffer(); void WakeUpReadersWaitingForData(); @@ -63,24 +48,16 @@ class InputBuffer final : public ConsoleObjectHeader void Flush(); void FlushAllButKeys(); - [[nodiscard]] NTSTATUS Read(_Out_ std::deque>& OutEvents, + [[nodiscard]] NTSTATUS Read(_Out_ InputEventQueue& OutEvents, const size_t AmountToRead, const bool Peek, const bool WaitForData, const bool Unicode, const bool Stream); - [[nodiscard]] NTSTATUS Read(_Out_ std::unique_ptr& inEvent, - const bool Peek, - const bool WaitForData, - const bool Unicode, - const bool Stream); - - size_t Prepend(_Inout_ std::deque>& inEvents); - - size_t Write(_Inout_ std::unique_ptr inEvent); - size_t Write(_Inout_ std::deque>& inEvents); - + size_t Prepend(const std::span& inEvents); + size_t Write(const INPUT_RECORD& inEvent); + size_t Write(const std::span& inEvents); void WriteFocusEvent(bool focused) noexcept; bool WriteMouseEvent(til::point position, unsigned int button, short keyState, short wheelDelta); @@ -102,11 +79,12 @@ class InputBuffer final : public ConsoleObjectHeader std::string_view _cachedTextReaderA; std::wstring _cachedTextW; std::wstring_view _cachedTextReaderW; - std::deque> _cachedInputEvents; + std::deque _cachedInputEvents; ReadingMode _readingMode = ReadingMode::StringA; - std::deque> _storage; - std::unique_ptr _writePartialByteSequence; + std::deque _storage; + INPUT_RECORD _writePartialByteSequence{}; + bool _writePartialByteSequenceAvailable = false; Microsoft::Console::VirtualTerminal::TerminalInput _termInput; Microsoft::Console::Render::VtEngine* _pTtyConnection; @@ -118,12 +96,8 @@ class InputBuffer final : public ConsoleObjectHeader void _switchReadingMode(ReadingMode mode); void _switchReadingModeSlowPath(ReadingMode mode); - - void _WriteBuffer(_Inout_ std::deque>& inRecords, - _Out_ size_t& eventsWritten, - _Out_ bool& setWaitEvent); - - bool _CoalesceEvent(const std::unique_ptr& inEvent) const noexcept; + void _WriteBuffer(const std::span& inRecords, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent); + bool _CoalesceEvent(const INPUT_RECORD& inEvent) noexcept; void _HandleTerminalInputCallback(const Microsoft::Console::VirtualTerminal::TerminalInput::StringType& text); #ifdef UNIT_TESTING diff --git a/src/host/output.cpp b/src/host/output.cpp index 75f522de314..beae8a8ef98 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -257,7 +257,7 @@ void ScreenBufferSizeChange(const til::size coordNewSize) try { - gci.pInputBuffer->Write(std::make_unique(coordNewSize)); + gci.pInputBuffer->Write(SynthesizeWindowBufferSizeEvent(coordNewSize)); } catch (...) { diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index e20f1e73f99..72d5353f4c8 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -33,18 +33,17 @@ ConhostInternalGetSet::ConhostInternalGetSet(_In_ IIoProvider& io) : // - void ConhostInternalGetSet::ReturnResponse(const std::wstring_view response) { - std::deque> inEvents; + InputEventQueue inEvents; // generate a paired key down and key up event for every // character to be sent into the console's input buffer for (const auto& wch : response) { // This wasn't from a real keyboard, so we're leaving key/scan codes blank. - KeyEvent keyEvent{ TRUE, 1, 0, 0, wch, 0 }; - - inEvents.push_back(std::make_unique(keyEvent)); - keyEvent.SetKeyDown(false); - inEvents.push_back(std::make_unique(keyEvent)); + auto keyEvent = SynthesizeKeyEvent(true, 1, 0, 0, wch, 0); + inEvents.push_back(keyEvent); + keyEvent.Event.KeyEvent.bKeyDown = false; + inEvents.push_back(keyEvent); } // TODO GH#4954 During the input refactor we may want to add a "priority" input list diff --git a/src/host/readDataDirect.cpp b/src/host/readDataDirect.cpp index 9f8edc3daad..778b585c4cd 100644 --- a/src/host/readDataDirect.cpp +++ b/src/host/readDataDirect.cpp @@ -47,7 +47,7 @@ DirectReadData::DirectReadData(_In_ InputBuffer* const pInputBuffer, // - pControlKeyState - For certain types of reads, this specifies // which modifier keys were held. // - pOutputData - a pointer to a -// std::deque> that is used to the read +// InputEventQueue that is used to the read // input events back to the server // Return Value: // - true if the wait is done and result buffer/status code can be sent back to the client. @@ -69,8 +69,6 @@ try *pControlKeyState = 0; *pNumBytes = 0; - std::deque> readEvents; - // If ctrl-c or ctrl-break was seen, ignore it. if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) { @@ -119,7 +117,7 @@ try } // move events to pOutputData - const auto pOutputDeque = static_cast>* const>(pOutputData); + const auto pOutputDeque = static_cast(pOutputData); *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD); *pOutputDeque = std::move(_outEvents); diff --git a/src/host/readDataDirect.hpp b/src/host/readDataDirect.hpp index 399b8a99ca8..44768c09d10 100644 --- a/src/host/readDataDirect.hpp +++ b/src/host/readDataDirect.hpp @@ -47,5 +47,5 @@ class DirectReadData final : public ReadData private: const size_t _eventReadCount; - std::deque> _outEvents; + InputEventQueue _outEvents; }; diff --git a/src/host/stream.cpp b/src/host/stream.cpp index c98ef9eaf08..e8dbe5aa2b0 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -16,10 +16,95 @@ #include "../interactivity/inc/ServiceLocator.hpp" -#pragma hdrstop - using Microsoft::Console::Interactivity::ServiceLocator; +static bool IsCommandLinePopupKey(const KEY_EVENT_RECORD& event) +{ + if (WI_AreAllFlagsClear(event.dwControlKeyState, RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + { + switch (event.wVirtualKeyCode) + { + case VK_ESCAPE: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_F2: + case VK_F4: + case VK_F7: + case VK_F9: + case VK_DELETE: + return true; + default: + break; + } + } + return false; +} + +static bool IsCommandLineEditingKey(const KEY_EVENT_RECORD& event) +{ + if (WI_AreAllFlagsClear(event.dwControlKeyState, RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + { + switch (event.wVirtualKeyCode) + { + case VK_ESCAPE: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_INSERT: + case VK_DELETE: + case VK_F1: + case VK_F2: + case VK_F3: + case VK_F4: + case VK_F5: + case VK_F6: + case VK_F7: + case VK_F8: + case VK_F9: + return true; + default: + break; + } + } + if (WI_IsAnyFlagSet(event.dwControlKeyState, RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED)) + { + switch (event.wVirtualKeyCode) + { + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_RIGHT: + return true; + default: + break; + } + } + if (WI_IsAnyFlagSet(event.dwControlKeyState, RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED)) + { + switch (event.wVirtualKeyCode) + { + case VK_F7: + case VK_F10: + return true; + default: + break; + } + } + + return false; +} + // Routine Description: // - This routine is used in stream input. It gets input and filters it for unicode characters. // Arguments: @@ -56,57 +141,50 @@ using Microsoft::Console::Interactivity::ServiceLocator; *pdwKeyState = 0; } - NTSTATUS Status; for (;;) { - std::unique_ptr inputEvent; - Status = pInputBuffer->Read(inputEvent, - false, // peek - Wait, - true, // unicode - true); // stream - + InputEventQueue events; + const auto Status = pInputBuffer->Read(events, 1, false, Wait, true, true); if (FAILED_NTSTATUS(Status)) { return Status; } - else if (inputEvent.get() == nullptr) + if (events.empty()) { - FAIL_FAST_IF(Wait); + assert(!Wait); return STATUS_UNSUCCESSFUL; } - if (inputEvent->EventType() == InputEventType::KeyEvent) + const auto& Event = events[0]; + if (Event.EventType == KEY_EVENT) { - auto keyEvent = std::unique_ptr(static_cast(inputEvent.release())); - auto commandLineEditKey = false; if (pCommandLineEditingKeys) { - commandLineEditKey = keyEvent->IsCommandLineEditingKey(); + commandLineEditKey = IsCommandLineEditingKey(Event.Event.KeyEvent); } else if (pPopupKeys) { - commandLineEditKey = keyEvent->IsPopupKey(); + commandLineEditKey = IsCommandLinePopupKey(Event.Event.KeyEvent); } if (pdwKeyState) { - *pdwKeyState = keyEvent->GetActiveModifierKeys(); + *pdwKeyState = Event.Event.KeyEvent.dwControlKeyState; } - if (keyEvent->GetCharData() != 0 && !commandLineEditKey) + if (Event.Event.KeyEvent.uChar.UnicodeChar != 0 && !commandLineEditKey) { // chars that are generated using alt + numpad - if (!keyEvent->IsKeyDown() && keyEvent->GetVirtualKeyCode() == VK_MENU) + if (!Event.Event.KeyEvent.bKeyDown && Event.Event.KeyEvent.wVirtualKeyCode == VK_MENU) { - if (keyEvent->IsAltNumpadSet()) + if (WI_IsFlagSet(Event.Event.KeyEvent.dwControlKeyState, ALTNUMPAD_BIT)) { - if (HIBYTE(keyEvent->GetCharData())) + if (HIBYTE(Event.Event.KeyEvent.uChar.UnicodeChar)) { - char chT[2] = { - static_cast(HIBYTE(keyEvent->GetCharData())), - static_cast(LOBYTE(keyEvent->GetCharData())), + const char chT[2] = { + static_cast(HIBYTE(Event.Event.KeyEvent.uChar.UnicodeChar)), + static_cast(LOBYTE(Event.Event.KeyEvent.uChar.UnicodeChar)), }; *pwchOut = CharToWchar(chT, 2); } @@ -115,64 +193,54 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Because USER doesn't know our codepage, // it gives us the raw OEM char and we // convert it to a Unicode character. - char chT = LOBYTE(keyEvent->GetCharData()); + char chT = LOBYTE(Event.Event.KeyEvent.uChar.UnicodeChar); *pwchOut = CharToWchar(&chT, 1); } } else { - *pwchOut = keyEvent->GetCharData(); + *pwchOut = Event.Event.KeyEvent.uChar.UnicodeChar; } return STATUS_SUCCESS; } + // Ignore Escape and Newline chars - else if (keyEvent->IsKeyDown() && - (WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT) || - (keyEvent->GetVirtualKeyCode() != VK_ESCAPE && - keyEvent->GetCharData() != UNICODE_LINEFEED))) + if (Event.Event.KeyEvent.bKeyDown && + (WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT) || + (Event.Event.KeyEvent.wVirtualKeyCode != VK_ESCAPE && + Event.Event.KeyEvent.uChar.UnicodeChar != UNICODE_LINEFEED))) { - *pwchOut = keyEvent->GetCharData(); + *pwchOut = Event.Event.KeyEvent.uChar.UnicodeChar; return STATUS_SUCCESS; } } - if (keyEvent->IsKeyDown()) + if (Event.Event.KeyEvent.bKeyDown) { if (pCommandLineEditingKeys && commandLineEditKey) { *pCommandLineEditingKeys = true; - *pwchOut = static_cast(keyEvent->GetVirtualKeyCode()); + *pwchOut = static_cast(Event.Event.KeyEvent.wVirtualKeyCode); return STATUS_SUCCESS; } - else if (pPopupKeys && commandLineEditKey) + + if (pPopupKeys && commandLineEditKey) { *pPopupKeys = true; - *pwchOut = static_cast(keyEvent->GetVirtualKeyCode()); + *pwchOut = static_cast(Event.Event.KeyEvent.wVirtualKeyCode); return STATUS_SUCCESS; } - else - { - const auto zeroVkeyData = OneCoreSafeVkKeyScanW(0); - const auto zeroVKey = LOBYTE(zeroVkeyData); - const auto zeroControlKeyState = HIBYTE(zeroVkeyData); - try - { - // Convert real Windows NT modifier bit into bizarre Console bits - auto consoleModKeyState = FromVkKeyScan(zeroControlKeyState); + const auto zeroKey = OneCoreSafeVkKeyScanW(0); - if (zeroVKey == keyEvent->GetVirtualKeyCode() && - keyEvent->DoActiveModifierKeysMatch(consoleModKeyState)) - { - // This really is the character 0x0000 - *pwchOut = keyEvent->GetCharData(); - return STATUS_SUCCESS; - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } + if (LOBYTE(zeroKey) == Event.Event.KeyEvent.wVirtualKeyCode && + WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, ALT_PRESSED) == WI_IsFlagSet(zeroKey, 0x400) && + WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, CTRL_PRESSED) == WI_IsFlagSet(zeroKey, 0x200) && + WI_IsAnyFlagSet(Event.Event.KeyEvent.dwControlKeyState, SHIFT_PRESSED) == WI_IsFlagSet(zeroKey, 0x100)) + { + // This really is the character 0x0000 + *pwchOut = Event.Event.KeyEvent.uChar.UnicodeChar; + return STATUS_SUCCESS; } } } diff --git a/src/host/ut_host/ClipboardTests.cpp b/src/host/ut_host/ClipboardTests.cpp index 3c32b6efc85..2f9e03c88bb 100644 --- a/src/host/ut_host/ClipboardTests.cpp +++ b/src/host/ut_host/ClipboardTests.cpp @@ -153,107 +153,46 @@ class ClipboardTests VERIFY_IS_NOT_NULL(ptr); } - TEST_METHOD(CanConvertTextToInputEvents) + TEST_METHOD(CanConvertText) { - std::wstring wstr = L"hello world"; - auto events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), - wstr.size()); - VERIFY_ARE_EQUAL(wstr.size() * 2, events.size()); - for (auto wch : wstr) + static constexpr std::wstring_view input{ L"HeLlO WoRlD" }; + const auto events = Clipboard::Instance().TextToKeyEvents(input.data(), input.size()); + + const auto shiftSC = static_cast(OneCoreSafeMapVirtualKeyW(VK_SHIFT, MAPVK_VK_TO_VSC)); + const auto shiftDown = SynthesizeKeyEvent(true, 1, VK_SHIFT, shiftSC, 0, SHIFT_PRESSED); + const auto shiftUp = SynthesizeKeyEvent(false, 1, VK_SHIFT, shiftSC, 0, 0); + + InputEventQueue expectedEvents; + + for (auto wch : input) { - std::deque keydownPattern{ true, false }; - for (auto isKeyDown : keydownPattern) + const auto state = OneCoreSafeVkKeyScanW(wch); + const auto vk = LOBYTE(state); + const auto sc = static_cast(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC)); + const auto shift = WI_IsFlagSet(state, 0x100); + auto event = SynthesizeKeyEvent(true, 1, vk, sc, wch, shift ? SHIFT_PRESSED : 0); + + if (shift) { - VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); - std::unique_ptr keyEvent; - keyEvent.reset(static_cast(events.front().release())); - events.pop_front(); - - const auto keyState = OneCoreSafeVkKeyScanW(wch); - VERIFY_ARE_NOT_EQUAL(-1, keyState); - const auto virtualScanCode = static_cast(OneCoreSafeMapVirtualKeyW(LOBYTE(keyState), MAPVK_VK_TO_VSC)); - - VERIFY_ARE_EQUAL(wch, keyEvent->GetCharData()); - VERIFY_ARE_EQUAL(isKeyDown, keyEvent->IsKeyDown()); - VERIFY_ARE_EQUAL(1, keyEvent->GetRepeatCount()); - VERIFY_ARE_EQUAL(static_cast(0), keyEvent->GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(virtualScanCode, keyEvent->GetVirtualScanCode()); - VERIFY_ARE_EQUAL(LOBYTE(keyState), keyEvent->GetVirtualKeyCode()); + expectedEvents.push_back(shiftDown); } - } - } - TEST_METHOD(CanConvertUppercaseText) - { - std::wstring wstr = L"HeLlO WoRlD"; - size_t uppercaseCount = 0; - for (auto wch : wstr) - { - std::isupper(wch) ? ++uppercaseCount : 0; - } - auto events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), - wstr.size()); + expectedEvents.push_back(event); + event.Event.KeyEvent.bKeyDown = FALSE; + expectedEvents.push_back(event); - VERIFY_ARE_EQUAL((wstr.size() + uppercaseCount) * 2, events.size()); - for (auto wch : wstr) - { - std::deque keydownPattern{ true, false }; - for (auto isKeyDown : keydownPattern) + if (shift) { - Log::Comment(NoThrowString().Format(L"testing char: %C; keydown: %d", wch, isKeyDown)); - - VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); - std::unique_ptr keyEvent; - keyEvent.reset(static_cast(events.front().release())); - events.pop_front(); - - const short keyScanError = -1; - const auto keyState = OneCoreSafeVkKeyScanW(wch); - VERIFY_ARE_NOT_EQUAL(keyScanError, keyState); - const auto virtualScanCode = static_cast(OneCoreSafeMapVirtualKeyW(LOBYTE(keyState), MAPVK_VK_TO_VSC)); - - if (std::isupper(wch)) - { - // uppercase letters have shift key events - // surrounding them, making two events per letter - // (and another two for the keyup) - VERIFY_IS_FALSE(events.empty()); - - VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); - std::unique_ptr keyEvent2; - keyEvent2.reset(static_cast(events.front().release())); - events.pop_front(); - - const auto keyState2 = OneCoreSafeVkKeyScanW(wch); - VERIFY_ARE_NOT_EQUAL(keyScanError, keyState2); - const auto virtualScanCode2 = static_cast(OneCoreSafeMapVirtualKeyW(LOBYTE(keyState2), MAPVK_VK_TO_VSC)); - - if (isKeyDown) - { - // shift then letter - const KeyEvent shiftDownEvent{ TRUE, 1, VK_SHIFT, leftShiftScanCode, L'\0', SHIFT_PRESSED }; - VERIFY_ARE_EQUAL(shiftDownEvent, *keyEvent); - - const KeyEvent expectedKeyEvent{ TRUE, 1, LOBYTE(keyState2), virtualScanCode2, wch, SHIFT_PRESSED }; - VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent2); - } - else - { - // letter then shift - const KeyEvent expectedKeyEvent{ FALSE, 1, LOBYTE(keyState), virtualScanCode, wch, SHIFT_PRESSED }; - VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); - - const KeyEvent shiftUpEvent{ FALSE, 1, VK_SHIFT, leftShiftScanCode, L'\0', 0 }; - VERIFY_ARE_EQUAL(shiftUpEvent, *keyEvent2); - } - } - else - { - const KeyEvent expectedKeyEvent{ !!isKeyDown, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }; - VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); - } + expectedEvents.push_back(shiftUp); } } + + VERIFY_ARE_EQUAL(expectedEvents.size(), events.size()); + + for (size_t i = 0; i < events.size(); ++i) + { + VERIFY_ARE_EQUAL(expectedEvents[i], events[i]); + } } TEST_METHOD(CanConvertCharsRequiringAltGr) @@ -274,23 +213,22 @@ class ClipboardTests auto events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); - std::deque expectedEvents; + InputEventQueue expectedEvents; // should be converted to: // 1. AltGr keydown // 2. € keydown // 3. € keyup // 4. AltGr keyup - expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); - expectedEvents.push_back({ TRUE, 1, virtualKeyCode, virtualScanCode, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); - expectedEvents.push_back({ FALSE, 1, virtualKeyCode, virtualScanCode, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); - expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, L'\0', ENHANCED_KEY }); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, L'\0', (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, virtualKeyCode, virtualScanCode, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, virtualKeyCode, virtualScanCode, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, L'\0', ENHANCED_KEY)); VERIFY_ARE_EQUAL(expectedEvents.size(), events.size()); for (size_t i = 0; i < events.size(); ++i) { - const auto currentKeyEvent = *reinterpret_cast(events[i].get()); - VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); + VERIFY_ARE_EQUAL(expectedEvents[i], events[i]); } } @@ -302,7 +240,7 @@ class ClipboardTests auto events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); - std::deque expectedEvents; + InputEventQueue expectedEvents; if constexpr (Feature_UseNumpadEventsForClipboardInput::IsEnabled()) { // Inside Windows, where numpad events are enabled, this generated numpad events. @@ -313,26 +251,25 @@ class ClipboardTests // 4. 2nd numpad keydown // 5. 2nd numpad keyup // 6. left alt keyup - expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); - expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 }); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED)); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED)); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED)); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED)); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED)); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, wstr[0], 0)); } else { // Outside Windows, without numpad events, we just emit the key with a nonzero UnicodeChar - expectedEvents.push_back({ TRUE, 1, 0, 0, wstr[0], 0 }); - expectedEvents.push_back({ FALSE, 1, 0, 0, wstr[0], 0 }); + expectedEvents.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wstr[0], 0)); + expectedEvents.push_back(SynthesizeKeyEvent(false, 1, 0, 0, wstr[0], 0)); } VERIFY_ARE_EQUAL(expectedEvents.size(), events.size()); for (size_t i = 0; i < events.size(); ++i) { - const auto currentKeyEvent = *reinterpret_cast(events[i].get()); - VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); + VERIFY_ARE_EQUAL(expectedEvents[i], events[i]); } } }; diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj index 31f0f8e5a95..36afffca49c 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -39,7 +39,6 @@ - Create diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters index 2dcabcef1d4..d7045f877f0 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj.filters +++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters @@ -63,9 +63,6 @@ Source Files - - Source Files - Source Files diff --git a/src/host/ut_host/InputBufferTests.cpp b/src/host/ut_host/InputBufferTests.cpp index 6a3c859ca8c..f81d304bda7 100644 --- a/src/host/ut_host/InputBufferTests.cpp +++ b/src/host/ut_host/InputBufferTests.cpp @@ -61,12 +61,12 @@ class InputBufferTests { InputBuffer inputBuffer; auto record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); // add another event, check again INPUT_RECORD record2; record2.EventType = MENU_EVENT; - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record2)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record2), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 2u); } @@ -77,8 +77,8 @@ class InputBufferTests { INPUT_RECORD record; record.EventType = MENU_EVENT; - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); - VERIFY_ARE_EQUAL(record, inputBuffer._storage.back()->ToInputRecord()); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record), 0u); + VERIFY_ARE_EQUAL(record, inputBuffer._storage.back()); } VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); } @@ -86,19 +86,19 @@ class InputBufferTests TEST_METHOD(CanBulkInsertIntoInputBuffer) { InputBuffer inputBuffer; - std::deque> events; + InputEventQueue events; INPUT_RECORD record; record.EventType = MENU_EVENT; for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - events.push_back(IInputEvent::Create(record)); + events.push_back(record); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); // verify that the events are the same in storage for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(inputBuffer._storage[i]->ToInputRecord(), record); + VERIFY_ARE_EQUAL(inputBuffer._storage[i], record); } } @@ -115,23 +115,22 @@ class InputBufferTests { mouseRecord.Event.MouseEvent.dwMousePosition.X = static_cast(i + 1); mouseRecord.Event.MouseEvent.dwMousePosition.Y = static_cast(i + 1) * 2; - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(mouseRecord)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(mouseRecord), 0u); } // check that they coalesced VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); // check that the mouse position is being updated correctly - const IInputEvent* const pOutEvent = inputBuffer._storage.front().get(); - const auto pMouseEvent = static_cast(pOutEvent); - VERIFY_ARE_EQUAL(pMouseEvent->GetPosition().x, static_cast(RECORD_INSERT_COUNT)); - VERIFY_ARE_EQUAL(pMouseEvent->GetPosition().y, static_cast(RECORD_INSERT_COUNT * 2)); + const auto& pMouseEvent = inputBuffer._storage.front().Event.MouseEvent; + VERIFY_ARE_EQUAL(pMouseEvent.dwMousePosition.X, static_cast(RECORD_INSERT_COUNT)); + VERIFY_ARE_EQUAL(pMouseEvent.dwMousePosition.Y, static_cast(RECORD_INSERT_COUNT * 2)); // add a key event and another mouse event to make sure that // an event between two mouse events stopped the coalescing. INPUT_RECORD keyRecord; keyRecord.EventType = KEY_EVENT; - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(keyRecord)), 0u); - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(mouseRecord)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(keyRecord), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(mouseRecord), 0u); // verify VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 3u); @@ -143,29 +142,25 @@ class InputBufferTests InputBuffer inputBuffer; INPUT_RECORD mouseRecords[RECORD_INSERT_COUNT]; - std::deque> events; + InputEventQueue events; for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { mouseRecords[i].EventType = MOUSE_EVENT; mouseRecords[i].Event.MouseEvent.dwEventFlags = MOUSE_MOVED; - events.push_back(IInputEvent::Create(mouseRecords[i])); + events.push_back(mouseRecords[i]); } - // add an extra event - events.push_front(IInputEvent::Create(mouseRecords[0])); - inputBuffer.Flush(); // send one mouse event to possibly coalesce into later - VERIFY_IS_GREATER_THAN(inputBuffer.Write(std::move(events.front())), 0u); - events.pop_front(); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(mouseRecords[0]), 0u); // write the others in bulk VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); // no events should have been coalesced VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT + 1); // check that the events stored match those inserted - VERIFY_ARE_EQUAL(inputBuffer._storage.front()->ToInputRecord(), mouseRecords[0]); + VERIFY_ARE_EQUAL(inputBuffer._storage.front(), mouseRecords[0]); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1]->ToInputRecord(), mouseRecords[i]); + VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1], mouseRecords[i]); } } @@ -180,7 +175,7 @@ class InputBufferTests inputBuffer.Flush(); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record), 0u); } // all events should have been coalesced into one @@ -188,16 +183,12 @@ class InputBufferTests // the single event should have a repeat count for each // coalesced event - std::unique_ptr outEvent; - VERIFY_NT_SUCCESS(inputBuffer.Read(outEvent, - true, - false, - false, - false)); + InputEventQueue outEvents; + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, 1, true, false, false, false)); - VERIFY_ARE_NOT_EQUAL(nullptr, outEvent.get()); - const auto pKeyEvent = static_cast(outEvent.get()); - VERIFY_ARE_EQUAL(pKeyEvent->GetRepeatCount(), RECORD_INSERT_COUNT); + VERIFY_IS_FALSE(outEvents.empty()); + const auto& pKeyEvent = outEvents.front().Event.KeyEvent; + VERIFY_ARE_EQUAL(pKeyEvent.wRepeatCount, RECORD_INSERT_COUNT); } TEST_METHOD(InputBufferDoesNotCoalesceBulkKeyEvents) @@ -206,25 +197,25 @@ class InputBufferTests InputBuffer inputBuffer; INPUT_RECORD keyRecords[RECORD_INSERT_COUNT]; - std::deque> events; + InputEventQueue events; for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { keyRecords[i] = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); - events.push_back(IInputEvent::Create(keyRecords[i])); + events.push_back(keyRecords[i]); } inputBuffer.Flush(); // send one key event to possibly coalesce into later - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(keyRecords[0])), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(keyRecords[0]), 0u); // write the others in bulk VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); // no events should have been coalesced VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT + 1); // check that the events stored match those inserted - VERIFY_ARE_EQUAL(inputBuffer._storage.front()->ToInputRecord(), keyRecords[0]); + VERIFY_ARE_EQUAL(inputBuffer._storage.front(), keyRecords[0]); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1]->ToInputRecord(), keyRecords[i]); + VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1], keyRecords[i]); } } @@ -238,8 +229,8 @@ class InputBufferTests inputBuffer.Flush(); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); - VERIFY_ARE_EQUAL(inputBuffer._storage.back()->ToInputRecord(), record); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record), 0u); + VERIFY_ARE_EQUAL(inputBuffer._storage.back(), record); } // The events shouldn't be coalesced @@ -249,14 +240,14 @@ class InputBufferTests TEST_METHOD(CanFlushAllOutput) { InputBuffer inputBuffer; - std::deque> events; + InputEventQueue events; // put some events in the buffer so we can remove them INPUT_RECORD record; record.EventType = MENU_EVENT; for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - events.push_back(IInputEvent::Create(record)); + events.push_back(record); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); @@ -270,13 +261,13 @@ class InputBufferTests { InputBuffer inputBuffer; INPUT_RECORD records[RECORD_INSERT_COUNT] = { 0 }; - std::deque> inEvents; + InputEventQueue inEvents; // create alternating mouse and key events for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { records[i].EventType = (i % 2 == 0) ? MENU_EVENT : KEY_EVENT; - inEvents.push_back(IInputEvent::Create(records[i])); + inEvents.push_back(records[i]); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); @@ -286,7 +277,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT / 2); // make sure that the non key events were the ones removed - std::deque> outEvents; + InputEventQueue outEvents; auto amountToRead = RECORD_INSERT_COUNT / 2; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, amountToRead, @@ -298,7 +289,7 @@ class InputBufferTests for (size_t i = 0; i < outEvents.size(); ++i) { - VERIFY_ARE_EQUAL(outEvents[i]->EventType(), InputEventType::KeyEvent); + VERIFY_ARE_EQUAL(outEvents[i].EventType, KEY_EVENT); } } @@ -306,18 +297,18 @@ class InputBufferTests { InputBuffer inputBuffer; INPUT_RECORD records[RECORD_INSERT_COUNT]; - std::deque> inEvents; + InputEventQueue inEvents; // write some input records for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); - inEvents.push_back(IInputEvent::Create(records[i])); + inEvents.push_back(records[i]); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); // read them back out - std::deque> outEvents; + InputEventQueue outEvents; auto amountToRead = RECORD_INSERT_COUNT; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, amountToRead, @@ -329,7 +320,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } @@ -339,16 +330,16 @@ class InputBufferTests // add some events so that we have something to peek at INPUT_RECORD records[RECORD_INSERT_COUNT]; - std::deque> inEvents; + InputEventQueue inEvents; for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); - inEvents.push_back(IInputEvent::Create(records[i])); + inEvents.push_back(records[i]); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); // peek at events - std::deque> outEvents; + InputEventQueue outEvents; auto amountToRead = RECORD_INSERT_COUNT; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, amountToRead, @@ -361,7 +352,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } @@ -373,11 +364,11 @@ class InputBufferTests // add some events so that we have something to stick in front of INPUT_RECORD records[RECORD_INSERT_COUNT]; - std::deque> inEvents; + InputEventQueue inEvents; for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); - inEvents.push_back(IInputEvent::Create(records[i])); + inEvents.push_back(records[i]); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); @@ -385,7 +376,7 @@ class InputBufferTests waitEvent.SetEvent(); // read one record, hInputEvent should still be signaled - std::deque> outEvents; + InputEventQueue outEvents; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, 1, false, @@ -435,17 +426,11 @@ class InputBufferTests outRecordsExpected[3] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0xa0, 0); outRecordsExpected[4].EventType = MOUSE_EVENT; - std::deque> inEvents; - for (size_t i = 0; i < inRecords.size(); ++i) - { - inEvents.push_back(IInputEvent::Create(inRecords[i])); - } - inputBuffer.Flush(); - VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inRecords), 0u); // read them out non-unicode style and compare - std::deque> outEvents; + InputEventQueue outEvents; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, outRecordsExpected.size(), false, @@ -455,7 +440,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(outEvents.size(), outRecordsExpected.size()); for (size_t i = 0; i < outEvents.size(); ++i) { - VERIFY_ARE_EQUAL(outEvents[i]->ToInputRecord(), outRecordsExpected[i]); + VERIFY_ARE_EQUAL(outEvents[i], outRecordsExpected[i]); } } @@ -465,11 +450,11 @@ class InputBufferTests // add some events so that we have something to stick in front of INPUT_RECORD records[RECORD_INSERT_COUNT]; - std::deque> inEvents; + InputEventQueue inEvents; for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); - inEvents.push_back(IInputEvent::Create(records[i])); + inEvents.push_back(records[i]); } VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); @@ -479,13 +464,13 @@ class InputBufferTests for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { prependRecords[i] = MakeKeyEvent(TRUE, 1, static_cast(L'a' + i), 0, static_cast(L'a' + i), 0); - inEvents.push_back(IInputEvent::Create(prependRecords[i])); + inEvents.push_back(prependRecords[i]); } auto eventsWritten = inputBuffer.Prepend(inEvents); VERIFY_ARE_EQUAL(eventsWritten, RECORD_INSERT_COUNT); // grab the first set of events and ensure they match prependRecords - std::deque> outEvents; + InputEventQueue outEvents; auto amountToRead = RECORD_INSERT_COUNT; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, amountToRead, @@ -497,7 +482,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(prependRecords[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(prependRecords[i], outEvents[i]); } outEvents.clear(); @@ -512,7 +497,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } @@ -524,7 +509,7 @@ class InputBufferTests // change the buffer's state a bit INPUT_RECORD record; record.EventType = MENU_EVENT; - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(record), 0u); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); inputBuffer.InputMode = 0x0; inputBuffer.ReinitializeInputBuffer(); @@ -544,7 +529,7 @@ class InputBufferTests VERIFY_IS_FALSE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); - VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(pauseRecord)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.Write(pauseRecord), 0u); // we should now be paused and the input record should be discarded VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); @@ -552,7 +537,7 @@ class InputBufferTests // the next key press should unpause us but be discarded auto unpauseRecord = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); - VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(unpauseRecord)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.Write(unpauseRecord), 0u); VERIFY_IS_FALSE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); @@ -569,7 +554,7 @@ class InputBufferTests VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); // pause the screen - VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(pauseRecord)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.Write(pauseRecord), 0u); // we should now be paused and the input record should be discarded VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); @@ -578,12 +563,12 @@ class InputBufferTests // sending a system key event should not stop the pause and // the record should be stored in the input buffer auto systemRecord = MakeKeyEvent(true, 1, VK_CONTROL, 0, 0, 0); - VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(systemRecord)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(systemRecord), 0u); VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); - std::deque> outEvents; + InputEventQueue outEvents; size_t amountToRead = 2; VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, amountToRead, @@ -597,18 +582,18 @@ class InputBufferTests { InputBuffer inputBuffer; auto record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); - auto inputEvent = IInputEvent::Create(record); + auto inputEvent = record; size_t eventsWritten; auto waitEvent = false; inputBuffer.Flush(); // write one event to an empty buffer - std::deque> storage; + InputEventQueue storage; storage.push_back(std::move(inputEvent)); inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent); VERIFY_IS_TRUE(waitEvent); // write another, it shouldn't signal this time auto record2 = MakeKeyEvent(true, 1, L'b', 0, L'b', 0); - auto inputEvent2 = IInputEvent::Create(record2); + auto inputEvent2 = record2; // write another event to a non-empty buffer waitEvent = false; storage.clear(); @@ -623,9 +608,9 @@ class InputBufferTests InputBuffer inputBuffer; const WORD repeatCount = 5; auto record = MakeKeyEvent(true, repeatCount, L'a', 0, L'a', 0); - std::deque> outEvents; + InputEventQueue outEvents; - VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(record)), 1u); + VERIFY_ARE_EQUAL(inputBuffer.Write(record), 1u); VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, 1, false, @@ -634,8 +619,8 @@ class InputBufferTests true)); VERIFY_ARE_EQUAL(outEvents.size(), 1u); VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); - VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount - 1); - VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.front().Event.KeyEvent.wRepeatCount, repeatCount - 1); + VERIFY_ARE_EQUAL(outEvents.front().Event.KeyEvent.wRepeatCount, 1u); } TEST_METHOD(StreamPeekingDeCoalesces) @@ -643,9 +628,9 @@ class InputBufferTests InputBuffer inputBuffer; const WORD repeatCount = 5; auto record = MakeKeyEvent(true, repeatCount, L'a', 0, L'a', 0); - std::deque> outEvents; + InputEventQueue outEvents; - VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(record)), 1u); + VERIFY_ARE_EQUAL(inputBuffer.Write(record), 1u); VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, 1, true, @@ -654,7 +639,7 @@ class InputBufferTests true)); VERIFY_ARE_EQUAL(outEvents.size(), 1u); VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); - VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount); - VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.front().Event.KeyEvent.wRepeatCount, repeatCount); + VERIFY_ARE_EQUAL(outEvents.front().Event.KeyEvent.wRepeatCount, 1u); } }; diff --git a/src/inc/til/small_vector.h b/src/inc/til/small_vector.h index 2a10dcb6b36..a88927e7159 100644 --- a/src/inc/til/small_vector.h +++ b/src/inc/til/small_vector.h @@ -278,6 +278,12 @@ namespace til return tmp; } + [[nodiscard]] friend constexpr small_vector_iterator operator+(const difference_type off, small_vector_iterator next) noexcept + { + next += off; + return next; + } + constexpr small_vector_iterator& operator-=(const difference_type off) noexcept { base::operator-=(off); diff --git a/src/interactivity/base/EventSynthesis.cpp b/src/interactivity/base/EventSynthesis.cpp index 3b283b001ab..b26f8a369c3 100644 --- a/src/interactivity/base/EventSynthesis.cpp +++ b/src/interactivity/base/EventSynthesis.cpp @@ -3,10 +3,6 @@ #include "precomp.h" #include "../inc/EventSynthesis.hpp" -#include "../../types/inc/convert.hpp" -#include "../inc/VtApiRedirection.hpp" - -#pragma hdrstop // TODO: MSFT 14150722 - can these const values be generated at // runtime without breaking compatibility? @@ -16,35 +12,29 @@ static constexpr WORD leftShiftScanCode = 0x2A; // Routine Description: // - naively determines the width of a UCS2 encoded wchar (with caveats noted above) #pragma warning(suppress : 4505) // this function will be deleted if numpad events are disabled -static CodepointWidth GetQuickCharWidthLegacyForNumpadEventSynthesis(const wchar_t wch) noexcept +static bool IsCharFullWidth(const wchar_t wch) noexcept { - if ((0x1100 <= wch && wch <= 0x115f) // From Unicode 9.0, Hangul Choseong is wide - || (0x2e80 <= wch && wch <= 0x303e) // From Unicode 9.0, this range is wide (assorted languages) - || (0x3041 <= wch && wch <= 0x3094) // Hiragana - || (0x30a1 <= wch && wch <= 0x30f6) // Katakana - || (0x3105 <= wch && wch <= 0x312c) // Bopomofo - || (0x3131 <= wch && wch <= 0x318e) // Hangul Elements - || (0x3190 <= wch && wch <= 0x3247) // From Unicode 9.0, this range is wide - || (0x3251 <= wch && wch <= 0x4dbf) // Unicode 9.0 CJK Unified Ideographs, Yi, Reserved, Han Ideograph (hexagrams from 4DC0..4DFF are ignored - || (0x4e00 <= wch && wch <= 0xa4c6) // Unicode 9.0 CJK Unified Ideographs, Yi, Reserved, Han Ideograph (hexagrams from 4DC0..4DFF are ignored - || (0xa960 <= wch && wch <= 0xa97c) // Wide Hangul Choseong - || (0xac00 <= wch && wch <= 0xd7a3) // Korean Hangul Syllables - || (0xf900 <= wch && wch <= 0xfaff) // From Unicode 9.0, this range is wide [CJK Compatibility Ideographs, Includes Han Compatibility Ideographs] - || (0xfe10 <= wch && wch <= 0xfe1f) // From Unicode 9.0, this range is wide [Presentation forms] - || (0xfe30 <= wch && wch <= 0xfe6b) // From Unicode 9.0, this range is wide [Presentation forms] - || (0xff01 <= wch && wch <= 0xff5e) // Fullwidth ASCII variants - || (0xffe0 <= wch && wch <= 0xffe6)) // Fullwidth symbol variants - { - return CodepointWidth::Wide; - } - - return CodepointWidth::Narrow; + return (0x1100 <= wch && wch <= 0x115f) || // From Unicode 9.0, Hangul Choseong is wide + (0x2e80 <= wch && wch <= 0x303e) || // From Unicode 9.0, this range is wide (assorted languages) + (0x3041 <= wch && wch <= 0x3094) || // Hiragana + (0x30a1 <= wch && wch <= 0x30f6) || // Katakana + (0x3105 <= wch && wch <= 0x312c) || // Bopomofo + (0x3131 <= wch && wch <= 0x318e) || // Hangul Elements + (0x3190 <= wch && wch <= 0x3247) || // From Unicode 9.0, this range is wide + (0x3251 <= wch && wch <= 0x4dbf) || // Unicode 9.0 CJK Unified Ideographs, Yi, Reserved, Han Ideograph (hexagrams from 4DC0..4DFF are ignored + (0x4e00 <= wch && wch <= 0xa4c6) || // Unicode 9.0 CJK Unified Ideographs, Yi, Reserved, Han Ideograph (hexagrams from 4DC0..4DFF are ignored + (0xa960 <= wch && wch <= 0xa97c) || // Wide Hangul Choseong + (0xac00 <= wch && wch <= 0xd7a3) || // Korean Hangul Syllables + (0xf900 <= wch && wch <= 0xfaff) || // From Unicode 9.0, this range is wide [CJK Compatibility Ideographs, Includes Han Compatibility Ideographs] + (0xfe10 <= wch && wch <= 0xfe1f) || // From Unicode 9.0, this range is wide [Presentation forms] + (0xfe30 <= wch && wch <= 0xfe6b) || // From Unicode 9.0, this range is wide [Presentation forms] + (0xff01 <= wch && wch <= 0xff5e) || // Fullwidth ASCII variants + (0xffe0 <= wch && wch <= 0xffe6); // Fullwidth symbol variants } -std::deque> Microsoft::Console::Interactivity::CharToKeyEvents(const wchar_t wch, - const unsigned int codepage) +void Microsoft::Console::Interactivity::CharToKeyEvents(const wchar_t wch, const unsigned int codepage, InputEventQueue& keyEvents) { - const short invalidKey = -1; + static constexpr short invalidKey = -1; auto keyState = OneCoreSafeVkKeyScanW(wch); if (keyState == invalidKey) @@ -57,18 +47,19 @@ std::deque> Microsoft::Console::Interactivity::CharToK WORD CharType = 0; GetStringTypeW(CT_CTYPE3, &wch, 1, &CharType); - if (!(WI_IsFlagSet(CharType, C3_ALPHA) || GetQuickCharWidthLegacyForNumpadEventSynthesis(wch) == CodepointWidth::Wide)) + if (WI_IsFlagClear(CharType, C3_ALPHA) && !IsCharFullWidth(wch)) { // It wasn't alphanumeric or determined to be wide by the old algorithm // if VkKeyScanW fails (char is not in kbd layout), we must // emulate the key being input through the numpad - return SynthesizeNumpadEvents(wch, codepage); + SynthesizeNumpadEvents(wch, codepage, keyEvents); + return; } } keyState = 0; // SynthesizeKeyboardEvents would rather get 0 than -1 } - return SynthesizeKeyboardEvents(wch, keyState); + SynthesizeKeyboardEvents(wch, keyState, keyEvents); } // Routine Description: @@ -80,80 +71,45 @@ std::deque> Microsoft::Console::Interactivity::CharToK // - deque of KeyEvents that represent the wchar_t being typed // Note: // - will throw exception on error -std::deque> Microsoft::Console::Interactivity::SynthesizeKeyboardEvents(const wchar_t wch, const short keyState) +void Microsoft::Console::Interactivity::SynthesizeKeyboardEvents(const wchar_t wch, const short keyState, InputEventQueue& keyEvents) { + const auto vk = LOBYTE(keyState); + const auto sc = gsl::narrow(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC)); + // The caller provides us with the result of VkKeyScanW() in keyState. + // The magic constants below are the expected (documented) return values from VkKeyScanW(). const auto modifierState = HIBYTE(keyState); + const auto shiftSet = WI_IsFlagSet(modifierState, 1); + const auto ctrlSet = WI_IsFlagSet(modifierState, 2); + const auto altSet = WI_IsFlagSet(modifierState, 4); + const auto altGrSet = WI_AreAllFlagsSet(modifierState, 4 | 2); - auto altGrSet = false; - auto shiftSet = false; - std::deque> keyEvents; - - // add modifier key event if necessary - if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) + if (altGrSet) { - altGrSet = true; - keyEvents.push_back(std::make_unique(true, - 1ui16, - static_cast(VK_MENU), - altScanCode, - UNICODE_NULL, - (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); + keyEvents.push_back(SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)); } - else if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) + else if (shiftSet) { - shiftSet = true; - keyEvents.push_back(std::make_unique(true, - 1ui16, - static_cast(VK_SHIFT), - leftShiftScanCode, - UNICODE_NULL, - SHIFT_PRESSED)); + keyEvents.push_back(SynthesizeKeyEvent(true, 1, VK_SHIFT, leftShiftScanCode, 0, SHIFT_PRESSED)); } - const auto vk = LOBYTE(keyState); - const auto virtualScanCode = gsl::narrow(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC)); - KeyEvent keyEvent{ true, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }; - - // add modifier flags if necessary - if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::Shift); - } - if (WI_IsFlagSet(modifierState, VkKeyScanModState::CtrlPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::LeftCtrl); - } - if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::RightAlt); - } + auto keyEvent = SynthesizeKeyEvent(true, 1, vk, sc, wch, 0); + WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, SHIFT_PRESSED, shiftSet); + WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, LEFT_CTRL_PRESSED, ctrlSet); + WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, RIGHT_ALT_PRESSED, altSet); - // add key event down and up - keyEvents.push_back(std::make_unique(keyEvent)); - keyEvent.SetKeyDown(false); - keyEvents.push_back(std::make_unique(keyEvent)); + keyEvents.push_back(keyEvent); + keyEvent.Event.KeyEvent.bKeyDown = FALSE; + keyEvents.push_back(keyEvent); - // add modifier key up event + // handle yucky alt-gr keys if (altGrSet) { - keyEvents.push_back(std::make_unique(false, - 1ui16, - static_cast(VK_MENU), - altScanCode, - UNICODE_NULL, - ENHANCED_KEY)); + keyEvents.push_back(SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY)); } else if (shiftSet) { - keyEvents.push_back(std::make_unique(false, - 1ui16, - static_cast(VK_SHIFT), - leftShiftScanCode, - UNICODE_NULL, - 0)); + keyEvents.push_back(SynthesizeKeyEvent(false, 1, VK_SHIFT, leftShiftScanCode, 0, 0)); } - - return keyEvents; } // Routine Description: @@ -166,64 +122,35 @@ std::deque> Microsoft::Console::Interactivity::Synthes // alt + numpad // Note: // - will throw exception on error -std::deque> Microsoft::Console::Interactivity::SynthesizeNumpadEvents(const wchar_t wch, const unsigned int codepage) +void Microsoft::Console::Interactivity::SynthesizeNumpadEvents(const wchar_t wch, const unsigned int codepage, InputEventQueue& keyEvents) { - std::deque> keyEvents; - - //alt keydown - keyEvents.push_back(std::make_unique(true, - 1ui16, - static_cast(VK_MENU), - altScanCode, - UNICODE_NULL, - LEFT_ALT_PRESSED)); - - std::wstring wstr{ wch }; - const auto convertedChars = ConvertToA(codepage, wstr); - if (convertedChars.size() == 1) + char converted = 0; + const auto result = WideCharToMultiByte(codepage, 0, &wch, 1, &converted, 1, nullptr, nullptr); + + if (result == 1) { + // alt keydown + keyEvents.push_back(SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, 0, LEFT_ALT_PRESSED)); + // It is OK if the char is "signed -1", we want to interpret that as "unsigned 255" for the // "integer to character" conversion below with ::to_string, thus the static_cast. // Prime example is nonbreaking space U+00A0 will convert to OEM by codepage 437 to 0xFF which is -1 signed. // But it is absolutely valid as 0xFF or 255 unsigned as the correct CP437 character. // We need to treat it as unsigned because we're going to pretend it was a keypad entry // and you don't enter negative numbers on the keypad. - const auto uch = static_cast(convertedChars.at(0)); - - // unsigned char values are in the range [0, 255] so we need to be - // able to store up to 4 chars from the conversion (including the end of string char) - auto charString = std::to_string(uch); + const auto charString = std::to_string(static_cast(converted)); - for (auto& ch : std::string_view(charString)) + for (const auto& ch : charString) { - if (ch == 0) - { - break; - } - const WORD virtualKey = ch - '0' + VK_NUMPAD0; - const auto virtualScanCode = gsl::narrow(OneCoreSafeMapVirtualKeyW(virtualKey, MAPVK_VK_TO_VSC)); - - keyEvents.push_back(std::make_unique(true, - 1ui16, - virtualKey, - virtualScanCode, - UNICODE_NULL, - LEFT_ALT_PRESSED)); - keyEvents.push_back(std::make_unique(false, - 1ui16, - virtualKey, - virtualScanCode, - UNICODE_NULL, - LEFT_ALT_PRESSED)); + const WORD vk = ch - '0' + VK_NUMPAD0; + const auto sc = gsl::narrow(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC)); + auto keyEvent = SynthesizeKeyEvent(true, 1, vk, sc, 0, LEFT_ALT_PRESSED); + keyEvents.push_back(keyEvent); + keyEvent.Event.KeyEvent.bKeyDown = FALSE; + keyEvents.push_back(keyEvent); } - } - // alt keyup - keyEvents.push_back(std::make_unique(false, - 1ui16, - static_cast(VK_MENU), - altScanCode, - wch, - 0)); - return keyEvents; + // alt keyup + keyEvents.push_back(SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, wch, 0)); + } } diff --git a/src/interactivity/inc/EventSynthesis.hpp b/src/interactivity/inc/EventSynthesis.hpp index a1ebb64f1a0..0a114c4369b 100644 --- a/src/interactivity/inc/EventSynthesis.hpp +++ b/src/interactivity/inc/EventSynthesis.hpp @@ -14,16 +14,10 @@ Module Name: --*/ #pragma once -#include -#include -#include "../../types/inc/IInputEvent.hpp" namespace Microsoft::Console::Interactivity { - std::deque> CharToKeyEvents(const wchar_t wch, const unsigned int codepage); - - std::deque> SynthesizeKeyboardEvents(const wchar_t wch, - const short keyState); - - std::deque> SynthesizeNumpadEvents(const wchar_t wch, const unsigned int codepage); + void CharToKeyEvents(wchar_t wch, unsigned int codepage, InputEventQueue& out); + void SynthesizeKeyboardEvents(wchar_t wch, short keyState, InputEventQueue& out); + void SynthesizeNumpadEvents(wchar_t wch, unsigned int codepage, InputEventQueue& out); } diff --git a/src/interactivity/onecore/ConIoSrvComm.cpp b/src/interactivity/onecore/ConIoSrvComm.cpp index 642e1c1f29e..3c543e442ff 100644 --- a/src/interactivity/onecore/ConIoSrvComm.cpp +++ b/src/interactivity/onecore/ConIoSrvComm.cpp @@ -266,9 +266,7 @@ VOID ConIoSrvComm::ServiceInputPipe() case CIS_EVENT_TYPE_INPUT: try { - const auto keyRecord = Event.InputEvent.Record.Event.KeyEvent; - const KeyEvent keyEvent{ keyRecord }; - HandleGenericKeyEvent(keyEvent, false); + HandleGenericKeyEvent(Event.InputEvent.Record, false); } catch (...) { diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp index 1be0b11f2d7..30138f6dad6 100644 --- a/src/interactivity/win32/Clipboard.cpp +++ b/src/interactivity/win32/Clipboard.cpp @@ -134,17 +134,17 @@ void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData, // - deque of KeyEvents that represent the string passed in // Note: // - will throw exception on error -std::deque> Clipboard::TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, - const size_t cchData, - const bool bracketedPaste) +InputEventQueue Clipboard::TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, + const size_t cchData, + const bool bracketedPaste) { THROW_HR_IF_NULL(E_INVALIDARG, pData); - std::deque> keyEvents; + InputEventQueue keyEvents; const auto pushControlSequence = [&](const std::wstring_view sequence) { std::for_each(sequence.begin(), sequence.end(), [&](const auto wch) { - keyEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); - keyEvents.push_back(std::make_unique(false, 1ui16, 0ui16, 0ui16, wch, 0)); + keyEvents.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0)); + keyEvents.push_back(SynthesizeKeyEvent(false, 1, 0, 0, wch, 0)); }); }; @@ -194,18 +194,14 @@ std::deque> Clipboard::TextToKeyEvents(_In_reads_(c } const auto codepage = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP; - auto convertedEvents = CharToKeyEvents(currentChar, codepage); - while (!convertedEvents.empty()) - { - keyEvents.push_back(std::move(convertedEvents.front())); - convertedEvents.pop_front(); - } + CharToKeyEvents(currentChar, codepage, keyEvents); } if (bracketedPaste) { pushControlSequence(L"\x1b[201~"); } + return keyEvents; } diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp index 31413eb61ed..f6bab9254cf 100644 --- a/src/interactivity/win32/clipboard.hpp +++ b/src/interactivity/win32/clipboard.hpp @@ -35,9 +35,9 @@ namespace Microsoft::Console::Interactivity::Win32 void Paste(); private: - std::deque> TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, - const size_t cchData, - const bool bracketedPaste = false); + InputEventQueue TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, + const size_t cchData, + const bool bracketedPaste = false); void StoreSelectionToClipboard(_In_ const bool fAlsoCopyFormatting); diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index c5a77b0e095..6331a6590f7 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -186,23 +186,19 @@ void HandleKeyEvent(const HWND hWnd, } } - KeyEvent keyEvent{ !!bKeyDown, RepeatCount, VirtualKeyCode, VirtualScanCode, UNICODE_NULL, 0 }; + auto keyEvent = SynthesizeKeyEvent(bKeyDown, RepeatCount, VirtualKeyCode, VirtualScanCode, UNICODE_NULL, 0); if (IsCharacterMessage) { // If this is a fake character, zero the scancode. if (lParam & 0x02000000) { - keyEvent.SetVirtualScanCode(0); + keyEvent.Event.KeyEvent.wVirtualScanCode = 0; } - keyEvent.SetActiveModifierKeys(GetControlKeyState(lParam)); + keyEvent.Event.KeyEvent.dwControlKeyState = GetControlKeyState(lParam); if (Message == WM_CHAR || Message == WM_SYSCHAR) { - keyEvent.SetCharData(static_cast(wParam)); - } - else - { - keyEvent.SetCharData(L'\0'); + keyEvent.Event.KeyEvent.uChar.UnicodeChar = static_cast(wParam); } } else @@ -212,8 +208,7 @@ void HandleKeyEvent(const HWND hWnd, { return; } - keyEvent.SetActiveModifierKeys(ControlKeyState); - keyEvent.SetCharData(L'\0'); + keyEvent.Event.KeyEvent.dwControlKeyState = ControlKeyState; } const INPUT_KEY_INFO inputKeyInfo(VirtualKeyCode, ControlKeyState); @@ -922,26 +917,12 @@ BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, break; } - ULONG EventsWritten = 0; - try - { - auto mouseEvent = std::make_unique( - MousePosition, - ConvertMouseButtonState(ButtonFlags, static_cast(wParam)), - GetControlKeyState(0), - EventFlags); - EventsWritten = static_cast(gci.pInputBuffer->Write(std::move(mouseEvent))); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - EventsWritten = 0; - } - - if (EventsWritten != 1) - { - RIPMSG1(RIP_WARNING, "PutInputInBuffer: EventsWritten != 1 (0x%x), 1 expected", EventsWritten); - } + const auto mouseEvent = SynthesizeMouseEvent( + MousePosition, + ConvertMouseButtonState(ButtonFlags, static_cast(wParam)), + GetControlKeyState(0), + EventFlags); + gci.pInputBuffer->Write(mouseEvent); return FALSE; } diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index 63e321d6c8a..2449b128742 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -212,19 +212,7 @@ static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) } else { - try - { - for (size_t i = 0; i < cRecords; ++i) - { - if (outEvents.empty()) - { - break; - } - rgRecords[i] = outEvents.front()->ToInputRecord(); - outEvents.pop_front(); - } - } - CATCH_RETURN(); + std::ranges::copy(outEvents, rgRecords); } if (SUCCEEDED(hr)) diff --git a/src/server/WaitBlock.cpp b/src/server/WaitBlock.cpp index 74ef76b5362..49a4663f1cd 100644 --- a/src/server/WaitBlock.cpp +++ b/src/server/WaitBlock.cpp @@ -135,7 +135,7 @@ bool ConsoleWaitBlock::Notify(const WaitTerminationReason TerminationReason) DWORD dwControlKeyState; auto fIsUnicode = true; - std::deque> outEvents; + InputEventQueue outEvents; // TODO: MSFT 14104228 - get rid of this void* and get the data // out of the read wait object properly. void* pOutputData = nullptr; @@ -193,15 +193,7 @@ bool ConsoleWaitBlock::Notify(const WaitTerminationReason TerminationReason) const auto pRecordBuffer = static_cast(buffer); a->NumRecords = static_cast(outEvents.size()); - for (size_t i = 0; i < a->NumRecords; ++i) - { - if (outEvents.empty()) - { - break; - } - pRecordBuffer[i] = outEvents.front()->ToInputRecord(); - outEvents.pop_front(); - } + std::ranges::copy(outEvents, pRecordBuffer); } else if (API_NUMBER_READCONSOLE == _WaitReplyMessage.msgHeader.ApiNumber) { diff --git a/src/terminal/adapter/IInteractDispatch.hpp b/src/terminal/adapter/IInteractDispatch.hpp index 33b9659c849..36278bd2e1a 100644 --- a/src/terminal/adapter/IInteractDispatch.hpp +++ b/src/terminal/adapter/IInteractDispatch.hpp @@ -28,9 +28,9 @@ namespace Microsoft::Console::VirtualTerminal virtual ~IInteractDispatch() = default; #pragma warning(pop) - virtual bool WriteInput(std::deque>& inputEvents) = 0; + virtual bool WriteInput(const std::span& inputEvents) = 0; - virtual bool WriteCtrlKey(const KeyEvent& event) = 0; + virtual bool WriteCtrlKey(const INPUT_RECORD& event) = 0; virtual bool WriteString(const std::wstring_view string) = 0; diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index 1dc9f446b54..cb06c985c91 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -17,7 +17,6 @@ #include "../../interactivity/inc/ServiceLocator.hpp" #include "../../interactivity/inc/EventSynthesis.hpp" #include "../../types/inc/Viewport.hpp" -#include "../../inc/unicode.hpp" using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::Types; @@ -38,7 +37,7 @@ InteractDispatch::InteractDispatch() : // - inputEvents: a collection of IInputEvents // Return Value: // - True. -bool InteractDispatch::WriteInput(std::deque>& inputEvents) +bool InteractDispatch::WriteInput(const std::span& inputEvents) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.GetActiveInputBuffer()->Write(inputEvents); @@ -52,17 +51,14 @@ bool InteractDispatch::WriteInput(std::deque>& inpu // client application. // Arguments: // - event: The key to send to the host. -// Return Value: -// - True. -bool InteractDispatch::WriteCtrlKey(const KeyEvent& event) +bool InteractDispatch::WriteCtrlKey(const INPUT_RECORD& event) { HandleGenericKeyEvent(event, false); return true; } // Method Description: -// - Writes a string of input to the host. The string is converted to keystrokes -// that will faithfully represent the input by CharToKeyEvents. +// - Writes a string of input to the host. // Arguments: // - string : a string to write to the console. // Return Value: @@ -72,15 +68,11 @@ bool InteractDispatch::WriteString(const std::wstring_view string) if (!string.empty()) { const auto codepage = _api.GetConsoleOutputCP(); - std::deque> keyEvents; + InputEventQueue keyEvents; for (const auto& wch : string) { - auto convertedEvents = CharToKeyEvents(wch, codepage); - - std::move(convertedEvents.begin(), - convertedEvents.end(), - std::back_inserter(keyEvents)); + CharToKeyEvents(wch, codepage, keyEvents); } WriteInput(keyEvents); diff --git a/src/terminal/adapter/InteractDispatch.hpp b/src/terminal/adapter/InteractDispatch.hpp index 8fd34868211..56fd9b2dd80 100644 --- a/src/terminal/adapter/InteractDispatch.hpp +++ b/src/terminal/adapter/InteractDispatch.hpp @@ -25,8 +25,8 @@ namespace Microsoft::Console::VirtualTerminal public: InteractDispatch(); - bool WriteInput(std::deque>& inputEvents) override; - bool WriteCtrlKey(const KeyEvent& event) override; + bool WriteInput(const std::span& inputEvents) override; + bool WriteCtrlKey(const INPUT_RECORD& event) override; bool WriteString(const std::wstring_view string) override; bool WindowManipulation(const DispatchTypes::WindowManipulationType function, const VTParameter parameter1, diff --git a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp index 9995d15d107..f3df50a0a6e 100644 --- a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp +++ b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp @@ -2,9 +2,8 @@ // Licensed under the MIT license. #include "precomp.h" -#include -#include "../../inc/consoletaeftemplates.hpp" +#include "../types/inc/IInputEvent.hpp" #include "../terminal/input/terminalInput.hpp" using namespace WEX::Common; diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp index 9977cb609cf..581f03ed925 100644 --- a/src/terminal/adapter/ut_adapter/inputTest.cpp +++ b/src/terminal/adapter/ut_adapter/inputTest.cpp @@ -2,14 +2,10 @@ // Licensed under the MIT license. #include "precomp.h" -#include "../precomp.h" -#include -#include -#include "../../inc/consoletaeftemplates.hpp" - -#include "../../input/terminalInput.hpp" #include "../../../interactivity/inc/VtApiRedirection.hpp" +#include "../../input/terminalInput.hpp" +#include "../types/inc/IInputEvent.hpp" using namespace WEX::Common; using namespace WEX::Logging; @@ -182,9 +178,8 @@ void InputTest::TerminalInputTests() break; } - auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been."); } Log::Comment(L"Sending every possible VKEY at the input stream for interception during key UP."); @@ -198,9 +193,8 @@ void InputTest::TerminalInputTests() irTest.Event.KeyEvent.wVirtualKeyCode = vkey; irTest.Event.KeyEvent.bKeyDown = FALSE; - auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify key was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(irTest), L"Verify key was NOT handled."); } Log::Comment(L"Verify other types of events are not handled/intercepted."); @@ -209,18 +203,15 @@ void InputTest::TerminalInputTests() Log::Comment(L"Testing MOUSE_EVENT"); irUnhandled.EventType = MOUSE_EVENT; - auto inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify MOUSE_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(irUnhandled), L"Verify MOUSE_EVENT was NOT handled."); Log::Comment(L"Testing WINDOW_BUFFER_SIZE_EVENT"); irUnhandled.EventType = WINDOW_BUFFER_SIZE_EVENT; - inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify WINDOW_BUFFER_SIZE_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(irUnhandled), L"Verify WINDOW_BUFFER_SIZE_EVENT was NOT handled."); Log::Comment(L"Testing MENU_EVENT"); irUnhandled.EventType = MENU_EVENT; - inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify MENU_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(irUnhandled), L"Verify MENU_EVENT was NOT handled."); // Testing FOCUS_EVENTs is handled by TestFocusEvents } @@ -232,40 +223,13 @@ void InputTest::TestFocusEvents() // We're relying on the fact that the INPUT_RECORD version of the ctor is only called by the API TerminalInput input; - INPUT_RECORD irTest = { 0 }; - irTest.EventType = FOCUS_EVENT; - - { - irTest.Event.FocusEvent.bSetFocus = false; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - irTest.Event.FocusEvent.bSetFocus = true; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(false), L"Verify FocusEvent from any other source was NOT handled."); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(true), L"Verify FocusEvent from any other source was NOT handled."); - - Log::Comment(L"Enable focus event handling"); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(false)); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(true)); input.SetInputMode(TerminalInput::Mode::FocusEvent, true); - { - irTest.Event.FocusEvent.bSetFocus = false; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - irTest.Event.FocusEvent.bSetFocus = true; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[O"), input.HandleFocus(false), L"Verify FocusEvent from any other source was handled."); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[I"), input.HandleFocus(true), L"Verify FocusEvent from any other source was handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[O"), input.HandleFocus(false)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[I"), input.HandleFocus(true)); } void InputTest::TerminalInputModifierKeyTests() @@ -482,9 +446,8 @@ void InputTest::TerminalInputModifierKeyTests() str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); } - auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been."); } } @@ -509,27 +472,23 @@ void InputTest::TerminalInputNullKeyTests() irTest.Event.KeyEvent.bKeyDown = TRUE; // Send key into object (will trigger callback and verification) - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); vkey = VK_SPACE; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.wVirtualKeyCode = vkey; irTest.Event.KeyEvent.uChar.UnicodeChar = vkey; - inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; - inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); uiKeystate = RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; - inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); } static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& input, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0) @@ -545,8 +504,7 @@ static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& in irTest.Event.KeyEvent.uChar.UnicodeChar = wch; // Send key into object (will trigger callback and verification) - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been."); } void InputTest::DifferentModifiersTest() @@ -706,23 +664,23 @@ void InputTest::BackarrowKeyModeTest() void InputTest::AutoRepeatModeTest() { - const auto down = std::make_unique(true, 1ui16, 'A', 0ui16, 'A', 0ui16); - const auto up = std::make_unique(false, 1ui16, 'A', 0ui16, 'A', 0ui16); + static constexpr auto down = SynthesizeKeyEvent(true, 1, 'A', 0, 'A', 0); + static constexpr auto up = SynthesizeKeyEvent(false, 1, 'A', 0, 'A', 0); TerminalInput input; Log::Comment(L"Sending repeating keypresses with DECARM disabled."); input.SetInputMode(TerminalInput::Mode::AutoRepeat, false); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up)); Log::Comment(L"Sending repeating keypresses with DECARM enabled."); input.SetInputMode(TerminalInput::Mode::AutoRepeat, true); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); - VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up)); } diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp index 1fe64047b80..ddcb81b7e9b 100644 --- a/src/terminal/input/mouseInput.cpp +++ b/src/terminal/input/mouseInput.cpp @@ -4,6 +4,7 @@ #include "precomp.h" #include "terminalInput.hpp" +#include "../types/inc/IInputEvent.hpp" #include "../types/inc/utils.hpp" using namespace Microsoft::Console::VirtualTerminal; diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 1a736bd9ed4..855ba74b28a 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -6,8 +6,9 @@ #include -#include "../../interactivity/inc/VtApiRedirection.hpp" #include "../../inc/unicode.hpp" +#include "../../interactivity/inc/VtApiRedirection.hpp" +#include "../types/inc/IInputEvent.hpp" using namespace Microsoft::Console::VirtualTerminal; @@ -268,45 +269,45 @@ void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexce _forceDisableWin32InputMode = win32InputMode; } -static const std::span _getKeyMapping(const KeyEvent& keyEvent, - const bool ansiMode, - const bool cursorApplicationMode, - const bool keypadApplicationMode) noexcept +static std::span _getKeyMapping(const KEY_EVENT_RECORD& keyEvent, const bool ansiMode, const bool cursorApplicationMode, const bool keypadApplicationMode) noexcept { + // Cursor keys: VK_END, VK_HOME, VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN + const auto isCursorKey = keyEvent.wVirtualKeyCode >= VK_END && keyEvent.wVirtualKeyCode <= VK_DOWN; + if (ansiMode) { - if (keyEvent.IsCursorKey()) + if (isCursorKey) { if (cursorApplicationMode) { - return { s_cursorKeysApplicationMapping.data(), s_cursorKeysApplicationMapping.size() }; + return s_cursorKeysApplicationMapping; } else { - return { s_cursorKeysNormalMapping.data(), s_cursorKeysNormalMapping.size() }; + return s_cursorKeysNormalMapping; } } else { if (keypadApplicationMode) { - return { s_keypadApplicationMapping.data(), s_keypadApplicationMapping.size() }; + return s_keypadApplicationMapping; } else { - return { s_keypadNumericMapping.data(), s_keypadNumericMapping.size() }; + return s_keypadNumericMapping; } } } else { - if (keyEvent.IsCursorKey()) + if (isCursorKey) { - return { s_cursorKeysVt52Mapping.data(), s_cursorKeysVt52Mapping.size() }; + return s_cursorKeysVt52Mapping; } else { - return { s_keypadVt52Mapping.data(), s_keypadVt52Mapping.size() }; + return s_keypadVt52Mapping; } } } @@ -318,12 +319,12 @@ static const std::span _getKeyMapping(const KeyEvent& keyEvent // - keyMapping - Array of key mappings to search // Return Value: // - Has value if there was a match to a key translation. -static std::optional _searchKeyMapping(const KeyEvent& keyEvent, +static std::optional _searchKeyMapping(const KEY_EVENT_RECORD& keyEvent, std::span keyMapping) noexcept { for (auto& map : keyMapping) { - if (map.vkey == keyEvent.GetVirtualKeyCode()) + if (map.vkey == keyEvent.wVirtualKeyCode) { // If the mapping has no modifiers set, then it doesn't really care // what the modifiers are on the key. The caller will likely do @@ -337,9 +338,9 @@ static std::optional _searchKeyMapping(const KeyEvent& keyEven // The modifier mapping expects certain modifier keys to be // pressed. Check those as well. modifiersMatch = - (WI_IsFlagSet(map.modifiers, SHIFT_PRESSED) == keyEvent.IsShiftPressed()) && - (WI_IsAnyFlagSet(map.modifiers, ALT_PRESSED) == keyEvent.IsAltPressed()) && - (WI_IsAnyFlagSet(map.modifiers, CTRL_PRESSED) == keyEvent.IsCtrlPressed()); + WI_IsAnyFlagSet(map.modifiers, SHIFT_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED) && + WI_IsAnyFlagSet(map.modifiers, ALT_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && + WI_IsAnyFlagSet(map.modifiers, CTRL_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED); } if (modifiersMatch) @@ -353,16 +354,16 @@ static std::optional _searchKeyMapping(const KeyEvent& keyEven // Searches the s_modifierKeyMapping for a entry corresponding to this key event. // Changes the second to last byte to correspond to the currently pressed modifier keys. -TerminalInput::OutputType TerminalInput::_searchWithModifier(const KeyEvent& keyEvent) +TerminalInput::OutputType TerminalInput::_searchWithModifier(const KEY_EVENT_RECORD& keyEvent) { if (const auto match = _searchKeyMapping(keyEvent, s_modifierKeyMapping)) { const auto& v = match.value(); if (!v.sequence.empty()) { - const auto shift = keyEvent.IsShiftPressed(); - const auto alt = keyEvent.IsAltPressed(); - const auto ctrl = keyEvent.IsCtrlPressed(); + const auto shift = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED); + const auto alt = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED); + const auto ctrl = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED); StringType str{ v.sequence }; str.at(str.size() - 2) = L'1' + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0); return str; @@ -407,12 +408,12 @@ TerminalInput::OutputType TerminalInput::_searchWithModifier(const KeyEvent& key const auto slashVkey = LOBYTE(slashKeyScan); const auto questionMarkVkey = LOBYTE(questionMarkKeyScan); - const auto ctrl = keyEvent.IsCtrlPressed(); - const auto alt = keyEvent.IsAltPressed(); - const auto shift = keyEvent.IsShiftPressed(); + const auto ctrl = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED); + const auto alt = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED); + const auto shift = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED); // From the KeyEvent we're translating, synthesize the equivalent VkKeyScan result - const auto vkey = keyEvent.GetVirtualKeyCode(); + const auto vkey = keyEvent.wVirtualKeyCode; const short keyScanFromEvent = vkey | (shift ? 0x100 : 0) | (ctrl ? 0x200 : 0) | @@ -474,20 +475,15 @@ TerminalInput::OutputType TerminalInput::MakeOutput(const std::wstring_view& str // Return Value: // - Returns an empty optional if we didn't handle the key event and the caller can opt to handle it in some other way. // - Returns a string if we successfully translated it into a VT input sequence. -TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInEvent) +TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) { - if (!pInEvent) - { - return MakeUnhandled(); - } - // On key presses, prepare to translate to VT compatible sequences - if (pInEvent->EventType() != InputEventType::KeyEvent) + if (event.EventType != KEY_EVENT) { return MakeUnhandled(); } - auto keyEvent = *static_cast(pInEvent); + auto keyEvent = event.Event.KeyEvent; // GH#4999 - If we're in win32-input mode, skip straight to doing that. // Since this mode handles all types of key events, do nothing else. @@ -498,10 +494,10 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE } // Check if this key matches the last recorded key code. - const auto matchingLastKeyPress = _lastVirtualKeyCode == keyEvent.GetVirtualKeyCode(); + const auto matchingLastKeyPress = _lastVirtualKeyCode == keyEvent.wVirtualKeyCode; // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) - if (!keyEvent.IsKeyDown()) + if (!keyEvent.bKeyDown) { // If this is a release of the last recorded key press, we can reset that. if (matchingLastKeyPress) @@ -519,17 +515,17 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // the event, otherwise the key press can still end up being submitted. return MakeOutput({}); } - _lastVirtualKeyCode = keyEvent.GetVirtualKeyCode(); + _lastVirtualKeyCode = keyEvent.wVirtualKeyCode; // The VK_BACK key depends on the state of Backarrow Key mode (DECBKM). // If the mode is set, we should send BS. If reset, we should send DEL. - if (keyEvent.GetVirtualKeyCode() == VK_BACK) + if (keyEvent.wVirtualKeyCode == VK_BACK) { // The Ctrl modifier reverses the interpretation of DECBKM. - const auto backarrowMode = _inputMode.test(Mode::BackarrowKey) != keyEvent.IsCtrlPressed(); + const auto backarrowMode = _inputMode.test(Mode::BackarrowKey) != WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED); const auto seq = backarrowMode ? L'\x08' : L'\x7f'; // The Alt modifier adds an escape prefix. - if (keyEvent.IsAltPressed()) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED)) { return _makeEscapedOutput(seq); } @@ -542,7 +538,7 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // When the Line Feed mode is set, a VK_RETURN key should send both CR and LF. // When reset, we fall through to the default behavior, which is to send just // CR, or when the Ctrl modifier is pressed, just LF. - if (keyEvent.GetVirtualKeyCode() == VK_RETURN && _inputMode.test(Mode::LineFeed)) + if (keyEvent.wVirtualKeyCode == VK_RETURN && _inputMode.test(Mode::LineFeed)) { return MakeOutput(L"\r\n"); } @@ -550,12 +546,11 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // Many keyboard layouts have an AltGr key, which makes widely used characters accessible. // For instance on a German keyboard layout "[" is written by pressing AltGr+8. // Furthermore Ctrl+Alt is traditionally treated as an alternative way to AltGr by Windows. - // When AltGr is pressed, the caller needs to make sure to send us a pretranslated character in GetCharData(). + // When AltGr is pressed, the caller needs to make sure to send us a pretranslated character in uChar.UnicodeChar. // --> Strip out the AltGr flags, in order for us to not step into the Alt/Ctrl conditions below. - if (keyEvent.IsAltGrPressed()) + if (WI_AreAllFlagsSet(keyEvent.dwControlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)) { - keyEvent.DeactivateModifierKey(ModifierKeyState::LeftCtrl); - keyEvent.DeactivateModifierKey(ModifierKeyState::RightAlt); + WI_ClearAllFlags(keyEvent.dwControlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED); } // The Alt modifier initiates a so called "escape sequence". @@ -565,16 +560,16 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // This section in particular handles Alt+Ctrl combinations though. // The Ctrl modifier causes all of the char code's bits except // for the 5 least significant ones to be zeroed out. - if (keyEvent.IsAltPressed() && keyEvent.IsCtrlPressed()) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED)) { - const auto ch = keyEvent.GetCharData(); - const auto vkey = keyEvent.GetVirtualKeyCode(); + const auto ch = keyEvent.uChar.UnicodeChar; + const auto vkey = keyEvent.wVirtualKeyCode; - // For Alt+Ctrl+Key messages GetCharData() usually returns 0. + // For Alt+Ctrl+Key messages uChar.UnicodeChar usually returns 0. // Luckily the numerical values of the ASCII characters and virtual key codes // of and A-Z, as used below, are numerically identical. // -> Get the char from the virtual key if it's 0. - const auto ctrlAltChar = keyEvent.GetCharData() != 0 ? keyEvent.GetCharData() : keyEvent.GetVirtualKeyCode(); + const auto ctrlAltChar = keyEvent.uChar.UnicodeChar != 0 ? keyEvent.uChar.UnicodeChar : keyEvent.wVirtualKeyCode; // Alt+Ctrl acts as a substitute for AltGr on Windows. // For instance using a German keyboard both AltGr+< and Alt+Ctrl+< produce a | (pipe) character. @@ -597,7 +592,7 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE } // If a modifier key was pressed, then we need to try and send the modified sequence. - if (keyEvent.IsModifierPressed()) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, MOD_PRESSED)) { if (auto out = _searchWithModifier(keyEvent)) { @@ -607,9 +602,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // This section is similar to the Alt modifier section above, // but handles cases without Ctrl modifiers. - if (keyEvent.IsAltPressed() && !keyEvent.IsCtrlPressed() && keyEvent.GetCharData() != 0) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && !WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED) && keyEvent.uChar.UnicodeChar != 0) { - return _makeEscapedOutput(keyEvent.GetCharData()); + return _makeEscapedOutput(keyEvent.uChar.UnicodeChar); } // Pressing the control key causes all bits but the 5 least @@ -620,10 +615,10 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE // -> Send a "null input sequence" in that case. // We don't need to handle other kinds of Ctrl combinations, // as we rely on the caller to pretranslate those to characters for us. - if (!keyEvent.IsAltPressed() && keyEvent.IsCtrlPressed()) + if (!WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED)) { - const auto ch = keyEvent.GetCharData(); - const auto vkey = keyEvent.GetVirtualKeyCode(); + const auto ch = keyEvent.uChar.UnicodeChar; + const auto vkey = keyEvent.wVirtualKeyCode; // Currently, when we're called with Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte. // VkKeyScanW(0) in turn returns the vkey for the null character (ASCII @). @@ -639,7 +634,7 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE if (ch == UNICODE_NULL) { // -> Try to infer the character from the vkey. - auto mappedChar = LOWORD(OneCoreSafeMapVirtualKeyW(keyEvent.GetVirtualKeyCode(), MAPVK_VK_TO_CHAR)); + auto mappedChar = LOWORD(OneCoreSafeMapVirtualKeyW(keyEvent.wVirtualKeyCode, MAPVK_VK_TO_CHAR)); if (mappedChar) { // Pressing the control key causes all bits but the 5 least @@ -660,9 +655,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInE } // If all else fails we can finally try to send the character itself if there is any. - if (keyEvent.GetCharData() != 0) + if (keyEvent.uChar.UnicodeChar != 0) { - return _makeCharOutput(keyEvent.GetCharData()); + return _makeCharOutput(keyEvent.uChar.UnicodeChar); } return MakeUnhandled(); @@ -720,16 +715,16 @@ TerminalInput::OutputType TerminalInput::_makeEscapedOutput(const wchar_t wch) // Turns an KEY_EVENT_RECORD into a win32-input-mode VT sequence. // It allows us to send KEY_EVENT_RECORD data losslessly to conhost. -TerminalInput::OutputType TerminalInput::_makeWin32Output(const KeyEvent& key) +TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD& key) { // .uChar.UnicodeChar must be cast to an integer because we want its numerical value. // Casting the rest to uint16_t as well doesn't hurt because that's MAX_PARAMETER_VALUE anyways. - const auto kd = gsl::narrow_cast(key.IsKeyDown() ? 1 : 0); - const auto rc = gsl::narrow_cast(key.GetRepeatCount()); - const auto vk = gsl::narrow_cast(key.GetVirtualKeyCode()); - const auto sc = gsl::narrow_cast(key.GetVirtualScanCode()); - const auto uc = gsl::narrow_cast(key.GetCharData()); - const auto cs = gsl::narrow_cast(key.GetActiveModifierKeys()); + const auto kd = gsl::narrow_cast(key.bKeyDown ? 1 : 0); + const auto rc = gsl::narrow_cast(key.wRepeatCount); + const auto vk = gsl::narrow_cast(key.wVirtualKeyCode); + const auto sc = gsl::narrow_cast(key.wVirtualScanCode); + const auto uc = gsl::narrow_cast(key.uChar.UnicodeChar); + const auto cs = gsl::narrow_cast(key.dwControlKeyState); // Sequences are formatted as follows: // diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 8fcabe48987..820d22f8a90 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -1,22 +1,8 @@ -/*+ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- terminalInput.hpp - -Abstract: -- This serves as an adapter between virtual key input from a user and the virtual terminal sequences that are - typically emitted by an xterm-compatible console. - -Author(s): -- Michael Niksa (MiNiksa) 30-Oct-2015 ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once -#include "../../types/inc/IInputEvent.hpp" - namespace Microsoft::Console::VirtualTerminal { class TerminalInput final @@ -34,7 +20,7 @@ namespace Microsoft::Console::VirtualTerminal static [[nodiscard]] OutputType MakeUnhandled() noexcept; static [[nodiscard]] OutputType MakeOutput(const std::wstring_view& str); - [[nodiscard]] OutputType HandleKey(const IInputEvent* const pInEvent); + [[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); @@ -89,8 +75,8 @@ namespace Microsoft::Console::VirtualTerminal [[nodiscard]] OutputType _makeCharOutput(wchar_t ch); static [[nodiscard]] OutputType _makeEscapedOutput(wchar_t wch); - static [[nodiscard]] OutputType _makeWin32Output(const KeyEvent& key); - static [[nodiscard]] OutputType _searchWithModifier(const KeyEvent& keyEvent); + static [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key); + static [[nodiscard]] OutputType _searchWithModifier(const KEY_EVENT_RECORD& keyEvent); #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index 5d0a6c51db0..95fb5a3e509 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -132,8 +132,11 @@ bool InputStateMachineEngine::_DoControlCharacter(const wchar_t wch, const bool if (wch == UNICODE_ETX && !writeAlt) { // This is Ctrl+C, which is handled specially by the host. - const auto [keyDown, keyUp] = KeyEvent::MakePair(1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); - success = _pDispatch->WriteCtrlKey(keyDown) && _pDispatch->WriteCtrlKey(keyUp); + static constexpr auto keyDown = SynthesizeKeyEvent(true, 1, L'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); + static constexpr auto keyUp = SynthesizeKeyEvent(false, 1, L'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); + _pDispatch->WriteCtrlKey(keyDown); + _pDispatch->WriteCtrlKey(keyUp); + success = true; } else if (wch >= '\x0' && wch < '\x20') { @@ -278,10 +281,10 @@ bool InputStateMachineEngine::ActionPassThroughString(const std::wstring_view st // similar to TerminalInput::_SendInputSequence if (!string.empty()) { - std::deque> inputEvents; + InputEventQueue inputEvents; for (const auto& wch : string) { - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); + inputEvents.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0)); } return _pDispatch->WriteInput(inputEvents); } @@ -558,7 +561,7 @@ bool InputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, void InputStateMachineEngine::_GenerateWrappedSequence(const wchar_t wch, const short vkey, const DWORD modifierState, - std::vector& input) + InputEventQueue& input) { input.reserve(input.size() + 8); @@ -570,44 +573,31 @@ void InputStateMachineEngine::_GenerateWrappedSequence(const wchar_t wch, const auto ctrl = WI_IsFlagSet(modifierState, LEFT_CTRL_PRESSED); const auto alt = WI_IsFlagSet(modifierState, LEFT_ALT_PRESSED); - INPUT_RECORD next{ 0 }; - + auto next = SynthesizeKeyEvent(true, 1, 0, 0, 0, 0); DWORD currentModifiers = 0; if (shift) { WI_SetFlag(currentModifiers, SHIFT_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = TRUE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_SHIFT; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_SHIFT, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } if (alt) { WI_SetFlag(currentModifiers, LEFT_ALT_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = TRUE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_MENU; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_MENU, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } if (ctrl) { WI_SetFlag(currentModifiers, LEFT_CTRL_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = TRUE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_CONTROL; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_CONTROL, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } @@ -616,40 +606,30 @@ void InputStateMachineEngine::_GenerateWrappedSequence(const wchar_t wch, // through on the KeyPress. _GetSingleKeypress(wch, vkey, modifierState, input); + next.Event.KeyEvent.bKeyDown = FALSE; + if (ctrl) { WI_ClearFlag(currentModifiers, LEFT_CTRL_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = FALSE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_CONTROL; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_CONTROL, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } if (alt) { WI_ClearFlag(currentModifiers, LEFT_ALT_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = FALSE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_MENU; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_MENU, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } if (shift) { WI_ClearFlag(currentModifiers, SHIFT_PRESSED); - next.EventType = KEY_EVENT; - next.Event.KeyEvent.bKeyDown = FALSE; next.Event.KeyEvent.dwControlKeyState = currentModifiers; - next.Event.KeyEvent.wRepeatCount = 1; next.Event.KeyEvent.wVirtualKeyCode = VK_SHIFT; next.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(VK_SHIFT, MAPVK_VK_TO_VSC)); - next.Event.KeyEvent.uChar.UnicodeChar = 0x0; input.push_back(next); } } @@ -668,21 +648,14 @@ void InputStateMachineEngine::_GenerateWrappedSequence(const wchar_t wch, void InputStateMachineEngine::_GetSingleKeypress(const wchar_t wch, const short vkey, const DWORD modifierState, - std::vector& input) + InputEventQueue& input) { input.reserve(input.size() + 2); - INPUT_RECORD rec; + const auto sc = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_VSC)); + auto rec = SynthesizeKeyEvent(true, 1, vkey, sc, wch, modifierState); - rec.EventType = KEY_EVENT; - rec.Event.KeyEvent.bKeyDown = TRUE; - rec.Event.KeyEvent.dwControlKeyState = modifierState; - rec.Event.KeyEvent.wRepeatCount = 1; - rec.Event.KeyEvent.wVirtualKeyCode = vkey; - rec.Event.KeyEvent.wVirtualScanCode = gsl::narrow_cast(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_VSC)); - rec.Event.KeyEvent.uChar.UnicodeChar = wch; input.push_back(rec); - rec.Event.KeyEvent.bKeyDown = FALSE; input.push_back(rec); } @@ -701,10 +674,8 @@ void InputStateMachineEngine::_GetSingleKeypress(const wchar_t wch, bool InputStateMachineEngine::_WriteSingleKey(const wchar_t wch, const short vkey, const DWORD modifierState) { // At most 8 records - 2 for each of shift,ctrl,alt up and down, and 2 for the actual key up and down. - std::vector input; - _GenerateWrappedSequence(wch, vkey, modifierState, input); - auto inputEvents = IInputEvent::Create(std::span{ input }); - + InputEventQueue inputEvents; + _GenerateWrappedSequence(wch, vkey, modifierState, inputEvents); return _pDispatch->WriteInput(inputEvents); } @@ -734,18 +705,8 @@ bool InputStateMachineEngine::_WriteSingleKey(const short vkey, const DWORD modi // - true iff we successfully wrote the keypress to the input callback. bool InputStateMachineEngine::_WriteMouseEvent(const til::point uiPos, const DWORD buttonState, const DWORD controlKeyState, const DWORD eventFlags) { - INPUT_RECORD rgInput; - rgInput.EventType = MOUSE_EVENT; - rgInput.Event.MouseEvent.dwMousePosition.X = ::base::saturated_cast(uiPos.x); - rgInput.Event.MouseEvent.dwMousePosition.Y = ::base::saturated_cast(uiPos.y); - rgInput.Event.MouseEvent.dwButtonState = buttonState; - rgInput.Event.MouseEvent.dwControlKeyState = controlKeyState; - rgInput.Event.MouseEvent.dwEventFlags = eventFlags; - - // pack and write input record - // 1 record - the modifiers don't get their own events - auto inputEvents = IInputEvent::Create(std::span{ &rgInput, 1 }); - return _pDispatch->WriteInput(inputEvents); + const auto rgInput = SynthesizeMouseEvent(uiPos, buttonState, controlKeyState, eventFlags); + return _pDispatch->WriteInput({ &rgInput, 1 }); } // Method Description: @@ -1127,7 +1088,7 @@ bool InputStateMachineEngine::_GetWindowManipulationType(const std::span(parameters.at(0).value_or(0))); - key.SetVirtualScanCode(::base::saturated_cast(parameters.at(1).value_or(0))); - key.SetCharData(::base::saturated_cast(parameters.at(2).value_or(0))); - key.SetKeyDown(parameters.at(3).value_or(0)); - key.SetActiveModifierKeys(::base::saturated_cast(parameters.at(4).value_or(0))); - key.SetRepeatCount(::base::saturated_cast(parameters.at(5).value_or(1))); - return key; + return SynthesizeKeyEvent( + parameters.at(3).value_or(0), + ::base::saturated_cast(parameters.at(5).value_or(1)), + ::base::saturated_cast(parameters.at(0).value_or(0)), + ::base::saturated_cast(parameters.at(1).value_or(0)), + ::base::saturated_cast(parameters.at(2).value_or(0)), + ::base::saturated_cast(parameters.at(4).value_or(0))); } diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index a2ab34623fd..74af20a9b39 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -197,17 +197,17 @@ namespace Microsoft::Console::VirtualTerminal void _GenerateWrappedSequence(const wchar_t wch, const short vkey, const DWORD modifierState, - std::vector& input); + InputEventQueue& input); void _GetSingleKeypress(const wchar_t wch, const short vkey, const DWORD modifierState, - std::vector& input); + InputEventQueue& input); bool _GetWindowManipulationType(const std::span parameters, unsigned int& function) const noexcept; - KeyEvent _GenerateWin32Key(const VTParameters parameters); + static INPUT_RECORD _GenerateWin32Key(const VTParameters& parameters); bool _DoControlCharacter(const wchar_t wch, const bool writeAlt); diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp index 5ae36992574..2e21c77c74f 100644 --- a/src/terminal/parser/ut_parser/InputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -75,11 +75,10 @@ class TestState { } - void RoundtripTerminalInputCallback(_In_ std::deque>& inEvents) + void RoundtripTerminalInputCallback(_In_ const std::span& inputRecords) { // Take all the characters out of the input records here, and put them into // the input state machine. - auto inputRecords = IInputEvent::ToInputRecords(inEvents); std::wstring vtseq = L""; for (auto& inRec : inputRecords) { @@ -96,10 +95,8 @@ class TestState Log::Comment(L"String processed"); } - void TestInputCallback(std::deque>& inEvents) + void TestInputCallback(const std::span& records) { - auto records = IInputEvent::ToInputRecords(inEvents); - // This callback doesn't work super well for the Ctrl+C iteration of the // C0Test. For ^C, we always send a keydown and a key up event, however, // both calls to WriteCtrlKey happen in one single call to @@ -146,10 +143,8 @@ class TestState vExpectedInput.clear(); } - void TestInputStringCallback(std::deque>& inEvents) + void TestInputStringCallback(const std::span& records) { - auto records = IInputEvent::ToInputRecords(inEvents); - for (auto expected : vExpectedInput) { Log::Comment( @@ -211,9 +206,9 @@ class Microsoft::Console::VirtualTerminal::InputEngineTest TestState testState; - void RoundtripTerminalInputCallback(std::deque>& inEvents); - void TestInputCallback(std::deque>& inEvents); - void TestInputStringCallback(std::deque>& inEvents); + void RoundtripTerminalInputCallback(const std::span& inEvents); + void TestInputCallback(const std::span& inEvents); + void TestInputStringCallback(const std::span& inEvents); std::wstring GenerateSgrMouseSequence(const CsiMouseButtonCodes button, const unsigned short modifiers, const til::point position, @@ -318,11 +313,11 @@ void InputEngineTest::VerifyExpectedInputDrained() class Microsoft::Console::VirtualTerminal::TestInteractDispatch final : public IInteractDispatch { public: - TestInteractDispatch(_In_ std::function>&)> pfn, + TestInteractDispatch(_In_ std::function&)> pfn, _In_ TestState* testState); - virtual bool WriteInput(_In_ std::deque>& inputEvents) override; + virtual bool WriteInput(_In_ const std::span& inputEvents) override; - virtual bool WriteCtrlKey(const KeyEvent& event) override; + virtual bool WriteCtrlKey(const INPUT_RECORD& event) override; virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType function, const VTParameter parameter1, const VTParameter parameter2) override; // DTTERM_WindowManipulation @@ -336,29 +331,27 @@ class Microsoft::Console::VirtualTerminal::TestInteractDispatch final : public I virtual bool FocusChanged(const bool focused) const override; private: - std::function>&)> _pfnWriteInputCallback; + std::function&)> _pfnWriteInputCallback; TestState* _testState; // non-ownership pointer }; -TestInteractDispatch::TestInteractDispatch(_In_ std::function>&)> pfn, +TestInteractDispatch::TestInteractDispatch(_In_ std::function&)> pfn, _In_ TestState* testState) : _pfnWriteInputCallback(pfn), _testState(testState) { } -bool TestInteractDispatch::WriteInput(_In_ std::deque>& inputEvents) +bool TestInteractDispatch::WriteInput(_In_ const std::span& inputEvents) { _pfnWriteInputCallback(inputEvents); return true; } -bool TestInteractDispatch::WriteCtrlKey(const KeyEvent& event) +bool TestInteractDispatch::WriteCtrlKey(const INPUT_RECORD& event) { VERIFY_IS_TRUE(_testState->_expectSendCtrlC); - std::deque> inputEvents; - inputEvents.push_back(std::make_unique(event)); - return WriteInput(inputEvents); + return WriteInput({ &event, 1 }); } bool TestInteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulationType function, @@ -374,16 +367,13 @@ bool TestInteractDispatch::WindowManipulation(const DispatchTypes::WindowManipul bool TestInteractDispatch::WriteString(const std::wstring_view string) { - std::deque> keyEvents; + InputEventQueue keyEvents; for (const auto& wch : string) { // We're forcing the translation to CP_USA, so that it'll be constant // regardless of the CP the test is running in - auto convertedEvents = Microsoft::Console::Interactivity::CharToKeyEvents(wch, CP_USA); - std::move(convertedEvents.begin(), - convertedEvents.end(), - std::back_inserter(keyEvents)); + Microsoft::Console::Interactivity::CharToKeyEvents(wch, CP_USA, keyEvents); } return WriteInput(keyEvents); @@ -966,12 +956,12 @@ void InputEngineTest::AltIntermediateTest() // Create the callback that's fired when the state machine wants to write // input. We'll take the events and put them straight into the // TerminalInput. - auto pfnInputStateMachineCallback = [&](std::deque>& inEvents) { + auto pfnInputStateMachineCallback = [&](const std::span& inEvents) { for (auto& ev : inEvents) { - if (const auto out = terminalInput.HandleKey(ev.get())) + if (const auto str = terminalInput.HandleKey(ev)) { - translation.append(*out); + translation.append(*str); } } }; @@ -1474,73 +1464,73 @@ void InputEngineTest::TestWin32InputParsing() { std::vector params{ 1 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(0, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\0', key.GetCharData()); - VERIFY_ARE_EQUAL(false, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(0, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\0', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(FALSE, key.bKeyDown); + VERIFY_ARE_EQUAL(0u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\0', key.GetCharData()); - VERIFY_ARE_EQUAL(false, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\0', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(FALSE, key.bKeyDown); + VERIFY_ARE_EQUAL(0u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2, 3 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); - VERIFY_ARE_EQUAL(false, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\x03', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(FALSE, key.bKeyDown); + VERIFY_ARE_EQUAL(0u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2, 3, 4 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); - VERIFY_ARE_EQUAL(true, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\x03', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(TRUE, key.bKeyDown); + VERIFY_ARE_EQUAL(0u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2, 3, 1 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); - VERIFY_ARE_EQUAL(true, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\x03', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(TRUE, key.bKeyDown); + VERIFY_ARE_EQUAL(0u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2, 3, 4, 5 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); - VERIFY_ARE_EQUAL(true, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0x5u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\x03', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(TRUE, key.bKeyDown); + VERIFY_ARE_EQUAL(0x5u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(1, key.wRepeatCount); } { std::vector params{ 1, 2, 3, 4, 5, 6 }; - auto key = engine->_GenerateWin32Key({ params.data(), params.size() }); - VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); - VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); - VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); - VERIFY_ARE_EQUAL(true, key.IsKeyDown()); - VERIFY_ARE_EQUAL(0x5u, key.GetActiveModifierKeys()); - VERIFY_ARE_EQUAL(6, key.GetRepeatCount()); + auto key = engine->_GenerateWin32Key({ params.data(), params.size() }).Event.KeyEvent; + VERIFY_ARE_EQUAL(1, key.wVirtualKeyCode); + VERIFY_ARE_EQUAL(2, key.wVirtualScanCode); + VERIFY_ARE_EQUAL(L'\x03', key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL(TRUE, key.bKeyDown); + VERIFY_ARE_EQUAL(0x5u, key.dwControlKeyState); + VERIFY_ARE_EQUAL(6, key.wRepeatCount); } } @@ -1580,26 +1570,26 @@ void InputEngineTest::TestWin32InputOptionals() provideRepeatCount ? 6 : 0, }; - auto key = engine->_GenerateWin32Key({ params.data(), numParams }); + auto key = engine->_GenerateWin32Key({ params.data(), numParams }).Event.KeyEvent; VERIFY_ARE_EQUAL((provideVirtualKeyCode && numParams > 0) ? 1 : 0, - key.GetVirtualKeyCode()); + key.wVirtualKeyCode); VERIFY_ARE_EQUAL((provideVirtualScanCode && numParams > 1) ? 2 : 0, - key.GetVirtualScanCode()); + key.wVirtualScanCode); VERIFY_ARE_EQUAL((provideCharData && numParams > 2) ? L'\x03' : L'\0', - key.GetCharData()); - VERIFY_ARE_EQUAL((provideKeyDown && numParams > 3) ? true : false, - key.IsKeyDown()); + key.uChar.UnicodeChar); + VERIFY_ARE_EQUAL((provideKeyDown && numParams > 3) ? TRUE : FALSE, + key.bKeyDown); VERIFY_ARE_EQUAL((provideActiveModifierKeys && numParams > 4) ? 5u : 0u, - key.GetActiveModifierKeys()); + key.dwControlKeyState); if (numParams == 6) { VERIFY_ARE_EQUAL((provideRepeatCount) ? 6 : 0, - key.GetRepeatCount()); + key.wRepeatCount); } else { VERIFY_ARE_EQUAL((provideRepeatCount && numParams > 5) ? 6 : 1, - key.GetRepeatCount()); + key.wRepeatCount); } } } diff --git a/src/types/FocusEvent.cpp b/src/types/FocusEvent.cpp deleted file mode 100644 index 09a93b2f4ae..00000000000 --- a/src/types/FocusEvent.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -// BODGY: GH#13238 -// -// It appears that some applications (libuv) like to send a FOCUS_EVENT_RECORD -// as a way to jiggle the input handle. Focus events really shouldn't ever be -// sent via the API, they don't really do anything internally. However, focus -// events in the input buffer do get translated by the TerminalInput to VT -// sequences if we're in the right input mode. -// -// To not prevent libuv from jiggling the handle with a focus event, and also -// make sure that we don't erroneously translate that to a sequence of -// characters, we're going to filter out focus events that came from the API -// when translating to VT. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -FocusEvent::~FocusEvent() = default; - -INPUT_RECORD FocusEvent::ToInputRecord() const noexcept -{ - INPUT_RECORD record{ 0 }; - record.EventType = FOCUS_EVENT; - record.Event.FocusEvent.bSetFocus = !!_focus; - return record; -} - -InputEventType FocusEvent::EventType() const noexcept -{ - return InputEventType::FocusEvent; -} - -void FocusEvent::SetFocus(const bool focus) noexcept -{ - _focus = focus; -} diff --git a/src/types/IInputEvent.cpp b/src/types/IInputEvent.cpp deleted file mode 100644 index c2285caec66..00000000000 --- a/src/types/IInputEvent.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -#include - -std::unique_ptr IInputEvent::Create(const INPUT_RECORD& record) -{ - switch (record.EventType) - { - case KEY_EVENT: - return std::make_unique(record.Event.KeyEvent); - case MOUSE_EVENT: - return std::make_unique(record.Event.MouseEvent); - case WINDOW_BUFFER_SIZE_EVENT: - return std::make_unique(record.Event.WindowBufferSizeEvent); - case MENU_EVENT: - return std::make_unique(record.Event.MenuEvent); - case FOCUS_EVENT: - return std::make_unique(record.Event.FocusEvent); - default: - THROW_HR(E_INVALIDARG); - } -} - -std::deque> IInputEvent::Create(std::span records) -{ - std::deque> outEvents; - - for (const auto& record : records) - { - outEvents.push_back(Create(record)); - } - - return outEvents; -} - -// Routine Description: -// - Converts std::deque to std::deque> -// Arguments: -// - inRecords - records to convert -// Return Value: -// - std::deque of IInputEvents on success. Will throw exception on failure. -std::deque> IInputEvent::Create(const std::deque& records) -{ - std::deque> outEvents; - for (const auto& record : records) - { - auto event = Create(record); - outEvents.push_back(std::move(event)); - } - return outEvents; -} - -std::vector IInputEvent::ToInputRecords(const std::deque>& events) -{ - std::vector records; - records.reserve(events.size()); - - for (auto& evt : events) - { - records.push_back(evt->ToInputRecord()); - } - - return records; -} diff --git a/src/types/IInputEventStreams.cpp b/src/types/IInputEventStreams.cpp deleted file mode 100644 index 376cd3c5872..00000000000 --- a/src/types/IInputEventStreams.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" -#include - -std::wostream& operator<<(std::wostream& stream, const IInputEvent* const pEvent) -{ - if (pEvent == nullptr) - { - return stream << L"nullptr"; - } - - try - { - switch (pEvent->EventType()) - { - case InputEventType::KeyEvent: - return stream << static_cast(pEvent); - case InputEventType::MouseEvent: - return stream << static_cast(pEvent); - case InputEventType::WindowBufferSizeEvent: - return stream << static_cast(pEvent); - case InputEventType::MenuEvent: - return stream << static_cast(pEvent); - case InputEventType::FocusEvent: - return stream << static_cast(pEvent); - default: - return stream << L"IInputEvent()"; - } - } - catch (...) - { - return stream << L"IInputEvent stream error"; - } -} - -std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent) -{ - if (pKeyEvent == nullptr) - { - return stream << L"nullptr"; - } - - std::wstring keyMotion = pKeyEvent->_keyDown ? L"keyDown" : L"keyUp"; - std::wstring charData = { pKeyEvent->_charData }; - if (pKeyEvent->_charData == L'\0') - { - charData = L"null"; - } - - // clang-format off - return stream << L"KeyEvent(" << - keyMotion << L", " << - L"repeat: " << pKeyEvent->_repeatCount << L", " << - L"keyCode: " << pKeyEvent->_virtualKeyCode << L", " << - L"scanCode: " << pKeyEvent->_virtualScanCode << L", " << - L"char: " << charData << L", " << - L"mods: " << pKeyEvent->GetActiveModifierKeys() << L")"; - // clang-format on -} - -std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent) -{ - if (pMouseEvent == nullptr) - { - return stream << L"nullptr"; - } - - // clang-format off - return stream << L"MouseEvent(" << - L"X: " << pMouseEvent->_position.x << L", " << - L"Y: " << pMouseEvent->_position.y << L", " << - L"buttons: " << pMouseEvent->_buttonState << L", " << - L"mods: " << pMouseEvent->_activeModifierKeys << L", " << - L"events: " << pMouseEvent->_eventFlags << L")"; - // clang-format on -} - -std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent) -{ - if (pEvent == nullptr) - { - return stream << L"nullptr"; - } - - // clang-format off - return stream << L"WindowbufferSizeEvent(" << - L"X: " << pEvent->_size.width << L", " << - L"Y: " << pEvent->_size.height << L")"; - // clang-format on -} - -std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent) -{ - if (pMenuEvent == nullptr) - { - return stream << L"nullptr"; - } - - return stream << L"MenuEvent(" << L"CommandId" << pMenuEvent->_commandId << L")"; -} - -std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent) -{ - if (pFocusEvent == nullptr) - { - return stream << L"nullptr"; - } - - return stream << L"FocusEvent(" << L"focus" << pFocusEvent->_focus << L")"; -} diff --git a/src/types/KeyEvent.cpp b/src/types/KeyEvent.cpp deleted file mode 100644 index 475cd837411..00000000000 --- a/src/types/KeyEvent.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -KeyEvent::~KeyEvent() = default; - -INPUT_RECORD KeyEvent::ToInputRecord() const noexcept -{ - INPUT_RECORD record{}; - record.EventType = KEY_EVENT; - record.Event.KeyEvent.bKeyDown = !!_keyDown; - record.Event.KeyEvent.wRepeatCount = _repeatCount; - record.Event.KeyEvent.wVirtualKeyCode = _virtualKeyCode; - record.Event.KeyEvent.wVirtualScanCode = _virtualScanCode; - record.Event.KeyEvent.uChar.UnicodeChar = _charData; - record.Event.KeyEvent.dwControlKeyState = GetActiveModifierKeys(); - return record; -} - -InputEventType KeyEvent::EventType() const noexcept -{ - return InputEventType::KeyEvent; -} - -void KeyEvent::SetKeyDown(const bool keyDown) noexcept -{ - _keyDown = keyDown; -} - -void KeyEvent::SetRepeatCount(const WORD repeatCount) noexcept -{ - _repeatCount = repeatCount; -} - -void KeyEvent::SetVirtualKeyCode(const WORD virtualKeyCode) noexcept -{ - _virtualKeyCode = virtualKeyCode; -} - -void KeyEvent::SetVirtualScanCode(const WORD virtualScanCode) noexcept -{ - _virtualScanCode = virtualScanCode; -} - -void KeyEvent::SetCharData(const char character) noexcept -{ - // With MSVC char is signed by default and the conversion to wchar_t (unsigned) would turn negative - // chars into very large wchar_t values. While this doesn't pose a problem per se (even with such sign - // extension, the lower 8 bit stay the same), it makes debugging and reading key events more difficult. - _charData = til::as_unsigned(character); -} - -void KeyEvent::SetCharData(const wchar_t character) noexcept -{ - _charData = character; -} - -void KeyEvent::SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept -{ - _activeModifierKeys = static_cast(activeModifierKeys); -} - -void KeyEvent::DeactivateModifierKey(const ModifierKeyState modifierKey) noexcept -{ - const auto bitFlag = ToConsoleControlKeyFlag(modifierKey); - auto keys = GetActiveModifierKeys(); - WI_ClearAllFlags(keys, bitFlag); - SetActiveModifierKeys(keys); -} - -void KeyEvent::ActivateModifierKey(const ModifierKeyState modifierKey) noexcept -{ - const auto bitFlag = ToConsoleControlKeyFlag(modifierKey); - auto keys = GetActiveModifierKeys(); - WI_SetAllFlags(keys, bitFlag); - SetActiveModifierKeys(keys); -} - -bool KeyEvent::DoActiveModifierKeysMatch(const std::unordered_set& consoleModifiers) const noexcept -{ - DWORD consoleBits = 0; - for (const auto& mod : consoleModifiers) - { - WI_SetAllFlags(consoleBits, ToConsoleControlKeyFlag(mod)); - } - return consoleBits == GetActiveModifierKeys(); -} - -// Routine Description: -// - checks if this key event is a special key for line editing -// Arguments: -// - none -// Return Value: -// - true if this key has special relevance to line editing, false otherwise -bool KeyEvent::IsCommandLineEditingKey() const noexcept -{ - if (!IsAltPressed() && !IsCtrlPressed()) - { - switch (GetVirtualKeyCode()) - { - case VK_ESCAPE: - case VK_PRIOR: - case VK_NEXT: - case VK_END: - case VK_HOME: - case VK_LEFT: - case VK_UP: - case VK_RIGHT: - case VK_DOWN: - case VK_INSERT: - case VK_DELETE: - case VK_F1: - case VK_F2: - case VK_F3: - case VK_F4: - case VK_F5: - case VK_F6: - case VK_F7: - case VK_F8: - case VK_F9: - return true; - default: - break; - } - } - if (IsCtrlPressed()) - { - switch (GetVirtualKeyCode()) - { - case VK_END: - case VK_HOME: - case VK_LEFT: - case VK_RIGHT: - return true; - default: - break; - } - } - - if (IsAltPressed()) - { - switch (GetVirtualKeyCode()) - { - case VK_F7: - case VK_F10: - return true; - default: - break; - } - } - return false; -} - -// Routine Description: -// - checks if this key event is a special key for popups -// Arguments: -// - None -// Return Value: -// - true if this key has special relevance to popups, false otherwise -bool KeyEvent::IsPopupKey() const noexcept -{ - if (!IsAltPressed() && !IsCtrlPressed()) - { - switch (GetVirtualKeyCode()) - { - case VK_ESCAPE: - case VK_PRIOR: - case VK_NEXT: - case VK_END: - case VK_HOME: - case VK_LEFT: - case VK_UP: - case VK_RIGHT: - case VK_DOWN: - case VK_F2: - case VK_F4: - case VK_F7: - case VK_F9: - case VK_DELETE: - return true; - default: - break; - } - } - - return false; -} diff --git a/src/types/MenuEvent.cpp b/src/types/MenuEvent.cpp deleted file mode 100644 index 445a215f4b8..00000000000 --- a/src/types/MenuEvent.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -MenuEvent::~MenuEvent() = default; - -INPUT_RECORD MenuEvent::ToInputRecord() const noexcept -{ - INPUT_RECORD record{ 0 }; - record.EventType = MENU_EVENT; - record.Event.MenuEvent.dwCommandId = _commandId; - return record; -} - -InputEventType MenuEvent::EventType() const noexcept -{ - return InputEventType::MenuEvent; -} - -void MenuEvent::SetCommandId(const UINT commandId) noexcept -{ - _commandId = commandId; -} diff --git a/src/types/ModifierKeyState.cpp b/src/types/ModifierKeyState.cpp deleted file mode 100644 index 603b52c7999..00000000000 --- a/src/types/ModifierKeyState.cpp +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -#include - -// Routine Description: -// - checks if flag is present in flags -// Arguments: -// - flags - bit pattern to check for flag -// - flag - bit pattern to search for -// Return Value: -// - true if flag is present in flags -// Note: -// - The wil version of IsFlagSet is only to operate on values that -// are compile time constants. This will work with runtime calculated -// values. -constexpr bool RuntimeIsFlagSet(const DWORD flags, const DWORD flag) noexcept -{ - return !!(flags & flag); -} - -std::unordered_set FromVkKeyScan(const short vkKeyScanFlags) -{ - std::unordered_set keyState; - - switch (vkKeyScanFlags) - { - case VkKeyScanModState::None: - break; - case VkKeyScanModState::ShiftPressed: - keyState.insert(ModifierKeyState::Shift); - break; - case VkKeyScanModState::CtrlPressed: - keyState.insert(ModifierKeyState::LeftCtrl); - keyState.insert(ModifierKeyState::RightCtrl); - break; - case VkKeyScanModState::ShiftAndCtrlPressed: - keyState.insert(ModifierKeyState::Shift); - keyState.insert(ModifierKeyState::LeftCtrl); - keyState.insert(ModifierKeyState::RightCtrl); - break; - case VkKeyScanModState::AltPressed: - keyState.insert(ModifierKeyState::LeftAlt); - keyState.insert(ModifierKeyState::RightAlt); - break; - case VkKeyScanModState::ShiftAndAltPressed: - keyState.insert(ModifierKeyState::Shift); - keyState.insert(ModifierKeyState::LeftAlt); - keyState.insert(ModifierKeyState::RightAlt); - break; - case VkKeyScanModState::CtrlAndAltPressed: - keyState.insert(ModifierKeyState::LeftCtrl); - keyState.insert(ModifierKeyState::RightCtrl); - keyState.insert(ModifierKeyState::LeftAlt); - keyState.insert(ModifierKeyState::RightAlt); - break; - case VkKeyScanModState::ModPressed: - keyState.insert(ModifierKeyState::Shift); - keyState.insert(ModifierKeyState::LeftCtrl); - keyState.insert(ModifierKeyState::RightCtrl); - keyState.insert(ModifierKeyState::LeftAlt); - keyState.insert(ModifierKeyState::RightAlt); - break; - default: - THROW_HR(E_INVALIDARG); - break; - } - - return keyState; -} - -using ModifierKeyStateMapping = std::pair; - -constexpr static ModifierKeyStateMapping ModifierKeyStateTranslationTable[] = { - { ModifierKeyState::RightAlt, RIGHT_ALT_PRESSED }, - { ModifierKeyState::LeftAlt, LEFT_ALT_PRESSED }, - { ModifierKeyState::RightCtrl, RIGHT_CTRL_PRESSED }, - { ModifierKeyState::LeftCtrl, LEFT_CTRL_PRESSED }, - { ModifierKeyState::Shift, SHIFT_PRESSED }, - { ModifierKeyState::NumLock, NUMLOCK_ON }, - { ModifierKeyState::ScrollLock, SCROLLLOCK_ON }, - { ModifierKeyState::CapsLock, CAPSLOCK_ON }, - { ModifierKeyState::EnhancedKey, ENHANCED_KEY }, - { ModifierKeyState::NlsDbcsChar, NLS_DBCSCHAR }, - { ModifierKeyState::NlsAlphanumeric, NLS_ALPHANUMERIC }, - { ModifierKeyState::NlsKatakana, NLS_KATAKANA }, - { ModifierKeyState::NlsHiragana, NLS_HIRAGANA }, - { ModifierKeyState::NlsRoman, NLS_ROMAN }, - { ModifierKeyState::NlsImeConversion, NLS_IME_CONVERSION }, - { ModifierKeyState::AltNumpad, ALTNUMPAD_BIT }, - { ModifierKeyState::NlsImeDisable, NLS_IME_DISABLE } -}; - -static_assert(size(ModifierKeyStateTranslationTable) == static_cast(ModifierKeyState::ENUM_COUNT), - "ModifierKeyStateTranslationTable must have a valid mapping for each modifier value"); - -// Routine Description: -// - Expands legacy control keys bitsets into a stl set -// Arguments: -// - flags - legacy bitset to expand -// Return Value: -// - set of ModifierKeyState values that represent flags -std::unordered_set FromConsoleControlKeyFlags(const DWORD flags) -{ - std::unordered_set keyStates; - - for (const auto& mapping : ModifierKeyStateTranslationTable) - { - if (RuntimeIsFlagSet(flags, mapping.second)) - { - keyStates.insert(mapping.first); - } - } - - return keyStates; -} - -// Routine Description: -// - Converts ModifierKeyState back to the bizarre console bitflag associated with it. -// Arguments: -// - modifierKey - modifier to convert -// Return Value: -// - console bitflag associated with modifierKey -DWORD ToConsoleControlKeyFlag(const ModifierKeyState modifierKey) noexcept -{ - for (const auto& mapping : ModifierKeyStateTranslationTable) - { - if (mapping.first == modifierKey) - { - return mapping.second; - } - } - return 0; -} diff --git a/src/types/MouseEvent.cpp b/src/types/MouseEvent.cpp deleted file mode 100644 index 4179cf40117..00000000000 --- a/src/types/MouseEvent.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -MouseEvent::~MouseEvent() = default; - -INPUT_RECORD MouseEvent::ToInputRecord() const noexcept -{ - INPUT_RECORD record{ 0 }; - record.EventType = MOUSE_EVENT; - record.Event.MouseEvent.dwMousePosition.X = ::base::saturated_cast(_position.x); - record.Event.MouseEvent.dwMousePosition.Y = ::base::saturated_cast(_position.y); - record.Event.MouseEvent.dwButtonState = _buttonState; - record.Event.MouseEvent.dwControlKeyState = _activeModifierKeys; - record.Event.MouseEvent.dwEventFlags = _eventFlags; - return record; -} - -InputEventType MouseEvent::EventType() const noexcept -{ - return InputEventType::MouseEvent; -} - -void MouseEvent::SetPosition(const til::point position) noexcept -{ - _position = position; -} - -void MouseEvent::SetButtonState(const DWORD buttonState) noexcept -{ - _buttonState = buttonState; -} - -void MouseEvent::SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept -{ - _activeModifierKeys = activeModifierKeys; -} -void MouseEvent::SetEventFlags(const DWORD eventFlags) noexcept -{ - _eventFlags = eventFlags; -} diff --git a/src/types/WindowBufferSizeEvent.cpp b/src/types/WindowBufferSizeEvent.cpp deleted file mode 100644 index 456d452cae8..00000000000 --- a/src/types/WindowBufferSizeEvent.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/IInputEvent.hpp" - -WindowBufferSizeEvent::~WindowBufferSizeEvent() = default; - -INPUT_RECORD WindowBufferSizeEvent::ToInputRecord() const noexcept -{ - INPUT_RECORD record{ 0 }; - record.EventType = WINDOW_BUFFER_SIZE_EVENT; - record.Event.WindowBufferSizeEvent.dwSize.X = ::base::saturated_cast(_size.width); - record.Event.WindowBufferSizeEvent.dwSize.Y = ::base::saturated_cast(_size.height); - return record; -} - -InputEventType WindowBufferSizeEvent::EventType() const noexcept -{ - return InputEventType::WindowBufferSizeEvent; -} - -void WindowBufferSizeEvent::SetSize(const til::size size) noexcept -{ - _size = size; -} diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp index 8e5dfb0afd7..4641c17332d 100644 --- a/src/types/inc/IInputEvent.hpp +++ b/src/types/inc/IInputEvent.hpp @@ -1,553 +1,89 @@ -/*++ -Copyright (c) Microsoft Corporation - -Module Name: -- IInputEvent.hpp - -Abstract: -- Internal representation of public INPUT_RECORD struct. - -Author: -- Austin Diviness (AustDi) 18-Aug-2017 ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once -#include -#include - -#ifndef ALTNUMPAD_BIT -// from winconp.h -#define ALTNUMPAD_BIT 0x04000000 // AltNumpad OEM char (copied from ntuser/inc/kbd.h) -#endif - -#include - -#include -#include -#include -#include - -enum class InputEventType -{ - KeyEvent, - MouseEvent, - WindowBufferSizeEvent, - MenuEvent, - FocusEvent -}; - -class IInputEvent -{ -public: - static std::unique_ptr Create(const INPUT_RECORD& record); - static std::deque> Create(std::span records); - static std::deque> Create(const std::deque& records); - - static std::vector ToInputRecords(const std::deque>& events); - - virtual ~IInputEvent() = 0; - IInputEvent() = default; - IInputEvent(const IInputEvent&) = default; - IInputEvent(IInputEvent&&) = default; - IInputEvent& operator=(const IInputEvent&) & = default; - IInputEvent& operator=(IInputEvent&&) & = default; - - virtual INPUT_RECORD ToInputRecord() const noexcept = 0; - - virtual InputEventType EventType() const noexcept = 0; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const IInputEvent* const pEvent); -#endif -}; - -inline IInputEvent::~IInputEvent() = default; - -using InputEventQueue = std::deque>; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const IInputEvent* pEvent); -#endif +#include #define ALT_PRESSED (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED) #define CTRL_PRESSED (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED) #define MOD_PRESSED (SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED) -// Note taken from VkKeyScan docs (https://msdn.microsoft.com/en-us/library/windows/desktop/ms646329(v=vs.85).aspx): -// For keyboard layouts that use the right-hand ALT key as a shift key -// (for example, the French keyboard layout), the shift state is -// represented by the value 6, because the right-hand ALT key is -// converted internally into CTRL+ALT. -struct VkKeyScanModState -{ - static const byte None = 0; - static const byte ShiftPressed = 1; - static const byte CtrlPressed = 2; - static const byte ShiftAndCtrlPressed = ShiftPressed | CtrlPressed; - static const byte AltPressed = 4; - static const byte ShiftAndAltPressed = ShiftPressed | AltPressed; - static const byte CtrlAndAltPressed = CtrlPressed | AltPressed; - static const byte ModPressed = ShiftPressed | CtrlPressed | AltPressed; -}; - -enum class ModifierKeyState -{ - RightAlt, - LeftAlt, - RightCtrl, - LeftCtrl, - Shift, - NumLock, - ScrollLock, - CapsLock, - EnhancedKey, - NlsDbcsChar, - NlsAlphanumeric, - NlsKatakana, - NlsHiragana, - NlsRoman, - NlsImeConversion, - AltNumpad, - NlsImeDisable, - ENUM_COUNT // must be the last element in the enum class -}; +using InputEventQueue = til::small_vector; -std::unordered_set FromVkKeyScan(const short vkKeyScanFlags); -std::unordered_set FromConsoleControlKeyFlags(const DWORD flags); -DWORD ToConsoleControlKeyFlag(const ModifierKeyState modifierKey) noexcept; - -class KeyEvent : public IInputEvent +// The following abstractions exist to hopefully make it easier to migrate +// to a different underlying InputEvent type if we ever choose to do so. +// (As unlikely as that is, given that the main user is the console API which will always use INPUT_RECORD.) +constexpr INPUT_RECORD SynthesizeKeyEvent(bool bKeyDown, uint16_t wRepeatCount, uint16_t wVirtualKeyCode, uint16_t wVirtualScanCode, wchar_t UnicodeChar, uint32_t dwControlKeyState) { -public: - enum class Modifiers : DWORD - { - None = 0, - RightAlt = RIGHT_ALT_PRESSED, - LeftAlt = LEFT_ALT_PRESSED, - RightCtrl = RIGHT_CTRL_PRESSED, - LeftCtrl = LEFT_CTRL_PRESSED, - Shift = SHIFT_PRESSED, - NumLock = NUMLOCK_ON, - ScrollLock = SCROLLLOCK_ON, - CapsLock = CAPSLOCK_ON, - EnhancedKey = ENHANCED_KEY, - DbcsChar = NLS_DBCSCHAR, - Alphanumeric = NLS_ALPHANUMERIC, - Katakana = NLS_KATAKANA, - Hiragana = NLS_HIRAGANA, - Roman = NLS_ROMAN, - ImeConvert = NLS_IME_CONVERSION, - AltNumpad = ALTNUMPAD_BIT, - ImeDisable = NLS_IME_DISABLE - }; - - constexpr KeyEvent(const KEY_EVENT_RECORD& record) : - _keyDown{ !!record.bKeyDown }, - _repeatCount{ record.wRepeatCount }, - _virtualKeyCode{ record.wVirtualKeyCode }, - _virtualScanCode{ record.wVirtualScanCode }, - _charData{ record.uChar.UnicodeChar }, - _activeModifierKeys{ record.dwControlKeyState } - { - } - - constexpr KeyEvent(const bool keyDown, - const WORD repeatCount, - const WORD virtualKeyCode, - const WORD virtualScanCode, - const wchar_t charData, - const DWORD activeModifierKeys) : - _keyDown{ keyDown }, - _repeatCount{ repeatCount }, - _virtualKeyCode{ virtualKeyCode }, - _virtualScanCode{ virtualScanCode }, - _charData{ charData }, - _activeModifierKeys{ activeModifierKeys } - { - } - - constexpr KeyEvent() noexcept : - _keyDown{ 0 }, - _repeatCount{ 0 }, - _virtualKeyCode{ 0 }, - _virtualScanCode{ 0 }, - _charData{ 0 }, - _activeModifierKeys{ 0 } - { - } - - static std::pair MakePair( - const WORD repeatCount, - const WORD virtualKeyCode, - const WORD virtualScanCode, - const wchar_t charData, - const DWORD activeModifierKeys) - { - return std::make_pair( - { true, - repeatCount, - virtualKeyCode, - virtualScanCode, - charData, - activeModifierKeys }, - { false, - repeatCount, - virtualKeyCode, - virtualScanCode, - charData, - activeModifierKeys }); - } - - ~KeyEvent(); - KeyEvent(const KeyEvent&) = default; - KeyEvent(KeyEvent&&) = default; -// For these two operators, there seems to be a bug in the compiler: -// See https://stackoverflow.com/a/60206505/1481137 -// > C.128 applies only to virtual member functions, but operator= is not -// > virtual in your base class and neither does it have the same signature as -// > in the derived class, so there is no reason for it to apply. -#pragma warning(push) -#pragma warning(disable : 26456) - KeyEvent& operator=(const KeyEvent&) & = default; - KeyEvent& operator=(KeyEvent&&) & = default; -#pragma warning(pop) - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr bool IsShiftPressed() const noexcept - { - return WI_IsFlagSet(GetActiveModifierKeys(), SHIFT_PRESSED); - } - - constexpr bool IsAltPressed() const noexcept - { - return WI_IsAnyFlagSet(GetActiveModifierKeys(), ALT_PRESSED); - } - - constexpr bool IsCtrlPressed() const noexcept - { - return WI_IsAnyFlagSet(GetActiveModifierKeys(), CTRL_PRESSED); - } - - constexpr bool IsAltGrPressed() const noexcept - { - return WI_IsFlagSet(GetActiveModifierKeys(), LEFT_CTRL_PRESSED) && - WI_IsFlagSet(GetActiveModifierKeys(), RIGHT_ALT_PRESSED); - } - - constexpr bool IsModifierPressed() const noexcept - { - return WI_IsAnyFlagSet(GetActiveModifierKeys(), MOD_PRESSED); - } - - constexpr bool IsCursorKey() const noexcept - { - // true iff vk in [End, Home, Left, Up, Right, Down] - return (_virtualKeyCode >= VK_END) && (_virtualKeyCode <= VK_DOWN); - } - - constexpr bool IsAltNumpadSet() const noexcept - { - return WI_IsFlagSet(GetActiveModifierKeys(), ALTNUMPAD_BIT); - } - - constexpr bool IsKeyDown() const noexcept - { - return _keyDown; - } - - constexpr bool IsPauseKey() const noexcept - { - return (_virtualKeyCode == VK_PAUSE); - } - - constexpr WORD GetRepeatCount() const noexcept - { - return _repeatCount; - } - - constexpr WORD GetVirtualKeyCode() const noexcept - { - return _virtualKeyCode; - } - - constexpr WORD GetVirtualScanCode() const noexcept - { - return _virtualScanCode; - } - - constexpr wchar_t GetCharData() const noexcept - { - return _charData; - } - - constexpr DWORD GetActiveModifierKeys() const noexcept - { - return static_cast(_activeModifierKeys); - } - - void SetKeyDown(const bool keyDown) noexcept; - void SetRepeatCount(const WORD repeatCount) noexcept; - void SetVirtualKeyCode(const WORD virtualKeyCode) noexcept; - void SetVirtualScanCode(const WORD virtualScanCode) noexcept; - void SetCharData(const char character) noexcept; - void SetCharData(const wchar_t character) noexcept; - - void SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept; - void DeactivateModifierKey(const ModifierKeyState modifierKey) noexcept; - void ActivateModifierKey(const ModifierKeyState modifierKey) noexcept; - bool DoActiveModifierKeysMatch(const std::unordered_set& consoleModifiers) const noexcept; - bool IsCommandLineEditingKey() const noexcept; - bool IsPopupKey() const noexcept; - - // Function Description: - // - Returns true if the given VKey represents a modifier key - shift, alt, - // control or the Win key. - // Arguments: - // - vkey: the VKEY to check - // Return Value: - // - true iff the key is a modifier key. - constexpr static bool IsModifierKey(const WORD vkey) - { - return (vkey == VK_CONTROL) || - (vkey == VK_LCONTROL) || - (vkey == VK_RCONTROL) || - (vkey == VK_MENU) || - (vkey == VK_LMENU) || - (vkey == VK_RMENU) || - (vkey == VK_SHIFT) || - (vkey == VK_LSHIFT) || - (vkey == VK_RSHIFT) || - // There is no VK_WIN - (vkey == VK_LWIN) || - (vkey == VK_RWIN); + return INPUT_RECORD{ + .EventType = KEY_EVENT, + .Event = { + .KeyEvent = { + .bKeyDown = bKeyDown, + .wRepeatCount = wRepeatCount, + .wVirtualKeyCode = wVirtualKeyCode, + .wVirtualScanCode = wVirtualScanCode, + .uChar = { .UnicodeChar = UnicodeChar }, + .dwControlKeyState = dwControlKeyState, + }, + }, }; - -private: - bool _keyDown; - WORD _repeatCount; - WORD _virtualKeyCode; - WORD _virtualScanCode; - wchar_t _charData; - Modifiers _activeModifierKeys; - - friend constexpr bool operator==(const KeyEvent& a, const KeyEvent& b) noexcept; -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent); -#endif -}; - -constexpr bool operator==(const KeyEvent& a, const KeyEvent& b) noexcept -{ - return (a._keyDown == b._keyDown && - a._repeatCount == b._repeatCount && - a._virtualKeyCode == b._virtualKeyCode && - a._virtualScanCode == b._virtualScanCode && - a._charData == b._charData && - a._activeModifierKeys == b._activeModifierKeys); } -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent); -#endif - -class MouseEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeMouseEvent(til::point dwMousePosition, uint32_t dwButtonState, uint32_t dwControlKeyState, uint32_t dwEventFlags) { -public: - constexpr MouseEvent(const MOUSE_EVENT_RECORD& record) : - _position{ til::wrap_coord(record.dwMousePosition) }, - _buttonState{ record.dwButtonState }, - _activeModifierKeys{ record.dwControlKeyState }, - _eventFlags{ record.dwEventFlags } - { - } - - constexpr MouseEvent(const til::point position, - const DWORD buttonState, - const DWORD activeModifierKeys, - const DWORD eventFlags) : - _position{ position }, - _buttonState{ buttonState }, - _activeModifierKeys{ activeModifierKeys }, - _eventFlags{ eventFlags } - { - } - - ~MouseEvent(); - MouseEvent(const MouseEvent&) = default; - MouseEvent(MouseEvent&&) = default; - MouseEvent& operator=(const MouseEvent&) & = default; - MouseEvent& operator=(MouseEvent&&) & = default; - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr bool IsMouseMoveEvent() const noexcept - { - return _eventFlags == MOUSE_MOVED; - } - - constexpr til::point GetPosition() const noexcept - { - return _position; - } - - constexpr DWORD GetButtonState() const noexcept - { - return _buttonState; - } - - constexpr DWORD GetActiveModifierKeys() const noexcept - { - return _activeModifierKeys; - } - - constexpr DWORD GetEventFlags() const noexcept - { - return _eventFlags; - } - - void SetPosition(const til::point position) noexcept; - void SetButtonState(const DWORD buttonState) noexcept; - void SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept; - void SetEventFlags(const DWORD eventFlags) noexcept; - -private: - til::point _position; - DWORD _buttonState; - DWORD _activeModifierKeys; - DWORD _eventFlags; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent); -#endif -}; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent); -#endif + return INPUT_RECORD{ + .EventType = MOUSE_EVENT, + .Event = { + .MouseEvent = { + .dwMousePosition = { + ::base::saturated_cast(dwMousePosition.x), + ::base::saturated_cast(dwMousePosition.y), + }, + .dwButtonState = dwButtonState, + .dwControlKeyState = dwControlKeyState, + .dwEventFlags = dwEventFlags, + }, + }, + }; +} -class WindowBufferSizeEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeWindowBufferSizeEvent(til::size dwSize) { -public: - constexpr WindowBufferSizeEvent(const WINDOW_BUFFER_SIZE_RECORD& record) : - _size{ til::wrap_coord_size(record.dwSize) } - { - } - - constexpr WindowBufferSizeEvent(const til::size size) : - _size{ size } - { - } - - ~WindowBufferSizeEvent(); - WindowBufferSizeEvent(const WindowBufferSizeEvent&) = default; - WindowBufferSizeEvent(WindowBufferSizeEvent&&) = default; - WindowBufferSizeEvent& operator=(const WindowBufferSizeEvent&) & = default; - WindowBufferSizeEvent& operator=(WindowBufferSizeEvent&&) & = default; - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr til::size GetSize() const noexcept - { - return _size; - } - - void SetSize(const til::size size) noexcept; - -private: - til::size _size; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent); -#endif -}; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent); -#endif + return INPUT_RECORD{ + .EventType = WINDOW_BUFFER_SIZE_EVENT, + .Event = { + .WindowBufferSizeEvent = { + .dwSize = { + ::base::saturated_cast(dwSize.width), + ::base::saturated_cast(dwSize.height), + }, + }, + }, + }; +} -class MenuEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeMenuEvent(uint32_t dwCommandId) { -public: - constexpr MenuEvent(const MENU_EVENT_RECORD& record) : - _commandId{ record.dwCommandId } - { - } - - constexpr MenuEvent(const UINT commandId) : - _commandId{ commandId } - { - } - - ~MenuEvent(); - MenuEvent(const MenuEvent&) = default; - MenuEvent(MenuEvent&&) = default; - MenuEvent& operator=(const MenuEvent&) & = default; - MenuEvent& operator=(MenuEvent&&) & = default; - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr UINT GetCommandId() const noexcept - { - return _commandId; - } - - void SetCommandId(const UINT commandId) noexcept; - -private: - UINT _commandId; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); -#endif -}; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); -#endif + return INPUT_RECORD{ + .EventType = MENU_EVENT, + .Event = { + .MenuEvent = { + .dwCommandId = dwCommandId, + }, + }, + }; +} -class FocusEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeFocusEvent(bool bSetFocus) { -public: - constexpr FocusEvent(const FOCUS_EVENT_RECORD& record) : - _focus{ !!record.bSetFocus } - { - } - - constexpr FocusEvent(const bool focus) : - _focus{ focus } - { - } - - ~FocusEvent(); - FocusEvent(const FocusEvent&) = default; - FocusEvent(FocusEvent&&) = default; - FocusEvent& operator=(const FocusEvent&) & = default; - FocusEvent& operator=(FocusEvent&&) & = default; - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr bool GetFocus() const noexcept - { - return _focus; - } - - void SetFocus(const bool focus) noexcept; - -private: - bool _focus; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); -#endif -}; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); -#endif + return INPUT_RECORD{ + .EventType = FOCUS_EVENT, + .Event = { + .FocusEvent = { + .bSetFocus = bSetFocus, + }, + }, + }; +} diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj index bceb8348e8c..4da14e915bf 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -16,12 +16,6 @@ - - - - - - @@ -30,7 +24,6 @@ - Create @@ -66,4 +59,4 @@ - + \ No newline at end of file diff --git a/src/types/lib/types.vcxproj.filters b/src/types/lib/types.vcxproj.filters index ce73211829c..efb65b2bc42 100644 --- a/src/types/lib/types.vcxproj.filters +++ b/src/types/lib/types.vcxproj.filters @@ -24,30 +24,9 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files - - Source Files - Source Files @@ -69,9 +48,6 @@ Source Files - - Source Files - Source Files @@ -116,24 +92,6 @@ Header Files - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - Header Files @@ -152,9 +110,6 @@ Header Files - - Header Files - Header Files diff --git a/src/types/sources.inc b/src/types/sources.inc index 7df45eae88b..6527fbed578 100644 --- a/src/types/sources.inc +++ b/src/types/sources.inc @@ -30,15 +30,9 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\CodepointWidthDetector.cpp \ ..\ColorFix.cpp \ - ..\IInputEvent.cpp \ - ..\FocusEvent.cpp \ ..\GlyphWidth.cpp \ - ..\KeyEvent.cpp \ - ..\MenuEvent.cpp \ ..\ModifierKeyState.cpp \ - ..\MouseEvent.cpp \ ..\Viewport.cpp \ - ..\WindowBufferSizeEvent.cpp \ ..\convert.cpp \ ..\colorTable.cpp \ ..\utils.cpp \ diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index c7e3342ba0d..fa1975e3e48 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -45,9 +45,28 @@ - - {{↓ wch:{_charData} mod:{_activeModifierKeys} repeat:{_repeatCount} vk:{_virtualKeyCode} vsc:{_virtualScanCode}} - {{↑ wch:{_charData} mod:{_activeModifierKeys} repeat:{_repeatCount} vk:{_virtualKeyCode} vsc:{_virtualScanCode}} + + ↓ rep:{wRepeatCount} vk:{wVirtualKeyCode} sc:{wVirtualScanCode} ch:{uChar.UnicodeChar} ctrl:{(unsigned short)dwControlKeyState,x} + ↑ rep:{wRepeatCount} vk:{wVirtualKeyCode} sc:{wVirtualScanCode} ch:{uChar.UnicodeChar} ctrl:{(unsigned short)dwControlKeyState,x} + + + pos:{dwMousePosition.X},{dwMousePosition.Y} state:{dwButtonState,x} ctrl:{(unsigned short)dwControlKeyState,x} flags:{(unsigned short)dwEventFlags,x} + + + size:{dwSize.X},{dwSize.Y} + + + id:{dwCommandId} + + + focus:{bSetFocus} + + + Key {Event.KeyEvent} + Mouse {Event.MouseEvent} + WindowBufferSize {Event.WindowBufferSizeEvent} + Menu {Event.MenuEvent} + Focus {Event.FocusEvent} From d943fa41f6b8a8572d04fab64501a03dcbd96929 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 11 Aug 2023 09:57:40 -0500 Subject: [PATCH 16/59] Add walkthroughs to CONTRIBUTING.md (#15799) Change the "good first issues" text to talk about walkthroughs instead. --- .github/actions/spelling/allow/allow.txt | 2 ++ CONTRIBUTING.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 13e2e78ed3b..e757c327fe2 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -111,6 +111,8 @@ und unregister versioned vsdevcmd +walkthrough +walkthroughs We'd wildcards XBox diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2673e6e8936..c7f3b1a710f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,7 @@ If you don't have any additional info/context to add but would like to indicate If you're able & willing to help fix issues and/or implement features, we'd love your contribution! -The best place to start is the list of ["good first issue"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22++label%3A%22good+first+issue%22+)s. These are bugs or tasks that we on the team believe would be easier to implement for someone without any prior experience in the codebase. Once you're feeling more comfortable in the codebase, feel free to just use the ["Help Wanted"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22+) label, or just find an issue you're interested in and hop in! +The best place to start is the list of ["walkthroughs"](https://aka.ms/terminal-walkthroughs). This is a collection of issues where we've written a "walkthrough", little guides to help get started with a particular issue. These are usually good first issues, and are a great way to get familiar with the codebase. Additionally, the list of ["good first issue"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22++label%3A%22good+first+issue%22+)s is another set of issues that might be easier for first-time contributors. Once you're feeling more comfortable in the codebase, feel free to just use the ["Help Wanted"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22+) label, or just find any issue you're interested in and hop in! Generally, we categorize issues in the following way, which is largely derived from our old internal work tracking system: * ["Bugs"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Issue-Bug%22+) are parts of the Terminal & Console that are not quite working the right way. There's code to already support some scenario, but it's not quite working right. Fixing these is generally a matter of debugging the broken functionality and fixing the wrong code. From 69eff7e9fdf07afc9f6c29788466cb1e12cc5938 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 11 Aug 2023 14:06:30 -0500 Subject: [PATCH 17/59] Rewrite the entire Azure DevOps build system (#15808) This pull request rewrites the entire Azure DevOps build system. The guiding principles behind this rewrite are: - No pipeline definitions should contain steps (or tasks) directly. - All jobs should be in template files. - Any set of steps that is reused across multiple jobs must be in template files. - All artifact names can be customized (via a property called `artifactStem` on all templates that produce or consume artifacts). - No compilation happens outside of the "Build" phase, to consolidate the production and indexing of PDBs. - **Building the project produces a `bin` directory.** That `bin` directory is therefore the primary currency of the build. Jobs will either produce or consume `bin` if they want to do anything with the build outputs. - All step and job templates are named with `step` or `job` _first_, which disambiguates them in the templates directory. - Most jobs can be run on different `pool`s, so that we can put expensive jobs on expensive build agents and cheap jobs on cheap build agents. Some jobs handle pool selection on their own, however. Our original build pipelines used the `VSBuild` task _all over the place._ This resulted in Terminal being built in myriad ways, different for every pipeline. There was an attempt at standardization early on, where `ci.yml` consumed jobs and steps templates... but when `release.yml` was added, all of that went out the window. The new pipelines are consistent and focus on a small, well-defined set of jobs: - `job-build-project` - This is the big one! - Takes a list of build configurations and platforms. - Produces an artifact named `build-PLATFORM-CONFIG` for the entire matrix of possibilities. - Optionally signs the output and produces a bill of materials. - Admittedly has a lot going on. - `job-build-package-wpf` - Takes a list of build configurations and platforms. - Consumes the `build-` artifact for every config/platform possibility, plus one for "Any CPU" (hardcoded; this is where the .NET code builds) - Produces one `wpf-nupkg-CONFIG` for each configuration, merging all platforms. - Optionally signs the output and produces a bill of materials. - `job-merge-msix-into-bundle` - Takes a list of build configurations and platforms. - Consumes the `build-` artifact for every config/platform - Produces one `appxbundle-CONFIG` for each configuration, merging all platforms for that config into one `msixbundle`. - Optionally signs the output and produces a bill of materials. - `job-package-conpty` - Takes a list of build configurations and platforms. - Consumes the `build-` artifact for every config/platform - Produces one `conpty-nupkg-CONFIG` for each configuration, merging all platforms. - Optionally signs the output and produces a bill of materials. - `job-test-project` - Takes **one** build config and **one** platform. - Consumes `build-PLATFORM-CONFIG` - Selects its own pools (hardcoded) because it knows about architectures and must choose the right agent arch. - Runs tests (directly on the build agent). - `job-run-pgo-tests` - Just like the above, but runs tests where `IsPgo` is `true` - Collects all of the PGO counts and publishes a `pgc-intermediates` artifact for that platform and configuration. - `job-pgo-merge-pgd` - Takes **one** build config and multiple platforms. - Consumes `build-$platform-CONFIG` for each platform. - Consumes `pgc-intermediates-$platform-CONFIG` for each platform. - Merges the `pgc` files into `pgd` files - Produces a new `pgd-` artifact. - `job-pgo-build-nuget-and-publish` - Consumes the `pgd-` artifact from above. - Packs it into a `nupkg` and publishes it. - `job-submit-windows-vpack` - Only expected to run against `Release`. - Consumes the `appxbundle-CONFIG` artifact. - Publishes it to a vpack for Windows to consume. - `job-check-code-format` - Does not use artifacts. Runs `clang-format`. - `job-index-github-codenav` - Does not use artifacts. Fuzz submission is broken due to changes in the `onefuzz` client. I have removed the compliance and security build because it is no longer supported. Finally, this pull request has some additional benefits: - I've expanded the PGO build phase to cover ARM64! - We can remove everything Helix-related except the WTT parser - We no longer depend on Helix submission or Helix pools - The WPF control's inner DLLs are now codesigned (#15404) - Symbols for the WPF control, both .NET and C++, are published alongside all other symbols. - The files we submit to ESRP for signing are batched up into a single step[^1] Closes #11874 Closes #11974 Closes #15404 [^1]: This will have to change if we want to sign the individual per-architecture `.appx` files before bundling so that they can be directly installed. --- build/Helix/AzurePipelinesHelperScripts.ps1 | 175 ---- build/Helix/EnsureMachineState.ps1 | 112 --- build/Helix/GenerateTestProjFile.ps1 | 336 -------- build/Helix/InstallTestAppDependencies.ps1 | 12 - build/Helix/OutputFailedTestQuery.ps1 | 8 - build/Helix/OutputSubResultsJsonFiles.ps1 | 32 - build/Helix/OutputTestResults.ps1 | 131 --- build/Helix/PrepareHelixPayload.ps1 | 62 -- build/Helix/ProcessHelixFiles.ps1 | 130 --- build/Helix/RunTestsInHelix.proj | 20 - build/Helix/UpdateUnreliableTests.ps1 | 136 --- build/Helix/global.json | 5 - build/Helix/packages.config | 9 - build/Helix/readme.md | 32 - build/Helix/runtests.cmd | 105 --- build/config/ESRPSigning_ConPTY.json | 51 -- build/config/ESRPSigning_Terminal.json | 72 -- build/config/esrp.build.batch.conpty.json | 47 + ...srp.build.batch.terminal_constituents.json | 65 ++ build/config/esrp.build.batch.wpf.json | 46 + build/config/esrp.build.batch.wpfdotnet.json | 47 + build/pgo/PGO.DB.proj | 3 +- build/pgo/Terminal.PGO.DB.nuspec | 1 + build/pgo/Terminal.PGO.props | 3 +- build/pipelines/ci.yml | 58 +- build/pipelines/daily-loc-submission.yml | 1 + build/pipelines/feature-flag-ci.yml | 21 +- build/pipelines/fuzz.yml | 80 +- build/pipelines/pgo.yml | 79 +- build/pipelines/release.yml | 804 +++--------------- .../templates-v2/job-build-package-wpf.yml | 134 +++ .../templates-v2/job-build-project.yml | 263 ++++++ .../templates-v2/job-check-code-format.yml | 15 + .../job-index-github-codenav.yml} | 10 +- .../job-merge-msix-into-bundle.yml | 133 +++ .../templates-v2/job-package-conpty.yml | 118 +++ .../job-pgo-build-nuget-and-publish.yml} | 54 +- .../templates-v2/job-pgo-merge-pgd.yml | 75 ++ .../templates-v2/job-publish-symbols.yml | 81 ++ .../templates-v2/job-run-pgo-tests.yml | 83 ++ .../templates-v2/job-submit-windows-vpack.yml | 70 ++ .../job-test-project.yml} | 30 +- .../steps-create-signing-config.yml | 35 + .../steps-download-bin-dir-artifact.yml | 24 + .../steps-ensure-nuget-version.yml | 5 + .../steps-fetch-and-prepare-localizations.yml | 27 + .../steps-restore-nuget.yml} | 6 +- .../templates/build-console-audit-job.yml | 34 - .../pipelines/templates/build-console-ci.yml | 31 - .../build-console-compliance-job.yml | 203 ----- .../templates/build-console-fuzzing.yml | 90 -- .../pipelines/templates/build-console-pgo.yml | 55 -- .../templates/build-console-steps.yml | 137 --- .../pipelines/templates/check-formatting.yml | 17 - .../templates/console-ci-helix-job.yml | 25 - .../templates/helix-createprojfile-steps.yml | 15 - .../helix-processtestresults-job.yml | 71 -- .../templates/helix-runtests-job.yml | 164 ---- .../pipelines/templates/pgo-merge-pgd-job.yml | 70 -- build/scripts/Run-Tests.ps1 | 5 +- .../WpfTerminalControl.csproj | 13 + src/common.nugetversions.props | 2 +- src/common.nugetversions.targets | 7 +- 63 files changed, 1637 insertions(+), 3148 deletions(-) delete mode 100644 build/Helix/AzurePipelinesHelperScripts.ps1 delete mode 100644 build/Helix/EnsureMachineState.ps1 delete mode 100644 build/Helix/GenerateTestProjFile.ps1 delete mode 100644 build/Helix/InstallTestAppDependencies.ps1 delete mode 100644 build/Helix/OutputFailedTestQuery.ps1 delete mode 100644 build/Helix/OutputSubResultsJsonFiles.ps1 delete mode 100644 build/Helix/OutputTestResults.ps1 delete mode 100644 build/Helix/PrepareHelixPayload.ps1 delete mode 100644 build/Helix/ProcessHelixFiles.ps1 delete mode 100644 build/Helix/RunTestsInHelix.proj delete mode 100644 build/Helix/UpdateUnreliableTests.ps1 delete mode 100644 build/Helix/global.json delete mode 100644 build/Helix/packages.config delete mode 100644 build/Helix/readme.md delete mode 100644 build/Helix/runtests.cmd delete mode 100644 build/config/ESRPSigning_ConPTY.json delete mode 100644 build/config/ESRPSigning_Terminal.json create mode 100644 build/config/esrp.build.batch.conpty.json create mode 100644 build/config/esrp.build.batch.terminal_constituents.json create mode 100644 build/config/esrp.build.batch.wpf.json create mode 100644 build/config/esrp.build.batch.wpfdotnet.json create mode 100644 build/pipelines/templates-v2/job-build-package-wpf.yml create mode 100644 build/pipelines/templates-v2/job-build-project.yml create mode 100644 build/pipelines/templates-v2/job-check-code-format.yml rename build/pipelines/{templates/codenav-indexer.yml => templates-v2/job-index-github-codenav.yml} (57%) create mode 100644 build/pipelines/templates-v2/job-merge-msix-into-bundle.yml create mode 100644 build/pipelines/templates-v2/job-package-conpty.yml rename build/pipelines/{templates/pgo-build-and-publish-nuget-job.yml => templates-v2/job-pgo-build-nuget-and-publish.yml} (57%) create mode 100644 build/pipelines/templates-v2/job-pgo-merge-pgd.yml create mode 100644 build/pipelines/templates-v2/job-publish-symbols.yml create mode 100644 build/pipelines/templates-v2/job-run-pgo-tests.yml create mode 100644 build/pipelines/templates-v2/job-submit-windows-vpack.yml rename build/pipelines/{templates/test-console-ci.yml => templates-v2/job-test-project.yml} (72%) create mode 100644 build/pipelines/templates-v2/steps-create-signing-config.yml create mode 100644 build/pipelines/templates-v2/steps-download-bin-dir-artifact.yml create mode 100644 build/pipelines/templates-v2/steps-ensure-nuget-version.yml create mode 100644 build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml rename build/pipelines/{templates/restore-nuget-steps.yml => templates-v2/steps-restore-nuget.yml} (88%) delete mode 100644 build/pipelines/templates/build-console-audit-job.yml delete mode 100644 build/pipelines/templates/build-console-ci.yml delete mode 100644 build/pipelines/templates/build-console-compliance-job.yml delete mode 100644 build/pipelines/templates/build-console-fuzzing.yml delete mode 100644 build/pipelines/templates/build-console-pgo.yml delete mode 100644 build/pipelines/templates/build-console-steps.yml delete mode 100644 build/pipelines/templates/check-formatting.yml delete mode 100644 build/pipelines/templates/console-ci-helix-job.yml delete mode 100644 build/pipelines/templates/helix-createprojfile-steps.yml delete mode 100644 build/pipelines/templates/helix-processtestresults-job.yml delete mode 100644 build/pipelines/templates/helix-runtests-job.yml delete mode 100644 build/pipelines/templates/pgo-merge-pgd-job.yml diff --git a/build/Helix/AzurePipelinesHelperScripts.ps1 b/build/Helix/AzurePipelinesHelperScripts.ps1 deleted file mode 100644 index 8934a8548d1..00000000000 --- a/build/Helix/AzurePipelinesHelperScripts.ps1 +++ /dev/null @@ -1,175 +0,0 @@ -function GetAzureDevOpsBaseUri -{ - Param( - [string]$CollectionUri, - [string]$TeamProject - ) - - return $CollectionUri + $TeamProject -} - -function GetQueryTestRunsUri -{ - Param( - [string]$CollectionUri, - [string]$TeamProject, - [string]$BuildUri, - [switch]$IncludeRunDetails - ) - - if ($IncludeRunDetails) - { - $includeRunDetailsParameter = "&includeRunDetails=true" - } - else - { - $includeRunDetailsParameter = "" - } - - $baseUri = GetAzureDevOpsBaseUri -CollectionUri $CollectionUri -TeamProject $TeamProject - $queryUri = "$baseUri/_apis/test/runs?buildUri=$BuildUri$includeRunDetailsParameter&api-version=5.0" - return $queryUri -} - -function Get-HelixJobTypeFromTestRun -{ - Param ($testRun) - - $testRunSingleResultUri = "$($testRun.url)/results?`$top=1&`$skip=0&api-version=5.1" - $singleTestResult = Invoke-RestMethod -Uri $testRunSingleResultUri -Method Get -Headers $azureDevOpsRestApiHeaders - $count = $singleTestResult.value.Length - if($count -eq 0) - { - # If the count is 0, then results have not yet been reported for this run. - # We only care about completed runs with results, so it is ok to just return 'UNKNOWN' for this run. - return "UNKNOWN" - } - else - { - $info = ConvertFrom-Json $singleTestResult.value.comment - $helixJobId = $info.HelixJobId - $job = Invoke-RestMethodWithRetries "https://helix.dot.net/api/2019-06-17/jobs/${helixJobId}?access_token=${HelixAccessToken}" - return $job.Type - } -} - -function Append-HelixAccessTokenToUrl -{ - Param ([string]$url, [string]$token) - if($url.Contains("?")) - { - $url = "$($url)&access_token=$($token)" - } - else - { - $url = "$($url)?access_token=$($token)" - } - return $url -} - - -# The Helix Rest api is sometimes unreliable. So we call these apis with retry logic. -# Note: The Azure DevOps apis are stable and do not need to be called with this retry logic. -$helixApiRetries = 0 -$helixApiRetriesMax = 10 - -function Download-StringWithRetries -{ - Param ([string]$fileName, [string]$url) - - $result = "" - $done = $false - - while(!($done)) - { - try - { - Write-Host "Downloading $fileName" - $result = (New-Object System.Net.WebClient).DownloadString($url) - $done = $true - } - catch - { - Write-Host "Failed to download $fileName $($PSItem.Exception)" - - $helixApiRetries = $helixApiRetries + 1 - if($helixApiRetries -lt $helixApiRetriesMax) - { - Write-Host "Sleep and retry download of $fileName" - Start-Sleep 60 - } - else - { - throw "Failed to download $fileName" - } - } - } - - return $result -} - -function Invoke-RestMethodWithRetries -{ - Param ([string]$url,$Headers) - - $result = @() - $done = $false - - while(!($done)) - { - try - { - $result = Invoke-RestMethod -Uri $url -Method Get -Headers $Headers - $done = $true - } - catch - { - Write-Host "Failed to invoke Rest method $($PSItem.Exception)" - - $helixApiRetries = $helixApiRetries + 1 - if($helixApiRetries -lt $helixApiRetriesMax) - { - Write-Host "Sleep and retry invoke" - Start-Sleep 60 - } - else - { - throw "Failed to invoke Rest method" - } - } - } - - return $result -} - -function Download-FileWithRetries -{ - Param ([string]$fileurl, [string]$destination) - - $done = $false - - while(!($done)) - { - try - { - Write-Host "Downloading $destination" - $webClient.DownloadFile($fileurl, $destination) - $done = $true - } - catch - { - Write-Host "Failed to download $destination $($PSItem.Exception)" - - $helixApiRetries = $helixApiRetries + 1 - if($helixApiRetries -lt $helixApiRetriesMax) - { - Write-Host "Sleep and retry download of $destination" - Start-Sleep 60 - } - else - { - throw "Failed to download $destination" - } - } - } -} \ No newline at end of file diff --git a/build/Helix/EnsureMachineState.ps1 b/build/Helix/EnsureMachineState.ps1 deleted file mode 100644 index 6f26da63a56..00000000000 --- a/build/Helix/EnsureMachineState.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -$scriptDirectory = $script:MyInvocation.MyCommand.Path | Split-Path -Parent - -# List all processes to aid debugging: -Write-Host "All processes running:" -Get-Process - -tasklist /svc - -# Add this test directory as an exclusion for Windows Defender -Write-Host "Add $scriptDirectory as Exclusion Path" -Add-MpPreference -ExclusionPath $scriptDirectory -Write-Host "Add $($env:HELIX_CORRELATION_PAYLOAD) as Exclusion Path" -Add-MpPreference -ExclusionPath $env:HELIX_CORRELATION_PAYLOAD -Get-MpPreference -Get-MpComputerStatus - - -# Minimize all windows: -$shell = New-Object -ComObject "Shell.Application" -$shell.minimizeall() - -# Kill any instances of Windows Security Alert: -$windowTitleToMatch = "*Windows Security Alert*" -$procs = Get-Process | Where {$_.MainWindowTitle -like "*Windows Security Alert*"} -foreach ($proc in $procs) -{ - Write-Host "Found process with '$windowTitleToMatch' title: $proc" - $proc.Kill(); -} - -# Kill processes by name that are known to interfere with our tests: -$processNamesToStop = @("Microsoft.Photos", "WinStore.App", "SkypeApp", "SkypeBackgroundHost", "OneDriveSetup", "OneDrive") -foreach($procName in $processNamesToStop) -{ - Write-Host "Attempting to kill $procName if it is running" - Stop-Process -ProcessName $procName -Verbose -ErrorAction Ignore -} -Write-Host "All processes running after attempting to kill unwanted processes:" -Get-Process - -tasklist /svc - -$platform = $env:testbuildplatform -if(!$platform) -{ - $platform = "x86" -} - -function UninstallApps { - Param([string[]]$appsToUninstall) - - foreach($pkgName in $appsToUninstall) - { - foreach($pkg in (Get-AppxPackage $pkgName).PackageFullName) - { - Write-Output "Removing: $pkg" - Remove-AppxPackage $pkg - } - } -} - -function UninstallTestApps { - Param([string[]]$appsToUninstall) - - foreach($pkgName in $appsToUninstall) - { - foreach($pkg in (Get-AppxPackage $pkgName).PackageFullName) - { - Write-Output "Removing: $pkg" - Remove-AppxPackage $pkg - } - - # Sometimes an app can get into a state where it is no longer returned by Get-AppxPackage, but it is still present - # which prevents other versions of the app from being installed. - # To handle this, we can directly call Remove-AppxPackage against the full name of the package. However, without - # Get-AppxPackage to find the PackageFullName, we just have to manually construct the name. - $packageFullName = "$($pkgName)_1.0.0.0_$($platform)__8wekyb3d8bbwe" - Write-Host "Removing $packageFullName if installed" - Remove-AppPackage $packageFullName -ErrorVariable appxerror -ErrorAction SilentlyContinue - if($appxerror) - { - foreach($error in $appxerror) - { - # In most cases, Remove-AppPackage will fail due to the package not being found. Don't treat this as an error. - if(!($error.Exception.Message -match "0x80073CF1")) - { - Write-Error $error - } - } - } - else - { - Write-Host "Successfully removed $packageFullName" - } - } -} - -Write-Host "Uninstall AppX packages that are known to cause issues with our tests" -UninstallApps("*Skype*", "*Windows.Photos*") - -Write-Host "Uninstall any of our test apps that may have been left over from previous test runs" -UninstallTestApps("NugetPackageTestApp", "NugetPackageTestAppCX", "IXMPTestApp", "MUXControlsTestApp") - -Write-Host "Uninstall MUX Framework package that may have been left over from previous test runs" -# We don't want to uninstall all versions of the MUX Framework package, as there may be other apps preinstalled on the system -# that depend on it. We only uninstall the Framework package that corresponds to the version of MUX that we are testing. -[xml]$versionData = (Get-Content "version.props") -$versionMajor = $versionData.GetElementsByTagName("MUXVersionMajor").'#text' -$versionMinor = $versionData.GetElementsByTagName("MUXVersionMinor").'#text' -UninstallApps("Microsoft.UI.Xaml.$versionMajor.$versionMinor") - -Get-Process \ No newline at end of file diff --git a/build/Helix/GenerateTestProjFile.ps1 b/build/Helix/GenerateTestProjFile.ps1 deleted file mode 100644 index 07a3550a79a..00000000000 --- a/build/Helix/GenerateTestProjFile.ps1 +++ /dev/null @@ -1,336 +0,0 @@ -[CmdLetBinding()] -Param( - [Parameter(Mandatory = $true)] - [string]$TestFile, - - [Parameter(Mandatory = $true)] - [string]$OutputProjFile, - - [Parameter(Mandatory = $true)] - [string]$JobTestSuiteName, - - [Parameter(Mandatory = $true)] - [string]$TaefPath, - - [string]$TaefQuery -) - -Class TestCollection -{ - [string]$Name - [string]$SetupMethodName - [string]$TeardownMethodName - [System.Collections.Generic.Dictionary[string, string]]$Properties - - TestCollection() - { - if ($this.GetType() -eq [TestCollection]) - { - throw "This class should never be instantiated directly; it should only be derived from." - } - } - - TestCollection([string]$name) - { - $this.Init($name) - } - - hidden Init([string]$name) - { - $this.Name = $name - $this.Properties = @{} - } -} - -Class Test : TestCollection -{ - Test([string]$name) - { - $this.Init($name) - } -} - -Class TestClass : TestCollection -{ - [System.Collections.Generic.List[Test]]$Tests - - TestClass([string]$name) - { - $this.Init($name) - $this.Tests = @{} - } -} - -Class TestModule : TestCollection -{ - [System.Collections.Generic.List[TestClass]]$TestClasses - - TestModule([string]$name) - { - $this.Init($name) - $this.TestClasses = @{} - } -} - -function Parse-TestInfo([string]$taefOutput) -{ - enum LineType - { - None - TestModule - TestClass - Test - Setup - Teardown - Property - } - - [string]$testModuleIndentation = " " - [string]$testClassIndentation = " " - [string]$testIndentation = " " - [string]$setupBeginning = "Setup: " - [string]$teardownBeginning = "Teardown: " - [string]$propertyBeginning = "Property[" - - function Get-LineType([string]$line) - { - if ($line.Contains($setupBeginning)) - { - return [LineType]::Setup; - } - elseif ($line.Contains($teardownBeginning)) - { - return [LineType]::Teardown; - } - elseif ($line.Contains($propertyBeginning)) - { - return [LineType]::Property; - } - elseif ($line.StartsWith($testModuleIndentation) -and -not $line.StartsWith("$testModuleIndentation ")) - { - return [LineType]::TestModule; - } - elseif ($line.StartsWith($testClassIndentation) -and -not $line.StartsWith("$testClassIndentation ")) - { - return [LineType]::TestClass; - } - elseif ($line.StartsWith($testIndentation) -and -not $line.StartsWith("$testIndentation ")) - { - return [LineType]::Test; - } - else - { - return [LineType]::None; - } - } - - [string[]]$lines = $taefOutput.Split(@([Environment]::NewLine, "`n"), [StringSplitOptions]::RemoveEmptyEntries) - [System.Collections.Generic.List[TestModule]]$testModules = @() - - [TestModule]$currentTestModule = $null - [TestClass]$currentTestClass = $null - [Test]$currentTest = $null - - [TestCollection]$lastTestCollection = $null - - foreach ($rawLine in $lines) - { - [LineType]$lineType = (Get-LineType $rawLine) - - # We don't need the whitespace around the line anymore, so we'll discard it to make things easier. - [string]$line = $rawLine.Trim() - - if ($lineType -eq [LineType]::TestModule) - { - if ($currentTest -ne $null -and $currentTestClass -ne $null) - { - $currentTestClass.Tests.Add($currentTest) - } - - if ($currentTestClass -ne $null -and $currentTestModule -ne $null) - { - $currentTestModule.TestClasses.Add($currentTestClass) - } - - if ($currentTestModule -ne $null) - { - $testModules.Add($currentTestModule) - } - - $currentTestModule = [TestModule]::new($line) - $currentTestClass = $null - $currentTest = $null - $lastTestCollection = $currentTestModule - } - elseif ($lineType -eq [LineType]::TestClass) - { - if ($currentTest -ne $null -and $currentTestClass -ne $null) - { - $currentTestClass.Tests.Add($currentTest) - } - - if ($currentTestClass -ne $null -and $currentTestModule -ne $null) - { - $currentTestModule.TestClasses.Add($currentTestClass) - } - - $currentTestClass = [TestClass]::new($line) - $currentTest = $null - $lastTestCollection = $currentTestClass - } - elseif ($lineType -eq [LineType]::Test) - { - if ($currentTest -ne $null -and $currentTestClass -ne $null) - { - $currentTestClass.Tests.Add($currentTest) - } - - $currentTest = [Test]::new($line) - $lastTestCollection = $currentTest - } - elseif ($lineType -eq [LineType]::Setup) - { - if ($lastTestCollection -ne $null) - { - $lastTestCollection.SetupMethodName = $line.Replace($setupBeginning, "") - } - } - elseif ($lineType -eq [LineType]::Teardown) - { - if ($lastTestCollection -ne $null) - { - $lastTestCollection.TeardownMethodName = $line.Replace($teardownBeginning, "") - } - } - elseif ($lineType -eq [LineType]::Property) - { - if ($lastTestCollection -ne $null) - { - foreach ($match in [Regex]::Matches($line, "Property\[(.*)\]\s+=\s+(.*)")) - { - [string]$propertyKey = $match.Groups[1].Value; - [string]$propertyValue = $match.Groups[2].Value; - $lastTestCollection.Properties.Add($propertyKey, $propertyValue); - } - } - } - } - - if ($currentTest -ne $null -and $currentTestClass -ne $null) - { - $currentTestClass.Tests.Add($currentTest) - } - - if ($currentTestClass -ne $null -and $currentTestModule -ne $null) - { - $currentTestModule.TestClasses.Add($currentTestClass) - } - - if ($currentTestModule -ne $null) - { - $testModules.Add($currentTestModule) - } - - return $testModules -} - -Write-Verbose "TaefQuery = $TaefQuery" - -$TaefSelectQuery = "" -$TaefQueryToAppend = "" -if($TaefQuery) -{ - $TaefSelectQuery = "/select:`"$TaefQuery`"" - $TaefQueryToAppend = " and $TaefQuery" -} -Write-Verbose "TaefSelectQuery = $TaefSelectQuery" - - -$taefExe = "$TaefPath\te.exe" -[string]$taefOutput = & "$taefExe" /listproperties $TaefSelectQuery $TestFile | Out-String - -[System.Collections.Generic.List[TestModule]]$testModules = (Parse-TestInfo $taefOutput) - -$projFileContent = @" - - -"@ - -foreach ($testModule in $testModules) -{ - foreach ($testClass in $testModules.TestClasses) - { - Write-Host "Generating Helix work item for test class $($testClass.Name)..." - [System.Collections.Generic.List[string]]$testSuiteNames = @() - - $testSuiteExists = $false - $suitelessTestExists = $false - - foreach ($test in $testClass.Tests) - { - # A test method inherits its 'TestSuite' property from its TestClass - if (!$test.Properties.ContainsKey("TestSuite") -and $testClass.Properties.ContainsKey("TestSuite")) - { - $test.Properties["TestSuite"] = $testClass.Properties["TestSuite"] - } - - if ($test.Properties.ContainsKey("TestSuite")) - { - [string]$testSuite = $test.Properties["TestSuite"] - - if (-not $testSuiteNames.Contains($testSuite)) - { - Write-Host " Found test suite $testSuite. Generating Helix work item for it as well." - $testSuiteNames.Add($testSuite) - } - - $testSuiteExists = $true - } - else - { - $suitelessTestExists = $true - } - } - - $testClassSelectPattern = "$($testClass.Name).*" - if($testClass.Name.Contains("::")) - { - $testClassSelectPattern = "$($testClass.Name)::*" - } - $testNameQuery= "(@Name='$testClassSelectPattern')" - - $workItemName = $testClass.Name - # Native tests use '::' as a separator, which is not valid for workItem names. - $workItemName = $workItemName -replace "::", "-" - - if ($suitelessTestExists) - { - $projFileContent += @" - - - 00:30:00 - call %HELIX_CORRELATION_PAYLOAD%\runtests.cmd /select:"(@Name='$($testClass.Name)*'$(if ($testSuiteExists) { "and not @TestSuite='*'" }))$($TaefQueryToAppend)" - -"@ - } - - foreach ($testSuiteName in $testSuiteNames) - { - $projFileContent += @" - - - 00:30:00 - call %HELIX_CORRELATION_PAYLOAD%\runtests.cmd /select:"(@Name='$($testClass.Name)*' and @TestSuite='$testSuiteName')$($TaefQueryToAppend)" - -"@ - } - } -} - -$projFileContent += @" - - - -"@ - -Set-Content $OutputProjFile $projFileContent -NoNewline -Encoding UTF8 \ No newline at end of file diff --git a/build/Helix/InstallTestAppDependencies.ps1 b/build/Helix/InstallTestAppDependencies.ps1 deleted file mode 100644 index da0a049d27d..00000000000 --- a/build/Helix/InstallTestAppDependencies.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -# Displaying progress is unnecessary and is just distracting. -$ProgressPreference = "SilentlyContinue" - -$dependencyFiles = Get-ChildItem -Filter "*Microsoft.VCLibs.*.appx" - -foreach ($file in $dependencyFiles) -{ - Write-Host "Adding dependency $($file)..." - - Add-AppxPackage $file - -} \ No newline at end of file diff --git a/build/Helix/OutputFailedTestQuery.ps1 b/build/Helix/OutputFailedTestQuery.ps1 deleted file mode 100644 index 3ea7abf80ad..00000000000 --- a/build/Helix/OutputFailedTestQuery.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -Param( - [Parameter(Mandatory = $true)] - [string]$WttInputPath -) - -Add-Type -Language CSharp -ReferencedAssemblies System.Xml,System.Xml.Linq,System.Runtime.Serialization,System.Runtime.Serialization.Json (Get-Content $PSScriptRoot\HelixTestHelpers.cs -Raw) - -[HelixTestHelpers.FailedTestDetector]::OutputFailedTestQuery($WttInputPath) \ No newline at end of file diff --git a/build/Helix/OutputSubResultsJsonFiles.ps1 b/build/Helix/OutputSubResultsJsonFiles.ps1 deleted file mode 100644 index 476502e9878..00000000000 --- a/build/Helix/OutputSubResultsJsonFiles.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -Param( - [Parameter(Mandatory = $true)] - [string]$WttInputPath, - - [Parameter(Mandatory = $true)] - [string]$WttSingleRerunInputPath, - - [Parameter(Mandatory = $true)] - [string]$WttMultipleRerunInputPath, - - [Parameter(Mandatory = $true)] - [string]$TestNamePrefix -) - -# Ideally these would be passed as parameters to the script. However ps makes it difficult to deal with string literals containing '&', so we just -# read the values directly from the environment variables -$helixResultsContainerUri = $Env:HELIX_RESULTS_CONTAINER_URI -$helixResultsContainerRsas = $Env:HELIX_RESULTS_CONTAINER_RSAS - -Add-Type -Language CSharp -ReferencedAssemblies System.Xml,System.Xml.Linq,System.Runtime.Serialization,System.Runtime.Serialization.Json (Get-Content $PSScriptRoot\HelixTestHelpers.cs -Raw) - -$testResultParser = [HelixTestHelpers.TestResultParser]::new($TestNamePrefix, $helixResultsContainerUri, $helixResultsContainerRsas) -[System.Collections.Generic.Dictionary[string, string]]$subResultsJsonByMethodName = $testResultParser.GetSubResultsJsonByMethodName($WttInputPath, $WttSingleRerunInputPath, $WttMultipleRerunInputPath) - -$subResultsJsonDirectory = [System.IO.Path]::GetDirectoryName($WttInputPath) - -foreach ($methodName in $subResultsJsonByMethodName.Keys) -{ - $subResultsJson = $subResultsJsonByMethodName[$methodName] - $subResultsJsonPath = [System.IO.Path]::Combine($subResultsJsonDirectory, $methodName + "_subresults.json") - Out-File $subResultsJsonPath -Encoding utf8 -InputObject $subResultsJson -} \ No newline at end of file diff --git a/build/Helix/OutputTestResults.ps1 b/build/Helix/OutputTestResults.ps1 deleted file mode 100644 index a23b456afe8..00000000000 --- a/build/Helix/OutputTestResults.ps1 +++ /dev/null @@ -1,131 +0,0 @@ -Param( - [Parameter(Mandatory = $true)] - [int]$MinimumExpectedTestsExecutedCount, - - [string]$AccessToken = $env:SYSTEM_ACCESSTOKEN, - [string]$CollectionUri = $env:SYSTEM_COLLECTIONURI, - [string]$TeamProject = $env:SYSTEM_TEAMPROJECT, - [string]$BuildUri = $env:BUILD_BUILDURI, - [bool]$CheckJobAttempt -) - -$azureDevOpsRestApiHeaders = @{ - "Accept"="application/json" - "Authorization"="Basic $([System.Convert]::ToBase64String([System.Text.ASCIIEncoding]::ASCII.GetBytes(":$AccessToken")))" -} - -. "$PSScriptRoot/AzurePipelinesHelperScripts.ps1" - -Write-Host "Checking test results..." - -$queryUri = GetQueryTestRunsUri -CollectionUri $CollectionUri -TeamProject $TeamProject -BuildUri $BuildUri -IncludeRunDetails -Write-Host "queryUri = $queryUri" - -$testRuns = Invoke-RestMethodWithRetries $queryUri -Headers $azureDevOpsRestApiHeaders -[System.Collections.Generic.List[string]]$failingTests = @() -[System.Collections.Generic.List[string]]$unreliableTests = @() -[System.Collections.Generic.List[string]]$unexpectedResultTest = @() - -[System.Collections.Generic.List[string]]$namesOfProcessedTestRuns = @() -$totalTestsExecutedCount = 0 - -# We assume that we only have one testRun with a given name that we care about -# We only process the last testRun with a given name (based on completedDate) -# The name of a testRun is set to the Helix queue that it was run on (e.g. windows.10.amd64.client21h1.xaml) -# If we have multiple test runs on the same queue that we care about, we will need to re-visit this logic -foreach ($testRun in ($testRuns.value | Sort-Object -Property "completedDate" -Descending)) -{ - if ($CheckJobAttempt) - { - if ($namesOfProcessedTestRuns -contains $testRun.name) - { - Write-Host "Skipping test run '$($testRun.name)', since we have already processed a test run of that name." - continue - } - } - - Write-Host "Processing results from test run '$($testRun.name)'" - $namesOfProcessedTestRuns.Add($testRun.name) - - $totalTestsExecutedCount += $testRun.totalTests - - $testRunResultsUri = "$($testRun.url)/results?api-version=5.0" - $testResults = Invoke-RestMethodWithRetries "$($testRun.url)/results?api-version=5.0" -Headers $azureDevOpsRestApiHeaders - - foreach ($testResult in $testResults.value) - { - $shortTestCaseTitle = $testResult.testCaseTitle -replace "[a-zA-Z0-9]+.[a-zA-Z0-9]+.Windows.UI.Xaml.Tests.MUXControls.","" - - if ($testResult.outcome -eq "Failed") - { - if (-not $failingTests.Contains($shortTestCaseTitle)) - { - $failingTests.Add($shortTestCaseTitle) - } - } - elseif ($testResult.outcome -eq "Warning") - { - if (-not $unreliableTests.Contains($shortTestCaseTitle)) - { - $unreliableTests.Add($shortTestCaseTitle) - } - } - elseif ($testResult.outcome -ne "Passed") - { - # We should only see tests with result "Passed", "Failed" or "Warning" - if (-not $unexpectedResultTest.Contains($shortTestCaseTitle)) - { - $unexpectedResultTest.Add($shortTestCaseTitle) - } - } - } -} - -if ($unreliableTests.Count -gt 0) -{ - Write-Host @" -##vso[task.logissue type=warning;]Unreliable tests: -##vso[task.logissue type=warning;]$($unreliableTests -join "$([Environment]::NewLine)##vso[task.logissue type=warning;]") - -"@ -} - -if ($failingTests.Count -gt 0) -{ - Write-Host @" -##vso[task.logissue type=error;]Failing tests: -##vso[task.logissue type=error;]$($failingTests -join "$([Environment]::NewLine)##vso[task.logissue type=error;]") - -"@ -} - -if ($unexpectedResultTest.Count -gt 0) -{ - Write-Host @" -##vso[task.logissue type=error;]Tests with unexpected results: -##vso[task.logissue type=error;]$($unexpectedResultTest -join "$([Environment]::NewLine)##vso[task.logissue type=error;]") - -"@ -} - -if($totalTestsExecutedCount -lt $MinimumExpectedTestsExecutedCount) -{ - Write-Host "Expected at least $MinimumExpectedTestsExecutedCount tests to be executed." - Write-Host "Actual executed test count is: $totalTestsExecutedCount" - Write-Host "##vso[task.complete result=Failed;]" -} -elseif ($failingTests.Count -gt 0) -{ - Write-Host "At least one test failed." - Write-Host "##vso[task.complete result=Failed;]" -} -elseif ($unreliableTests.Count -gt 0) -{ - Write-Host "All tests eventually passed, but some initially failed." - Write-Host "##vso[task.complete result=Succeeded;]" -} -else -{ - Write-Host "All tests passed." - Write-Host "##vso[task.complete result=Succeeded;]" -} diff --git a/build/Helix/PrepareHelixPayload.ps1 b/build/Helix/PrepareHelixPayload.ps1 deleted file mode 100644 index 42ba9c6f77b..00000000000 --- a/build/Helix/PrepareHelixPayload.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -[CmdLetBinding()] -Param( - [string]$Platform, - [string]$Configuration, - [string]$ArtifactName='drop' -) - -$payloadDir = "HelixPayload\$Configuration\$Platform" - -$repoDirectory = Join-Path (Split-Path -Parent $script:MyInvocation.MyCommand.Path) "..\..\" -$nugetPackagesDir = Join-Path (Split-Path -Parent $script:MyInvocation.MyCommand.Path) "packages" - -# Create the payload directory. Remove it if it already exists. -If(test-path $payloadDir) -{ - Remove-Item $payloadDir -Recurse -} -New-Item -ItemType Directory -Force -Path $payloadDir - -# Copy files from nuget packages -Copy-Item "$nugetPackagesDir\microsoft.windows.apps.test.1.0.181203002\lib\netcoreapp2.1\*.dll" $payloadDir -Copy-Item "$nugetPackagesDir\Microsoft.Taef.10.60.210621002\build\Binaries\$Platform\*" $payloadDir -Copy-Item "$nugetPackagesDir\Microsoft.Taef.10.60.210621002\build\Binaries\$Platform\NetFx4.5\*" $payloadDir -New-Item -ItemType Directory -Force -Path "$payloadDir\.NETCoreApp2.1\" -Copy-Item "$nugetPackagesDir\runtime.win-$Platform.microsoft.netcore.app.2.1.0\runtimes\win-$Platform\lib\netcoreapp2.1\*" "$payloadDir\.NETCoreApp2.1\" -Copy-Item "$nugetPackagesDir\runtime.win-$Platform.microsoft.netcore.app.2.1.0\runtimes\win-$Platform\native\*" "$payloadDir\.NETCoreApp2.1\" -New-Item -ItemType Directory -Force -Path "$payloadDir\content\" -Copy-Item "$nugetPackagesDir\Microsoft.Internal.Windows.Terminal.TestContent.1.0.1\content\*" "$payloadDir\content\" - -function Copy-If-Exists -{ - Param($source, $destinationDir) - - if (Test-Path $source) - { - Write-Host "Copy from '$source' to '$destinationDir'" - Copy-Item -Force $source $destinationDir - } - else - { - Write-Host "'$source' does not exist." - } -} - -# Copy files from the 'drop' artifact dir -Copy-Item "$repoDirectory\Artifacts\$ArtifactName\$Configuration\$Platform\Test\*" $payloadDir -Recurse - -# Copy files from the repo -New-Item -ItemType Directory -Force -Path "$payloadDir" -Copy-Item "build\helix\ConvertWttLogToXUnit.ps1" "$payloadDir" -Copy-Item "build\helix\OutputFailedTestQuery.ps1" "$payloadDir" -Copy-Item "build\helix\OutputSubResultsJsonFiles.ps1" "$payloadDir" -Copy-Item "build\helix\HelixTestHelpers.cs" "$payloadDir" -Copy-Item "build\helix\runtests.cmd" $payloadDir -Copy-Item "build\helix\InstallTestAppDependencies.ps1" "$payloadDir" -Copy-Item "build\Helix\EnsureMachineState.ps1" "$payloadDir" - -# Extract the unpackaged distribution of Windows Terminal to the payload directory, -# where it will create a subdirectory named terminal-0.0.1.0 -# This is referenced in TerminalApp.cs later as part of the test harness. -& tar -x -v -f "$repoDirectory\Artifacts\$ArtifactName\unpackaged\WindowsTerminalDev_0.0.1.0_x64.zip" -C "$payloadDir" -Copy-Item "res\fonts\*.ttf" "$payloadDir\terminal-0.0.1.0" diff --git a/build/Helix/ProcessHelixFiles.ps1 b/build/Helix/ProcessHelixFiles.ps1 deleted file mode 100644 index ae9f7582812..00000000000 --- a/build/Helix/ProcessHelixFiles.ps1 +++ /dev/null @@ -1,130 +0,0 @@ -Param( - [string]$AccessToken = $env:SYSTEM_ACCESSTOKEN, - [string]$HelixAccessToken = $env:HelixAccessToken, - [string]$CollectionUri = $env:SYSTEM_COLLECTIONURI, - [string]$TeamProject = $env:SYSTEM_TEAMPROJECT, - [string]$BuildUri = $env:BUILD_BUILDURI, - [string]$OutputFolder = "HelixOutput" -) - -$helixLinkFile = "$OutputFolder\LinksToHelixTestFiles.html" - - -function Generate-File-Links -{ - Param ([Array[]]$files,[string]$sectionName) - if($files.Count -gt 0) - { - Out-File -FilePath $helixLinkFile -Append -InputObject "
" - Out-File -FilePath $helixLinkFile -Append -InputObject "

$sectionName

" - Out-File -FilePath $helixLinkFile -Append -InputObject "
    " - foreach($file in $files) - { - $url = Append-HelixAccessTokenToUrl $file.Link "{Your-Helix-Access-Token-Here}" - Out-File -FilePath $helixLinkFile -Append -InputObject "
  • $($url)
  • " - } - Out-File -FilePath $helixLinkFile -Append -InputObject "
" - Out-File -FilePath $helixLinkFile -Append -InputObject "
" - } -} - -function Append-HelixAccessTokenToUrl -{ - Param ([string]$url, [string]$token) - if($token) - { - if($url.Contains("?")) - { - $url = "$($url)&access_token=$($token)" - } - else - { - $url = "$($url)?access_token=$($token)" - } - } - return $url -} - -#Create output directory -New-Item $OutputFolder -ItemType Directory - -$azureDevOpsRestApiHeaders = @{ - "Accept"="application/json" - "Authorization"="Basic $([System.Convert]::ToBase64String([System.Text.ASCIIEncoding]::ASCII.GetBytes(":$AccessToken")))" -} - -. "$PSScriptRoot/AzurePipelinesHelperScripts.ps1" - -$queryUri = GetQueryTestRunsUri -CollectionUri $CollectionUri -TeamProject $TeamProject -BuildUri $BuildUri -IncludeRunDetails -Write-Host "queryUri = $queryUri" - -$testRuns = Invoke-RestMethodWithRetries $queryUri -Headers $azureDevOpsRestApiHeaders -$webClient = New-Object System.Net.WebClient -[System.Collections.Generic.List[string]]$workItems = @() - -foreach ($testRun in $testRuns.value) -{ - Write-Host "testRunUri = $testRun.url" - $testResults = Invoke-RestMethodWithRetries "$($testRun.url)/results?api-version=5.0" -Headers $azureDevOpsRestApiHeaders - $isTestRunNameShown = $false - - foreach ($testResult in $testResults.value) - { - $info = ConvertFrom-Json ([System.Web.HttpUtility]::HtmlDecode($testResult.comment)) - $helixJobId = $info.HelixJobId - $helixWorkItemName = $info.HelixWorkItemName - - $workItem = "$helixJobId-$helixWorkItemName" - - Write-Host "Helix Work Item = $workItem" - - if (-not $workItems.Contains($workItem)) - { - $workItems.Add($workItem) - $filesQueryUri = "https://helix.dot.net/api/2019-06-17/jobs/$helixJobId/workitems/$helixWorkItemName/files" - $filesQueryUri = Append-HelixAccessTokenToUrl $filesQueryUri $helixAccessToken - $files = Invoke-RestMethodWithRetries $filesQueryUri - - $screenShots = $files | where { $_.Name.EndsWith(".jpg") } - $dumps = $files | where { $_.Name.EndsWith(".dmp") } - $pgcFiles = $files | where { $_.Name.EndsWith(".pgc") } - if ($screenShots.Count + $dumps.Count + $pgcFiles.Count -gt 0) - { - if(-Not $isTestRunNameShown) - { - Out-File -FilePath $helixLinkFile -Append -InputObject "

$($testRun.name)

" - $isTestRunNameShown = $true - } - Out-File -FilePath $helixLinkFile -Append -InputObject "

$helixWorkItemName

" - Generate-File-Links $screenShots "Screenshots" - Generate-File-Links $dumps "CrashDumps" - Generate-File-Links $pgcFiles "PGC files" - $misc = $files | where { ($screenShots -NotContains $_) -And ($dumps -NotContains $_) -And ($visualTreeVerificationFiles -NotContains $_) -And ($pgcFiles -NotContains $_) } - Generate-File-Links $misc "Misc" - - foreach($pgcFile in $pgcFiles) - { - $flavorPath = $testResult.automatedTestName.Split('.')[0] - $archPath = $testResult.automatedTestName.Split('.')[1] - $fileName = $pgcFile.Name - $fullPath = "$OutputFolder\PGO\$flavorPath\$archPath" - $destination = "$fullPath\$fileName" - - Write-Host "Copying $($pgcFile.Name) to $destination" - - if (-Not (Test-Path $fullPath)) - { - New-Item $fullPath -ItemType Directory - } - - $link = $pgcFile.Link - - Write-Host "Downloading $link to $destination" - - $link = Append-HelixAccessTokenToUrl $link $HelixAccessToken - Download-FileWithRetries $link $destination - } - } - } - } -} diff --git a/build/Helix/RunTestsInHelix.proj b/build/Helix/RunTestsInHelix.proj deleted file mode 100644 index db00314ff06..00000000000 --- a/build/Helix/RunTestsInHelix.proj +++ /dev/null @@ -1,20 +0,0 @@ - - - pr/terminal/$(BUILD_SOURCEBRANCH)/ - true - true - true - $(HelixPreCommands);set testnameprefix=$(Configuration).$(Platform);set testbuildplatform=$(Platform);set rerunPassesRequiredToAvoidFailure=$(rerunPassesRequiredToAvoidFailure) - ..\..\bin\$(Platform)\$(Configuration)\ - - - - - - - - - - - - \ No newline at end of file diff --git a/build/Helix/UpdateUnreliableTests.ps1 b/build/Helix/UpdateUnreliableTests.ps1 deleted file mode 100644 index ecf4e8bf7a4..00000000000 --- a/build/Helix/UpdateUnreliableTests.ps1 +++ /dev/null @@ -1,136 +0,0 @@ -[CmdLetBinding()] -Param( - [Parameter(Mandatory = $true)] - [int]$RerunPassesRequiredToAvoidFailure, - - [string]$AccessToken = $env:SYSTEM_ACCESSTOKEN, - [string]$CollectionUri = $env:SYSTEM_COLLECTIONURI, - [string]$TeamProject = $env:SYSTEM_TEAMPROJECT, - [string]$BuildUri = $env:BUILD_BUILDURI -) - -. "$PSScriptRoot/AzurePipelinesHelperScripts.ps1" - - -$azureDevOpsRestApiHeaders = @{ - "Accept"="application/json" - "Authorization"="Basic $([System.Convert]::ToBase64String([System.Text.ASCIIEncoding]::ASCII.GetBytes(":$AccessToken")))" -} - -$queryUri = GetQueryTestRunsUri -CollectionUri $CollectionUri -TeamProject $TeamProject -BuildUri $BuildUri -Write-Host "queryUri = $queryUri" - -# To account for unreliable tests, we'll iterate through all of the tests associated with this build, check to see any tests that were unreliable -# (denoted by being marked as "skipped"), and if so, we'll instead mark those tests with a warning and enumerate all of the attempted runs -# with their pass/fail states as well as any relevant error messages for failed attempts. -$testRuns = Invoke-RestMethodWithRetries $queryUri -Headers $azureDevOpsRestApiHeaders - -$timesSeenByRunName = @{} - -foreach ($testRun in $testRuns.value) -{ - $testRunResultsUri = "$($testRun.url)/results?api-version=5.0" - - Write-Host "Marking test run `"$($testRun.name)`" as in progress so we can change its results to account for unreliable tests." - Invoke-RestMethod "$($testRun.url)?api-version=5.0" -Method Patch -Body (ConvertTo-Json @{ "state" = "InProgress" }) -Headers $azureDevOpsRestApiHeaders -ContentType "application/json" | Out-Null - - Write-Host "Retrieving test results..." - $testResults = Invoke-RestMethodWithRetries $testRunResultsUri -Headers $azureDevOpsRestApiHeaders - - foreach ($testResult in $testResults.value) - { - $testNeedsSubResultProcessing = $false - if ($testResult.outcome -eq "NotExecuted") - { - $testNeedsSubResultProcessing = $true - } - elseif($testResult.outcome -eq "Failed") - { - $testNeedsSubResultProcessing = $testResult.errorMessage -like "*_subresults.json*" - } - - if ($testNeedsSubResultProcessing) - { - Write-Host " Test $($testResult.testCaseTitle) was detected as unreliable. Updating..." - - # The errorMessage field contains a link to the JSON-encoded rerun result data. - $resultsJson = Download-StringWithRetries "Error results" $testResult.errorMessage - $rerunResults = ConvertFrom-Json $resultsJson - [System.Collections.Generic.List[System.Collections.Hashtable]]$rerunDataList = @() - $attemptCount = 0 - $passCount = 0 - $totalDuration = 0 - - foreach ($rerun in $rerunResults.results) - { - $rerunData = @{ - "displayName" = "Attempt #$($attemptCount + 1) - $($testResult.testCaseTitle)"; - "durationInMs" = $rerun.duration; - "outcome" = $rerun.outcome; - } - - if ($rerun.outcome -eq "Passed") - { - $passCount++ - } - - if ($attemptCount -gt 0) - { - $rerunData["sequenceId"] = $attemptCount - } - - Write-Host " Attempt #$($attemptCount + 1): $($rerun.outcome)" - - if ($rerun.outcome -ne "Passed") - { - $screenshots = "$($rerunResults.blobPrefix)/$($rerun.screenshots -join @" -$($rerunResults.blobSuffix) -$($rerunResults.blobPrefix) -"@)$($rerunResults.blobSuffix)" - - # We subtract 1 from the error index because we added 1 so we could use 0 - # as a default value not injected into the JSON in order to keep its size down. - # We did this because there's a maximum size enforced for the errorMessage parameter - # in the Azure DevOps REST API. - $fullErrorMessage = @" -Log: $($rerunResults.blobPrefix)/$($rerun.log)$($rerunResults.blobSuffix) - -Screenshots: -$screenshots - -Error log: -$($rerunResults.errors[$rerun.errorIndex - 1]) -"@ - - $rerunData["errorMessage"] = $fullErrorMessage - } - - $attemptCount++ - $totalDuration += $rerun.duration - $rerunDataList.Add($rerunData) - } - - $overallOutcome = "Warning" - - if ($attemptCount -eq 2) - { - Write-Host " Test $($testResult.testCaseTitle) passed on the immediate rerun, so we'll mark it as unreliable." - } - elseif ($passCount -gt $RerunPassesRequiredToAvoidFailure) - { - Write-Host " Test $($testResult.testCaseTitle) passed on $passCount of $attemptCount attempts, which is greater than or equal to the $RerunPassesRequiredToAvoidFailure passes required to avoid being marked as failed. Marking as unreliable." - } - else - { - Write-Host " Test $($testResult.testCaseTitle) passed on only $passCount of $attemptCount attempts, which is less than the $RerunPassesRequiredToAvoidFailure passes required to avoid being marked as failed. Marking as failed." - $overallOutcome = "Failed" - } - - $updateBody = ConvertTo-Json @(@{ "id" = $testResult.id; "outcome" = $overallOutcome; "errorMessage" = " "; "durationInMs" = $totalDuration; "subResults" = $rerunDataList; "resultGroupType" = "rerun" }) -Depth 5 - Invoke-RestMethod -Uri $testRunResultsUri -Method Patch -Headers $azureDevOpsRestApiHeaders -Body $updateBody -ContentType "application/json" | Out-Null - } - } - - Write-Host "Finished updates. Re-marking test run `"$($testRun.name)`" as completed." - Invoke-RestMethod -Uri "$($testRun.url)?api-version=5.0" -Method Patch -Body (ConvertTo-Json @{ "state" = "Completed" }) -Headers $azureDevOpsRestApiHeaders -ContentType "application/json" | Out-Null -} diff --git a/build/Helix/global.json b/build/Helix/global.json deleted file mode 100644 index 53d8ed33c45..00000000000 --- a/build/Helix/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "msbuild-sdks": { - "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.22525.5" - } -} diff --git a/build/Helix/packages.config b/build/Helix/packages.config deleted file mode 100644 index b101a50d6fe..00000000000 --- a/build/Helix/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/build/Helix/readme.md b/build/Helix/readme.md deleted file mode 100644 index d51fc973c1e..00000000000 --- a/build/Helix/readme.md +++ /dev/null @@ -1,32 +0,0 @@ -This directory contains code and configuration files to run WinUI tests in Helix. - -Helix is a cloud hosted test execution environment which is accessed via the Arcade SDK. -More details: -* [Arcade](https://github.com/dotnet/arcade) -* [Helix](https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.Helix/Sdk) - -WinUI tests are scheduled in Helix by the Azure DevOps Pipeline: [RunHelixTests.yml](../RunHelixTests.yml). - -The workflow is as follows: -1. NuGet Restore is called on the packages.config in this directory. This downloads any runtime dependencies -that are needed to run tests. -2. PrepareHelixPayload.ps1 is called. This copies the necessary files from various locations into a Helix -payload directory. This directory is what will get sent to the Helix machines. -3. RunTestsInHelix.proj is executed. This proj has a dependency on -[Microsoft.DotNet.Helix.Sdk](https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.Helix/Sdk) -which it uses to publish the Helix payload directory and to schedule the Helix Work Items. The WinUI tests -are parallelized into multiple Helix Work Items. -4. Each Helix Work Item calls [runtests.cmd](runtests.cmd) with a specific query to pass to -[TAEF](https://docs.microsoft.com/en-us/windows-hardware/drivers/taef/) which runs the tests. -5. If a test is detected to have failed, we run it again, first once, then eight more times if it fails again. -If it fails all ten times, we report the test as failed; otherwise, we report it as unreliable, -which will show up as a warning, but which will not fail the build. When a test is reported as unreliable, -we include the results for each individual run via a JSON string in the original test's errorMessage field. -6. TAEF produces logs in WTT format. Helix is able to process logs in XUnit format. We run -[ConvertWttLogToXUnit.ps1](ConvertWttLogToXUnit.ps1) to convert the logs into the necessary format. -7. RunTestsInHelix.proj has EnableAzurePipelinesReporter set to true. This allows the XUnit formatted test -results to be reported back to the Azure DevOps Pipeline. -8. We process unreliable tests once all tests have been reported by reading the JSON string from the -errorMessage field and calling the Azure DevOps REST API to modify the unreliable tests to have sub-results -added to the test and to mark the test as "warning", which will enable people to see exactly how the test -failed in runs where it did. \ No newline at end of file diff --git a/build/Helix/runtests.cmd b/build/Helix/runtests.cmd deleted file mode 100644 index 5c543a141bf..00000000000 --- a/build/Helix/runtests.cmd +++ /dev/null @@ -1,105 +0,0 @@ -setlocal ENABLEDELAYEDEXPANSION - -echo %TIME% - -robocopy %HELIX_CORRELATION_PAYLOAD% . /s /NP > NUL - -echo %TIME% - -reg add HKLM\Software\Policies\Microsoft\Windows\Appx /v AllowAllTrustedApps /t REG_DWORD /d 1 /f - -rem enable dump collection for our test apps: -rem note, this script is run from a 32-bit cmd, but we need to set the native reg-key -FOR %%A IN (TestHostApp.exe,te.exe,te.processhost.exe,conhost.exe,OpenConsole.exe,WindowsTerminal.exe) DO ( - %systemroot%\sysnative\cmd.exe /c reg add "HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\%%A" /v DumpFolder /t REG_EXPAND_SZ /d %HELIX_DUMP_FOLDER% /f - %systemroot%\sysnative\cmd.exe /c reg add "HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\%%A" /v DumpType /t REG_DWORD /d 2 /f - %systemroot%\sysnative\cmd.exe /c reg add "HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\%%A" /v DumpCount /t REG_DWORD /d 10 /f -) - -echo %TIME% - -:: kill dhandler, which is a tool designed to handle unexpected windows appearing. But since our tests are -:: expected to show UI we don't want it running. -taskkill -f -im dhandler.exe - -echo %TIME% -powershell -ExecutionPolicy Bypass .\EnsureMachineState.ps1 -echo %TIME% -powershell -ExecutionPolicy Bypass .\InstallTestAppDependencies.ps1 -echo %TIME% - -set testBinaryCandidates=TerminalApp.LocalTests.dll SettingsModel.LocalTests.dll Conhost.UIA.Tests.dll WindowsTerminal.UIA.Tests.dll -set testBinaries= -for %%B in (%testBinaryCandidates%) do ( - if exist %%B ( - set "testBinaries=!testBinaries! %%B" - ) -) - -echo %TIME% -te.exe %testBinaries% /enablewttlogging /unicodeOutput:false /sessionTimeout:0:15 /testtimeout:0:10 /screenCaptureOnError %* -echo %TIME% - -powershell -ExecutionPolicy Bypass Get-Process - -move te.wtl te_original.wtl - -copy /y te_original.wtl %HELIX_WORKITEM_UPLOAD_ROOT% -copy /y WexLogFileOutput\*.jpg %HELIX_WORKITEM_UPLOAD_ROOT% -copy /y *.pgc %HELIX_WORKITEM_UPLOAD_ROOT% - -set FailedTestQuery= -for /F "tokens=* usebackq" %%I IN (`powershell -ExecutionPolicy Bypass .\OutputFailedTestQuery.ps1 te_original.wtl`) DO ( - set FailedTestQuery=%%I -) - -rem The first time, we'll just re-run failed tests once. In many cases, tests fail very rarely, such that -rem a single re-run will be sufficient to detect many unreliable tests. -if "%FailedTestQuery%" == "" goto :SkipReruns - -echo %TIME% -te.exe %testBinaries% /enablewttlogging /unicodeOutput:false /sessionTimeout:0:15 /testtimeout:0:10 /screenCaptureOnError /select:"%FailedTestQuery%" -echo %TIME% - -move te.wtl te_rerun.wtl - -copy /y te_rerun.wtl %HELIX_WORKITEM_UPLOAD_ROOT% -copy /y WexLogFileOutput\*.jpg %HELIX_WORKITEM_UPLOAD_ROOT% - -rem If there are still failing tests remaining, we'll run them eight more times, so they'll have been run a total of ten times. -rem If any tests fail all ten times, we can be pretty confident that these are actual test failures rather than unreliable tests. -if not exist te_rerun.wtl goto :SkipReruns - -set FailedTestQuery= -for /F "tokens=* usebackq" %%I IN (`powershell -ExecutionPolicy Bypass .\OutputFailedTestQuery.ps1 te_rerun.wtl`) DO ( - set FailedTestQuery=%%I -) - -if "%FailedTestQuery%" == "" goto :SkipReruns - -echo %TIME% -te.exe %testBinaries% /enablewttlogging /unicodeOutput:false /sessionTimeout:0:15 /testtimeout:0:10 /screenCaptureOnError /testmode:Loop /LoopTest:8 /select:"%FailedTestQuery%" -echo %TIME% - -powershell -ExecutionPolicy Bypass Get-Process - -move te.wtl te_rerun_multiple.wtl - -copy /y te_rerun_multiple.wtl %HELIX_WORKITEM_UPLOAD_ROOT% -copy /y WexLogFileOutput\*.jpg %HELIX_WORKITEM_UPLOAD_ROOT% -powershell -ExecutionPolicy Bypass .\CopyVisualTreeVerificationFiles.ps1 - -:SkipReruns - -powershell -ExecutionPolicy Bypass Get-Process - -echo %TIME% -powershell -ExecutionPolicy Bypass .\OutputSubResultsJsonFiles.ps1 te_original.wtl te_rerun.wtl te_rerun_multiple.wtl %testnameprefix% -powershell -ExecutionPolicy Bypass .\ConvertWttLogToXUnit.ps1 te_original.wtl te_rerun.wtl te_rerun_multiple.wtl testResults.xml %testnameprefix% -echo %TIME% - -copy /y *_subresults.json %HELIX_WORKITEM_UPLOAD_ROOT% - -type testResults.xml - -echo %TIME% diff --git a/build/config/ESRPSigning_ConPTY.json b/build/config/ESRPSigning_ConPTY.json deleted file mode 100644 index d01e9c9eabe..00000000000 --- a/build/config/ESRPSigning_ConPTY.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "Version": "1.0.0", - "UseMinimatch": false, - "SignBatches": [ - { - "MatchedPath": [ - "conpty.dll", - "OpenConsole.exe" - ], - "SigningInfo": { - "Operations": [ - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolSign", - "Parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "Microsoft" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolVerify", - "Parameters": [], - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - } - } - ] -} diff --git a/build/config/ESRPSigning_Terminal.json b/build/config/ESRPSigning_Terminal.json deleted file mode 100644 index 01780c2df3c..00000000000 --- a/build/config/ESRPSigning_Terminal.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "Version": "1.0.0", - "UseMinimatch": false, - "SignBatches": [ - { - "MatchedPath": [ - // Namespaced DLLs - "Microsoft.Terminal.*.dll", - "Microsoft.Terminal.*.winmd", - - // ConPTY and DefTerm - "OpenConsole.exe", - "OpenConsoleProxy.dll", - - // VCRT Forwarders - "*_app.dll", - - // Legacy DLLs with old names - "TerminalApp.dll", - "TerminalApp.winmd", - "TerminalConnection.dll", - "TerminalThemeHelpers.dll", - "WindowsTerminalShellExt.dll", - - // The rest - "TerminalAzBridge.exe", - "wt.exe", - "WindowsTerminal.exe", - "elevate-shim.exe" - ], - "SigningInfo": { - "Operations": [ - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolSign", - "Parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "Microsoft" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolVerify", - "Parameters": [], - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - } - } - ] -} diff --git a/build/config/esrp.build.batch.conpty.json b/build/config/esrp.build.batch.conpty.json new file mode 100644 index 00000000000..b0097056ec5 --- /dev/null +++ b/build/config/esrp.build.batch.conpty.json @@ -0,0 +1,47 @@ +[ + { + "MatchedPath": [ + "conpty.dll", + "OpenConsole.exe" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } +] diff --git a/build/config/esrp.build.batch.terminal_constituents.json b/build/config/esrp.build.batch.terminal_constituents.json new file mode 100644 index 00000000000..c33123a16de --- /dev/null +++ b/build/config/esrp.build.batch.terminal_constituents.json @@ -0,0 +1,65 @@ +[ + { + "MatchedPath": [ + // Namespaced DLLs + "PackageContents/Microsoft.Terminal.*.dll", + "PackageContents/Microsoft.Terminal.*.winmd", + + // ConPTY and DefTerm + "PackageContents/OpenConsole.exe", + "PackageContents/OpenConsoleProxy.dll", + + // Legacy DLLs with old names + "PackageContents/TerminalApp.dll", + "PackageContents/TerminalApp.winmd", + "PackageContents/TerminalConnection.dll", + "PackageContents/TerminalThemeHelpers.dll", + "PackageContents/WindowsTerminalShellExt.dll", + + // The rest + "PackageContents/TerminalAzBridge.exe", + "PackageContents/wt.exe", + "PackageContents/WindowsTerminal.exe", + "PackageContents/elevate-shim.exe" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } +] diff --git a/build/config/esrp.build.batch.wpf.json b/build/config/esrp.build.batch.wpf.json new file mode 100644 index 00000000000..e9c9af87891 --- /dev/null +++ b/build/config/esrp.build.batch.wpf.json @@ -0,0 +1,46 @@ +[ + { + "MatchedPath": [ + "PublicTerminalCore.dll" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } +] diff --git a/build/config/esrp.build.batch.wpfdotnet.json b/build/config/esrp.build.batch.wpfdotnet.json new file mode 100644 index 00000000000..0699353f64c --- /dev/null +++ b/build/config/esrp.build.batch.wpfdotnet.json @@ -0,0 +1,47 @@ +[ + { + "MatchedPath": [ + "WpfTerminalControl/net472/Microsoft.Terminal.Wpf.dll", + "WpfTerminalControl/net6.0-windows/Microsoft.Terminal.Wpf.dll" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } +] diff --git a/build/pgo/PGO.DB.proj b/build/pgo/PGO.DB.proj index f89eff931ac..ce7e1c6ebcb 100644 --- a/build/pgo/PGO.DB.proj +++ b/build/pgo/PGO.DB.proj @@ -1,4 +1,5 @@ + - \ No newline at end of file + diff --git a/build/pgo/Terminal.PGO.DB.nuspec b/build/pgo/Terminal.PGO.DB.nuspec index d49574423a8..b1437af2a0b 100644 --- a/build/pgo/Terminal.PGO.DB.nuspec +++ b/build/pgo/Terminal.PGO.DB.nuspec @@ -12,5 +12,6 @@ + diff --git a/build/pgo/Terminal.PGO.props b/build/pgo/Terminal.PGO.props index dedf48a6723..109bdd7ffa0 100644 --- a/build/pgo/Terminal.PGO.props +++ b/build/pgo/Terminal.PGO.props @@ -46,6 +46,5 @@ true - - + diff --git a/build/pipelines/ci.yml b/build/pipelines/ci.yml index 3b833d8e718..3a142688443 100644 --- a/build/pipelines/ci.yml +++ b/build/pipelines/ci.yml @@ -35,11 +35,7 @@ parameters: type: boolean default: true - name: runTests - displayName: "Run Unit and Feature Tests" - type: boolean - default: true - - name: submitHelix - displayName: "Submit to Helix for testing and PGO" + displayName: "Run Tests" type: boolean default: true - name: buildPlatforms @@ -54,28 +50,43 @@ stages: - stage: Audit_x64 displayName: Audit Mode dependsOn: [] - condition: succeeded() jobs: - - template: ./templates/build-console-audit-job.yml + - template: ./templates-v2/job-build-project.yml parameters: - platform: x64 + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-OSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-INT-L + buildPlatforms: [x64] + buildConfigurations: [AuditMode] + buildEverything: true + keepAllExpensiveBuildOutputs: false - - stage: Scripts - displayName: Code Health Scripts + - stage: CodeHealth + displayName: Code Health dependsOn: [] - condition: succeeded() jobs: - - template: ./templates/check-formatting.yml + - template: ./templates-v2/job-check-code-format.yml - ${{ each platform in parameters.buildPlatforms }}: - stage: Build_${{ platform }} displayName: Build ${{ platform }} dependsOn: [] - condition: succeeded() jobs: - - template: ./templates/build-console-ci.yml + - template: ./templates-v2/job-build-project.yml parameters: - platform: ${{ platform }} + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-OSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-INT-L + buildPlatforms: + - ${{ platform }} + buildConfigurations: [Release] + buildEverything: true + keepAllExpensiveBuildOutputs: false + - ${{ if eq(parameters.runTests, true) }}: - stage: Test_${{ platform }} displayName: Test ${{ platform }} @@ -83,22 +94,13 @@ stages: - Build_${{ platform }} condition: succeeded() jobs: - - template: ./templates/test-console-ci.yml + - template: ./templates-v2/job-test-project.yml parameters: platform: ${{ platform }} - - ${{ if and(containsValue(parameters.buildPlatforms, 'x64'), eq(parameters.submitHelix, true), ne(variables['Build.Reason'], 'PullRequest')) }}: - - stage: Helix_x64 - displayName: Helix x64 - dependsOn: [Build_x64] - jobs: - - template: ./templates/console-ci-helix-job.yml - parameters: - platform: x64 - - - ${{ if and(containsValue(parameters.buildPlatforms, 'x64'), ne(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - stage: CodeIndexer displayName: Github CodeNav Indexer - dependsOn: [Build_x64] + dependsOn: [] jobs: - - template: ./templates/codenav-indexer.yml + - template: ./templates-v2/job-index-github-codenav.yml diff --git a/build/pipelines/daily-loc-submission.yml b/build/pipelines/daily-loc-submission.yml index 126b2e10359..890c1f86a47 100644 --- a/build/pipelines/daily-loc-submission.yml +++ b/build/pipelines/daily-loc-submission.yml @@ -27,6 +27,7 @@ steps: clean: true submodules: false fetchDepth: 1 # Don't need a deep checkout for loc files! + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here persistCredentials: true path: s # Adding a second repo made Azure DevOps change where we're checked out. diff --git a/build/pipelines/feature-flag-ci.yml b/build/pipelines/feature-flag-ci.yml index 0ca4400463d..ead379dbf36 100644 --- a/build/pipelines/feature-flag-ci.yml +++ b/build/pipelines/feature-flag-ci.yml @@ -7,6 +7,7 @@ pr: paths: include: - src/features.xml + - build/pipelines/feature-flag-ci.yml variables: - name: runCodesignValidationInjectionBG @@ -21,9 +22,19 @@ parameters: # Dev is built automatically # WindowsInbox does not typically build with VS. -jobs: +stages: - ${{ each branding in parameters.buildBrandings }}: - - template: ./templates/build-console-ci.yml - parameters: - platform: x64 - branding: ${{ branding }} + - stage: Build_${{ branding }} + dependsOn: [] + displayName: Build ${{ branding }} + jobs: + - template: ./templates-v2/job-build-project.yml + parameters: + pool: # This only runs in CI + name: SHINE-OSS-L + buildPlatforms: [x64] + buildConfigurations: [Release] + buildEverything: true + branding: ${{ branding }} + keepAllExpensiveBuildOutputs: false + artifactStem: -${{ branding }} # Disambiguate artifacts with the same config/platform diff --git a/build/pipelines/fuzz.yml b/build/pipelines/fuzz.yml index b5604b5f227..75b781d33c6 100644 --- a/build/pipelines/fuzz.yml +++ b/build/pipelines/fuzz.yml @@ -16,44 +16,52 @@ pr: none name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) stages: - - stage: Build_Fuzz_Config - displayName: Build Fuzzers + - stage: Build + displayName: Fuzzing Build dependsOn: [] condition: succeeded() jobs: - - template: ./templates/build-console-fuzzing.yml - parameters: - platform: x64 - - stage: OneFuzz - displayName: Submit OneFuzz Job - dependsOn: ['Build_Fuzz_Config'] + - template: ./templates-v2/job-build-project.yml + parameters: + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-OSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-INT-L + buildPlatforms: [x64] + buildConfigurations: [Fuzzing] + buildEverything: true + keepAllExpensiveBuildOutputs: false + + - stage: Submit + displayName: Submit to OneFuzz + dependsOn: [Build] condition: succeeded() - pool: - vmImage: 'ubuntu-latest' - variables: - artifactName: fuzzingBuildOutput jobs: - - job: - steps: - - task: DownloadBuildArtifacts@0 - inputs: - artifactName: $(artifactName) - downloadPath: $(Build.ArtifactStagingDirectory) - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.x' - addToPath: true - architecture: 'x64' - - bash: | - set -ex - pip -q install onefuzz - onefuzz config --endpoint $(endpoint) --client_id $(client_id) --authority $(authority) --tenant_domain $(tenant_domain) --client_secret $(client_secret) - sed -i s/INSERT_PAT_HERE/$(ado_pat)/ build/Fuzz/notifications-ado.json - sed -i s/INSERT_ASSIGNED_HERE/$(ado_assigned_to)/ build/Fuzz/notifications-ado.json - displayName: Configure OneFuzz - - bash: | - onefuzz template libfuzzer basic --colocate_all_tasks --vm_count 1 --target_exe $target_exe_path --notification_config @./build/Fuzz/notifications-ado.json OpenConsole $test_name $(Build.SourceVersion) default - displayName: Submit OneFuzz Job - env: - target_exe_path: $(Build.ArtifactStagingDirectory)/$(artifactName)/Fuzzing/x64/test/OpenConsoleFuzzer.exe - test_name: WriteCharsLegacy + - job: + pool: + vmImage: 'ubuntu-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download artifacts + inputs: + artifactName: build-x64-Fuzzing + downloadPath: $(Build.ArtifactStagingDirectory) + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.x' + addToPath: true + architecture: 'x64' + - bash: | + set -ex + pip -q install onefuzz + onefuzz config --endpoint $(endpoint) --client_id $(client_id) --authority $(authority) --tenant_domain $(tenant_domain) --client_secret $(client_secret) + sed -i s/INSERT_PAT_HERE/$(ado_pat)/ build/Fuzz/notifications-ado.json + sed -i s/INSERT_ASSIGNED_HERE/$(ado_assigned_to)/ build/Fuzz/notifications-ado.json + displayName: Configure OneFuzz + - bash: | + onefuzz template libfuzzer basic --colocate_all_tasks --vm_count 1 --target_exe $target_exe_path --notification_config @./build/Fuzz/notifications-ado.json OpenConsole $test_name $(Build.SourceVersion) default + displayName: Submit OneFuzz Job + env: + target_exe_path: $(Build.ArtifactStagingDirectory)/OpenConsoleFuzzer.exe + test_name: WriteCharsLegacy diff --git a/build/pipelines/pgo.yml b/build/pipelines/pgo.yml index 1c93a56d8cd..d659c4e2520 100644 --- a/build/pipelines/pgo.yml +++ b/build/pipelines/pgo.yml @@ -1,5 +1,27 @@ trigger: none pr: none +schedules: + - cron: "0 5 * * 2-6" # Run at 05:00 UTC Tuesday through Saturday (Even later than Localization, after the work day in Pacific, Mon-Fri) + displayName: "Nightly Instrumentation Build" + branches: + include: + - main + always: false # only run if there's code changes! + +parameters: + - name: branding + displayName: "Branding (Build Type)" + type: string + default: Preview # By default, we'll PGO the Preview builds to get max coverage + values: + - Release + - Preview + - Dev + - name: buildPlatforms + type: object + default: + - x64 + - arm64 variables: - name: runCodesignValidationInjectionBG @@ -10,18 +32,57 @@ variables: name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) stages: - - stage: Build_x64 - displayName: Build x64 + - stage: Build + displayName: Build dependsOn: [] condition: succeeded() jobs: - - template: ./templates/build-console-pgo.yml + - template: ./templates-v2/job-build-project.yml parameters: - platform: x64 - - stage: Publish_PGO_Databases - displayName: Publish PGO databases - dependsOn: ['Build_x64'] + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-OSS-L + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + name: SHINE-INT-L + branding: ${{ parameters.branding }} + buildPlatforms: ${{ parameters.buildPlatforms }} + buildConfigurations: [Release] + buildEverything: true + pgoBuildMode: Instrument + artifactStem: -instrumentation + + - stage: RunPGO + displayName: Run PGO + dependsOn: [Build] + condition: succeeded() + jobs: + - ${{ each platform in parameters.buildPlatforms }}: + - template: ./templates-v2/job-run-pgo-tests.yml + parameters: + # This job chooses its own pools based on platform + buildPlatform: ${{ platform }} + buildConfiguration: Release + artifactStem: -instrumentation + + - stage: FinalizePGO + displayName: Finalize PGO and Publish + dependsOn: [RunPGO] + condition: succeeded() jobs: - - template: ./templates/pgo-build-and-publish-nuget-job.yml + # This job takes multiple platforms and fans them back in to a single artifact. + - template: ./templates-v2/job-pgo-merge-pgd.yml + parameters: + jobName: MergePGD + pool: + vmImage: 'windows-2022' + buildConfiguration: Release + buildPlatforms: ${{ parameters.buildPlatforms }} + artifactStem: -instrumentation + + - template: ./templates-v2/job-pgo-build-nuget-and-publish.yml parameters: - pgoArtifact: 'PGO' + pool: + vmImage: 'windows-2022' + dependsOn: MergePGD + buildConfiguration: Release + artifactStem: -instrumentation diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index cea5c7d0fc5..fd388ea1d0e 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -14,22 +14,11 @@ parameters: values: - Release - Preview + - Dev - name: buildTerminal displayName: "Build Windows Terminal MSIX" type: boolean default: true - - name: runCompliance - displayName: "Run Compliance and Security Build" - type: boolean - default: true - - name: publishSymbolsToPublic - displayName: "Publish Symbols to MSDL" - type: boolean - default: true - - name: buildTerminalVPack - displayName: "Build Windows Terminal VPack" - type: boolean - default: false - name: buildConPTY displayName: "Build ConPTY NuGet" type: boolean @@ -47,19 +36,44 @@ parameters: - Instrument - None - name: buildConfigurations + displayName: "Build Configurations" type: object default: - Release - name: buildPlatforms + displayName: "Build Platforms" type: object default: - 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 + default: '0.0.8' + + - name: runCompliance + displayName: "Run Compliance and Security Build" + type: boolean + default: true + - name: publishSymbolsToPublic + displayName: "Publish Symbols to MSDL" + type: boolean + default: true + - name: publishVpackToWindows + displayName: "Publish VPack to Windows" + type: boolean + default: false variables: - MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe' - TerminalInternalPackageVersion: "0.0.8" # 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. @@ -84,671 +98,113 @@ variables: NuGetPackBetaVersion: experimental name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + resources: repositories: - repository: self type: git ref: main -jobs: -- job: Build - pool: - name: SHINE-INT-L # Run the compilation on the large agent pool, rather than the default small one. - demands: ImageOverride -equals SHINE-VS17-Latest - strategy: - matrix: - ${{ each config in parameters.buildConfigurations }}: - ${{ each platform in parameters.buildPlatforms }}: - ${{ config }}_${{ platform }}: - BuildConfiguration: ${{ config }} - BuildPlatform: ${{ platform }} - displayName: Build - timeoutInMinutes: 240 - cancelTimeoutInMinutes: 1 - steps: - - checkout: self - clean: true - submodules: true - persistCredentials: True - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - inputs: - disableOutputRedirect: true - - task: PowerShell@2 - displayName: Rationalize Build Platform - inputs: - targetType: inline - script: >- - $Arch = "$(BuildPlatform)" - - If ($Arch -Eq "x86") { $Arch = "Win32" } - - Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" - - template: .\templates\restore-nuget-steps.yml - - task: UniversalPackages@0 - displayName: Download terminal-internal Universal Package - inputs: - feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 - packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 - versionListDownload: $(TerminalInternalPackageVersion) - - task: TouchdownBuildTask@1 - displayName: Download Localization Files - inputs: - teamId: 7105 - authId: $(TouchdownAppId) - authKey: $(TouchdownAppKey) - resourceFilePath: >- - src\cascadia\TerminalApp\Resources\en-US\Resources.resw - - src\cascadia\TerminalApp\Resources\en-US\ContextMenu.resw - - src\cascadia\TerminalControl\Resources\en-US\Resources.resw - - src\cascadia\TerminalConnection\Resources\en-US\Resources.resw - - src\cascadia\TerminalSettingsModel\Resources\en-US\Resources.resw - - src\cascadia\TerminalSettingsEditor\Resources\en-US\Resources.resw - - src\cascadia\CascadiaPackage\Resources\en-US\Resources.resw - appendRelativeDir: true - localizationTarget: false - pseudoSetting: Included - - task: PowerShell@2 - displayName: Move Loc files one level up - inputs: - targetType: inline - script: >- - $Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw' - - $Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore } - pwsh: true - - task: PowerShell@2 - displayName: Copy the Context Menu Loc Resources to CascadiaPackage - inputs: - filePath: ./build/scripts/Copy-ContextMenuResourcesToCascadiaPackage.ps1 - pwsh: true - - task: PowerShell@2 - displayName: Generate NOTICE.html from NOTICE.md - inputs: - filePath: .\build\scripts\Generate-ThirdPartyNotices.ps1 - arguments: -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html - pwsh: true - - ${{ if eq(parameters.buildTerminal, true) }}: - - task: VSBuild@1 - displayName: Build solution **\OpenConsole.sln - condition: true - inputs: - solution: '**\OpenConsole.sln' - msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} /t:Terminal\CascadiaPackage /p:WindowsTerminalReleaseBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: binlog' - condition: failed() - continueOnError: True - inputs: - PathtoPublish: $(Build.SourcesDirectory)\msbuild.binlog - ArtifactName: binlog-$(BuildPlatform) - - task: PowerShell@2 - displayName: Check MSIX for common regressions - inputs: - targetType: inline - script: >- - $Package = Get-ChildItem -Recurse -Filter "CascadiaPackage_*.msix" - - .\build\scripts\Test-WindowsTerminalPackage.ps1 -Verbose -Path $Package.FullName - pwsh: true - - ${{ if eq(parameters.buildWPF, true) }}: - - task: VSBuild@1 - displayName: Build solution **\OpenConsole.sln for PublicTerminalCore - inputs: - solution: '**\OpenConsole.sln' - msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} /p:WindowsTerminalReleaseBuild=true /t:Terminal\wpf\PublicTerminalCore - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - - ${{ if eq(parameters.buildConPTY, true) }}: - - task: VSBuild@1 - displayName: Build solution **\OpenConsole.sln for ConPTY - inputs: - solution: '**\OpenConsole.sln' - msbuildArgs: /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} /p:WindowsTerminalReleaseBuild=true /t:Conhost\Host_EXE;Conhost\winconpty_DLL - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - - task: PowerShell@2 - displayName: Source Index PDBs - inputs: - filePath: build\scripts\Index-Pdbs.ps1 - arguments: -SearchDir '$(Build.SourcesDirectory)' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) - errorActionPreference: silentlyContinue - pwsh: true - - task: PowerShell@2 - displayName: Run Unit Tests - condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) - enabled: False - inputs: - filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' - - task: PowerShell@2 - displayName: Run Feature Tests - condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) - enabled: False - inputs: - filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' - - ${{ if eq(parameters.buildTerminal, true) }}: - - task: CopyFiles@2 - displayName: Copy *.msix and symbols to Artifacts - inputs: - Contents: >- - **/*.msix - - **/*.appxsym - TargetFolder: $(Build.ArtifactStagingDirectory)/appx - OverWrite: true - flattenFolders: true - - - pwsh: |- - $Package = (Get-ChildItem "$(Build.ArtifactStagingDirectory)/appx" -Recurse -Filter "Cascadia*.msix" | Select -First 1) - $PackageFilename = $Package.FullName - Write-Host "##vso[task.setvariable variable=WindowsTerminalPackagePath]${PackageFilename}" - & "$(MakeAppxPath)" unpack /p $PackageFilename /d "$(Build.SourcesDirectory)\UnpackedTerminalPackage" - displayName: Unpack the new Terminal package for signing - - - task: EsrpCodeSigning@1 - displayName: Submit Terminal's binaries for signing - inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a - FolderPath: '$(Build.SourcesDirectory)\UnpackedTerminalPackage' - signType: batchSigning - batchSignPolicyFile: '$(Build.SourcesDirectory)\build\config\ESRPSigning_Terminal.json' - - - pwsh: |- - $PackageFilename = "$(WindowsTerminalPackagePath)" - Remove-Item "$(Build.SourcesDirectory)\UnpackedTerminalPackage\CodeSignSummary*" - & "$(MakeAppxPath)" pack /h SHA256 /o /p $PackageFilename /d "$(Build.SourcesDirectory)\UnpackedTerminalPackage" - displayName: Re-pack the new Terminal package after signing - - - pwsh: |- - $XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName - & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/appx" - displayName: Build Unpackaged Distribution - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: 'Generate SBOM manifest (application)' - inputs: - BuildDropPath: '$(System.ArtifactsDirectory)/appx' - - - task: DropValidatorTask@0 - displayName: 'Validate application SBOM manifest' - inputs: - BuildDropPath: '$(System.ArtifactsDirectory)/appx' - OutputPath: 'output.json' - ValidateSignature: true - Verbosity: 'Verbose' - - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (Terminal app) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/appx - ArtifactName: terminal-$(BuildPlatform)-$(BuildConfiguration) - - - ${{ if eq(parameters.buildConPTY, true) }}: - - task: CopyFiles@2 - displayName: Copy ConPTY to Artifacts - inputs: - Contents: |- - $(Build.SourcesDirectory)/bin/**/conpty.dll - $(Build.SourcesDirectory)/bin/**/conpty.lib - $(Build.SourcesDirectory)/bin/**/conpty.pdb - $(Build.SourcesDirectory)/bin/**/OpenConsole.exe - $(Build.SourcesDirectory)/bin/**/OpenConsole.pdb - TargetFolder: $(Build.ArtifactStagingDirectory)/conpty - OverWrite: true - flattenFolders: true - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (ConPTY) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/conpty - ArtifactName: conpty-dll-$(BuildPlatform)-$(BuildConfiguration) - - ${{ if eq(parameters.buildWPF, true) }}: - - task: CopyFiles@2 - displayName: Copy PublicTerminalCore.dll to Artifacts - inputs: - Contents: >- - **/PublicTerminalCore.dll - TargetFolder: $(Build.ArtifactStagingDirectory)/wpf - OverWrite: true - flattenFolders: true - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (PublicTerminalCore) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/wpf - ArtifactName: wpf-dll-$(BuildPlatform)-$(BuildConfiguration) - - - task: PublishSymbols@2 - displayName: Publish symbols path - continueOnError: True - inputs: - SearchPattern: | - $(Build.SourcesDirectory)/bin/**/*.pdb - $(Build.SourcesDirectory)/bin/**/*.exe - $(Build.SourcesDirectory)/bin/**/*.dll - IndexSources: false - SymbolServerType: TeamServices - -- ${{ if eq(parameters.runCompliance, true) }}: - - template: ./templates/build-console-compliance-job.yml - -- ${{ if eq(parameters.buildTerminal, true) }}: - - job: BundleAndSign - displayName: Create and sign AppX/MSIX bundles - variables: - ${{ if eq(parameters.branding, 'Release') }}: - BundleStemName: Microsoft.WindowsTerminal - ${{ elseif eq(parameters.branding, 'Preview') }}: - BundleStemName: Microsoft.WindowsTerminalPreview - ${{ else }}: - BundleStemName: WindowsTerminalDev - dependsOn: Build - steps: - - checkout: self - clean: true - fetchDepth: 1 - submodules: true - persistCredentials: True - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - inputs: - disableOutputRedirect: true - - ${{ each platform in parameters.buildPlatforms }}: - - task: DownloadBuildArtifacts@1 - displayName: Download Artifacts ${{ platform }} - inputs: - # Make sure to download the entire artifact, because it includes the SPDX SBOM - artifactName: terminal-${{ platform }}-Release - # Downloading to the source directory should ensure that the later SBOM generator can see the earlier SBOMs. - downloadPath: '$(Build.SourcesDirectory)/appx-artifacts' - # Add 3000 to the major version component, but only for the bundle. - # This is to ensure that it is newer than "2022.xx.yy.zz" or whatever the original bundle versions were before - # we switched to uniform naming. - - pwsh: |- - $VersionEpoch = 3000 - $Components = "$(XES_APPXMANIFESTVERSION)" -Split "\." - $Components[0] = ([int]$Components[0] + $VersionEpoch) - $BundleVersion = $Components -Join "." - New-Item -Type Directory "$(System.ArtifactsDirectory)\bundle" - .\build\scripts\Create-AppxBundle.ps1 -InputPath "$(Build.SourcesDirectory)/appx-artifacts" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" - displayName: Create WindowsTerminal*.msixbundle - - task: EsrpCodeSigning@1 - displayName: Submit *.msixbundle to ESRP for code signing - inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a - FolderPath: $(System.ArtifactsDirectory)\bundle - Pattern: $(BundleStemName)*.msixbundle - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "Dynamic", - "CertTemplateName": "WINMSAPP1ST", - "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "OperationCode": "SigntoolSign", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "http://www.microsoft.com", - "FileDigest": "/fd \"SHA256\"", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - }, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "Dynamic", - "CertTemplateName": "WINMSAPP1ST", - "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "OperationCode": "SigntoolVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: 'Generate SBOM manifest (bundle)' - inputs: - BuildDropPath: '$(System.ArtifactsDirectory)/bundle' - BuildComponentPath: '$(Build.SourcesDirectory)/appx-artifacts' - - - task: DropValidatorTask@0 - displayName: 'Validate bundle SBOM manifest' - inputs: - BuildDropPath: '$(System.ArtifactsDirectory)/bundle' - OutputPath: 'output.json' - ValidateSignature: true - Verbosity: 'Verbose' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: appxbundle-signed' - inputs: - PathtoPublish: $(System.ArtifactsDirectory)\bundle - ArtifactName: appxbundle-signed - -- ${{ if eq(parameters.buildConPTY, true) }}: - - job: PackageAndSignConPTY - strategy: - matrix: - ${{ each config in parameters.buildConfigurations }}: - ${{ config }}: - BuildConfiguration: ${{ config }} - displayName: Create NuGet Package (ConPTY) - dependsOn: Build - steps: - - checkout: self - clean: true - fetchDepth: 1 - submodules: true - persistCredentials: True - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - inputs: - disableOutputRedirect: true - - ${{ each platform in parameters.buildPlatforms }}: - - task: DownloadBuildArtifacts@1 - displayName: Download ${{ platform }} ConPTY binaries - inputs: - artifactName: conpty-dll-${{ platform }}-$(BuildConfiguration) - downloadPath: bin\${{ platform }}\$(BuildConfiguration)\ - extractTars: false - - task: PowerShell@2 - displayName: Move downloaded artifacts around - inputs: - targetType: inline - # Find all artifact files and move them up a directory. Ugh. - script: |- - Get-ChildItem bin -Recurse -Directory -Filter conpty-dll-* | % { - $_ | Get-ChildItem -Recurse -File | % { - Move-Item -Verbose $_.FullName $_.Directory.Parent.FullName - } - } - Move-Item bin\x86 bin\Win32 - - - task: EsrpCodeSigning@1 - displayName: Submit ConPTY libraries and OpenConsole for code signing - inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a - FolderPath: '$(Build.SourcesDirectory)/bin' - signType: batchSigning - batchSignPolicyFile: '$(Build.SourcesDirectory)\build\config\ESRPSigning_ConPTY.json' - - - task: NuGetToolInstaller@1 - displayName: Use NuGet 5.10.0 - inputs: - versionSpec: 5.10.0 - - task: NuGetCommand@2 - displayName: NuGet pack - inputs: - command: pack - packagesToPack: $(Build.SourcesDirectory)\src\winconpty\package\winconpty.nuspec - packDestination: '$(Build.ArtifactStagingDirectory)/nupkg' - versioningScheme: byEnvVar - versionEnvVar: XES_PACKAGEVERSIONNUMBER - - task: EsrpCodeSigning@1 - displayName: Submit *.nupkg to ESRP for code signing - inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a - FolderPath: $(Build.ArtifactStagingDirectory)/nupkg - Pattern: '*.nupkg' - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (nupkg) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)\nupkg - ArtifactName: conpty-nupkg-$(BuildConfiguration) - - -- ${{ if eq(parameters.buildWPF, true) }}: - - job: PackageAndSignWPF - strategy: - matrix: - ${{ each config in parameters.buildConfigurations }}: - ${{ config }}: - BuildConfiguration: ${{ config }} - displayName: Create NuGet Package (WPF Terminal Control) - dependsOn: Build - steps: - - checkout: self - clean: true - fetchDepth: 1 - submodules: true - persistCredentials: True - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - inputs: - disableOutputRedirect: true - - ${{ each platform in parameters.buildPlatforms }}: - - task: DownloadBuildArtifacts@1 - displayName: Download ${{ platform }} PublicTerminalCore - inputs: - artifactName: wpf-dll-${{ platform }}-$(BuildConfiguration) - itemPattern: '**/*.dll' - downloadPath: bin\${{ platform }}\$(BuildConfiguration)\ - extractTars: false - - task: PowerShell@2 - displayName: Move downloaded artifacts around - inputs: - targetType: inline - # Find all artifact files and move them up a directory. Ugh. - script: |- - Get-ChildItem bin -Recurse -Directory -Filter wpf-dll-* | % { - $_ | Get-ChildItem -Recurse -File | % { - Move-Item -Verbose $_.FullName $_.Directory.Parent.FullName - } - } - Move-Item bin\x86 bin\Win32 - - task: NuGetToolInstaller@1 - displayName: Use NuGet 5.10.0 - inputs: - versionSpec: 5.10.0 - - task: NuGetCommand@2 - displayName: NuGet restore copy - inputs: - selectOrConfig: config - nugetConfigPath: NuGet.Config - - task: VSBuild@1 - displayName: Build solution **\OpenConsole.sln for WPF Control - inputs: - solution: '**\OpenConsole.sln' - msbuildArgs: /p:WindowsTerminalReleaseBuild=$(UseReleaseBranding);Version=$(XES_PACKAGEVERSIONNUMBER) /t:Pack - platform: Any CPU - configuration: $(BuildConfiguration) - maximumCpuCount: true - - task: PublishSymbols@2 - displayName: Publish symbols path - continueOnError: True - inputs: - SearchPattern: | - $(Build.SourcesDirectory)/bin/**/*.pdb - $(Build.SourcesDirectory)/bin/**/*.exe - $(Build.SourcesDirectory)/bin/**/*.dll - IndexSources: false - SymbolServerType: TeamServices - SymbolsArtifactName: Symbols_WPF_$(BuildConfiguration) - - task: CopyFiles@2 - displayName: Copy *.nupkg to Artifacts - inputs: - Contents: '**/*Wpf*.nupkg' - TargetFolder: $(Build.ArtifactStagingDirectory)/nupkg - OverWrite: true - flattenFolders: true - - task: EsrpCodeSigning@1 - displayName: Submit *.nupkg to ESRP for code signing - inputs: - ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a - FolderPath: $(Build.ArtifactStagingDirectory)/nupkg - Pattern: '*.nupkg' - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (nupkg) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)\nupkg - ArtifactName: wpf-nupkg-$(BuildConfiguration) - -- ${{ if eq(parameters.publishSymbolsToPublic, true) }}: - - job: PublishSymbols - displayName: Publish Symbols - dependsOn: BundleAndSign - steps: - - checkout: self - clean: true - fetchDepth: 1 - submodules: true - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - - - template: .\templates\restore-nuget-steps.yml - - # Download the terminal-PLATFORM-CONFIG-VERSION artifact for every platform/version combo - - ${{ each platform in parameters.buildPlatforms }}: - - task: DownloadBuildArtifacts@1 - displayName: Download Symbols ${{ platform }} - inputs: - artifactName: terminal-${{ platform }}-Release - itemPattern: '**/*.appxsym' - - # It seems easier to do this -- download every appxsym -- then enumerate all the PDBs in the build directory for the - # public symbol push. Otherwise, we would have to list all of the PDB files one by one. - - pwsh: |- - mkdir $(Build.SourcesDirectory)/appxsym-temp - Get-ChildItem "$(System.ArtifactsDirectory)" -Filter *.appxsym -Recurse | % { - $src = $_.FullName - $dest = Join-Path "$(Build.SourcesDirectory)/appxsym-temp/" $_.Name - - mkdir $dest - Write-Host "Extracting $src to $dest..." - tar -x -v -f $src -C $dest - } - displayName: Extract symbols for public consumption - - - task: PowerShell@2 - displayName: Source Index PDBs (the public ones) - inputs: - filePath: build\scripts\Index-Pdbs.ps1 - arguments: -SearchDir '$(Build.SourcesDirectory)/appxsym-temp' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) - pwsh: true - - # Publish the app symbols to the public MSDL symbol server - # accessible via https://msdl.microsoft.com/download/symbols - - task: PublishSymbols@2 - displayName: 'Publish app symbols to MSDL' - inputs: - symbolsFolder: '$(Build.SourcesDirectory)/appxsym-temp' - searchPattern: '**/*.pdb' - SymbolsMaximumWaitTime: 30 - SymbolServerType: 'TeamServices' - SymbolsProduct: 'Windows Terminal Application Binaries' - SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' - # The ADO task does not support indexing of GitHub sources. - indexSources: false - detailedLog: true - # 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. - # Copied from https://github.com/microsoft/icu/blob/f869c214adc87415dfe751d81f42f1bca55dcf5f/build/azure-nuget.yml#L564-L583 - env: - LIB: $(Build.SourcesDirectory) - ArtifactServices_Symbol_AccountName: microsoftpublicsymbols - ArtifactServices_Symbol_PAT: $(ADO_microsoftpublicsymbols_PAT) - - -- ${{ if eq(parameters.buildTerminalVPack, true) }}: - - job: VPack - displayName: Create Windows vPack - dependsOn: BundleAndSign - steps: - - checkout: self - clean: true - fetchDepth: 1 - submodules: true - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - - task: DownloadBuildArtifacts@1 - displayName: Download Build Artifacts - inputs: - artifactName: appxbundle-signed - extractTars: false - - task: PowerShell@2 - displayName: Rename and stage packages for vpack - inputs: - targetType: inline - script: >- - # Rename to known/fixed name for Windows build system - - Get-ChildItem Microsoft.WindowsTerminal_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } - - - # Create vpack directory and place item inside - mkdir WindowsTerminal.app +stages: + - stage: Build + displayName: Build + dependsOn: [] + jobs: + - template: ./templates-v2/job-build-project.yml + parameters: + pool: + name: SHINE-INT-L # Run the compilation on the large agent pool, rather than the default small one. + demands: ImageOverride -equals SHINE-VS17-Latest + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + 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: ./templates-v2/steps-fetch-and-prepare-localizations.yml + parameters: + includePseudoLoc: true + + - ${{ if eq(parameters.buildWPF, true) }}: + # Add an Any CPU build flavor for the WPF control bits + - template: ./templates-v2/job-build-project.yml + parameters: + # This job is allowed to run on the default small pool. + jobName: BuildWPF + branding: ${{ parameters.branding }} + buildTerminal: false + buildWPFDotNetComponents: true + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: + - Any CPU + generateSbom: ${{ parameters.generateSbom }} + 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: ./templates-v2/job-merge-msix-into-bundle.yml + parameters: + jobName: Bundle + branding: ${{ parameters.branding }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildConPTY, true) }}: + - template: ./templates-v2/job-package-conpty.yml + parameters: + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildWPF, true) }}: + - template: ./templates-v2/job-build-package-wpf.yml + parameters: + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - stage: Publish + displayName: Publish + dependsOn: [Build, Package] + jobs: + # We only support the vpack for Release builds that include Terminal + - ${{ if and(containsValue(parameters.buildConfigurations, 'Release'), parameters.buildTerminal, parameters.publishVpackToWindows) }}: + - template: ./templates-v2/job-submit-windows-vpack.yml + parameters: + buildConfiguration: Release + generateSbom: ${{ parameters.generateSbom }} + + - template: ./templates-v2/job-publish-symbols.yml + parameters: + includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} - mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ - workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed - - task: PkgESVPack@12 - displayName: 'Package ES - VPack' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - inputs: - sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed\WindowsTerminal.app - description: VPack for the Windows Terminal Application - pushPkgName: WindowsTerminal.app - owner: conhost - githubToken: $(GitHubTokenForVpackProvenance) - - task: PublishPipelineArtifact@1 - displayName: 'Copy VPack Manifest to Drop' - inputs: - targetPath: $(XES_VPACKMANIFESTDIRECTORY) - artifactName: VPackManifest - - task: PkgESFCIBGit@12 - displayName: 'Submit VPack Manifest to Windows' - inputs: - configPath: '$(Build.SourcesDirectory)\build\config\GitCheckin.json' - artifactsDirectory: $(XES_VPACKMANIFESTDIRECTORY) - prTimeOut: 5 ... diff --git a/build/pipelines/templates-v2/job-build-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml new file mode 100644 index 00000000000..fe668058b20 --- /dev/null +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -0,0 +1,134 @@ +parameters: + - name: buildConfigurations + type: object + - name: buildPlatforms + type: object + - name: generateSbom + type: boolean + default: false + - name: codeSign + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: PackWPF + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.codeSign, true) }}: + displayName: Pack and Sign Microsoft.Terminal.Wpf + ${{ else }}: + displayName: Pack Microsoft.Terminal.Wpf + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ config }}: + BuildConfiguration: ${{ config }} + dependsOn: ${{ parameters.dependsOn }} + variables: + OutputBuildPlatform: AnyCPU + Terminal.BinDir: $(Build.SourcesDirectory)/bin/$(OutputBuildPlatform)/$(BuildConfiguration) + steps: + - 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: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - template: steps-download-bin-dir-artifact.yml + parameters: + buildPlatforms: + - ${{ parameters.buildPlatforms }} + - Any CPU # Make sure we grab the precompiled WPF bits + # This build is already matrix'd on configuration, so + # just pass a single config into the download template. + buildConfigurations: + - $(BuildConfiguration) + artifactStem: ${{ parameters.artifactStem }} + + - template: .\steps-restore-nuget.yml + + - task: VSBuild@1 + displayName: Build solution OpenConsole.sln for WPF Control (Pack) + inputs: + solution: 'OpenConsole.sln' + msbuildArgs: >- + /p:WindowsTerminalReleaseBuild=true;Version=$(XES_PACKAGEVERSIONNUMBER) + /p:NoBuild=true + /p:IncludeSymbols=true + /t:Terminal\wpf\WpfTerminalControl:Pack + platform: Any CPU + configuration: $(BuildConfiguration) + maximumCpuCount: true + clean: false + + - task: CopyFiles@2 + displayName: Copy *.nupkg to Artifacts + inputs: + Contents: 'bin/**/*Wpf*.nupkg' + TargetFolder: $(Build.ArtifactStagingDirectory)/nupkg + OverWrite: true + flattenFolders: true + + - ${{ if eq(parameters.codeSign, true) }}: + - task: EsrpCodeSigning@1 + displayName: Submit *.nupkg to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + + - ${{ if eq(parameters.generateSbom, true) }}: + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest (wpf)' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/nupkg' + BuildComponentPath: '$(Build.SourcesDirectory)/bin' + + - task: DropValidatorTask@0 + displayName: 'Validate wpf SBOM manifest' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/nupkg' + OutputPath: 'output.json' + ValidateSignature: true + Verbosity: 'Verbose' + + - publish: $(Build.ArtifactStagingDirectory)\nupkg + artifact: wpf-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + displayName: Publish nupkg diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml new file mode 100644 index 00000000000..1c07bb9008e --- /dev/null +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -0,0 +1,263 @@ +parameters: + - name: branding + type: string + default: Dev + values: [Release, Preview, Dev] + - name: additionalBuildOptions + type: string + default: '' + - name: buildTerminal + type: boolean + default: true + - name: buildConPTY + type: boolean + default: false + - name: buildWPF + type: boolean + default: false + - name: buildWPFDotNetComponents # This weird hack is to make sure we sign and source index the .NET pieces + type: boolean + default: false + - name: buildEverything + displayName: "Build Everything (Overrides all other build options)" + type: boolean + default: false + - name: pgoBuildMode + type: string + default: None + values: [Optimize, Instrument, None] + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + - name: generateSbom + type: boolean + default: false + - name: codeSign + type: boolean + default: false + - name: keepAllExpensiveBuildOutputs + type: boolean + default: true + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: 'Build' + - name: pool + type: object + default: [] + - name: beforeBuildSteps + type: stepList + default: [] + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ each platform in parameters.buildPlatforms }}: + ${{ config }}_${{ platform }}: + BuildConfiguration: ${{ config }} + BuildPlatform: ${{ platform }} + ${{ if eq(platform, 'x86') }}: + OutputBuildPlatform: Win32 + ${{ elseif eq(platform, 'Any CPU') }}: + OutputBuildPlatform: AnyCPU + ${{ else }}: + OutputBuildPlatform: ${{ platform }} + variables: + MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe' + Terminal.BinDir: $(Build.SourcesDirectory)/bin/$(OutputBuildPlatform)/$(BuildConfiguration) + # Azure DevOps abhors a vacuum + # If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names* + # later on. We'll just... set them to a single space and if we need to, check IsNullOrWhiteSpace. + # Yup. + BuildTargetParameter: ' ' + SelectedSigningFragments: ' ' + displayName: Build + timeoutInMinutes: 240 + cancelTimeoutInMinutes: 1 + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + # This generates either nothing for BuildTargetParameter, or /t:X;Y;Z, to control targets later. + - pwsh: |- + If (-Not [bool]::Parse("${{ parameters.buildEverything }}")) { + $BuildTargets = @() + $SignFragments = @() + If ([bool]::Parse("${{ parameters.buildTerminal }}")) { + $BuildTargets += "Terminal\CascadiaPackage" + $SignFragments += "terminal_constituents" + } + If ([bool]::Parse("${{ parameters.buildWPFDotNetComponents }}")) { + $BuildTargets += "Terminal\wpf\WpfTerminalControl" + $SignFragments += "wpfdotnet" + } + If ([bool]::Parse("${{ parameters.buildWPF }}")) { + $BuildTargets += "Terminal\wpf\PublicTerminalCore" + $SignFragments += "wpf" + } + If ([bool]::Parse("${{ parameters.buildConPTY }}")) { + $BuildTargets += "Conhost\Host_EXE;Conhost\winconpty_DLL" + $SignFragments += "conpty" + } + Write-Host "Targets: $($BuildTargets -Join ";")" + Write-Host "Sign targets: $($SignFragments -Join ";")" + Write-Host "##vso[task.setvariable variable=BuildTargetParameter]/t:$($BuildTargets -Join ";")" + Write-Host "##vso[task.setvariable variable=SelectedSigningFragments]$($SignFragments -Join ";")" + } + displayName: Prepare Build and Sign Targets + + - pwsh: |- + .\build\scripts\Generate-ThirdPartyNotices.ps1 -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html + displayName: Generate NOTICE.html from NOTICE.md + + - template: .\steps-restore-nuget.yml + + - ${{ parameters.beforeBuildSteps }} + + - task: VSBuild@1 + displayName: Build OpenConsole.sln + inputs: + solution: 'OpenConsole.sln' + msbuildArgs: >- + /p:WindowsTerminalOfficialBuild=true;WindowsTerminalBranding=${{ parameters.branding }};PGOBuildMode=${{ parameters.pgoBuildMode }} + ${{ parameters.additionalBuildOptions }} + /bl:$(Build.SourcesDirectory)\msbuild.binlog + $(BuildTargetParameter) + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + maximumCpuCount: true + + - publish: $(Build.SourcesDirectory)/msbuild.binlog + artifact: logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + condition: always() + displayName: Publish Build Log + + # This saves ~2GiB per architecture. We won't need these later. + # Removes: + # - All .lib that do not have an associated .exp (which would indicate that they are import libs) + # - All .pdbs from those .libs (which were only used during linking) + # - 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. + - pwsh: |- + $binDir = '$(Terminal.BinDir)' + $ImportLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.exp' | ForEach-Object { $_.FullName -Replace "exp$","lib" } + $StaticLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.lib' | Where-Object FullName -NotIn $ImportLibs + + $Items = @() + $Items += $StaticLibs + $Items += Get-Item ($StaticLibs.FullName -Replace "lib$","pdb") -ErrorAction:Ignore + $Items += Get-ChildItem $binDir -Directory -Filter '*Lib' + $Items += Get-ChildItem $binDir -Directory -Filter 'LocalTests_*' + $Items += Get-ChildItem "${$binDir}\WindowsTerminal" -Filter '*.pdb' -ErrorAction:Ignore + + If (-Not [bool]::Parse('${{ parameters.keepAllExpensiveBuildOutputs }}')) { + $Items += Get-ChildItem '$(Terminal.BinDir)' -Filter '*.pdb' -Recurse + } + + $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 + + # We cannot index PDBs that we have deleted! + - ${{ if eq(parameters.keepAllExpensiveBuildOutputs, true) }}: + - pwsh: |- + build\scripts\Index-Pdbs.ps1 -SearchDir '$(Terminal.BinDir)' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) + displayName: Source Index PDBs + errorActionPreference: silentlyContinue + + - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: + - pwsh: |- + $Package = (Get-ChildItem -Recurse -Filter "CascadiaPackage*.msix" | Select -First 1) + $PackageFilename = $Package.FullName + Write-Host "##vso[task.setvariable variable=WindowsTerminalPackagePath]${PackageFilename}" + displayName: Locate the MSIX + + # CHECK EXCEPTION + # PGO requires a desktop CRT + - ${{ if ne(parameters.pgoBuildMode, 'Instrument') }}: + - pwsh: |- + .\build\scripts\Test-WindowsTerminalPackage.ps1 -Verbose -Path "$(WindowsTerminalPackagePath)" + displayName: Check MSIX for common regressions + condition: and(succeeded(), ne(variables.WindowsTerminalPackagePath, '')) + + - ${{ if eq(parameters.codeSign, true) }}: + - pwsh: |- + & "$(MakeAppxPath)" unpack /p "$(WindowsTerminalPackagePath)" /d "$(Terminal.BinDir)/PackageContents" + displayName: Unpack the MSIX for signing + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-create-signing-config.yml + parameters: + outFile: '$(Build.SourcesDirectory)/ESRPSigningConfig.json' + stage: build + fragments: $(SelectedSigningFragments) + + # 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 + displayName: Submit Signing Request + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: '$(Terminal.BinDir)' + signType: batchSigning + batchSignPolicyFile: '$(Build.SourcesDirectory)/ESRPSigningConfig.json' + + # We only need to re-pack the MSIX if we actually signed, so this can stay in the codeSign conditional + - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: + - pwsh: |- + $outDir = New-Item -Type Directory "$(Terminal.BinDir)/_appx" -ErrorAction:Ignore + $PackageFilename = Join-Path $outDir.FullName (Split-Path -Leaf "$(WindowsTerminalPackagePath)") + & "$(MakeAppxPath)" pack /h SHA256 /o /p $PackageFilename /d "$(Terminal.BinDir)/PackageContents" + Write-Host "##vso[task.setvariable variable=WindowsTerminalPackagePath]${PackageFilename}" + displayName: Re-pack the new Terminal package after signing + + - ${{ else }}: # No Signing + - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: + - pwsh: |- + $outDir = New-Item -Type Directory "$(Terminal.BinDir)/_appx" -ErrorAction:Ignore + $PackageFilename = Join-Path $outDir.FullName (Split-Path -Leaf "$(WindowsTerminalPackagePath)") + Copy-Item "$(WindowsTerminalPackagePath)" $PackageFilename + Write-Host "##vso[task.setvariable variable=WindowsTerminalPackagePath]${PackageFilename}" + displayName: Stage the package (unsigned) + condition: and(succeeded(), ne(variables.WindowsTerminalPackagePath, '')) + + - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: + - 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 + displayName: Build Unpackaged Distribution (from MSIX) + condition: and(succeeded(), ne(variables.WindowsTerminalPackagePath, '')) + + - ${{ if eq(parameters.generateSbom, true) }}: + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest' + inputs: + BuildDropPath: '$(Terminal.BinDir)' + + - task: DropValidatorTask@0 + displayName: 'Validate SBOM manifest' + inputs: + BuildDropPath: '$(Terminal.BinDir)' + OutputPath: 'output.json' + ValidateSignature: true + Verbosity: 'Verbose' + + - publish: $(Terminal.BinDir) + artifact: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + displayName: Publish All Outputs diff --git a/build/pipelines/templates-v2/job-check-code-format.yml b/build/pipelines/templates-v2/job-check-code-format.yml new file mode 100644 index 00000000000..e889fc8756d --- /dev/null +++ b/build/pipelines/templates-v2/job-check-code-format.yml @@ -0,0 +1,15 @@ +jobs: +- job: CodeFormatCheck + displayName: Check Code Format + pool: { vmImage: windows-2022 } + + steps: + - checkout: self + fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here + submodules: false + clean: true + + - powershell: |- + .\build\scripts\Invoke-FormattingCheck.ps1 + displayName: 'Run formatters' diff --git a/build/pipelines/templates/codenav-indexer.yml b/build/pipelines/templates-v2/job-index-github-codenav.yml similarity index 57% rename from build/pipelines/templates/codenav-indexer.yml rename to build/pipelines/templates-v2/job-index-github-codenav.yml index 04e018ed2a0..e2edf55e651 100644 --- a/build/pipelines/templates/codenav-indexer.yml +++ b/build/pipelines/templates-v2/job-index-github-codenav.yml @@ -1,21 +1,15 @@ -parameters: - artifactName: 'drop' - jobs: - job: CodeNavIndexer displayName: Run Github CodeNav Indexer - pool: { vmImage: windows-2019 } + pool: { vmImage: windows-2022 } steps: - checkout: self fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here submodules: false clean: true - - task: DownloadBuildArtifacts@0 - inputs: - artifactName: ${{ parameters.artifactName }} - - task: RichCodeNavIndexer@0 inputs: languages: 'cpp,csharp' diff --git a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml new file mode 100644 index 00000000000..318d3b2673a --- /dev/null +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -0,0 +1,133 @@ +parameters: + - name: branding + type: string + - name: buildConfigurations + type: object + - name: buildPlatforms + type: object + - name: generateSbom + type: boolean + default: false + - name: codeSign + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: Bundle + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.codeSign, true) }}: + displayName: Pack and Sign Terminal MSIXBundle + ${{ else }}: + displayName: Pack Terminal MSIXBundle + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ config }}: + BuildConfiguration: ${{ config }} + variables: + ${{ if eq(parameters.branding, 'Release') }}: + BundleStemName: Microsoft.WindowsTerminal + ${{ elseif eq(parameters.branding, 'Preview') }}: + BundleStemName: Microsoft.WindowsTerminalPreview + ${{ else }}: + BundleStemName: WindowsTerminalDev + dependsOn: ${{ parameters.dependsOn }} + steps: + - 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: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + - template: steps-download-bin-dir-artifact.yml + parameters: + buildPlatforms: ${{ parameters.buildPlatforms }} + # This build is already matrix'd on configuration, so + # just pass a single config into the download template. + buildConfigurations: + - $(BuildConfiguration) + artifactStem: ${{ parameters.artifactStem }} + + # Add 3000 to the major version component, but only for the bundle. + # This is to ensure that it is newer than "2022.xx.yy.zz" or whatever the original bundle versions were before + # we switched to uniform naming. + - pwsh: |- + $VersionEpoch = 3000 + $Components = "$(XES_APPXMANIFESTVERSION)" -Split "\." + $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" + displayName: Create msixbundle + + - ${{ if eq(parameters.codeSign, true) }}: + - task: EsrpCodeSigning@1 + displayName: Submit *.msixbundle to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(System.ArtifactsDirectory)\bundle + Pattern: $(BundleStemName)*.msixbundle + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + + - ${{ if eq(parameters.generateSbom, true) }}: + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest (bundle)' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/bundle' + BuildComponentPath: '$(Build.SourcesDirectory)/bin' + + - task: DropValidatorTask@0 + displayName: 'Validate bundle SBOM manifest' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/bundle' + OutputPath: 'output.json' + ValidateSignature: true + Verbosity: 'Verbose' + + - publish: '$(System.ArtifactsDirectory)/bundle' + artifact: appxbundle-$(BuildConfiguration)${{ parameters.artifactStem }} + displayName: Publish msixbundle diff --git a/build/pipelines/templates-v2/job-package-conpty.yml b/build/pipelines/templates-v2/job-package-conpty.yml new file mode 100644 index 00000000000..db648d905f2 --- /dev/null +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -0,0 +1,118 @@ +parameters: + - name: buildConfigurations + type: object + - name: buildPlatforms + type: object + - name: generateSbom + type: boolean + default: false + - name: codeSign + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: PackConPTY + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.codeSign, true) }}: + displayName: Pack and Sign Microsoft.Windows.Console.ConPTY + ${{ else }}: + displayName: Pack Microsoft.Windows.Console.ConPTY + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ config }}: + BuildConfiguration: ${{ config }} + dependsOn: ${{ parameters.dependsOn }} + steps: + - 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: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - template: steps-download-bin-dir-artifact.yml + parameters: + buildPlatforms: ${{ parameters.buildPlatforms }} + # This build is already matrix'd on configuration, so + # just pass a single config into the download template. + buildConfigurations: + - $(BuildConfiguration) + artifactStem: ${{ parameters.artifactStem }} + + - template: steps-ensure-nuget-version.yml + +# In the Microsoft Azure DevOps tenant, NuGetCommand is ambiguous. +# This should be `task: NuGetCommand@2` + - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 + displayName: NuGet pack + inputs: + command: pack + packagesToPack: $(Build.SourcesDirectory)\src\winconpty\package\winconpty.nuspec + packDestination: '$(Build.ArtifactStagingDirectory)/nupkg' + versioningScheme: byEnvVar + versionEnvVar: XES_PACKAGEVERSIONNUMBER + + - ${{ if eq(parameters.codeSign, true) }}: + - task: EsrpCodeSigning@1 + displayName: Submit *.nupkg to ESRP for code signing + inputs: + ConnectedServiceName: 9d6d2960-0793-4d59-943e-78dcb434840a + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + + - ${{ if eq(parameters.generateSbom, true) }}: + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest (conpty)' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/nupkg' + BuildComponentPath: '$(Build.SourcesDirectory)/bin' + + - task: DropValidatorTask@0 + displayName: 'Validate conpty SBOM manifest' + inputs: + BuildDropPath: '$(System.ArtifactsDirectory)/nupkg' + OutputPath: 'output.json' + ValidateSignature: true + Verbosity: 'Verbose' + + - publish: $(Build.ArtifactStagingDirectory)\nupkg + artifact: conpty-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + displayName: Publish nupkg diff --git a/build/pipelines/templates/pgo-build-and-publish-nuget-job.yml b/build/pipelines/templates-v2/job-pgo-build-nuget-and-publish.yml similarity index 57% rename from build/pipelines/templates/pgo-build-and-publish-nuget-job.yml rename to build/pipelines/templates-v2/job-pgo-build-nuget-and-publish.yml index 2c36f7cf158..b61a22c4fc1 100644 --- a/build/pipelines/templates/pgo-build-and-publish-nuget-job.yml +++ b/build/pipelines/templates-v2/job-pgo-build-nuget-and-publish.yml @@ -1,14 +1,26 @@ -# From our friends at MUX: https://github.com/microsoft/microsoft-ui-xaml/blob/main/build/AzurePipelinesTemplates/MUX-BuildAndPublishPGONuGet-Job.yml - parameters: - dependsOn: '' - pgoArtifact: PGO + - name: buildConfiguration + type: string + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: BuildAndPublishPGONuget jobs: -- job: BuildAndPublishPGONuGet +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} dependsOn: ${{ parameters.dependsOn }} - pool: - vmImage: 'windows-2019' + displayName: Package and Publish PGO Databases + variables: artifactsPath: $(Build.SourcesDirectory)\Artifacts pgoToolsPath: $(Build.SourcesDirectory)\build\PGO @@ -16,20 +28,25 @@ jobs: nuspecFilename: PGO.nuspec steps: - - task: DownloadBuildArtifacts@0 + - checkout: self + clean: true + # It is important that this be 0, otherwise git will not fetch the branch ref names that the PGO rules require. + fetchDepth: 0 + submodules: false + persistCredentials: false + + - task: DownloadPipelineArtifact@2 + displayName: Download Final PGO Databases inputs: - artifactName: ${{ parameters.pgoArtifact }} + artifact: pgd-merged-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} downloadPath: $(artifactsPath) + - template: steps-ensure-nuget-version.yml + - task: NuGetAuthenticate@0 inputs: nuGetServiceConnections: 'Terminal Public Artifact Feed' - - task: NuGetToolInstaller@0 - displayName: 'Use NuGet 5.8.0' - inputs: - versionSpec: 5.8.0 - # In the Microsoft Azure DevOps tenant, NuGetCommand is ambiguous. # This should be `task: NuGetCommand@2` - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 @@ -45,12 +62,11 @@ jobs: displayName: 'Create PGO Nuget' inputs: solution: $(pgoToolsPath)\PGO.DB.proj - msbuildArguments: '/t:CreatePGONuGet /p:PGOBuildMode=Instrument /p:PGDPathForAllArch=$(artifactsPath)\${{ parameters.pgoArtifact }} /p:PGOOutputPath=$(Build.ArtifactStagingDirectory)' + msbuildArguments: '/t:CreatePGONuGet /p:PGOBuildMode=Instrument /p:PGDPathForAllArch=$(artifactsPath) /p:PGOOutputPath=$(Build.ArtifactStagingDirectory)' - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.pgoArtifact }} + - publish: $(Build.ArtifactStagingDirectory) + artifact: pgo-nupkg-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + displayName: "Publish Pipeline Artifact" - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 displayName: 'NuGet push' diff --git a/build/pipelines/templates-v2/job-pgo-merge-pgd.yml b/build/pipelines/templates-v2/job-pgo-merge-pgd.yml new file mode 100644 index 00000000000..8b341b463eb --- /dev/null +++ b/build/pipelines/templates-v2/job-pgo-merge-pgd.yml @@ -0,0 +1,75 @@ +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: jobName + type: string + default: MergePGD + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + dependsOn: ${{ parameters.dependsOn }} + displayName: Merge PGO Counts for ${{ parameters.buildConfiguration }} + + steps: + # The environment variable VCToolsInstallDir isn't defined on lab machines, so we need to retrieve it ourselves. + - script: | + "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -Latest -requires Microsoft.Component.MSBuild -property InstallationPath > %TEMP%\vsinstalldir.txt + set /p _VSINSTALLDIR15=<%TEMP%\vsinstalldir.txt + del %TEMP%\vsinstalldir.txt + call "%_VSINSTALLDIR15%\Common7\Tools\VsDevCmd.bat" + echo VCToolsInstallDir = %VCToolsInstallDir% + echo ##vso[task.setvariable variable=VCToolsInstallDir]%VCToolsInstallDir% + displayName: 'Retrieve VC tools directory' + + - ${{ each platform in parameters.buildPlatforms }}: + - task: DownloadPipelineArtifact@2 + displayName: Download PGO Databases for ${{ platform }} + inputs: + artifactName: build-${{ platform }}-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + itemPattern: '**/*.pgd' + downloadPath: '$(Build.SourcesDirectory)/pgd/${{ platform }}/${{ parameters.buildConfiguration }}' + - task: DownloadPipelineArtifact@2 + displayName: Download PGO Counts for ${{ platform }} + inputs: + artifactName: pgc-intermediates-${{ platform }}-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + downloadPath: '$(Build.SourcesDirectory)/pgc/${{ platform }}/${{ parameters.buildConfiguration }}' + - pwsh: |- + $Arch = '${{ platform }}' + $Conf = '${{ parameters.buildConfiguration }}' + $PGCDir = '$(Build.SourcesDirectory)/pgc/${{ platform }}/${{ parameters.buildConfiguration }}' + $PGDDir = '$(Build.SourcesDirectory)/pgd/${{ platform }}/${{ parameters.buildConfiguration }}' + # Flatten the PGD directory + Get-ChildItem $PGDDir -Recurse -Filter *.pgd | Move-Item -Destination $PGDDir -Verbose + Get-ChildItem $PGCDir -Filter *.pgc | + ForEach-Object { + $Parts = $_.Name -Split "!"; + $_ | Add-Member Module $Parts[0] -PassThru + } | + Group-Object Module | + ForEach-Object { + & "$(VCToolsInstallDir)\bin\Hostx64\${{ platform }}\pgomgr.exe" /merge $_.Group.FullName "$PGDDir\$($_.Name).pgd" + } + displayName: Merge PGO Counts for ${{ platform }} + - task: CopyFiles@2 + displayName: 'Copy merged pgds to artifact staging' + inputs: + sourceFolder: '$(Build.SourcesDirectory)/pgd/${{ platform }}/${{ parameters.buildConfiguration }}' + contents: '**\*.pgd' + targetFolder: '$(Build.ArtifactStagingDirectory)\out-pgd\${{ platform }}' + + - publish: $(Build.ArtifactStagingDirectory)\out-pgd + artifact: pgd-merged-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + displayName: "Publish merged PGDs" diff --git a/build/pipelines/templates-v2/job-publish-symbols.yml b/build/pipelines/templates-v2/job-publish-symbols.yml new file mode 100644 index 00000000000..6c37f0f4cbd --- /dev/null +++ b/build/pipelines/templates-v2/job-publish-symbols.yml @@ -0,0 +1,81 @@ +parameters: + - name: includePublicSymbolServer + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: PublishSymbols + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.includePublicSymbolServer, true) }}: + displayName: Publish Symbols to Internal and MSDL + ${{ else }}: + displayName: Publish Symbols Internally + dependsOn: ${{ parameters.dependsOn }} + steps: + - 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: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - task: DownloadPipelineArtifact@2 + displayName: Download all PDBs from all prior build phases + inputs: + itemPattern: '**/*.pdb' + targetPath: '$(Build.SourcesDirectory)/bin' + + - task: PublishSymbols@2 + displayName: Publish Symbols (to current Azure DevOps tenant) + continueOnError: True + inputs: + SymbolsFolder: '$(Build.SourcesDirectory)/bin' + SearchPattern: '**/*.pdb' + IndexSources: false + DetailedLog: true + SymbolsMaximumWaitTime: 30 + SymbolServerType: 'TeamServices' + SymbolsProduct: 'Windows Terminal Converged Symbols' + SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' + env: + LIB: $(Build.SourcesDirectory) + + - ${{ if eq(parameters.includePublicSymbolServer, true) }}: + - task: PublishSymbols@2 + displayName: 'Publish symbols to MSDL' + continueOnError: True + inputs: + SymbolsFolder: '$(Build.SourcesDirectory)/bin' + SearchPattern: '**/*.pdb' + IndexSources: false + DetailedLog: true + SymbolsMaximumWaitTime: 30 + SymbolServerType: 'TeamServices' + SymbolsProduct: 'Windows Terminal Converged Symbols' + SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' + # 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. + # Copied from https://github.com/microsoft/icu/blob/f869c214adc87415dfe751d81f42f1bca55dcf5f/build/azure-nuget.yml#L564-L583 + env: + LIB: $(Build.SourcesDirectory) + ArtifactServices_Symbol_AccountName: microsoftpublicsymbols + ArtifactServices_Symbol_PAT: $(ADO_microsoftpublicsymbols_PAT) diff --git a/build/pipelines/templates-v2/job-run-pgo-tests.yml b/build/pipelines/templates-v2/job-run-pgo-tests.yml new file mode 100644 index 00000000000..5270ca1bf85 --- /dev/null +++ b/build/pipelines/templates-v2/job-run-pgo-tests.yml @@ -0,0 +1,83 @@ +parameters: + buildConfiguration: 'Release' + buildPlatform: '' + artifactStem: '' + testLogPath: '$(Build.BinariesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\testsOnBuildMachine.wtl' + +jobs: +- job: PGO${{ parameters.buildPlatform }}${{ parameters.buildConfiguration }} + displayName: PGO ${{ parameters.buildPlatform }} ${{ parameters.buildConfiguration }} + variables: + BuildConfiguration: ${{ parameters.buildConfiguration }} + BuildPlatform: ${{ parameters.buildPlatform }} + OutputBuildPlatform: ${{ parameters.buildPlatform }} + Terminal.BinDir: $(Build.SourcesDirectory)/bin/$(OutputBuildPlatform)/$(BuildConfiguration) + pool: + ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + ${{ if ne(parameters.buildPlatform, 'ARM64') }}: + name: SHINE-OSS-Testing-x64 + ${{ else }}: + name: SHINE-OSS-Testing-arm64 + ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: + ${{ if ne(parameters.buildPlatform, 'ARM64') }}: + name: SHINE-INT-Testing-x64 + ${{ else }}: + name: SHINE-INT-Testing-arm64 + + steps: + - checkout: self + submodules: false + clean: true + fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here + + - task: DownloadPipelineArtifact@2 + displayName: Download artifacts + inputs: + artifactName: build-${{ parameters.buildPlatform }}-$(BuildConfiguration)${{ parameters.artifactStem }} + downloadPath: $(Terminal.BinDir) + + # The tests expect Terminal to be an unpackaged distribution named terminal-0.0.1.0 (after the dev build version scheme) + # Extract to that folder explicitly and strip the embedded folder name from the unpackaged archive. + - powershell: |- + $TargetDirectory = New-Item -Type Directory (Join-Path "$(Terminal.BinDir)" "terminal-0.0.1.0") + & tar.exe -x -v -f (Get-Item "$(Terminal.BinDir)/_unpackaged/*.zip") -C $TargetDirectory.FullName --strip-components=1 + displayName: Extract the unpackaged build for PGO + + - template: steps-ensure-nuget-version.yml + + - powershell: |- + $Package = 'Microsoft.Internal.Windows.Terminal.TestContent' + $Version = '1.0.1' + & nuget.exe install $Package -Version $Version + Write-Host "##vso[task.setvariable variable=TerminalTestContentPath]$(Build.SourcesDirectory)\packages\${Package}.${Version}\content" + displayName: Install Test Content + + - task: PowerShell@2 + displayName: 'Run PGO Tests' + inputs: + targetType: filePath + filePath: build\scripts\Run-Tests.ps1 + arguments: >- + -MatchPattern '*UIA.Tests.dll' + -Platform '$(OutputBuildPlatform)' + -Configuration '$(BuildConfiguration)' + -LogPath '${{ parameters.testLogPath }}' + -Root "$(Terminal.BinDir)" + -AdditionalTaefArguments '/select:(@IsPGO=true)','/p:WTTestContent=$(TerminalTestContentPath)' + condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) + + - task: CopyFiles@2 + displayName: 'Copy PGO outputs to Artifacts' + condition: always() + inputs: + Contents: | + **/*.pgc + ${{ parameters.testLogPath }} + TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/pgc' + OverWrite: true + flattenFolders: true + + - publish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/pgc' + artifact: pgc-intermediates-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + condition: always() diff --git a/build/pipelines/templates-v2/job-submit-windows-vpack.yml b/build/pipelines/templates-v2/job-submit-windows-vpack.yml new file mode 100644 index 00000000000..2bed2899cf0 --- /dev/null +++ b/build/pipelines/templates-v2/job-submit-windows-vpack.yml @@ -0,0 +1,70 @@ +parameters: + - name: buildConfiguration + type: string + - name: generateSbom + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + +jobs: +- job: VPack + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + displayName: Create and Submit Windows vPack + dependsOn: ${{ parameters.dependsOn }} + steps: + - 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: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - task: DownloadPipelineArtifact@2 + displayName: Download MSIX Bundle Artifact + inputs: + artifactName: appxbundle-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + downloadPath: '$(Build.SourcesDirectory)/bundle' + + # Rename to known/fixed name for Windows build system + - powershell: |- + # Create vpack directory and place item inside + $TargetFolder = New-Item -Type Directory '$(Build.SourcesDirectory)/WindowsTerminal.app' + Get-ChildItem bundle/Microsoft.WindowsTerminal_*.msixbundle | Move-Item (Join-Path $TargetFolder.FullName 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle') -Verbose + displayName: Stage packages for vpack + + - task: PkgESVPack@12 + displayName: 'Package ES - VPack' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + sourceDirectory: '$(Build.SourcesDirectory)/WindowsTerminal.app' + description: VPack for the Windows Terminal Application + pushPkgName: WindowsTerminal.app + owner: conhost + githubToken: $(GitHubTokenForVpackProvenance) + + - publish: $(XES_VPACKMANIFESTDIRECTORY) + artifact: vpack-manifest${{ parameters.artifactStem }} + displayName: 'Publish VPack Manifest to Drop' + + - task: PkgESFCIBGit@12 + displayName: 'Submit VPack Manifest to Windows' + inputs: + configPath: '$(Build.SourcesDirectory)\build\config\GitCheckin.json' + artifactsDirectory: $(XES_VPACKMANIFESTDIRECTORY) + prTimeOut: 5 + diff --git a/build/pipelines/templates/test-console-ci.yml b/build/pipelines/templates-v2/job-test-project.yml similarity index 72% rename from build/pipelines/templates/test-console-ci.yml rename to build/pipelines/templates-v2/job-test-project.yml index 5a7db6475bd..483a32893ce 100644 --- a/build/pipelines/templates/test-console-ci.yml +++ b/build/pipelines/templates-v2/job-test-project.yml @@ -1,9 +1,8 @@ parameters: configuration: 'Release' platform: '' - additionalBuildArguments: '' - artifactName: 'drop' testLogPath: '$(Build.BinariesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\testsOnBuildMachine.wtl' + artifactStem: '' jobs: - job: Test${{ parameters.platform }}${{ parameters.configuration }} @@ -11,6 +10,11 @@ jobs: variables: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} + ${{ if eq(parameters.platform, 'x86') }}: + OutputBuildPlatform: Win32 + ${{ else }}: + OutputBuildPlatform: ${{ parameters.platform }} + Terminal.BinDir: $(Build.SourcesDirectory)/bin/$(OutputBuildPlatform)/$(BuildConfiguration) pool: ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: ${{ if ne(parameters.platform, 'ARM64') }}: @@ -28,26 +32,20 @@ jobs: submodules: false clean: true fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here - - task: DownloadBuildArtifacts@0 + - task: DownloadPipelineArtifact@2 + displayName: Download artifacts inputs: - artifactName: ${{ parameters.artifactName }} - - - task: PowerShell@2 - displayName: 'Rationalize build platform' - inputs: - targetType: inline - script: | - $Arch = "$(BuildPlatform)" - If ($Arch -Eq "x86") { $Arch = "Win32" } - Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" + artifactName: build-${{ parameters.platform }}-$(BuildConfiguration)${{ parameters.artifactStem }} + downloadPath: $(Terminal.BinDir) - task: PowerShell@2 displayName: 'Run Unit Tests' inputs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(System.ArtifactsDirectory)\\${{ parameters.artifactName }}\\$(BuildConfiguration)\\$(BuildPlatform)\\test" + arguments: -MatchPattern '*unit.test*.dll' -Platform '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - ${{ if or(eq(parameters.platform, 'x64'), eq(parameters.platform, 'arm64')) }}: @@ -56,7 +54,7 @@ jobs: inputs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(System.ArtifactsDirectory)\\${{ parameters.artifactName }}\\$(BuildConfiguration)\\$(BuildPlatform)\\test" + arguments: -MatchPattern '*feature.test*.dll' -Platform '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - task: PowerShell@2 @@ -89,4 +87,4 @@ jobs: flattenFolders: true - publish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test-logs' - artifact: TestLogs$(BuildPlatform)$(BuildConfiguration) + artifact: test-logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} diff --git a/build/pipelines/templates-v2/steps-create-signing-config.yml b/build/pipelines/templates-v2/steps-create-signing-config.yml new file mode 100644 index 00000000000..790d7a3fa86 --- /dev/null +++ b/build/pipelines/templates-v2/steps-create-signing-config.yml @@ -0,0 +1,35 @@ +parameters: + - name: stage + type: string + - name: outFile + type: string + - name: fragments + type: string + +# This build step template takes all files named "esrp.STAGE.batch.*.json" +# and merges them into a single output signing config. +# +# We generate the batch signing config by sticking together multiple "batches". +# The filter below (with Fragments) works by splitting the filename, esrp.s.batch.x.json, +# to get 'x' and then checking whether x is in Fragments. +# We have to manually strip comments out of the batch fragments due to https://github.com/PowerShell/PowerShell/issues/14553 + +steps: + - pwsh: |- + $SignBatchFiles = (Get-Item build/config/esrp.${{ parameters.stage }}.batch.*.json) + $Fragments = "${{ parameters.fragments }}" + If (-Not [String]::IsNullOrWhiteSpace($Fragments)) { + $FragmentList = $Fragments -Split ";" + If ($FragmentList.Length -Gt 0) { + $SignBatchFiles = $SignBatchFiles | Where-Object { ($_.Name -Split '\.')[3] -In $FragmentList } + } + } + Write-Host "Found $(@($SignBatchFiles).Length) Signing Configs" + Write-Host ($SignBatchFiles.Name -Join ";") + $FinalSignConfig = @{ + Version = "1.0.0"; + UseMinimatch = $false; + SignBatches = @($SignBatchFiles | ForEach-Object { Get-Content $_ | Where-Object { $_ -NotMatch "^\s*\/\/" } | ConvertFrom-Json -Depth 10 }); + } + $FinalSignConfig | ConvertTo-Json -Depth 10 | Out-File -Encoding utf8 "${{ parameters.outFile }}" + displayName: Merge ${{ parameters.stage }} signing configs (${{ parameters.outFile }}) diff --git a/build/pipelines/templates-v2/steps-download-bin-dir-artifact.yml b/build/pipelines/templates-v2/steps-download-bin-dir-artifact.yml new file mode 100644 index 00000000000..e5e90cc8ea2 --- /dev/null +++ b/build/pipelines/templates-v2/steps-download-bin-dir-artifact.yml @@ -0,0 +1,24 @@ +parameters: + - name: buildConfigurations + type: object + - name: buildPlatforms + type: object + - name: artifactStem + type: string + default: '' + +steps: +- ${{ each configuration in parameters.buildConfigurations }}: + - ${{ each platform in parameters.buildPlatforms }}: + - task: DownloadPipelineArtifact@2 + displayName: Download artifacts for ${{ platform }} ${{ configuration }} + inputs: + # Make sure to download the entire artifact, because it includes the SPDX SBOM + artifactName: build-${{ platform }}-${{ configuration }}${{ parameters.artifactStem }} + # Downloading to the source directory should ensure that the later SBOM generator can see the earlier SBOMs. + ${{ if eq(platform, 'x86') }}: + downloadPath: '$(Build.SourcesDirectory)/bin/Win32/${{ configuration }}' + ${{ elseif eq(platform, 'Any CPU') }}: + downloadPath: '$(Build.SourcesDirectory)/bin/AnyCPU/${{ configuration }}' + ${{ else }}: + downloadPath: '$(Build.SourcesDirectory)/bin/${{ platform }}/${{ configuration }}' diff --git a/build/pipelines/templates-v2/steps-ensure-nuget-version.yml b/build/pipelines/templates-v2/steps-ensure-nuget-version.yml new file mode 100644 index 00000000000..fb5cd75e4c8 --- /dev/null +++ b/build/pipelines/templates-v2/steps-ensure-nuget-version.yml @@ -0,0 +1,5 @@ +steps: +- task: NuGetToolInstaller@1 + displayName: Use NuGet 6.6.1 + inputs: + versionSpec: 6.6.1 diff --git a/build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml b/build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml new file mode 100644 index 00000000000..8ace357aad3 --- /dev/null +++ b/build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml @@ -0,0 +1,27 @@ +parameters: + - name: includePseudoLoc + type: boolean + default: true + +steps: + - task: TouchdownBuildTask@1 + displayName: Download Localization Files + inputs: + teamId: 7105 + authId: $(TouchdownAppId) + authKey: $(TouchdownAppKey) + resourceFilePath: | + src\cascadia\**\en-US\*.resw + appendRelativeDir: true + localizationTarget: false + ${{ if eq(parameters.includePseudoLoc, true) }}: + pseudoSetting: Included + + - pwsh: |- + $Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw' + $Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore } + displayName: Move Loc files into final locations + + - pwsh: |- + ./build/scripts/Copy-ContextMenuResourcesToCascadiaPackage.ps1 + displayName: Copy the Context Menu Loc Resources to CascadiaPackage diff --git a/build/pipelines/templates/restore-nuget-steps.yml b/build/pipelines/templates-v2/steps-restore-nuget.yml similarity index 88% rename from build/pipelines/templates/restore-nuget-steps.yml rename to build/pipelines/templates-v2/steps-restore-nuget.yml index aabae84504c..bd0c067531c 100644 --- a/build/pipelines/templates/restore-nuget-steps.yml +++ b/build/pipelines/templates-v2/steps-restore-nuget.yml @@ -1,14 +1,12 @@ steps: -- task: NuGetToolInstaller@0 - displayName: 'Use NuGet 6.3.0' - inputs: - versionSpec: 6.3.0 +- template: steps-ensure-nuget-version.yml - task: NuGetAuthenticate@0 - script: |- echo ##vso[task.setvariable variable=NUGET_RESTORE_MSBUILD_ARGS]/p:Platform=$(BuildPlatform) displayName: Ensure NuGet restores for $(BuildPlatform) + condition: and(succeeded(), ne(variables['BuildPlatform'], 'Any CPU')) # In the Microsoft Azure DevOps tenant, NuGetCommand is ambiguous. # This should be `task: NuGetCommand@2` diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml deleted file mode 100644 index 69b0b05ce31..00000000000 --- a/build/pipelines/templates/build-console-audit-job.yml +++ /dev/null @@ -1,34 +0,0 @@ -parameters: - platform: '' - additionalBuildArguments: '' - -jobs: -- job: Build${{ parameters.platform }}AuditMode - displayName: Static Analysis Build ${{ parameters.platform }} - variables: - BuildConfiguration: AuditMode - BuildPlatform: ${{ parameters.platform }} - pool: - ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-OSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-INT-L - demands: ImageOverride -equals SHINE-VS17-Latest - - steps: - - checkout: self - submodules: true - clean: true - fetchDepth: 1 - - - template: restore-nuget-steps.yml - - - task: VSBuild@1 - displayName: 'Build solution **\OpenConsole.sln' - inputs: - solution: '**\OpenConsole.sln' - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: ${{ parameters.additionalBuildArguments }} - clean: true - maximumCpuCount: true diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml deleted file mode 100644 index 48c27052e29..00000000000 --- a/build/pipelines/templates/build-console-ci.yml +++ /dev/null @@ -1,31 +0,0 @@ -parameters: - configuration: 'Release' - branding: 'Dev' - platform: '' - additionalBuildArguments: '' - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }}${{ parameters.branding }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} ${{ parameters.branding }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - WindowsTerminalBranding: ${{ parameters.branding }} - EnableRichCodeNavigation: true - pool: - ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-OSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-INT-L - demands: ImageOverride -equals SHINE-VS17-Latest - - steps: - - template: build-console-steps.yml - parameters: - additionalBuildArguments: ${{ parameters.additionalBuildArguments }} - - # It appears that the Component Governance build task that gets automatically injected stopped working - # when we renamed our main branch. - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Detection' - condition: and(succeededOrFailed(), not(eq(variables['Build.Reason'], 'PullRequest'))) diff --git a/build/pipelines/templates/build-console-compliance-job.yml b/build/pipelines/templates/build-console-compliance-job.yml deleted file mode 100644 index 0f885cfcce7..00000000000 --- a/build/pipelines/templates/build-console-compliance-job.yml +++ /dev/null @@ -1,203 +0,0 @@ -jobs: -- job: Compliance - # We don't *need* a matrix but there's no other way to set parameters on a "job" - # in the AzDO YAML syntax. It would have to be a "stage" or a "template". - # Doesn't matter. We're going to do compliance on Release x64 because - # that's the one all the tooling works against for sure. - strategy: - matrix: - Release_x64: - BuildConfiguration: Release - BuildPlatform: x64 - displayName: Validate Security and Compliance - timeoutInMinutes: 240 - steps: - - checkout: self - clean: true - submodules: true - persistCredentials: True - - task: PkgESSetupBuild@12 - displayName: Package ES - Setup Build - inputs: - disableOutputRedirect: true - - task: PowerShell@2 - displayName: Rationalize Build Platform - inputs: - targetType: inline - script: >- - $Arch = "$(BuildPlatform)" - - If ($Arch -Eq "x86") { $Arch = "Win32" } - - Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" - - template: restore-nuget-steps.yml - - task: UniversalPackages@0 - displayName: Download terminal-internal Universal Package - inputs: - feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 - packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 - versionListDownload: $(TerminalInternalPackageVersion) - - task: TouchdownBuildTask@1 - displayName: Download Localization Files - inputs: - teamId: 7105 - authId: $(TouchdownAppId) - authKey: $(TouchdownAppKey) - resourceFilePath: >- - src\cascadia\TerminalApp\Resources\en-US\Resources.resw - - src\cascadia\TerminalControl\Resources\en-US\Resources.resw - - src\cascadia\TerminalConnection\Resources\en-US\Resources.resw - - src\cascadia\TerminalSettingsModel\Resources\en-US\Resources.resw - - src\cascadia\TerminalSettingsEditor\Resources\en-US\Resources.resw - - src\cascadia\CascadiaPackage\Resources\en-US\Resources.resw - appendRelativeDir: true - localizationTarget: false - pseudoSetting: Included - - task: PowerShell@2 - displayName: Move Loc files one level up - inputs: - targetType: inline - script: >- - $Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw' - - $Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore } - pwsh: true - - # 1ES Component Governance onboarding (Detects open source components). See https://docs.opensource.microsoft.com/tools/cg.html - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: Component Detection - - # # PREfast and PoliCheck need Node. Install that first. - - task: NodeTool@0 - - # !!! NOTE !!! Run PREfast first. Some of the other tasks are going to run on a completed build. - # PREfast is going to build the code as a part of its analysis and the generated sources - # and output binaries will be sufficient for the rest of the analysis. - # If you disable this, the other tasks won't likely work. You would have to add a build - # step instead that builds the code normally before calling them. - # Also... PREfast will rebuild anyway so that's why we're not running a normal build first. - # Waste of time to build twice. - # PREfast. See https://www.1eswiki.com/wiki/SDL_Native_Rules_Build_Task - - # The following 1ES tasks all operate completely differently and have a different syntax for usage. - # Most notable is every one of them has a different way of excluding things. - # Go see their 1eswiki.com pages to figure out how to exclude things. - # When writing exclusions, try to make them narrow so when new projects/binaries are added, they - # cause an error here and have to be explicitly pulled out. Don't write an exclusion so broad - # that it will catch other new stuff. - - # https://www.1eswiki.com/wiki/PREfast_Build_Task - # Builds the project with C/C++ static analysis tools to find coding flaws and vulnerabilities - # !!! WARNING !!! It doesn't work with WAPPROJ packaging projects. Build the sub-projects instead. - - task: securedevelopmentteam.vss-secure-development-tools.build-task-prefast.SDLNativeRules@3 - displayName: 'Run the PREfast SDL Native Rules for MSBuild' - condition: succeededOrFailed() - inputs: - setupCommandlines: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsMSBuildCmd.bat"' - msBuildCommandline: msbuild.exe /nologo /m /p:WindowsTerminalOfficialBuild=true /p:WindowsTerminalBranding=${{ parameters.branding }} /p:WindowsTerminalReleaseBuild=true /p:platform=$(BuildPlatform) /p:configuration=$(BuildConfiguration) /t:Terminal\Window\WindowsTerminal /p:VisualStudioVersion=17.0 $(Build.SourcesDirectory)\OpenConsole.sln - msBuildVersion: "17.0" - - # Copies output from PREfast SDL Native Rules task to expected location for consumption by PkgESSecComp - - task: CopyFiles@1 - displayName: 'Copy PREfast xml files to SDLNativeRulesDir' - inputs: - SourceFolder: '$(Agent.BuildDirectory)' - Contents: | - **\*.nativecodeanalysis.xml - TargetFolder: '$(Agent.BuildDirectory)\_sdt\logs\SDLNativeRules' - - # https://www.1eswiki.com/index.php?title=PoliCheck_Build_Task - # Scans the text of source code, comments, and content for terminology that could be sensitive for legal, cultural, or geopolitical reasons. - # (Also finds vulgarities... takes all the fun out of everything.) - - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@2 - displayName: 'Run PoliCheck' - inputs: - targetType: F - targetArgument: $(Build.SourcesDirectory) - result: PoliCheck.xml - optionsFC: 1 - optionsXS: 1 - optionsUEPath: $(Build.SourcesDirectory)\build\config\PolicheckExclusions.xml - optionsHMENABLE: 0 - continueOnError: true - - # https://www.1eswiki.com/wiki/CredScan_Azure_DevOps_Build_Task - # Searches through source code and build outputs for a credential left behind in the open - - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 - displayName: 'Run CredScan' - inputs: - outputFormat: pre - # suppressionsFile: LocalSuppressions.json - batchSize: 20 - debugMode: false - continueOnError: true - - # https://www.1eswiki.com/wiki/BinSkim_Build_Task - # Searches managed and unmanaged binaries for known security vulnerabilities. - - task: securedevelopmentteam.vss-secure-development-tools.build-task-binskim.BinSkim@4 - displayName: 'Run BinSkim' - inputs: - TargetPattern: guardianGlob - # See https://aka.ms/gdn-globs for how to do match patterns - AnalyzeTargetGlob: $(Build.SourcesDirectory)\bin\**\*.dll;$(Build.SourcesDirectory)\bin\**\*.exe;-:file|**\Microsoft.UI.Xaml.dll;-:file|**\Microsoft.Toolkit.Win32.UI.XamlHost.dll;-:file|**\vcruntime*.dll;-:file|**\vcomp*.dll;-:file|**\vccorlib*.dll;-:file|**\vcamp*.dll;-:file|**\msvcp*.dll;-:file|**\concrt*.dll;-:file|**\TerminalThemeHelpers*.dll - continueOnError: true - - # Set XES_SERIALPOSTBUILDREADY to run Security and Compliance task once per build - - powershell: Write-Host "##vso[task.setvariable variable=XES_SERIALPOSTBUILDREADY;]true" - displayName: 'Set XES_SERIALPOSTBUILDREADY Vars' - - # https://www.osgwiki.com/wiki/Package_ES_Security_and_Compliance - # Does a few things: - # - Ensures that Windows-required compliance tasks are run either inside this task - # or were run as a previous step prior to this one - # (PREfast, PoliCheck, Credscan) - # - Runs Windows-specific compliance tasks inside the task - # + CheckCFlags - ensures that compiler and linker flags meet Windows standards - # + CFGCheck/XFGCheck - ensures that Control Flow Guard (CFG) or - # eXtended Flow Guard (XFG) are enabled on binaries - # NOTE: CFG is deprecated and XFG isn't fully ready yet. - # NOTE2: CFG fails on an XFG'd binary - # - Brokers all security/compliance task logs to "Trust Services Automation (TSA)" (https://aka.ms/tsa) - # which is a system that maps all errors into the appropriate bug database - # template for each organization since they all vary. It should also suppress - # new bugs when one already exists for the product. - # This one is set up to go to the OS repository and use the given parameters - # to file bugs to our AzDO product path. - # If we don't use PkgESSecComp to do this for us, we need to install the TSA task - # ourselves in this pipeline to finalize data upload and bug creation. - # !!! NOTE !!! This task goes *LAST* after any other compliance tasks so it catches their logs - - task: PkgESSecComp@10 - displayName: 'Security and Compliance tasks' - inputs: - fileNewBugs: false - areaPath: 'OS\WDX\DXP\WinDev\Terminal' - teamProject: 'OS' - iterationPath: 'OS\Future' - bugTags: 'TerminalReleaseCompliance' - scanAll: true - errOnBugs: false - failOnStdErr: true - taskLogVerbosity: Diagnostic - secCompConfigFromTask: | - # Overrides default build sources directory - sourceTargetOverrideAll: $(Build.SourcesDirectory) - # Overrides default build binaries directory when "Scan all" option is specified - binariesTargetOverrideAll: $(Build.SourcesDirectory)\bin - - # Set the tools to false if they should not run in the build - tools: - - toolName: CheckCFlags - enable: true - - toolName: CFGCheck - enable: true - - toolName: Policheck - enable: false - - toolName: CredScan - enable: false - - toolName: XFGCheck - enable: false diff --git a/build/pipelines/templates/build-console-fuzzing.yml b/build/pipelines/templates/build-console-fuzzing.yml deleted file mode 100644 index 8277ab9dc3a..00000000000 --- a/build/pipelines/templates/build-console-fuzzing.yml +++ /dev/null @@ -1,90 +0,0 @@ -parameters: - configuration: 'Fuzzing' - platform: '' - additionalBuildArguments: '' - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - pool: - ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-OSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-INT-L - demands: ImageOverride -equals SHINE-VS17-Latest - - steps: - - checkout: self - submodules: true - clean: true - - - template: restore-nuget-steps.yml - - # The environment variable VCToolsInstallDir isn't defined on lab machines, so we need to retrieve it ourselves. - - script: | - "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -Latest -requires Microsoft.Component.MSBuild -property InstallationPath > %TEMP%\vsinstalldir.txt - set /p _VSINSTALLDIR15=<%TEMP%\vsinstalldir.txt - del %TEMP%\vsinstalldir.txt - call "%_VSINSTALLDIR15%\Common7\Tools\VsDevCmd.bat" - echo VCToolsInstallDir = %VCToolsInstallDir% - echo ##vso[task.setvariable variable=VCToolsInstallDir]%VCToolsInstallDir% - displayName: 'Retrieve VC tools directory' - - - task: VSBuild@1 - displayName: 'Build solution **\OpenConsole.sln' - inputs: - solution: '**\OpenConsole.sln' - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: "${{ parameters.additionalBuildArguments }}" - clean: true - maximumCpuCount: true - - - task: PowerShell@2 - displayName: 'Rationalize build platform' - inputs: - targetType: inline - script: | - $Arch = "$(BuildPlatform)" - If ($Arch -Eq "x86") { $Arch = "Win32" } - Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" - - - task: CopyFiles@2 - displayName: 'Copy result logs to Artifacts' - inputs: - Contents: | - **/*.wtl - **/*onBuildMachineResults.xml - ${{ parameters.testLogPath }} - TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test' - OverWrite: true - flattenFolders: true - - - task: CopyFiles@2 - displayName: 'Copy outputs needed for test runs to Artifacts' - inputs: - Contents: | - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.exe - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.dll - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.xml - **/Microsoft.VCLibs.*.appx - **/TestHostApp/*.exe - **/TestHostApp/*.dll - **/TestHostApp/*.xml - !**/*.pdb - !**/*.ipdb - !**/*.obj - !**/*.pch - TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test' - OverWrite: true - flattenFolders: true - condition: succeeded() - - - task: PublishBuildArtifacts@1 - displayName: 'Publish All Build Artifacts' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'fuzzingBuildOutput' diff --git a/build/pipelines/templates/build-console-pgo.yml b/build/pipelines/templates/build-console-pgo.yml deleted file mode 100644 index b1b4c501633..00000000000 --- a/build/pipelines/templates/build-console-pgo.yml +++ /dev/null @@ -1,55 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - additionalBuildArguments: '' - minimumExpectedTestsExecutedCount: 1 # Sanity check for minimum expected tests to be reported - rerunPassesRequiredToAvoidFailure: 5 - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - PGOBuildMode: 'Instrument' - pool: - ${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-OSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: SHINE-INT-L - demands: ImageOverride -equals SHINE-VS17-Latest - - steps: - - template: build-console-steps.yml - parameters: - additionalBuildArguments: '${{ parameters.additionalBuildArguments }}' - -- template: helix-runtests-job.yml - parameters: - name: 'RunTestsInHelix' - dependsOn: Build${{ parameters.platform }}${{ parameters.configuration }} - condition: succeeded() - testSuite: 'PgoInstrumentationSuite' - taefQuery: '@IsPgo=true' - configuration: ${{ parameters.configuration }} - platform: ${{ parameters.platform }} - rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} - -- template: helix-processtestresults-job.yml - parameters: - name: 'ProcessTestResults' - pgoArtifact: 'PGO' - dependsOn: - - RunTestsInHelix - condition: succeededOrFailed() - rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} - minimumExpectedTestsExecutedCount: ${{ parameters.minimumExpectedTestsExecutedCount }} - -- template: pgo-merge-pgd-job.yml - parameters: - name: 'MergePGD' - dependsOn: - - ProcessTestResults - pgoArtifact: 'PGO' - platform: ${{ parameters.platform }} - configuration: ${{ parameters.configuration }} diff --git a/build/pipelines/templates/build-console-steps.yml b/build/pipelines/templates/build-console-steps.yml deleted file mode 100644 index 947dd353553..00000000000 --- a/build/pipelines/templates/build-console-steps.yml +++ /dev/null @@ -1,137 +0,0 @@ -parameters: - additionalBuildArguments: '' - -steps: -- checkout: self - submodules: true - clean: true - fetchDepth: 1 - -- template: restore-nuget-steps.yml - -# The environment variable VCToolsInstallDir isn't defined on lab machines, so we need to retrieve it ourselves. -- script: | - "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -Latest -requires Microsoft.Component.MSBuild -property InstallationPath > %TEMP%\vsinstalldir.txt - set /p _VSINSTALLDIR15=<%TEMP%\vsinstalldir.txt - del %TEMP%\vsinstalldir.txt - call "%_VSINSTALLDIR15%\Common7\Tools\VsDevCmd.bat" - echo VCToolsInstallDir = %VCToolsInstallDir% - echo ##vso[task.setvariable variable=VCToolsInstallDir]%VCToolsInstallDir% - displayName: 'Retrieve VC tools directory' - -- task: CmdLine@1 - displayName: 'Display build machine environment variables' - inputs: - filename: 'set' - -- task: VSBuild@1 - displayName: 'Build solution **\OpenConsole.sln' - inputs: - solution: '**\OpenConsole.sln' - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: "${{ parameters.additionalBuildArguments }} /p:PGOBuildMode=$(PGOBuildMode) /bl:$(Build.SourcesDirectory)\\msbuild.binlog" - clean: true - maximumCpuCount: true - -- task: PowerShell@2 - displayName: 'Check MSIX for common regressions' - # PGO runtime needs its own CRT and it's in the package for convenience. - # That will make this script mad so skip since we're not shipping the PGO Instrumentation one anyway. - condition: ne(variables['PGOBuildMode'], 'Instrument') - inputs: - targetType: inline - script: | - $Package = Get-ChildItem -Recurse -Filter "CascadiaPackage_*.msix" - .\build\scripts\Test-WindowsTerminalPackage.ps1 -Verbose -Path $Package.FullName - -- task: powershell@2 - displayName: 'Source Index PDBs' - condition: ne(variables['PGOBuildMode'], 'Instrument') - inputs: - targetType: filePath - filePath: build\scripts\Index-Pdbs.ps1 - arguments: -SearchDir '$(Build.SourcesDirectory)' -SourceRoot '$(Build.SourcesDirectory)' -recursive -Verbose -CommitId $(Build.SourceVersion) - errorActionPreference: silentlyContinue - -- task: PowerShell@2 - displayName: 'Rationalize build platform' - inputs: - targetType: inline - script: | - $Arch = "$(BuildPlatform)" - If ($Arch -Eq "x86") { $Arch = "Win32" } - Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" - -- task: CopyFiles@2 - displayName: 'Copy *.msix to Artifacts' - inputs: - Contents: | - **/*.msix - **/*.appxsym - TargetFolder: '$(Build.ArtifactStagingDirectory)/appx' - OverWrite: true - flattenFolders: true - -- pwsh: |- - $TerminalMsixPath = (Get-Item "$(Build.ArtifactStagingDirectory)\appx\Cascadia*.msix").FullName - $XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName - & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $TerminalMsixPath -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/unpackaged" - displayName: Build Unpackaged Distribution - -- publish: $(Build.ArtifactStagingDirectory)/unpackaged - artifact: unpackaged-$(BuildPlatform)-$(BuildConfiguration) - displayName: Publish Artifact (unpackaged) - -- task: CopyFiles@2 - displayName: 'Copy outputs needed for test runs to Artifacts' - inputs: - Contents: | - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.exe - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.dll - $(Build.SourcesDirectory)/bin/$(RationalizedBuildPlatform)/$(BuildConfiguration)/*.xml - **/Microsoft.VCLibs.*.appx - **/*unit.test*.dll - **/*unit.test*.manifest - **/TestHostApp/*.exe - **/TestHostApp/*.dll - **/TestHostApp/*.xml - !**/*.pdb - !**/*.ipdb - !**/*.obj - !**/*.pch - TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test' - OverWrite: true - flattenFolders: true - condition: succeeded() - -- task: PublishBuildArtifacts@1 - displayName: 'Publish All Build Artifacts' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'drop' - -- task: CopyFiles@2 - displayName: 'Copy PGO databases needed for PGO instrumentation run' - inputs: - Contents: | - **/*.pgd - TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/PGO/$(BuildPlatform)' - OverWrite: true - flattenFolders: true - condition: and(succeeded(), eq(variables['PGOBuildMode'], 'Instrument')) - -- task: PublishBuildArtifacts@1 - displayName: 'Publish All PGO Artifacts' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/PGO' - ArtifactName: 'PGO' - condition: and(succeeded(), eq(variables['PGOBuildMode'], 'Instrument')) - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: binlog' - condition: always() - continueOnError: True - inputs: - PathtoPublish: $(Build.SourcesDirectory)\msbuild.binlog - ArtifactName: binlog-$(BuildPlatform) diff --git a/build/pipelines/templates/check-formatting.yml b/build/pipelines/templates/check-formatting.yml deleted file mode 100644 index 46abca03240..00000000000 --- a/build/pipelines/templates/check-formatting.yml +++ /dev/null @@ -1,17 +0,0 @@ - -jobs: -- job: CodeFormatCheck - displayName: Proper Code Formatting Check - pool: { vmImage: windows-2022 } - - steps: - - checkout: self - fetchDepth: 1 - submodules: false - clean: true - - - task: PowerShell@2 - displayName: 'Code Formatting Check' - inputs: - targetType: filePath - filePath: '.\build\scripts\Invoke-FormattingCheck.ps1' diff --git a/build/pipelines/templates/console-ci-helix-job.yml b/build/pipelines/templates/console-ci-helix-job.yml deleted file mode 100644 index 07476712a02..00000000000 --- a/build/pipelines/templates/console-ci-helix-job.yml +++ /dev/null @@ -1,25 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - minimumExpectedTestsExecutedCount: 10 # Sanity check for minimum expected tests to be reported - rerunPassesRequiredToAvoidFailure: 5 - -jobs: -- template: helix-runtests-job.yml - parameters: - name: 'RunTestsInHelix' - # We're not setting dependsOn as we want to rely on the "stage" dependency above us - testSuite: 'DevTestSuite' - platform: ${{ parameters.platform }} - configuration: ${{ parameters.configuration }} - rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} - -- template: helix-processtestresults-job.yml - parameters: - dependsOn: - - RunTestsInHelix - # the default condition is succeededOrFailed(), and the "stage" condition ensures we only run as needed - platform: ${{ parameters.platform }} - configuration: ${{ parameters.configuration }} - rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} - minimumExpectedTestsExecutedCount: ${{ parameters.minimumExpectedTestsExecutedCount }} diff --git a/build/pipelines/templates/helix-createprojfile-steps.yml b/build/pipelines/templates/helix-createprojfile-steps.yml deleted file mode 100644 index 3f7dc8c0977..00000000000 --- a/build/pipelines/templates/helix-createprojfile-steps.yml +++ /dev/null @@ -1,15 +0,0 @@ -parameters: - condition: '' - testFilePath: '' - outputProjFileName: '' - testSuite: '' - taefQuery: '' - -steps: - - task: powershell@2 - displayName: 'Create ${{ parameters.outputProjFileName }}' - condition: ${{ parameters.condition }} - inputs: - targetType: filePath - filePath: build\Helix\GenerateTestProjFile.ps1 - arguments: -TestFile '${{ parameters.testFilePath }}' -OutputProjFile '$(Build.ArtifactStagingDirectory)\$(BuildConfiguration)\$(BuildPlatform)\${{ parameters.outputProjFileName }}' -JobTestSuiteName '${{ parameters.testSuite }}' -TaefPath '$(Build.SourcesDirectory)\build\Helix\packages\Microsoft.Taef.10.60.210621002\build\Binaries\x86' -TaefQuery '${{ parameters.taefQuery }}' \ No newline at end of file diff --git a/build/pipelines/templates/helix-processtestresults-job.yml b/build/pipelines/templates/helix-processtestresults-job.yml deleted file mode 100644 index bd8e967de92..00000000000 --- a/build/pipelines/templates/helix-processtestresults-job.yml +++ /dev/null @@ -1,71 +0,0 @@ -parameters: - condition: 'succeededOrFailed()' - dependsOn: '' - rerunPassesRequiredToAvoidFailure: 5 - minimumExpectedTestsExecutedCount: 10 - checkJobAttempt: false - pgoArtifact: '' - -jobs: -- job: ProcessTestResults - displayName: Process Helix Results ${{ parameters.platform }} ${{ parameters.configuration }} - condition: ${{ parameters.condition }} - dependsOn: ${{ parameters.dependsOn }} - pool: - vmImage: 'windows-2019' - timeoutInMinutes: 120 - variables: - helixOutputFolder: $(Build.SourcesDirectory)\HelixOutput - - steps: - - task: powershell@2 - displayName: 'UpdateUnreliableTests.ps1' - condition: succeededOrFailed() - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - HelixAccessToken: $(HelixApiAccessToken) - inputs: - targetType: filePath - filePath: build\Helix\UpdateUnreliableTests.ps1 - arguments: -RerunPassesRequiredToAvoidFailure '${{ parameters.rerunPassesRequiredToAvoidFailure }}' - - - task: powershell@2 - displayName: 'OutputTestResults.ps1' - condition: succeededOrFailed() - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - HelixAccessToken: $(HelixApiAccessToken) - inputs: - targetType: filePath - filePath: build\Helix\OutputTestResults.ps1 - arguments: -MinimumExpectedTestsExecutedCount '${{ parameters.minimumExpectedTestsExecutedCount }}' -CheckJobAttempt $${{ parameters.checkJobAttempt }} - - - task: powershell@2 - displayName: 'ProcessHelixFiles.ps1' - condition: succeededOrFailed() - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - HelixAccessToken: $(HelixApiAccessToken) - inputs: - targetType: filePath - filePath: build\Helix\ProcessHelixFiles.ps1 - arguments: -OutputFolder '$(helixOutputFolder)' - - - ${{if ne(parameters.pgoArtifact, '') }}: - - script: move /y $(helixOutputFolder)\PGO $(Build.ArtifactStagingDirectory) - displayName: 'Move pgc files to PGO artifact' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Helix files' - condition: succeededOrFailed() - inputs: - PathtoPublish: $(helixOutputFolder) - artifactName: drop - - - ${{if ne(parameters.pgoArtifact, '') }}: - - task: PublishBuildArtifacts@1 - displayName: 'Publish pgc files' - condition: succeededOrFailed() - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)\PGO\Release - artifactName: ${{ parameters.pgoArtifact }} diff --git a/build/pipelines/templates/helix-runtests-job.yml b/build/pipelines/templates/helix-runtests-job.yml deleted file mode 100644 index 0e7c22efff0..00000000000 --- a/build/pipelines/templates/helix-runtests-job.yml +++ /dev/null @@ -1,164 +0,0 @@ -parameters: - name: 'RunTestsInHelix' - dependsOn: '' - condition: '' - testSuite: '' - # If a Pipeline runs this template more than once, this parameter should be unique per build flavor to differentiate the - # the different test runs: - helixType: 'test/devtest' - artifactName: 'drop' - maxParallel: 4 - rerunPassesRequiredToAvoidFailure: 5 - taefQuery: '' - configuration: '' - platform: '' - # if 'useBuildOutputFromBuildId' is set, we will default to using a build from this pipeline: - useBuildOutputFromPipeline: $(System.DefinitionId) - openHelixTargetQueues: 'windows.11.amd64.client.open.reunion' - closedHelixTargetQueues: 'windows.11.amd64.client.reunion' - -jobs: -- job: ${{ parameters.name }} - displayName: Submit Helix ${{ parameters.platform }} ${{ parameters.configuration }} - dependsOn: ${{ parameters.dependsOn }} - condition: ${{ parameters.condition }} - pool: - vmImage: 'windows-2019' - timeoutInMinutes: 120 - strategy: - maxParallel: ${{ parameters.maxParallel }} - variables: - buildConfiguration: ${{ parameters.configuration }} - buildPlatform: ${{ parameters.platform }} - openHelixTargetQueues: ${{ parameters.openHelixTargetQueues }} - closedHelixTargetQueues: ${{ parameters.closedHelixTargetQueues }} - artifactsDir: $(Build.SourcesDirectory)\Artifacts - taefPath: $(Build.SourcesDirectory)\build\Helix\packages\Microsoft.Taef.10.60.210621002\build\Binaries\$(buildPlatform) - helixCommonArgs: '/binaryLogger:$(Build.SourcesDirectory)/${{parameters.name}}.$(buildPlatform).$(buildConfiguration).binlog /p:HelixBuild=$(Build.BuildId).$(buildPlatform).$(buildConfiguration) /p:Platform=$(buildPlatform) /p:Configuration=$(buildConfiguration) /p:HelixType=${{parameters.helixType}} /p:TestSuite=${{parameters.testSuite}} /p:ProjFilesPath=$(Build.ArtifactStagingDirectory) /p:rerunPassesRequiredToAvoidFailure=${{parameters.rerunPassesRequiredToAvoidFailure}}' - - steps: - - task: CmdLine@1 - displayName: 'Display build machine environment variables' - inputs: - filename: 'set' - - - task: NuGetToolInstaller@0 - displayName: 'Use NuGet 6.3.0' - inputs: - versionSpec: 6.3.0 - - - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 - displayName: 'NuGet restore build/Helix/packages.config' - inputs: - restoreSolution: build/Helix/packages.config - feedsToUse: config - nugetConfigPath: nuget.config - restoreDirectory: packages - - - task: DownloadBuildArtifacts@0 - condition: - and(succeeded(),eq(variables['useBuildOutputFromBuildId'],'')) - inputs: - artifactName: ${{ parameters.artifactName }} - downloadPath: '$(artifactsDir)' - - - task: DownloadBuildArtifacts@0 - condition: - and(succeeded(),ne(variables['useBuildOutputFromBuildId'],'')) - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(System.TeamProjectId) - pipeline: ${{ parameters.useBuildOutputFromPipeline }} - buildId: $(useBuildOutputFromBuildId) - artifactName: ${{ parameters.artifactName }} - downloadPath: '$(artifactsDir)' - - - task: CmdLine@1 - displayName: 'Display Artifact Directory payload contents' - inputs: - filename: 'dir' - arguments: '/s $(artifactsDir)' - - - task: powershell@2 - displayName: 'PrepareHelixPayload.ps1' - inputs: - targetType: filePath - filePath: build\Helix\PrepareHelixPayload.ps1 - arguments: -Platform '$(buildPlatform)' -Configuration '$(buildConfiguration)' -ArtifactName '${{ parameters.artifactName }}' - - - task: CmdLine@1 - displayName: 'Display Helix payload contents' - inputs: - filename: 'dir' - arguments: '/s $(Build.SourcesDirectory)\HelixPayload' - - - task: PowerShell@2 - displayName: 'Make artifact directories' - inputs: - targetType: inline - script: | - New-Item -ItemType Directory -Force -Path "$(Build.ArtifactStagingDirectory)\$(BuildConfiguration)\" - New-Item -ItemType Directory -Force -Path "$(Build.ArtifactStagingDirectory)\$(BuildConfiguration)\$(BuildPlatform)\" - - - template: helix-createprojfile-steps.yml - parameters: - condition: and(succeeded(),eq('${{ parameters.testSuite }}','DevTestSuite')) - testFilePath: '$(artifactsDir)\${{ parameters.artifactName }}\$(buildConfiguration)\$(buildPlatform)\Test\TerminalApp.LocalTests.dll' - outputProjFileName: 'RunTestsInHelix-TerminalAppLocalTests.proj' - testSuite: '${{ parameters.testSuite }}' - taefQuery: ${{ parameters.taefQuery }} - - - template: helix-createprojfile-steps.yml - parameters: - condition: and(succeeded(),eq('${{ parameters.testSuite }}','DevTestSuite')) - testFilePath: '$(artifactsDir)\${{ parameters.artifactName }}\$(buildConfiguration)\$(buildPlatform)\Test\SettingsModel.LocalTests.dll' - outputProjFileName: 'RunTestsInHelix-SettingsModelLocalTests.proj' - testSuite: '${{ parameters.testSuite }}' - taefQuery: ${{ parameters.taefQuery }} - - - - template: helix-createprojfile-steps.yml - parameters: - condition: and(succeeded(),eq('${{ parameters.testSuite }}','DevTestSuite')) - testFilePath: '$(artifactsDir)\${{ parameters.artifactName }}\$(buildConfiguration)\$(buildPlatform)\Test\Conhost.UIA.Tests.dll' - outputProjFileName: 'RunTestsInHelix-HostTestsUIA.proj' - testSuite: '${{ parameters.testSuite }}' - taefQuery: ${{ parameters.taefQuery }} - - - template: helix-createprojfile-steps.yml - parameters: - condition: and(succeeded(),or(eq('${{ parameters.testSuite }}','PgoInstrumentationSuite'),eq('${{ parameters.testSuite }}','DevTestSuite'))) - testFilePath: '$(artifactsDir)\${{ parameters.artifactName }}\$(buildConfiguration)\$(buildPlatform)\Test\WindowsTerminal.UIA.Tests.dll' - outputProjFileName: 'RunTestsInHelix-WindowsTerminalUIATests.proj' - testSuite: '${{ parameters.testSuite }}' - taefQuery: ${{ parameters.taefQuery }} - - - task: PublishBuildArtifacts@1 - displayName: 'Publish generated .proj files' - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.artifactName }} - - - task: DotNetCoreCLI@2 - displayName: 'Run tests in Helix (open queues)' - condition: and(succeeded(),eq(variables['System.CollectionUri'],'https://dev.azure.com/ms/')) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - inputs: - command: custom - projects: build\Helix\RunTestsInHelix.proj - custom: msbuild - arguments: '$(helixCommonArgs) /p:IsExternal=true /p:Creator=Terminal /p:HelixTargetQueues=$(openHelixTargetQueues)' - - - task: DotNetCoreCLI@2 - displayName: 'Run tests in Helix (closed queues)' - condition: and(succeeded(),ne(variables['System.CollectionUri'],'https://dev.azure.com/ms/')) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - HelixAccessToken: $(HelixApiAccessToken) - inputs: - command: custom - projects: build\Helix\RunTestsInHelix.proj - custom: msbuild - arguments: '$(helixCommonArgs) /p:HelixTargetQueues=$(closedHelixTargetQueues)' diff --git a/build/pipelines/templates/pgo-merge-pgd-job.yml b/build/pipelines/templates/pgo-merge-pgd-job.yml deleted file mode 100644 index 89cf5319ccd..00000000000 --- a/build/pipelines/templates/pgo-merge-pgd-job.yml +++ /dev/null @@ -1,70 +0,0 @@ -parameters: - dependsOn: '' - pgoArtifact: PGO - platform: '' - configuration: '' - -jobs: -- job: MergePGD - dependsOn: ${{ parameters.dependsOn }} - pool: - vmImage: 'windows-2019' - variables: - artifactsPath: $(Build.SourcesDirectory)\Artifacts - pgoArtifactsPath: $(artifactsPath)\${{ parameters.pgoArtifact }} - buildPlatform: ${{ parameters.platform }} - buildConfiguration: ${{ parameters.configuration }} - - steps: - # The environment variable VCToolsInstallDir isn't defined on lab machines, so we need to retrieve it ourselves. - - script: | - "%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -Latest -requires Microsoft.Component.MSBuild -property InstallationPath > %TEMP%\vsinstalldir.txt - set /p _VSINSTALLDIR15=<%TEMP%\vsinstalldir.txt - del %TEMP%\vsinstalldir.txt - call "%_VSINSTALLDIR15%\Common7\Tools\VsDevCmd.bat" - echo VCToolsInstallDir = %VCToolsInstallDir% - echo ##vso[task.setvariable variable=VCToolsInstallDir]%VCToolsInstallDir% - displayName: 'Retrieve VC tools directory' - - - task: NuGetToolInstaller@0 - displayName: 'Use NuGet 6.3.0' - inputs: - versionSpec: 6.3.0 - - - task: NuGetAuthenticate@0 - - # In the Microsoft Azure DevOps tenant, NuGetCommand is ambiguous. - # This should be `task: NuGetCommand@2` - - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 - displayName: Restore NuGet packages for extraneous build actions - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: build/packages.config - restoreDirectory: '$(Build.SourcesDirectory)\packages' - - - task: DownloadBuildArtifacts@0 - inputs: - artifactName: ${{ parameters.pgoArtifact }} - downloadPath: $(artifactsPath) - - - task: MSBuild@1 - displayName: Merge counts into PGD - inputs: - solution: $(Build.SourcesDirectory)\OpenConsole.sln - platform: $(buildPlatform) - configuration: $(buildConfiguration) - msbuildArguments: '/t:MergePGOCounts /p:PGOBuildMode=Instrument /p:PGDPath=$(pgoArtifactsPath)\$(buildPlatform) /p:PGCRootPath=$(pgoArtifactsPath)\$(buildPlatform)' - - - task: CopyFiles@2 - displayName: 'Copy merged pgd to artifact staging' - inputs: - sourceFolder: $(pgoArtifactsPath) - contents: '**\$(buildPlatform)\*.pgd' - targetFolder: $(Build.ArtifactStagingDirectory) - - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.pgoArtifact }} diff --git a/build/scripts/Run-Tests.ps1 b/build/scripts/Run-Tests.ps1 index 1e52a8757e2..bd4cf28fc50 100644 --- a/build/scripts/Run-Tests.ps1 +++ b/build/scripts/Run-Tests.ps1 @@ -9,7 +9,8 @@ Param( [Parameter(Mandatory=$false, Position=3)] [string]$LogPath, [Parameter(Mandatory=$false)] - [string]$Root = ".\bin\$Platform\$Configuration" + [string]$Root = ".\bin\$Platform\$Configuration", + [string[]]$AdditionalTaefArguments ) # Find test DLLs based on the provided root, match pattern, and recursion @@ -26,7 +27,7 @@ if ($LogPath) { } # Invoke the te.exe executable with arguments and test DLLs -& "$Root\te.exe" $args $testDlls.FullName +& "$Root\te.exe" $args $testDlls.FullName $AdditionalTaefArguments # Check the exit code of the te.exe process and exit accordingly if ($LASTEXITCODE -ne 0) { diff --git a/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj b/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj index 661f676b59d..dc3eebe8749 100644 --- a/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj +++ b/src/cascadia/WpfTerminalControl/WpfTerminalControl.csproj @@ -46,6 +46,19 @@ true runtimes\win-arm64\native\
+ + + true + runtimes\win-x86\native\ + + + true + runtimes\win-x64\native\ + + + true + runtimes\win-arm64\native\ + diff --git a/src/common.nugetversions.props b/src/common.nugetversions.props index 19d169a45a0..15e2fe774da 100644 --- a/src/common.nugetversions.props +++ b/src/common.nugetversions.props @@ -1,7 +1,7 @@ - + diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 3113c124072..2a3d5cc062e 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -37,7 +37,8 @@ - + + @@ -71,8 +72,8 @@ - - + + From a0c88bb5117b8062cee4c4cad934a60e72768eb1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 14 Aug 2023 05:46:42 -0500 Subject: [PATCH 18/59] Add Suggestions UI & experimental shell completions support (#14938) There's two parts to this PR that should be considered _separately_. 1. The Suggestions UI, a new graphical menu for displaying suggestions / completions to the user in the context of the terminal the user is working in. 2. The VsCode shell completions protocol. This enables the shell to invoke this UI via a VT sequence. These are being introduced at the same time, because they both require one another. However, I need to absolutely emphasize: ### THE FORMAT OF THE COMPLETION PROTOCOL IS EXPERIMENTAL AND SUBJECT TO CHANGE This is what we've prototyped with VsCode, but we're still working on how we want to conclusively define that protocol. However, we can also refine the Suggestions UI independently of how the protocol is actually implemented. This will let us rev the Suggestions UI to support other things like tooltips, recent commands, tasks, INDEPENDENTLY of us rev'ing the completion protocol. So yes, they're both here, but let's not nitpick that protocol for now. ### Checklist * Doesn't actually close anything * Heavily related to #3121, but I'm not gonna say that's closed till we settle on the protocol * See also: * #1595 * #14779 * https://github.com/microsoft/vscode/pull/171648 ### Detailed Description #### Suggestions UI The Suggestions UI is spec'ed over in #14864, so go read that. It's basically a transient Command Palette, that floats by the user's cursor. It's heavily forked from the Command Palette code, with all the business about switching modes removed. The major bit of new code is `SuggestionsControl::Anchor`. It also supports two "modes": * A "palette", which is like the command palette - a list with a text box * A "menu", which is more like the intellisense flyout. No text box. This is the mode that the shell completions use #### Shell Completions Protocol I literally cannot say this enough times - this protocol is experimental and subject to change. Build on it at your own peril. It's disabled in Release builds (but available in preview behind `globals.experimental.enableShellCompletionMenu`), so that when it ships, no one can take a dependency on it accidentally. Right now we're just taking a blob of JSON, passing that up to the App layer, who asks `Command` to parse it and build a list of `sendInput` actions to populate the menu with. It's not a particularly elegant solution, but it's good enough to prototype with. #### How do I test this? I've been testing this in two parts. You'll need a snippet in your powershell profile, and a keybinding in the Terminal settings to trigger it. The work together by binding Ctrl+space to _essentially_ send F12b. Wacky, but it works. ```json { "command": { "action": "sendInput","input": "\u001b[24~b" }, "keys": "ctrl+space" }, ``` ```ps1 function Send-Completions2 { $commandLine = "" $cursorIndex = 0 # TODO: Since fuzzy matching exists, should completions be provided only for character after the # last space and then filter on the client side? That would let you trigger ctrl+space # anywhere on a word and have full completions available [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex) $completionPrefix = $commandLine # Get completions $result = "`e]633;Completions" if ($completionPrefix.Length -gt 0) { # Get and send completions $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex if ($null -ne $completions.CompletionMatches) { $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);" $result += $completions.CompletionMatches | ConvertTo-Json -Compress } } $result += "`a" Write-Host -NoNewLine $result } function Set-MappedKeyHandlers { # VS Code send completions request (may override Ctrl+Spacebar) Set-PSReadLineKeyHandler -Chord 'F12,b' -ScriptBlock { Send-Completions2 } } # Register key handlers if PSReadLine is available if (Get-Module -Name PSReadLine) { Set-MappedKeyHandlers } ``` ### TODO * [x] `(prompt | format-hex).`Ctrl+space -> This always throws an exception. Seems like the payload is always clipped to ```{"CompletionText":"Ascii","ListItemText":"Ascii","ResultType":5,"ToolTip":"string Ascii { get``` and that ain't JSON. Investigate on the pwsh side? --- .github/actions/spelling/allow/allow.txt | 1 + .github/actions/spelling/expect/expect.txt | 2 + src/cascadia/LocalTests_TerminalApp/pch.h | 2 + .../TerminalApp/SuggestionsControl.cpp | 1103 +++++++++++++++++ src/cascadia/TerminalApp/SuggestionsControl.h | 132 ++ .../TerminalApp/SuggestionsControl.idl | 48 + .../TerminalApp/SuggestionsControl.xaml | 214 ++++ .../TerminalApp/TerminalAppLib.vcxproj | 13 + src/cascadia/TerminalApp/TerminalPage.cpp | 117 +- src/cascadia/TerminalApp/TerminalPage.h | 11 +- src/cascadia/TerminalApp/TerminalPage.xaml | 8 + src/cascadia/TerminalApp/pch.h | 2 + src/cascadia/TerminalControl/ControlCore.cpp | 13 + src/cascadia/TerminalControl/ControlCore.h | 5 + src/cascadia/TerminalControl/ControlCore.idl | 3 + src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 14 + src/cascadia/TerminalControl/EventArgs.idl | 6 + src/cascadia/TerminalControl/TermControl.cpp | 31 +- src/cascadia/TerminalControl/TermControl.h | 5 + src/cascadia/TerminalControl/TermControl.idl | 6 + src/cascadia/TerminalCore/Terminal.cpp | 5 + src/cascadia/TerminalCore/Terminal.hpp | 5 + src/cascadia/TerminalCore/TerminalApi.cpp | 8 + .../TerminalSettingsModel/Command.cpp | 98 ++ src/cascadia/TerminalSettingsModel/Command.h | 2 + .../TerminalSettingsModel/Command.idl | 2 + .../GlobalAppSettings.idl | 1 + .../TerminalSettingsModel/MTSMSettings.h | 1 + src/features.xml | 11 + src/host/outputStream.cpp | 4 + src/host/outputStream.hpp | 2 + src/terminal/adapter/ITermDispatch.hpp | 2 + src/terminal/adapter/ITerminalApi.hpp | 2 + src/terminal/adapter/adaptDispatch.cpp | 80 ++ src/terminal/adapter/adaptDispatch.hpp | 2 + src/terminal/adapter/termDispatch.hpp | 2 + .../adapter/ut_adapter/adapterTest.cpp | 36 + .../parser/OutputStateMachineEngine.cpp | 5 + .../parser/OutputStateMachineEngine.hpp | 1 + 40 files changed, 2003 insertions(+), 3 deletions(-) create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.cpp create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.h create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.idl create mode 100644 src/cascadia/TerminalApp/SuggestionsControl.xaml diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index e757c327fe2..20999b3a54e 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -95,6 +95,7 @@ slnt Sos ssh stakeholders +sxn timeline timelines timestamped diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 722bf19093b..626777f6bdb 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -315,6 +315,7 @@ CPLINFO cplusplus CPPCORECHECK cppcorecheckrules +cpprest cpprestsdk cppwinrt CProc @@ -1452,6 +1453,7 @@ PPEB ppf ppguid ppidl +pplx PPROC ppropvar ppsi diff --git a/src/cascadia/LocalTests_TerminalApp/pch.h b/src/cascadia/LocalTests_TerminalApp/pch.h index 75aabc570b8..f82561888b1 100644 --- a/src/cascadia/LocalTests_TerminalApp/pch.h +++ b/src/cascadia/LocalTests_TerminalApp/pch.h @@ -74,3 +74,5 @@ Author(s): #include "../../inc/DefaultSettings.h" #include + +#include diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp new file mode 100644 index 00000000000..83694267a6d --- /dev/null +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -0,0 +1,1103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ActionPaletteItem.h" +#include "CommandLinePaletteItem.h" +#include "SuggestionsControl.h" +#include + +#include "SuggestionsControl.g.cpp" + +using namespace winrt; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::System; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace winrt::TerminalApp::implementation +{ + SuggestionsControl::SuggestionsControl() + { + InitializeComponent(); + + _itemTemplateSelector = Resources().Lookup(winrt::box_value(L"PaletteItemTemplateSelector")).try_as(); + _listItemTemplate = Resources().Lookup(winrt::box_value(L"ListItemTemplate")).try_as(); + + _filteredActions = winrt::single_threaded_observable_vector(); + _nestedActionStack = winrt::single_threaded_vector(); + _currentNestedCommands = winrt::single_threaded_vector(); + _allCommands = winrt::single_threaded_vector(); + + _switchToMode(); + + // Whatever is hosting us will enable us by setting our visibility to + // "Visible". When that happens, set focus to our search box. + RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (Visibility() == Visibility::Visible) + { + // Force immediate binding update so we can select an item + Bindings->Update(); + UpdateLayout(); // THIS ONE IN PARTICULAR SEEMS LOAD BEARING. + // Without the UpdateLayout call, our ListView won't have a + // chance to instantiate ListViewItem's. If we don't have those, + // then our call to `SelectedItem()` below is going to return + // null. If it does that, then we won't be able to focus + // ourselves when we're opened. + + // Select the correct element in the list, depending on which + // direction we were opened in. + // + // Make sure to use _scrollToIndex, to move the scrollbar too! + if (_direction == TerminalApp::SuggestionsDirection::TopDown) + { + _scrollToIndex(0); + } + else // BottomUp + { + _scrollToIndex(_filteredActionsView().Items().Size() - 1); + } + + if (_mode == SuggestionsMode::Palette) + { + // Toss focus into the search box in palette mode + _searchBox().Visibility(Visibility::Visible); + _searchBox().Focus(FocusState::Programmatic); + } + else if (_mode == SuggestionsMode::Menu) + { + // Toss focus onto the selected item in menu mode. + // Don't just focus the _filteredActionsView, because that will always select the 0th element. + + _searchBox().Visibility(Visibility::Collapsed); + + if (const auto& dependencyObj = SelectedItem().try_as()) + { + Input::FocusManager::TryFocusAsync(dependencyObj, FocusState::Programmatic); + } + } + + TraceLoggingWrite( + g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider + "SuggestionsControlOpened", + TraceLoggingDescription("Event emitted when the Command Palette is opened"), + TraceLoggingWideString(L"Action", "Mode", "which mode the palette was opened in"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + else + { + // Raise an event to return control to the Terminal. + _dismissPalette(); + } + }); + + // Focusing the ListView when the Command Palette control is set to Visible + // for the first time fails because the ListView hasn't finished loading by + // the time Focus is called. Luckily, We can listen to SizeChanged to know + // when the ListView has been measured out and is ready, and we'll immediately + // revoke the handler because we only needed to handle it once on initialization. + _sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + // This does only fire once, when the size changes, which is the + // very first time it's opened. It does not fire for subsequent + // openings. + + _sizeChangedRevoker.revoke(); + }); + + _filteredActionsView().SelectionChanged({ this, &SuggestionsControl::_selectedCommandChanged }); + } + + TerminalApp::SuggestionsMode SuggestionsControl::Mode() const + { + return _mode; + } + void SuggestionsControl::Mode(TerminalApp::SuggestionsMode mode) + { + _mode = mode; + + if (_mode == SuggestionsMode::Palette) + { + _searchBox().Visibility(Visibility::Visible); + _searchBox().Focus(FocusState::Programmatic); + } + else if (_mode == SuggestionsMode::Menu) + { + _searchBox().Visibility(Visibility::Collapsed); + _filteredActionsView().Focus(FocusState::Programmatic); + } + } + + // Method Description: + // - Moves the focus up or down the list of commands. If we're at the top, + // we'll loop around to the bottom, and vice-versa. + // Arguments: + // - moveDown: if true, we're attempting to move to the next item in the + // list. Otherwise, we're attempting to move to the previous. + // Return Value: + // - + void SuggestionsControl::SelectNextItem(const bool moveDown) + { + auto selected = _filteredActionsView().SelectedIndex(); + const auto numItems = ::base::saturated_cast(_filteredActionsView().Items().Size()); + + // Do not try to select an item if + // - the list is empty + // - if no item is selected and "up" is pressed + if (numItems != 0 && (selected != -1 || moveDown)) + { + // Wraparound math. By adding numItems and then calculating modulo numItems, + // we clamp the values to the range [0, numItems) while still supporting moving + // upward from 0 to numItems - 1. + const auto newIndex = ((numItems + selected + (moveDown ? 1 : -1)) % numItems); + _filteredActionsView().SelectedIndex(newIndex); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + } + + // Method Description: + // - Scroll the command palette to the specified index + // Arguments: + // - index within a list view of commands + // Return Value: + // - + void SuggestionsControl::_scrollToIndex(uint32_t index) + { + auto numItems = _filteredActionsView().Items().Size(); + + if (numItems == 0) + { + // if the list is empty no need to scroll + return; + } + + auto clampedIndex = std::clamp(index, 0, numItems - 1); + _filteredActionsView().SelectedIndex(clampedIndex); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + + // Method Description: + // - Computes the number of visible commands + // Arguments: + // - + // Return Value: + // - the approximate number of items visible in the list (in other words the size of the page) + uint32_t SuggestionsControl::_getNumVisibleItems() + { + if (const auto container = _filteredActionsView().ContainerFromIndex(0)) + { + if (const auto item = container.try_as()) + { + const auto itemHeight = ::base::saturated_cast(item.ActualHeight()); + const auto listHeight = ::base::saturated_cast(_filteredActionsView().ActualHeight()); + return listHeight / itemHeight; + } + } + return 0; + } + + // Method Description: + // - Scrolls the focus one page up the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollPageUp() + { + auto selected = _filteredActionsView().SelectedIndex(); + auto numVisibleItems = _getNumVisibleItems(); + _scrollToIndex(selected - numVisibleItems); + } + + // Method Description: + // - Scrolls the focus one page down the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollPageDown() + { + auto selected = _filteredActionsView().SelectedIndex(); + auto numVisibleItems = _getNumVisibleItems(); + _scrollToIndex(selected + numVisibleItems); + } + + // Method Description: + // - Moves the focus to the top item in the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollToTop() + { + _scrollToIndex(0); + } + + // Method Description: + // - Moves the focus to the bottom item in the list of commands. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::ScrollToBottom() + { + _scrollToIndex(_filteredActionsView().Items().Size() - 1); + } + + Windows::UI::Xaml::FrameworkElement SuggestionsControl::SelectedItem() + { + auto index = _filteredActionsView().SelectedIndex(); + const auto container = _filteredActionsView().ContainerFromIndex(index); + const auto item = container.try_as(); + return item; + } + + // Method Description: + // - Called when the command selection changes. We'll use this to preview the selected action. + // Arguments: + // - + // Return Value: + // - + void SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/, + const Windows::UI::Xaml::RoutedEventArgs& /*args*/) + { + const auto selectedCommand = _filteredActionsView().SelectedItem(); + const auto filteredCommand{ selectedCommand.try_as() }; + + _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"SelectedItem" }); + + // Make sure to not send the preview if we're collapsed. This can + // sometimes fire after we've been closed, which can trigger us to + // preview the action for the empty text (as we've cleared the search + // text as a part of closing). + const bool isVisible{ this->Visibility() == Visibility::Visible }; + + if (filteredCommand != nullptr && + isVisible) + { + if (const auto actionPaletteItem{ filteredCommand.Item().try_as() }) + { + PreviewAction.raise(*this, actionPaletteItem.Command()); + } + } + } + + void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/, + const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + const auto coreWindow = CoreWindow::GetForCurrentThread(); + const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + + if (key == VirtualKey::Home && ctrlDown) + { + ScrollToTop(); + e.Handled(true); + } + else if (key == VirtualKey::End && ctrlDown) + { + ScrollToBottom(); + e.Handled(true); + } + else if (key == VirtualKey::Up) + { + // Move focus to the next item in the list. + SelectNextItem(false); + e.Handled(true); + } + else if (key == VirtualKey::Down) + { + // Move focus to the previous item in the list. + SelectNextItem(true); + e.Handled(true); + } + else if (key == VirtualKey::PageUp) + { + // Move focus to the first visible item in the list. + ScrollPageUp(); + e.Handled(true); + } + else if (key == VirtualKey::PageDown) + { + // Move focus to the last visible item in the list. + ScrollPageDown(); + e.Handled(true); + } + else if (key == VirtualKey::Enter || + key == VirtualKey::Tab || + key == VirtualKey::Right) + { + // If the user pressed enter, tab, or the right arrow key, then + // we'll want to dispatch the command that's selected as they + // accepted the suggestion. + + if (const auto& button = e.OriginalSource().try_as + + + + + + + + + + + +
+ + +
+ diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index e284d7abe2d..2b933ddfe91 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -68,6 +68,9 @@ Designer + + Designer + @@ -159,6 +162,9 @@ TerminalWindow.idl + + SuggestionsControl.xaml + @@ -262,6 +268,9 @@ + + SuggestionsControl.xaml + @@ -325,6 +334,10 @@ CommandPalette.xaml Code + + SuggestionsControl.xaml + Code + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 8140af22b3e..18515a8afa9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1466,6 +1466,11 @@ namespace winrt::TerminalApp::implementation { CommandPaletteElement().Visibility(Visibility::Collapsed); } + if (_suggestionsControlIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + SuggestionsElement().Visibility(Visibility::Collapsed); + } // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. @@ -1654,6 +1659,12 @@ namespace winrt::TerminalApp::implementation term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + // Don't even register for the event if the feature is compiled off. + if constexpr (Feature_ShellCompletions::IsEnabled()) + { + term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); + } + term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened }); term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened }); } @@ -1825,6 +1836,37 @@ namespace winrt::TerminalApp::implementation return p; } + SuggestionsControl TerminalPage::LoadSuggestionsUI() + { + if (const auto p = SuggestionsElement()) + { + return p; + } + + return _loadSuggestionsElementSlowPath(); + } + bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) + { + const auto p = SuggestionsElement(); + return p && p.Visibility() == visibility; + } + + SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() + { + const auto p = FindName(L"SuggestionsElement").as(); + + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (SuggestionsElement().Visibility() == Visibility::Collapsed) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + // Method Description: // - Warn the user that they are about to close all open windows, then // signal that we want to close everything. @@ -2787,7 +2829,7 @@ namespace winrt::TerminalApp::implementation // Arguments: // - sender (not used) // - args: the arguments specifying how to set the display status to ShowWindow for our window handle - void TerminalPage::_ShowWindowChangedHandler(const IInspectable& /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) + void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) { _ShowWindowChangedHandlers(*this, args); } @@ -4649,6 +4691,79 @@ namespace winrt::TerminalApp::implementation _updateThemeColors(); } + winrt::fire_and_forget TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, + const CompletionsChangedEventArgs args) + { + // This will come in on a background (not-UI, not output) thread. + + // This won't even get hit if the velocity flag is disabled - we gate + // registering for the event based off of + // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents + + // User must explicitly opt-in on Preview builds + if (!_settings.GlobalSettings().EnableShellCompletionMenu()) + { + co_return; + } + + // Parse the json string into a collection of actions + try + { + auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), + args.ReplacementLength()); + + auto weakThis{ get_weak() }; + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { + // On the UI thread... + if (const auto& page{ weakThis.get() }) + { + // Open the Suggestions UI with the commands from the control + page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu); + } + }); + } + CATCH_LOG(); + } + + void TerminalPage::_OpenSuggestions( + const TermControl& sender, + IVector commandsCollection, + winrt::TerminalApp::SuggestionsMode mode) + { + // ON THE UI THREAD + assert(Dispatcher().HasThreadAccess()); + + if (commandsCollection == nullptr) + { + return; + } + if (commandsCollection.Size() == 0) + { + if (const auto p = SuggestionsElement()) + { + p.Visibility(Visibility::Collapsed); + } + return; + } + + const auto& control{ sender ? sender : _GetActiveControl() }; + if (!control) + { + return; + } + + const auto& sxnUi{ LoadSuggestionsUI() }; + + const auto characterSize{ control.CharacterDimensions() }; + // This is in control-relative space. We'll need to convert it to page-relative space. + const auto cursorPos{ control.CursorPositionInDips() }; + const auto controlTransform = control.TransformToVisual(this->Root()); + const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos + const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; + + sxnUi.Open(mode, commandsCollection, realCursorPos, windowDimensions, characterSize.Height); + } + void TerminalPage::_ContextMenuOpened(const IInspectable& sender, const IInspectable& /*args*/) { diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 7a6866e20ec..e14b46fee72 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -117,6 +117,8 @@ namespace winrt::TerminalApp::implementation winrt::hstring ApplicationVersion(); CommandPalette LoadCommandPalette(); + SuggestionsControl LoadSuggestionsUI(); + winrt::fire_and_forget RequestQuit(); winrt::fire_and_forget CloseWindow(bool bypassDialog); @@ -280,6 +282,8 @@ namespace winrt::TerminalApp::implementation __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); + __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); + bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); @@ -481,6 +485,7 @@ namespace winrt::TerminalApp::implementation void _RunRestorePreviews(); void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); + winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; std::vector> _restorePreviewFuncs{}; @@ -513,7 +518,11 @@ namespace winrt::TerminalApp::implementation void _updateAllTabCloseButtons(const winrt::TerminalApp::TabBase& focusedTab); void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - void _ShowWindowChangedHandler(const IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + winrt::fire_and_forget _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); + void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode); + + void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 00fb12f86b4..600ec505c67 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -175,6 +175,14 @@ PreviewKeyDown="_KeyDownHandler" Visibility="Collapsed" /> + + From 127df073fadc70d98e2a4a0233aa3b4315b86056 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 15 Aug 2023 14:01:34 -0500 Subject: [PATCH 23/59] Add support for "Tasks" in the Suggestions UI (#15664) _targets #15027_ Adds a new suggestion source, `tasks`, that allows a user to open the Suggestions UI with `sendInput` commands saved in their settings. `source` becomes a flag setting, so it can be combined like so: ```json { "keys": "ctrl+shift+h", "command": { "action": "suggestions", "source": "commandHistory", "useCommandline":true }, }, { "keys": "ctrl+shift+y", "command": { "action": "suggestions", "source": "tasks", "useCommandline":false }, }, { "keys": "ctrl+shift+b", "command": { "action": "suggestions", "source": ["all"], "useCommandline":true }, }, ``` If a nested command has `sendInput` commands underneath it, this will build a tree of commands that only include `sendInput`s as leaves (but leave the rest of the nesting structure intact). ## References and Relevant Issues Closes #1595 See also #13445 As spec'd in #14864 ## Validation Steps Performed Tested manually --- doc/cascadia/profiles.schema.json | 54 +++++++++++++++ .../TerminalApp/AppActionHandlers.cpp | 63 +++++++++++++---- .../TerminalSettingsModel/ActionArgs.cpp | 37 +++++++--- .../TerminalSettingsModel/ActionArgs.idl | 9 ++- .../TerminalSettingsModel/ActionMap.cpp | 68 +++++++++++++++++++ .../TerminalSettingsModel/ActionMap.h | 2 + .../TerminalSettingsModel/ActionMap.idl | 2 + .../TerminalSettingsModel/Command.cpp | 13 +++- src/cascadia/TerminalSettingsModel/Command.h | 1 + .../Resources/en-US/Resources.resw | 54 +++++++-------- .../TerminalSettingsSerializationHelpers.h | 8 ++- 11 files changed, 255 insertions(+), 56 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index c681ef29267..50a8a6a854f 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -85,6 +85,32 @@ } ] }, + "BuiltinSuggestionSource": { + "enum": [ + "commandHistory", + "tasks", + "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`, and `tasks`. 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" } + } + ] + }, "AppearanceConfig": { "properties": { "colorScheme": { @@ -398,6 +424,7 @@ "sendInput", "setColorScheme", "setTabColor", + "showSuggestions", "splitPane", "switchToTab", "tabSearch", @@ -1767,6 +1794,30 @@ } ] }, + "ShowSuggestionsAction": { + "description": "Arguments corresponding to a Open Suggestions Action", + "allOf": [ + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "properties": { + "action": { + "type": "string", + "const": "showSuggestions" + }, + "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." + } + } + } + ] + }, "ShowCloseButton": { "enum": [ "always", @@ -2037,6 +2088,9 @@ { "$ref": "#/$defs/SearchWebAction" }, + { + "$ref": "#/$defs/ShowSuggestionsAction" + }, { "type": "null" } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index a65c3ddbab0..2fefc40aae4 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1264,25 +1264,62 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - auto source = realArgs.Source(); - - switch (source) - { - case SuggestionsSource::CommandHistory: + const auto source = realArgs.Source(); + std::vector commandsCollection; + Control::CommandHistoryContext context{ nullptr }; + winrt::hstring currentCommandline = L""; + + // If the user wanted to use the current commandline to filter results, + // OR they wanted command history (or some other source that + // requires context from the control) + // then get that here. + const bool shouldGetContext = realArgs.UseCommandline() || + WI_IsFlagSet(source, SuggestionsSource::CommandHistory); + if (shouldGetContext) { if (const auto& control{ _GetActiveControl() }) { - const auto context = control.CommandHistory(); - const auto& currentCmd{ realArgs.UseCommandline() ? context.CurrentCommandline() : L"" }; - _OpenSuggestions(control, - Command::HistoryToCommands(context.History(), currentCmd, false), - SuggestionsMode::Palette, - currentCmd); + context = control.CommandHistory(); + if (context) + { + currentCommandline = context.CurrentCommandline(); + } } - args.Handled(true); } - break; + + // Aggregate all the commands from the different sources that + // the user selected. + + // Tasks are all the sendInput commands the user has saved in + // their settings file. Ask the ActionMap for those. + if (WI_IsFlagSet(source, SuggestionsSource::Tasks)) + { + const auto tasks = _settings.GlobalSettings().ActionMap().FilterToSendInput(currentCommandline); + for (const auto& t : tasks) + { + commandsCollection.push_back(t); + } } + + // Command History comes from the commands in the buffer, + // assuming the user has enabled shell integration. Get those + // from the active control. + if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory) && + context != nullptr) + { + const auto recentCommands = Command::HistoryToCommands(context.History(), currentCommandline, false); + for (const auto& t : recentCommands) + { + commandsCollection.push_back(t); + } + } + + // Open the palette with all these commands in it. + _OpenSuggestions(_GetActiveControl(), + winrt::single_threaded_vector(std::move(commandsCollection)), + SuggestionsMode::Palette, + currentCommandline); + args.Handled(true); } } } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 1088ee27852..b2247321d5f 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -709,23 +709,42 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring SuggestionsArgs::GenerateName() const { - auto base{ RS_(L"SuggestionsCommandKey") }; - switch (Source()) + std::wstringstream ss; + ss << RS_(L"SuggestionsCommandKey").c_str(); + + if (UseCommandline()) { - case SuggestionsSource::CommandHistory: - base = RS_(L"SuggestionsCommandHistoryCommandKey"); + ss << L", useCommandline:true"; } - if (UseCommandline()) + // All of the source values will leave a trailing ", " that we need to chop later: + ss << L", source: "; + const auto source = Source(); + if (source == SuggestionsSource::All) { - return winrt::hstring{ - fmt::format(L"{}, useCommandline:true", std::wstring_view(base)) - }; + ss << L"all, "; + } + else if (source == static_cast(0)) + { + ss << L"none, "; } else { - return base; + if (WI_IsFlagSet(source, SuggestionsSource::Tasks)) + { + ss << L"tasks, "; + } + + if (WI_IsFlagSet(source, SuggestionsSource::CommandHistory)) + { + ss << L"commandHistory, "; + } } + // Chop off the last "," + auto result = ss.str(); + // use `resize`, to avoid duplicating the entire string. (substr doesn't create a view.) + result.resize(result.size() - 2); + return winrt::hstring{ result }; } winrt::hstring FindMatchArgs::GenerateName() const diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 936165b8b28..d38e3011281 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -112,11 +112,14 @@ namespace Microsoft.Terminal.Settings.Model ToCurrent, ToMouse, }; + + [flags] enum SuggestionsSource { - Tasks, - CommandHistory, - DirectoryHistory, + Tasks = 0x1, + CommandHistory = 0x2, + DirectoryHistory = 0x4, + All = 0xffffffff, }; [default_interface] runtimeclass NewTerminalArgs { diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index b2c78970d45..76565dff444 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -931,4 +931,72 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { return _ExpandedCommandsCache; } + + IVector _filterToSendInput(IMapView nameMap, + winrt::hstring currentCommandline) + { + auto results = winrt::single_threaded_vector(); + + const auto numBackspaces = currentCommandline.size(); + // Helper to clone a sendInput command into a new Command, with the + // input trimmed to account for the currentCommandline + auto createInputAction = [&](const Model::Command& command) -> Model::Command { + winrt::com_ptr cmdImpl; + cmdImpl.copy_from(winrt::get_self(command)); + + const auto inArgs{ command.ActionAndArgs().Args().try_as() }; + + auto args = winrt::make_self( + winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"), + L"", + numBackspaces, + (std::wstring_view)(inArgs ? inArgs.Input() : L"")) }); + Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args }; + + auto copy = cmdImpl->Copy(); + copy->ActionAndArgs(actionAndArgs); + + return *copy; + }; + + // iterate over all the commands in all our actions... + for (auto&& [name, command] : nameMap) + { + // If this is not a nested command, and it's a sendInput command... + if (!command.HasNestedCommands() && + command.ActionAndArgs().Action() == ShortcutAction::SendInput) + { + // copy it into the results. + results.Append(createInputAction(command)); + } + // If this is nested... + else if (command.HasNestedCommands()) + { + // Look for any sendInput commands nested underneath us + auto innerResults = _filterToSendInput(command.NestedCommands(), currentCommandline); + + if (innerResults.Size() > 0) + { + // This command did have at least one sendInput under it + + // Create a new Command, which is a copy of this Command, + // which only has SendInputs in it + winrt::com_ptr cmdImpl; + cmdImpl.copy_from(winrt::get_self(command)); + auto copy = cmdImpl->Copy(); + copy->NestedCommands(innerResults.GetView()); + + results.Append(*copy); + } + } + } + + return results; + } + + IVector ActionMap::FilterToSendInput( + winrt::hstring currentCommandline) + { + return _filterToSendInput(NameMap(), currentCommandline); + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 95ab9e9ab7f..4270a081118 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -79,6 +79,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, const Windows::Foundation::Collections::IMapView& schemes); + winrt::Windows::Foundation::Collections::IVector FilterToSendInput(winrt::hstring currentCommandline); + private: std::optional _GetActionByID(const InternalActionID actionID) const; std::optional _GetActionByKeyChordInternal(const Control::KeyChord& keys) const; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 99df487263b..b84305b9d0e 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -22,6 +22,8 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView GlobalHotkeys { get; }; IVector ExpandedCommands { get; }; + + IVector FilterToSendInput(String CurrentCommandline); }; [default_interface] runtimeclass ActionMap : IActionMapView diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 63d4e2dea6e..b211987b677 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -64,6 +64,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return _subcommands ? _subcommands.GetView() : nullptr; } + void Command::NestedCommands(const Windows::Foundation::Collections::IVectorView& nested) + { + _subcommands = winrt::single_threaded_map(); + + for (const auto& n : nested) + { + _subcommands.Insert(n.Name(), n); + } + } + // Function Description: // - reports if the current command has nested commands // - This CANNOT detect { "name": "foo", "commands": null } @@ -752,7 +762,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Iterate in reverse over the history, so that most recent commands are first for (auto i = history.Size(); i > 0; i--) { - std::wstring_view line{ history.GetAt(i - 1) }; + const auto& element{ history.GetAt(i - 1) }; + std::wstring_view line{ element }; if (line.empty()) { diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 59a080c4c0a..41037bf1a29 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -52,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool HasNestedCommands() const; bool IsNestedCommand() const noexcept; Windows::Foundation::Collections::IMapView NestedCommands() const; + void NestedCommands(const Windows::Foundation::Collections::IVectorView& nested); bool HasName() const noexcept; hstring Name() const noexcept; diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 887d523fb0e..74035df755d 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -1,17 +1,17 @@ - diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 73af3411e2d..0dc91e30b11 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -499,12 +499,14 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti }; }; -JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource) +JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource) { - JSON_MAPPINGS(3) = { + static constexpr std::array mappings = { + pair_type{ "none", AllClear }, pair_type{ "tasks", ValueType::Tasks }, pair_type{ "commandHistory", ValueType::CommandHistory }, - pair_type{ "directoryHistory", ValueType::DirectoryHistory } + pair_type{ "directoryHistory", ValueType::DirectoryHistory }, + pair_type{ "all", AllSet }, }; }; From 3701d0ee9586beb6b85bc758ce0be154e340cc97 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 15 Aug 2023 21:12:19 +0200 Subject: [PATCH 24/59] Avoid async infinite loop on tab close (#15335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a tab gets closed, `_RemoveTab` will call `TabBase::Shutdown()`, which then re-raises the `Closed` event, which will end up calling `_RemoveTab` again, etc. The only reason this didn't crash WT so far is because `_RemoveOnCloseRoutine` contains a `resume_foreground`, which would resolve the recursion and turn it into CPU usage. It would spin as long as WinUI hasn't discard the tab object, which takes an unpredictable amount of time. Raising the `Closed` event from `Shutdown()` is unnecessary, because the handlers of the event end up calling `_RemoveTab` anyways. Technically the entire `Closed` event can be removed now, but I left it in anyways because resolving the architectural "knot" around the way tab closing after the last pane closes is implemented requires much more significant changes. This commit additionally removes the `_createCloseLock` mutex in `Pane` as it was very likely not working as intended anyways. Only some methods were protected by it and it doesn't avoid any STA/MTA/NA issues either. ## Validation Steps Performed * Closing tabs and panes always ends up calling `Shutdown()` ✅ --- src/cascadia/TerminalApp/Pane.cpp | 276 ++++++++++----------- src/cascadia/TerminalApp/Pane.h | 6 +- src/cascadia/TerminalApp/TabBase.cpp | 1 - src/cascadia/TerminalApp/TabManagement.cpp | 14 +- src/cascadia/TerminalApp/TerminalPage.cpp | 20 +- src/cascadia/TerminalApp/TerminalPage.h | 2 - 6 files changed, 151 insertions(+), 168 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index c70b3fa27da..32cf032606f 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -684,8 +684,6 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) return false; } - std::unique_lock lock{ _createCloseLock }; - // Recurse through the tree to find the parent panes of each pane that is // being swapped. auto firstParent = _FindParentOfPane(first); @@ -1033,12 +1031,31 @@ Pane::PaneNeighborSearch Pane::_FindPaneAndNeighbor(const std::shared_ptr // - // Return Value: // - -void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) +winrt::fire_and_forget Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& /*args*/) { - std::unique_lock lock{ _createCloseLock }; - // It's possible that this event handler started being executed, then before - // we got the lock, another thread created another child. So our control is + auto newConnectionState = ConnectionState::Closed; + if (const auto coreState = sender.try_as()) + { + newConnectionState = coreState.ConnectionState(); + } + + if (newConnectionState < ConnectionState::Closed) + { + // Pane doesn't care if the connection isn't entering a terminal state. + co_return; + } + + const auto weakThis = weak_from_this(); + co_await wil::resume_foreground(_root.Dispatcher()); + const auto strongThis = weakThis.lock(); + 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 @@ -1046,24 +1063,16 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio // fired after this handler returns, and will properly cleanup state. if (!_IsLeaf()) { - return; + co_return; } - const auto newConnectionState = _control.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; - } - 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) @@ -1088,8 +1097,6 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio void Pane::_CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { - std::unique_lock lock{ _createCloseLock }; - // It's possible that this event handler started being executed, then before // we got the lock, another thread created another child. So our control is // actually no longer _our_ control, and instead could be a descendant. @@ -1238,10 +1245,6 @@ void Pane::Close() // and connections beneath it. void Pane::Shutdown() { - // Lock the create/close lock so that another operation won't concurrently - // modify our tree - std::unique_lock lock{ _createCloseLock }; - // Clear out our media player callbacks, and stop any playing media. This // will prevent the callback from being triggered after we've closed, and // also make sure that our sound stops when we're closed. @@ -1594,10 +1597,6 @@ std::shared_ptr Pane::DetachPane(std::shared_ptr pane) // - void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) { - // Lock the create/close lock so that another operation won't concurrently - // modify our tree - std::unique_lock lock{ _createCloseLock }; - // If we're a leaf, then chances are both our children closed in close // succession. We waited on the lock while the other child was closed, so // now we don't have a child to close anymore. Return here. When we moved @@ -1820,135 +1819,128 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) closedChild->_ClosedByParentHandlers(); } -winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) +void Pane::_CloseChildRoutine(const bool closeFirst) { - auto weakThis{ shared_from_this() }; - - co_await wil::resume_foreground(_root.Dispatcher()); + // This will query if animations are enabled via the "Show animations in + // Windows" setting in the OS + winrt::Windows::UI::ViewManagement::UISettings uiSettings; + const auto animationsEnabledInOS = uiSettings.AnimationsEnabled(); + const auto animationsEnabledInApp = Media::Animation::Timeline::AllowDependentAnimations(); - if (auto pane{ weakThis.get() }) + // GH#7252: If either child is zoomed, just skip the animation. It won't work. + const auto eitherChildZoomed = _firstChild->_zoomed || _secondChild->_zoomed; + // If animations are disabled, just skip this and go straight to + // _CloseChild. Curiously, the pane opening animation doesn't need this, + // and will skip straight to Completed when animations are disabled, but + // this one doesn't seem to. + if (!animationsEnabledInOS || !animationsEnabledInApp || eitherChildZoomed) { - // This will query if animations are enabled via the "Show animations in - // Windows" setting in the OS - winrt::Windows::UI::ViewManagement::UISettings uiSettings; - const auto animationsEnabledInOS = uiSettings.AnimationsEnabled(); - const auto animationsEnabledInApp = Media::Animation::Timeline::AllowDependentAnimations(); - - // GH#7252: If either child is zoomed, just skip the animation. It won't work. - const auto eitherChildZoomed = pane->_firstChild->_zoomed || pane->_secondChild->_zoomed; - // If animations are disabled, just skip this and go straight to - // _CloseChild. Curiously, the pane opening animation doesn't need this, - // and will skip straight to Completed when animations are disabled, but - // this one doesn't seem to. - if (!animationsEnabledInOS || !animationsEnabledInApp || eitherChildZoomed) - { - pane->_CloseChild(closeFirst, false); - co_return; - } + _CloseChild(closeFirst, false); + return; + } - // Setup the animation + // Setup the animation - auto removedChild = closeFirst ? _firstChild : _secondChild; - auto remainingChild = closeFirst ? _secondChild : _firstChild; - const auto splitWidth = _splitState == SplitState::Vertical; + auto removedChild = closeFirst ? _firstChild : _secondChild; + auto remainingChild = closeFirst ? _secondChild : _firstChild; + const auto splitWidth = _splitState == SplitState::Vertical; - Size removedOriginalSize{ - ::base::saturated_cast(removedChild->_root.ActualWidth()), - ::base::saturated_cast(removedChild->_root.ActualHeight()) - }; - Size remainingOriginalSize{ - ::base::saturated_cast(remainingChild->_root.ActualWidth()), - ::base::saturated_cast(remainingChild->_root.ActualHeight()) - }; + Size removedOriginalSize{ + ::base::saturated_cast(removedChild->_root.ActualWidth()), + ::base::saturated_cast(removedChild->_root.ActualHeight()) + }; + Size remainingOriginalSize{ + ::base::saturated_cast(remainingChild->_root.ActualWidth()), + ::base::saturated_cast(remainingChild->_root.ActualHeight()) + }; - // Remove both children from the grid - _borderFirst.Child(nullptr); - _borderSecond.Child(nullptr); + // Remove both children from the grid + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); - if (_splitState == SplitState::Vertical) - { - Controls::Grid::SetColumn(_borderFirst, 0); - Controls::Grid::SetColumn(_borderSecond, 1); - } - else if (_splitState == SplitState::Horizontal) - { - Controls::Grid::SetRow(_borderFirst, 0); - Controls::Grid::SetRow(_borderSecond, 1); - } + if (_splitState == SplitState::Vertical) + { + Controls::Grid::SetColumn(_borderFirst, 0); + Controls::Grid::SetColumn(_borderSecond, 1); + } + else if (_splitState == SplitState::Horizontal) + { + Controls::Grid::SetRow(_borderFirst, 0); + Controls::Grid::SetRow(_borderSecond, 1); + } - // Create the dummy grid. This grid will be the one we actually animate, - // in the place of the closed pane. - Controls::Grid dummyGrid; - // GH#603 - we can safely add a BG here, as the control is gone right - // away, to fill the space as the rest of the pane expands. - dummyGrid.Background(_themeResources.unfocusedBorderBrush); - // It should be the size of the closed pane. - dummyGrid.Width(removedOriginalSize.Width); - dummyGrid.Height(removedOriginalSize.Height); + // Create the dummy grid. This grid will be the one we actually animate, + // in the place of the closed pane. + Controls::Grid dummyGrid; + // GH#603 - we can safely add a BG here, as the control is gone right + // away, to fill the space as the rest of the pane expands. + dummyGrid.Background(_themeResources.unfocusedBorderBrush); + // It should be the size of the closed pane. + dummyGrid.Width(removedOriginalSize.Width); + dummyGrid.Height(removedOriginalSize.Height); - _borderFirst.Child(closeFirst ? dummyGrid : remainingChild->GetRootElement()); - _borderSecond.Child(closeFirst ? remainingChild->GetRootElement() : dummyGrid); + _borderFirst.Child(closeFirst ? dummyGrid : remainingChild->GetRootElement()); + _borderSecond.Child(closeFirst ? remainingChild->GetRootElement() : dummyGrid); - // Set up the rows/cols as auto/auto, so they'll only use the size of - // the elements in the grid. - // - // * For the closed pane, we want to make that row/col "auto" sized, so - // it takes up as much space as is available. - // * For the remaining pane, we'll make that row/col "*" sized, so it - // takes all the remaining space. As the dummy grid is resized down, - // the remaining pane will expand to take the rest of the space. - _root.ColumnDefinitions().Clear(); - _root.RowDefinitions().Clear(); - if (_splitState == SplitState::Vertical) - { - auto firstColDef = Controls::ColumnDefinition(); - auto secondColDef = Controls::ColumnDefinition(); - firstColDef.Width(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); - secondColDef.Width(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); - _root.ColumnDefinitions().Append(firstColDef); - _root.ColumnDefinitions().Append(secondColDef); - } - else if (_splitState == SplitState::Horizontal) - { - auto firstRowDef = Controls::RowDefinition(); - auto secondRowDef = Controls::RowDefinition(); - firstRowDef.Height(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); - secondRowDef.Height(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); - _root.RowDefinitions().Append(firstRowDef); - _root.RowDefinitions().Append(secondRowDef); - } + // Set up the rows/cols as auto/auto, so they'll only use the size of + // the elements in the grid. + // + // * For the closed pane, we want to make that row/col "auto" sized, so + // it takes up as much space as is available. + // * For the remaining pane, we'll make that row/col "*" sized, so it + // takes all the remaining space. As the dummy grid is resized down, + // the remaining pane will expand to take the rest of the space. + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); + if (_splitState == SplitState::Vertical) + { + auto firstColDef = Controls::ColumnDefinition(); + auto secondColDef = Controls::ColumnDefinition(); + firstColDef.Width(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); + secondColDef.Width(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); + _root.ColumnDefinitions().Append(firstColDef); + _root.ColumnDefinitions().Append(secondColDef); + } + else if (_splitState == SplitState::Horizontal) + { + auto firstRowDef = Controls::RowDefinition(); + auto secondRowDef = Controls::RowDefinition(); + firstRowDef.Height(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); + secondRowDef.Height(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto()); + _root.RowDefinitions().Append(firstRowDef); + _root.RowDefinitions().Append(secondRowDef); + } - // Animate the dummy grid from its current size down to 0 - Media::Animation::DoubleAnimation animation{}; - animation.Duration(AnimationDuration); - animation.From(splitWidth ? removedOriginalSize.Width : removedOriginalSize.Height); - animation.To(0.0); - // This easing is the same as the entrance animation. - animation.EasingFunction(Media::Animation::QuadraticEase{}); - animation.EnableDependentAnimation(true); + // Animate the dummy grid from its current size down to 0 + Media::Animation::DoubleAnimation animation{}; + animation.Duration(AnimationDuration); + animation.From(splitWidth ? removedOriginalSize.Width : removedOriginalSize.Height); + animation.To(0.0); + // This easing is the same as the entrance animation. + animation.EasingFunction(Media::Animation::QuadraticEase{}); + animation.EnableDependentAnimation(true); - Media::Animation::Storyboard s; - s.Duration(AnimationDuration); - s.Children().Append(animation); - s.SetTarget(animation, dummyGrid); - s.SetTargetProperty(animation, splitWidth ? L"Width" : L"Height"); + Media::Animation::Storyboard s; + s.Duration(AnimationDuration); + s.Children().Append(animation); + s.SetTarget(animation, dummyGrid); + s.SetTargetProperty(animation, splitWidth ? L"Width" : L"Height"); - // Start the animation. - s.Begin(); + // Start the animation. + s.Begin(); - std::weak_ptr weakThis{ shared_from_this() }; + std::weak_ptr weakThis{ shared_from_this() }; - // When the animation is completed, reparent the child's content up to - // us, and remove the child nodes from the tree. - animation.Completed([weakThis, closeFirst](auto&&, auto&&) { - if (auto pane{ weakThis.lock() }) - { - // We don't need to manually undo any of the above trickiness. - // We're going to re-parent the child's content into us anyways - pane->_CloseChild(closeFirst, false); - } - }); - } + // When the animation is completed, reparent the child's content up to + // us, and remove the child nodes from the tree. + animation.Completed([weakThis, closeFirst](auto&&, auto&&) { + if (auto pane{ weakThis.lock() }) + { + // We don't need to manually undo any of the above trickiness. + // We're going to re-parent the child's content into us anyways + pane->_CloseChild(closeFirst, false); + } + }); } // Method Description: @@ -2488,10 +2480,6 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect { auto actualSplitType = _convertAutomaticOrDirectionalSplitState(splitType); - // Lock the create/close lock so that another operation won't concurrently - // modify our tree - std::unique_lock lock{ _createCloseLock }; - if (_IsLeaf()) { // revoke our handler - the child will take care of the control now. diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index a6137534dac..6920a90b341 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -265,8 +265,6 @@ class Pane : public std::enable_shared_from_this winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker; - std::shared_mutex _createCloseLock{}; - Borders _borders{ Borders::None }; bool _zoomed{ false }; @@ -305,11 +303,11 @@ class Pane : public std::enable_shared_from_this const PanePoint offset); void _CloseChild(const bool closeFirst, const bool isDetaching); - winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); + void _CloseChildRoutine(const bool closeFirst); void _Focus(); void _FocusFirstChild(); - 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 _ControlGotFocusHandler(const winrt::Windows::Foundation::IInspectable& sender, diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp index 00aa0882e13..2d7293ccd6d 100644 --- a/src/cascadia/TerminalApp/TabBase.cpp +++ b/src/cascadia/TerminalApp/TabBase.cpp @@ -37,7 +37,6 @@ namespace winrt::TerminalApp::implementation ASSERT_UI_THREAD(); Content(nullptr); - _ClosedHandlers(nullptr, nullptr); } // Method Description: diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 8d8cca21ef6..ef0093dc30d 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -249,10 +249,13 @@ namespace winrt::TerminalApp::implementation }); // When the tab is closed, remove it from our list of tabs. - newTabImpl->Closed([tabViewItem, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) { - if (auto page{ weakThis.get() }) + newTabImpl->Closed([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) { + const auto page = weakThis.get(); + const auto tab = weakTab.get(); + + if (page && tab) { - page->_RemoveOnCloseRoutine(tabViewItem, page); + page->_RemoveTab(*tab); } }); @@ -521,6 +524,11 @@ namespace winrt::TerminalApp::implementation _mruTabs.RemoveAt(mruIndex); } + if (tab == _settingsTab) + { + _settingsTab = nullptr; + } + if (_stashed.draggedTab && *_stashed.draggedTab == tab) { _stashed.draggedTab = nullptr; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index c9c31b85247..d4ec0666b68 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1169,16 +1169,6 @@ namespace winrt::TerminalApp::implementation } } - winrt::fire_and_forget TerminalPage::_RemoveOnCloseRoutine(Microsoft::UI::Xaml::Controls::TabViewItem tabViewItem, winrt::com_ptr page) - { - co_await wil::resume_foreground(page->_tabView.Dispatcher()); - - if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) - { - _RemoveTab(tab); - } - } - std::wstring TerminalPage::_evaluatePathForCwd(const std::wstring_view path) { return Utils::EvaluateStartingDirectory(_WindowProperties.VirtualWorkingDirectory(), path); @@ -3851,11 +3841,13 @@ namespace winrt::TerminalApp::implementation }); // When the tab is closed, remove it from our list of tabs. - newTabImpl->Closed([tabViewItem, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) { - if (auto page{ weakThis.get() }) + newTabImpl->Closed([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) { + const auto page = weakThis.get(); + const auto tab = weakTab.get(); + + if (page && tab) { - page->_settingsTab = nullptr; - page->_RemoveOnCloseRoutine(tabViewItem, page); + page->_RemoveTab(*tab); } }); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2967954c343..2c0439f9d98 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -390,8 +390,6 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); void _AddPreviouslyClosedPaneOrTab(std::vector&& args); - winrt::fire_and_forget _RemoveOnCloseRoutine(Microsoft::UI::Xaml::Controls::TabViewItem tabViewItem, winrt::com_ptr page); - void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); void _SplitPane(const Microsoft::Terminal::Settings::Model::SplitDirection splitType, From a7a44901c2b514a37f0a3ec90d38d5647ae4c81f Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 15 Aug 2023 21:50:47 +0200 Subject: [PATCH 25/59] AtlasEngine: Fix ligature splitting for JetBrains Mono (#15810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some fonts implement ligatures by replacing a string like "&&" with a whitespace padding glyph, followed by the actual "&&" glyph which has a 1 column advance width. In that case the algorithm in `_drawTextOverlapSplit` will get confused because it strictly scans the input from left to right, searching for color changes. The initial color is the glyph's color and so it breaks for such fonts because then the first split will retain the last column's color. ## Validation Steps Performed * Use JetBrains Mono * Print ``"`e[91m`&`e[96m&`e[m"`` * Red and blue `&` appear ✅ --------- Co-authored-by: Tushar Singh Co-authored-by: Dustin L. Howett --- src/renderer/atlas/BackendD3D.cpp | 31 +++++++++++++++++++++++++------ src/tools/RenderingTests/main.cpp | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index f8e7a5478b2..677a92396d4 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -1086,11 +1086,15 @@ void BackendD3D::_drawText(RenderingPayload& p) // < ! - - void BackendD3D::_drawTextOverlapSplit(const RenderingPayload& p, u16 y) { - const auto& originalQuad = _getLastQuad(); + auto& originalQuad = _getLastQuad(); + // If the current row has a non-default line rendition then every glyph is scaled up by 2x horizontally. + // This requires us to make some changes: For instance, if the ligature occupies columns 3, 4 and 5 (0-indexed) + // then we need to get the foreground colors from columns 2 and 4, because columns 0,1 2,3 4,5 6,7 and so on form pairs. + // A wide glyph would be a total of 4 actual columns wide! In other words, we need to properly round our clip rects and columns. i32 columnAdvance = 1; - i32 columnAdvanceInPx{ p.s->font->cellSize.x }; - i32 cellCount{ p.s->viewportCellCount.x }; + i32 columnAdvanceInPx = p.s->font->cellSize.x; + i32 cellCount = p.s->viewportCellCount.x; if (p.rows[y]->lineRendition != LineRendition::SingleWidth) { @@ -1104,12 +1108,27 @@ void BackendD3D::_drawTextOverlapSplit(const RenderingPayload& p, u16 y) originalLeft = std::max(originalLeft, 0); originalRight = std::min(originalRight, cellCount * columnAdvanceInPx); - auto column = originalLeft / columnAdvanceInPx + 1; + if (originalLeft >= originalRight) + { + return; + } + + const auto colors = &p.foregroundBitmap[p.colorBitmapRowStride * y]; + + // As explained in the beginning, column and clipLeft should be in multiples of columnAdvance + // and columnAdvanceInPx respectively, because that's how line renditions work. + auto column = originalLeft / columnAdvanceInPx; auto clipLeft = column * columnAdvanceInPx; column *= columnAdvance; - const auto colors = &p.foregroundBitmap[p.colorBitmapRowStride * y]; - auto lastFg = originalQuad.color; + // Our loop below will split the ligature by processing it from left to right. + // Some fonts however implement ligatures by replacing a string like "&&" with a whitespace padding glyph, + // followed by the actual "&&" glyph which has a 1 column advance width. In that case the originalQuad + // will have the .color of the 2nd column and not of the 1st one. We need to fix that here. + auto lastFg = colors[column]; + originalQuad.color = lastFg; + column += columnAdvance; + clipLeft += columnAdvanceInPx; // We must ensure to exit the loop while `column` is less than `cellCount.x`, // otherwise we cause a potential out of bounds access into foregroundBitmap. diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp index 9db85e1e840..a82e38bb818 100644 --- a/src/tools/RenderingTests/main.cpp +++ b/src/tools/RenderingTests/main.cpp @@ -5,6 +5,7 @@ #include #include +#include // Another variant of "defer" for C++. namespace From d28b6bf1f2c5a0078e85fe68b3e7ca4c167c4223 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 17 Aug 2023 16:18:16 -0500 Subject: [PATCH 26/59] Move the Azure Cloud Shell icons from terminal-internals (#15841) I put them in that package like 40 years ago to get them into the build system faster. They actually belong here. I made them based on SVGs the Azure Cloud Shell team shared with us. --- ...2-4e3d-5e58-b989-0a998ec441b8}.scale-100.png | Bin 314 -> 480 bytes ...2-4e3d-5e58-b989-0a998ec441b8}.scale-200.png | Bin 573 -> 943 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-100.png b/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-100.png index e76345444276789d8d81a5f8935ae24aba260fa7..97efcdddfd1f679d1967224b8294443466132175 100644 GIT binary patch delta 467 zcmV;^0WAKy0^kFX7k>~41^@s6AM^iV00004b3#c}2nYxWdILq*zZu^2qdm-l&}_kG^)ec|6jWV2cMoxpWnOw;^~GntHN z%&VF_00+wnmZoF$btwQ? zZVk)zIyyHskbl6|nZaTz%E{UUo)3mqX%famLAv`Q6biDd`3oNt?54-KC_Hg|ZDLpz z^z|faK(MLbvoP6DIu%8TBo>ZwX*er0TJJf4(DSFVaR-)Lqxe!KIoLzt(Pe%-%;wxs z>*S_~pO_s9n`$HL`Stf`b%JoWLix?derd0LhgtKa|t002ov JPDHLkV1m}8(cb_7 delta 300 zcmV+{0n`5A1G)l`7k>;01^@s6Yv(AMb1&V+Xg0F4 zv1VhX{0CUfKaUdql}aBz%7YDo>V&Ne4riW!yQdRaJk5~F?tuos3V+56@4XwGOg;UCbOOc8 zS+M0~&}yJp&>cs)Gr&GGF|c+W(R6vtPaq9+U5obzl%{XKf%!^Ef3yn>r`YEsPGydB z_o8q$j{IhdavU{tG_@)U)j6c4)Ihr+%A$Ba2Eo>!0Q36-YfrI*g3(ln@xWVlJ${Al y+aBtTNyG>cb|r-r7%f)=SEaPjNdClBW55TBw?`rhCPRJz00007k?lK1^@s6b9#F800004b3#c}2nYxWd^|iCCegRRn5ji6EOUSbrc?Ay||RLa;!Bw)_Q1 zT|~+zT~v?_dkSn=K@n2_0Fc}YiUf$1N&%-NC9Y?tV|&K)^;poRPNtoS>2!hUSS$yy3`lgO2}K!R=?t;vhd=Zhb2@>|mZ;HCXlW+w6gGz^!8i)(b{!DJhQims)*ZEc^Q;*&+aRJDujsiZC5{{)mAT2{N3lD;KNs+ zQnEIXz#a zoL2$p`61^oH(71%f^<12?-q4a41vU!JM(!HfI0<>WMlw~s!KVaMN% zd*EPy5V3C90l(|LyOtHf>6j8%ZdYUg9IrR5EA@tTN=hj-fO5uXPtL{a`Lt}C>AvZm z8h_1GE!?+L{|1_bS>U63qm$EwK!0JhU43p?=ayw5$F`4Zt~kPus-*`Z-_r{D@|>y$ z)9dT&x(tKJZ_e#?R4L^*LWt@^uux8XuL0_7);m)2@k20hepdzn>J96ZghgODCGqb< zPrR~g0{~YxHi~XgnhT|TpmwN=a_vy9@E?N^BE4tz5&pCI8zE6KqjLZdoB#j-07*qo IM6N<$f>HIhz5oCK delta 561 zcmV-10?z%f2fYN47k?NC1^@s6n^XTZ00066NklWEUbIyUo6*l?ECj0Y$0EuUNG0rfm4u9e-vNQ0B)aXhlMYIQy z$Q{NwF_1ltX8ILO2KV?25ZwrfH$d{~G1MdwX%+hhH*{qq5Ul~?g%KRR2pI3`WEwY= z^NwqbXbq6eUjh!bC#pXX=P{Ibq52(__oO2qy>goa#9jvJ|M|d6p!K`HX6e^;o{W{7 z#jOu8nH#2TMt{Ma>q~Lt=zQh23Xm*}fsXgw z{O?p)<~I}E4;QNdXYyAO_W1@-F;Y8AI{Ij}1NqtB&40d0S*i=9BWG7TFqc6j;A;n< zh#Nj#8^L9SgZ={OXWU4<3J}kSaESW~VC^ezN3T@@PGz?c9GF5F@(H5Mik#zaXraD{ zDqGNU0c1zdA%r{J3K!~opyn9M25|;r7>)o=*FjXjAaV~k{I+ou{_B*;4r2QEqipXA zgc=KGkt(4N^Q2O?VL^ArpaT)_FIflT?WFzz+7FEx{&ZaY00000NkvXXu0mjf@$Lr8 From 59a7dabf3bb85fef9fb5bfe79ac64fe2e31b54c9 Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Mon, 21 Aug 2023 23:11:43 +0530 Subject: [PATCH 27/59] Fix C# warnings during a clean build (#15857) #### Fix warnings due to formatting during a clean build Seems like the compiler cares about them more than our formatter. Possibly introduced in https://github.com/microsoft/terminal/pull/15062 ## Validation Steps Performed - Tests passed --- src/cascadia/WpfTerminalControl/TerminalContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index c9bed99c5cb..206005a0924 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -113,23 +113,24 @@ private get { this.connection.TerminalOutput -= this.Connection_TerminalOutput; } - this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x001bc\x1b]104\x1b\\")); //reset console/clear screen - https://github.com/microsoft/terminal/pull/15062#issuecomment-1505654110 + + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x001bc\x1b]104\x1b\\")); // reset console/clear screen - https://github.com/microsoft/terminal/pull/15062#issuecomment-1505654110 var wasNull = this.connection == null; this.connection = value; if (this.connection != null) { if (wasNull) { - this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25h")); //show cursor + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25h")); // show cursor } + this.connection.TerminalOutput += this.Connection_TerminalOutput; this.connection.Start(); } else { - this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25l")); //hide cursor + this.Connection_TerminalOutput(this, new TerminalOutputEventArgs("\x1b[?25l")); // hide cursor } - } } From 6cb14d226d834ceecdc803f53f382cce0b735768 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 21 Aug 2023 13:09:17 -0500 Subject: [PATCH 28/59] Allow skipping artifact publication in all release build jobs (#15846) The OneBranch build system relies on the *build container host* being able to publish all artifacts all at once. Therefore, our build steps must not publish any artifacts. I made it configurable so that the impact on existing pipelines was minimal. For every job that produces artifacts and is part of the release pipeline, I am now exposing two variables that we can pass to OneBranch so that it can locate and name artifacts: - `JobOutputDirectory`, the output folder for the entire job - `JobOutputArtifactName`, the name of the artifact produced by the job I have also added a `variables` parameter to every job, so consuming pipelines can override or insert their own variables. --- .../templates-v2/job-build-package-wpf.yml | 16 ++++++++-- .../templates-v2/job-build-project.yml | 31 ++++++++++++++----- .../job-merge-msix-into-bundle.yml | 16 ++++++++-- .../templates-v2/job-package-conpty.yml | 17 ++++++++-- .../templates-v2/job-submit-windows-vpack.yml | 17 ++++++++-- 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/build/pipelines/templates-v2/job-build-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml index fe668058b20..c9329eab519 100644 --- a/build/pipelines/templates-v2/job-build-package-wpf.yml +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -21,6 +21,12 @@ parameters: - name: jobName type: string default: PackWPF + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true jobs: - job: ${{ parameters.jobName }} @@ -39,6 +45,9 @@ jobs: variables: OutputBuildPlatform: AnyCPU Terminal.BinDir: $(Build.SourcesDirectory)/bin/$(OutputBuildPlatform)/$(BuildConfiguration) + JobOutputDirectory: $(Build.ArtifactStagingDirectory)\nupkg + JobOutputArtifactName: wpf-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} steps: - checkout: self clean: true @@ -129,6 +138,7 @@ jobs: ValidateSignature: true Verbosity: 'Verbose' - - publish: $(Build.ArtifactStagingDirectory)\nupkg - artifact: wpf-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} - displayName: Publish nupkg + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + displayName: Publish nupkg diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index 1c07bb9008e..5dcc8d50cf8 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -57,6 +57,12 @@ parameters: - name: beforeBuildSteps type: stepList default: [] + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true jobs: - job: ${{ parameters.jobName }} @@ -84,6 +90,9 @@ jobs: # Yup. BuildTargetParameter: ' ' SelectedSigningFragments: ' ' + JobOutputDirectory: $(Terminal.BinDir) + JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} displayName: Build timeoutInMinutes: 240 cancelTimeoutInMinutes: 1 @@ -141,10 +150,17 @@ jobs: configuration: $(BuildConfiguration) maximumCpuCount: true - - publish: $(Build.SourcesDirectory)/msbuild.binlog - artifact: logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} - condition: always() - displayName: Publish Build Log + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(Build.SourcesDirectory)/msbuild.binlog + artifact: logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + condition: always() + displayName: Publish Build Log + - ${{ else }}: + - task: CopyFiles@2 + displayName: Copy Build Log + inputs: + contents: $(Build.SourcesDirectory)/msbuild.binlog + TargetFolder: $(Terminal.BinDir) # This saves ~2GiB per architecture. We won't need these later. # Removes: @@ -258,6 +274,7 @@ jobs: ValidateSignature: true Verbosity: 'Verbose' - - publish: $(Terminal.BinDir) - artifact: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} - displayName: Publish All Outputs + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(Terminal.BinDir) + artifact: $(JobOutputArtifactName) + displayName: Publish All Outputs 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 318d3b2673a..121e07b4417 100644 --- a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -23,6 +23,12 @@ parameters: - name: jobName type: string default: Bundle + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true jobs: - job: ${{ parameters.jobName }} @@ -44,6 +50,9 @@ jobs: BundleStemName: Microsoft.WindowsTerminalPreview ${{ else }}: BundleStemName: WindowsTerminalDev + JobOutputDirectory: '$(System.ArtifactsDirectory)/bundle' + JobOutputArtifactName: appxbundle-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} dependsOn: ${{ parameters.dependsOn }} steps: - checkout: self @@ -128,6 +137,7 @@ jobs: ValidateSignature: true Verbosity: 'Verbose' - - publish: '$(System.ArtifactsDirectory)/bundle' - artifact: appxbundle-$(BuildConfiguration)${{ parameters.artifactStem }} - displayName: Publish msixbundle + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + displayName: Publish msixbundle diff --git a/build/pipelines/templates-v2/job-package-conpty.yml b/build/pipelines/templates-v2/job-package-conpty.yml index db648d905f2..52f3a5c15d9 100644 --- a/build/pipelines/templates-v2/job-package-conpty.yml +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -21,6 +21,12 @@ parameters: - name: jobName type: string default: PackConPTY + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true jobs: - job: ${{ parameters.jobName }} @@ -36,6 +42,10 @@ jobs: ${{ config }}: BuildConfiguration: ${{ config }} dependsOn: ${{ parameters.dependsOn }} + variables: + JobOutputDirectory: $(Build.ArtifactStagingDirectory)\nupkg + JobOutputArtifactName: conpty-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} steps: - checkout: self clean: true @@ -113,6 +123,7 @@ jobs: ValidateSignature: true Verbosity: 'Verbose' - - publish: $(Build.ArtifactStagingDirectory)\nupkg - artifact: conpty-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} - displayName: Publish nupkg + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + displayName: Publish nupkg diff --git a/build/pipelines/templates-v2/job-submit-windows-vpack.yml b/build/pipelines/templates-v2/job-submit-windows-vpack.yml index 2bed2899cf0..2e9cb1e1fe2 100644 --- a/build/pipelines/templates-v2/job-submit-windows-vpack.yml +++ b/build/pipelines/templates-v2/job-submit-windows-vpack.yml @@ -13,6 +13,12 @@ parameters: - name: artifactStem type: string default: '' + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true jobs: - job: VPack @@ -20,6 +26,10 @@ jobs: pool: ${{ parameters.pool }} displayName: Create and Submit Windows vPack dependsOn: ${{ parameters.dependsOn }} + variables: + JobOutputDirectory: $(XES_VPACKMANIFESTDIRECTORY) + JobOutputArtifactName: vpack-manifest${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} steps: - checkout: self clean: true @@ -57,9 +67,10 @@ jobs: owner: conhost githubToken: $(GitHubTokenForVpackProvenance) - - publish: $(XES_VPACKMANIFESTDIRECTORY) - artifact: vpack-manifest${{ parameters.artifactStem }} - displayName: 'Publish VPack Manifest to Drop' + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + displayName: 'Publish VPack Manifest to Drop' - task: PkgESFCIBGit@12 displayName: 'Submit VPack Manifest to Windows' From df648208d52437073ad3f699cc07450db7b71d05 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Mon, 21 Aug 2023 13:55:50 -0500 Subject: [PATCH 29/59] Remove outdated or implied build conditions and parameters (#15842) We no longer multiplex PGO through the test runner. We also removed the compliance build. --- build/pipelines/release.yml | 4 ---- build/pipelines/templates-v2/job-run-pgo-tests.yml | 1 - build/pipelines/templates-v2/job-test-project.yml | 10 +++++----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index fd388ea1d0e..98980ac37b6 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -60,10 +60,6 @@ parameters: type: string default: '0.0.8' - - name: runCompliance - displayName: "Run Compliance and Security Build" - type: boolean - default: true - name: publishSymbolsToPublic displayName: "Publish Symbols to MSDL" type: boolean diff --git a/build/pipelines/templates-v2/job-run-pgo-tests.yml b/build/pipelines/templates-v2/job-run-pgo-tests.yml index 5270ca1bf85..817d97ff9ce 100644 --- a/build/pipelines/templates-v2/job-run-pgo-tests.yml +++ b/build/pipelines/templates-v2/job-run-pgo-tests.yml @@ -65,7 +65,6 @@ jobs: -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" -AdditionalTaefArguments '/select:(@IsPGO=true)','/p:WTTestContent=$(TerminalTestContentPath)' - condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - task: CopyFiles@2 displayName: 'Copy PGO outputs to Artifacts' diff --git a/build/pipelines/templates-v2/job-test-project.yml b/build/pipelines/templates-v2/job-test-project.yml index d8688e36aaf..78f5fa4db6a 100644 --- a/build/pipelines/templates-v2/job-test-project.yml +++ b/build/pipelines/templates-v2/job-test-project.yml @@ -46,7 +46,6 @@ jobs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 arguments: -MatchPattern '*unit.test*.dll' -Platform '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" - condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - ${{ if or(eq(parameters.platform, 'x64'), eq(parameters.platform, 'arm64')) }}: - task: PowerShell@2 @@ -55,27 +54,26 @@ jobs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 arguments: -MatchPattern '*feature.test*.dll' -Platform '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" - condition: and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')) - task: PowerShell@2 displayName: 'Convert Test Logs from WTL to xUnit format' + condition: always() inputs: targetType: filePath filePath: build\Helix\ConvertWttLogToXUnit.ps1 arguments: -WttInputPath '${{ parameters.testLogPath }}' -WttSingleRerunInputPath 'unused.wtl' -WttMultipleRerunInputPath 'unused2.wtl' -XUnitOutputPath 'onBuildMachineResults.xml' -TestNamePrefix '$(BuildConfiguration).$(BuildPlatform)' - condition: ne(variables['PGOBuildMode'], 'Instrument') - task: PowerShell@2 displayName: 'Manually log test failures' + condition: always() inputs: targetType: filePath filePath: build\Helix\OutputTestErrorsForAzureDevops.ps1 arguments: -XUnitOutputPath 'onBuildMachineResults.xml' - condition: ne(variables['PGOBuildMode'], 'Instrument') - task: PublishTestResults@2 displayName: 'Upload converted test logs' - condition: ne(variables['PGOBuildMode'], 'Instrument') + condition: always() inputs: testResultsFormat: 'xUnit' # Options: JUnit, NUnit, VSTest, xUnit, cTest testResultsFiles: '**/onBuildMachineResults.xml' @@ -85,6 +83,7 @@ jobs: - task: CopyFiles@2 displayName: 'Copy result logs to Artifacts' + condition: always() inputs: Contents: | **/*.wtl @@ -96,3 +95,4 @@ jobs: - publish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test-logs' artifact: test-logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + condition: always() From 21c2dee50aaeaeda1865f8d17c5889482025361d Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Tue, 22 Aug 2023 01:48:36 +0530 Subject: [PATCH 30/59] [Tests] Add test for subparameter based GraphicOptions (#15844) Add test for subparameter based `GraphicOptions`. `GraphicsSingleWithSubParamTests` is added for subparameter based `GraphicOptions`. This should've been included with #15729. Also, while working on #15795, I realized creating and passing subparameters for the tests is painful right now. I've added a small util `MakeSubParamsAndRanges(...)` that eases creating subparameters and subparameter ranges from a simple list of (lists of) subparameters. ## Validation Steps Performed - All tests passed. --- .../adapter/ut_adapter/adapterTest.cpp | 108 ++++++++++++------ 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 4661c4b7ecf..2b29d78bf49 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -332,6 +332,24 @@ class TestGetSet final : public ITerminalApi _expectedCursorPos = cursorPos; } + static void MakeSubParamsAndRanges(std::initializer_list> subParamList, _Out_ std::vector& subParams, _Out_ std::vector>& subParamRanges) + { + // Args are a list of lists of VTParameters: + // { {P1S1, P1S2, P1S3, ... }, { P2S1, P2S2, P2S3, ... } ... } + // + // P1 and P2 denotes the parameters, while S1, S2, S3 denotes the + // subparameters of the corresponding parameter. + size_t totalSubParams = 0; + subParams.clear(); + subParamRanges.clear(); + for (const auto& it : subParamList) + { + subParams.insert(subParams.end(), it.begin(), it.end()); + subParamRanges.push_back({ gsl::narrow_cast(totalSubParams), gsl::narrow_cast(it.size() + totalSubParams) }); + totalSubParams += it.size(); + } + } + void ValidateExpectedCursorPos() { VERIFY_ARE_EQUAL(_expectedCursorPos, _textBuffer->GetCursor().GetPosition()); @@ -1115,6 +1133,52 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); } + TEST_METHOD(GraphicsSingleWithSubParamTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{38, 48}") // corresponds to options in DispatchTypes::GraphicsOptions + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + _testGetSet->PrepData(); + + // Modify variables based on type of this test + DispatchTypes::GraphicsOptions graphicsOption; + std::vector subParams; + std::vector> subParamRanges; + size_t uiGraphicsOption; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiGraphicsOptions", uiGraphicsOption)); + graphicsOption = (DispatchTypes::GraphicsOptions)uiGraphicsOption; + + VTParameter rgOptions[16]; + size_t cOptions = 1; + rgOptions[0] = graphicsOption; + + TextAttribute startingAttribute; + switch (graphicsOption) + { + case DispatchTypes::GraphicsOptions::ForegroundExtended: + Log::Comment(L"Testing graphics 'ForegroundExtended'"); + _testGetSet->MakeSubParamsAndRanges({ { DispatchTypes::GraphicsOptions::BlinkOrXterm256Index, TextColor::DARK_RED } }, subParams, subParamRanges); + startingAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetIndexedForeground256(TextColor::DARK_RED); + break; + case DispatchTypes::GraphicsOptions::BackgroundExtended: + Log::Comment(L"Testing graphics 'BackgroundExtended'"); + _testGetSet->MakeSubParamsAndRanges({ { DispatchTypes::GraphicsOptions::BlinkOrXterm256Index, TextColor::BRIGHT_WHITE } }, subParams, subParamRanges); + startingAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetIndexedBackground256(TextColor::BRIGHT_WHITE); + break; + default: + VERIFY_FAIL(L"Test not implemented yet!"); + break; + } + _testGetSet->_textBuffer->SetCurrentAttributes(startingAttribute); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ std::span{ rgOptions, cOptions }, subParams, subParamRanges })); + } + TEST_METHOD(GraphicsPushPopTests) { Log::Comment(L"Starting test..."); @@ -2502,62 +2566,45 @@ class AdapterTest _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED VTParameter rgOptions[1]; - VTParameter rgSubParamOpts[16]; - std::pair subParamRanges[1]; + std::vector rgSubParamOpts; + std::vector> subParamRanges; _testGetSet->_expectedAttribute = _testGetSet->_textBuffer->GetCurrentAttributes(); Log::Comment(L"Test 1: Change Indexed Foreground with missing index sub parameter"); rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index; - subParamRanges[0] = { (BYTE)0, (BYTE)1 }; + _testGetSet->MakeSubParamsAndRanges({ { 5 } }, rgSubParamOpts, subParamRanges); _testGetSet->_expectedAttribute.SetIndexedForeground256(TextColor::DARK_BLACK); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); Log::Comment(L"Test 2: Change Indexed Background with default index sub parameter"); rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index; - rgSubParamOpts[1] = {}; - subParamRanges[0] = { (BYTE)0, (BYTE)2 }; + _testGetSet->MakeSubParamsAndRanges({ { 5, {} } }, rgSubParamOpts, subParamRanges); _testGetSet->_expectedAttribute.SetIndexedBackground256(TextColor::DARK_BLACK); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); Log::Comment(L"Test 3: Change RGB Foreground with all RGB sub parameters missing"); rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; - subParamRanges[0] = { (BYTE)0, (BYTE)1 }; + _testGetSet->MakeSubParamsAndRanges({ { 2 } }, rgSubParamOpts, subParamRanges); _testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 0)); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); Log::Comment(L"Test 4: Change RGB Background with some missing RGB sub parameters"); rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; - rgSubParamOpts[1] = {}; // color-space-id - rgSubParamOpts[2] = 123; - subParamRanges[0] = { (BYTE)0, (BYTE)3 }; + _testGetSet->MakeSubParamsAndRanges({ { 2, {}, 123 } }, rgSubParamOpts, subParamRanges); _testGetSet->_expectedAttribute.SetBackground(RGB(123, 0, 0)); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); Log::Comment(L"Test 5: Change RGB Foreground with some default RGB sub parameters"); rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; - rgSubParamOpts[1] = {}; // color-space-id - rgSubParamOpts[2] = {}; - rgSubParamOpts[3] = {}; - rgSubParamOpts[4] = 123; - subParamRanges[0] = { (BYTE)0, (BYTE)5 }; + _testGetSet->MakeSubParamsAndRanges({ { 2, {}, {}, {}, 123 } }, rgSubParamOpts, subParamRanges); _testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 123)); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); Log::Comment(L"Test 6: Ignore color when ColorSpaceID is not empty"); _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; - rgSubParamOpts[1] = 7; // color-space-id - rgSubParamOpts[2] = 182; - rgSubParamOpts[3] = 182; - rgSubParamOpts[4] = 123; - subParamRanges[0] = { (BYTE)0, (BYTE)5 }; + _testGetSet->MakeSubParamsAndRanges({ { 2, 7, 182, 182, 123 } }, rgSubParamOpts, subParamRanges); // expect no change _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); @@ -2565,14 +2612,9 @@ class AdapterTest Log::Comment(L"Test 7: Ignore Rgb color when R, G or B is out of range (>255)"); _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; - rgSubParamOpts[1] = {}; // color-space-id // Ensure r, g and b set a color that is different from current color. // Otherwise, the test will pass even if the color is not ignored. - rgSubParamOpts[2] = 128; - rgSubParamOpts[3] = 283; - rgSubParamOpts[4] = 155; - subParamRanges[0] = { (BYTE)0, (BYTE)5 }; + _testGetSet->MakeSubParamsAndRanges({ { 2, {}, 128, 283, 155 } }, rgSubParamOpts, subParamRanges); // expect no change _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); @@ -2580,9 +2622,7 @@ class AdapterTest Log::Comment(L"Test 8: Ignore indexed color when index is out of range (>255)"); _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; - rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index; - rgSubParamOpts[1] = 283; - subParamRanges[0] = { (BYTE)0, (BYTE)2 }; + _testGetSet->MakeSubParamsAndRanges({ { 5, 283 } }, rgSubParamOpts, subParamRanges); // expect no change _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges })); From 333fcd89b305a1c4f08b05c238c39b56eb4d03e9 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 22 Aug 2023 05:39:14 -0500 Subject: [PATCH 31/59] Use a different schema for each branding (#15856) Switch the schema depending on the branding we're being built for Ever since we started writing the entire settings file out ourselves, we've had the opportunity to control which schema it uses. This is a quality-of-life improvement for Preview users, and might make life easier for Dev users as well. For Debug builds, it even switches over to a local `file://` path to the schema in the source directory! Closes #6601 --- .../CascadiaSettingsSerialization.cpp | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 542b4b8398d..3063f549494 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -1227,6 +1227,15 @@ void CascadiaSettings::WriteSettingsToDisk() } } +#ifndef NDEBUG +static [[maybe_unused]] std::string _getDevPathToSchema() +{ + std::filesystem::path filePath{ __FILE__ }; + auto schemaPath = filePath.parent_path().parent_path().parent_path().parent_path() / "doc" / "cascadia" / "profiles.schema.json"; + return "file:///" + schemaPath.generic_string(); +} +#endif + // Method Description: // - Create a new serialized JsonObject from an instance of this class // Arguments: @@ -1238,7 +1247,17 @@ Json::Value CascadiaSettings::ToJson() const // top-level json object auto json{ _globals->ToJson() }; json["$help"] = "https://aka.ms/terminal-documentation"; - json["$schema"] = "https://aka.ms/terminal-profiles-schema"; + json["$schema"] = +#if defined(WT_BRANDING_RELEASE) + "https://aka.ms/terminal-profiles-schema" +#elif defined(WT_BRANDING_PREVIEW) + "https://aka.ms/terminal-profiles-schema-preview" +#elif !defined(NDEBUG) // DEBUG mode + _getDevPathToSchema() // magic schema path that refers to the local source directory +#else // All other brandings + "https://raw.githubusercontent.com/microsoft/terminal/main/doc/cascadia/profiles.schema.json" +#endif + ; // "profiles" will always be serialized as an object Json::Value profiles{ Json::ValueType::objectValue }; From 8f20ea6b2d12f27217ca962fa80f3a5a299d274f Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 23 Aug 2023 07:25:06 -0500 Subject: [PATCH 32/59] Include our Azure client ID in AzureConnection (#15866) Some folks over in MSAL land told us that client IDs don't need to be kept secret. This reduces the delta between "public" terminal and "release build" terminal by one more file, leaving only the telemetry header left (which won't be going public for obvious reasons). This will also make it easier for contributors to test out Azure Cloud Shell changes... and testing out VT without ConPTY interfering[^1]. [^1]: When Dev branding is selected, Azure Cloud Shell has the added perk of being wired directly to TerminalCore rather than going through ConPTY. --- src/cascadia/TerminalConnection/AzureClientID.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalConnection/AzureClientID.h b/src/cascadia/TerminalConnection/AzureClientID.h index 003e4d22b2c..aa7c2f092f3 100644 --- a/src/cascadia/TerminalConnection/AzureClientID.h +++ b/src/cascadia/TerminalConnection/AzureClientID.h @@ -3,4 +3,4 @@ #pragma once -inline constexpr std::wstring_view AzureClientID = L"0"; +inline constexpr std::wstring_view AzureClientID{ L"245e1dee-74ef-4257-a8c8-8208296e1dfd" }; From 921d7c3316ac71478d2dae90a9ad7de2c8b732a1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 23 Aug 2023 11:17:11 -0500 Subject: [PATCH 33/59] Add a Canary branding option (#15865) Obviously, icons are all wrong. Color is about right but they need CAN icons. I'll leave that as an exercise for @DHowett to generate the right ones as a follow-up. Related to #774 --- build/pipelines/release.yml | 1 + build/rules/Branding.targets | 1 + res/terminal/Terminal_Can.svg | 407 ++++++++++++++++++ res/terminal/Terminal_Can_HC.svg | 17 + .../images-Can/LargeTile.scale-100.png | Bin 0 -> 4199 bytes .../LargeTile.scale-100_contrast-black.png | Bin 0 -> 1779 bytes .../LargeTile.scale-100_contrast-white.png | Bin 0 -> 1682 bytes .../images-Can/LargeTile.scale-125.png | Bin 0 -> 5391 bytes .../LargeTile.scale-125_contrast-black.png | Bin 0 -> 2101 bytes .../LargeTile.scale-125_contrast-white.png | Bin 0 -> 2057 bytes .../images-Can/LargeTile.scale-150.png | Bin 0 -> 6604 bytes .../LargeTile.scale-150_contrast-black.png | Bin 0 -> 2358 bytes .../LargeTile.scale-150_contrast-white.png | Bin 0 -> 2367 bytes .../images-Can/LargeTile.scale-200.png | Bin 0 -> 9248 bytes .../LargeTile.scale-200_contrast-black.png | Bin 0 -> 3152 bytes .../LargeTile.scale-200_contrast-white.png | Bin 0 -> 3108 bytes .../images-Can/LargeTile.scale-400.png | Bin 0 -> 21726 bytes .../LargeTile.scale-400_contrast-black.png | Bin 0 -> 7162 bytes .../LargeTile.scale-400_contrast-white.png | Bin 0 -> 7154 bytes .../images-Can/LockScreenLogo.scale-100.png | Bin 0 -> 834 bytes ...ockScreenLogo.scale-100_contrast-black.png | Bin 0 -> 595 bytes ...ockScreenLogo.scale-100_contrast-white.png | Bin 0 -> 586 bytes .../images-Can/LockScreenLogo.scale-125.png | Bin 0 -> 1002 bytes ...ockScreenLogo.scale-125_contrast-black.png | Bin 0 -> 770 bytes ...ockScreenLogo.scale-125_contrast-white.png | Bin 0 -> 680 bytes .../images-Can/LockScreenLogo.scale-150.png | Bin 0 -> 1204 bytes ...ockScreenLogo.scale-150_contrast-black.png | Bin 0 -> 842 bytes ...ockScreenLogo.scale-150_contrast-white.png | Bin 0 -> 730 bytes .../images-Can/LockScreenLogo.scale-200.png | Bin 0 -> 1524 bytes ...ockScreenLogo.scale-200_contrast-black.png | Bin 0 -> 963 bytes ...ockScreenLogo.scale-200_contrast-white.png | Bin 0 -> 792 bytes .../images-Can/LockScreenLogo.scale-400.png | Bin 0 -> 3180 bytes ...ockScreenLogo.scale-400_contrast-black.png | Bin 0 -> 1721 bytes ...ockScreenLogo.scale-400_contrast-white.png | Bin 0 -> 1298 bytes .../images-Can/SmallTile.scale-100.png | Bin 0 -> 1520 bytes .../SmallTile.scale-100_contrast-black.png | Bin 0 -> 805 bytes .../SmallTile.scale-100_contrast-white.png | Bin 0 -> 798 bytes .../images-Can/SmallTile.scale-125.png | Bin 0 -> 1827 bytes .../SmallTile.scale-125_contrast-black.png | Bin 0 -> 906 bytes .../SmallTile.scale-125_contrast-white.png | Bin 0 -> 904 bytes .../images-Can/SmallTile.scale-150.png | Bin 0 -> 1987 bytes .../SmallTile.scale-150_contrast-black.png | Bin 0 -> 1026 bytes .../SmallTile.scale-150_contrast-white.png | Bin 0 -> 1026 bytes .../images-Can/SmallTile.scale-200.png | Bin 0 -> 2945 bytes .../SmallTile.scale-200_contrast-black.png | Bin 0 -> 1236 bytes .../SmallTile.scale-200_contrast-white.png | Bin 0 -> 1174 bytes .../images-Can/SmallTile.scale-400.png | Bin 0 -> 5908 bytes .../SmallTile.scale-400_contrast-black.png | Bin 0 -> 2082 bytes .../SmallTile.scale-400_contrast-white.png | Bin 0 -> 2100 bytes .../images-Can/SplashScreen.scale-100.png | Bin 0 -> 4613 bytes .../SplashScreen.scale-100_contrast-black.png | Bin 0 -> 1940 bytes .../SplashScreen.scale-100_contrast-white.png | Bin 0 -> 1847 bytes .../images-Can/SplashScreen.scale-125.png | Bin 0 -> 6058 bytes .../SplashScreen.scale-125_contrast-black.png | Bin 0 -> 2408 bytes .../SplashScreen.scale-125_contrast-white.png | Bin 0 -> 2363 bytes .../images-Can/SplashScreen.scale-150.png | Bin 0 -> 7480 bytes .../SplashScreen.scale-150_contrast-black.png | Bin 0 -> 2777 bytes .../SplashScreen.scale-150_contrast-white.png | Bin 0 -> 2780 bytes .../images-Can/SplashScreen.scale-200.png | Bin 0 -> 10851 bytes .../SplashScreen.scale-200_contrast-black.png | Bin 0 -> 3898 bytes .../SplashScreen.scale-200_contrast-white.png | Bin 0 -> 3848 bytes .../images-Can/SplashScreen.scale-400.png | Bin 0 -> 27860 bytes .../SplashScreen.scale-400_contrast-black.png | Bin 0 -> 10033 bytes .../SplashScreen.scale-400_contrast-white.png | Bin 0 -> 10012 bytes .../Square150x150Logo.scale-100.png | Bin 0 -> 1801 bytes ...re150x150Logo.scale-100_contrast-black.png | Bin 0 -> 954 bytes ...re150x150Logo.scale-100_contrast-white.png | Bin 0 -> 936 bytes .../Square150x150Logo.scale-125.png | Bin 0 -> 2321 bytes ...re150x150Logo.scale-125_contrast-black.png | Bin 0 -> 1208 bytes ...re150x150Logo.scale-125_contrast-white.png | Bin 0 -> 1152 bytes .../Square150x150Logo.scale-150.png | Bin 0 -> 3150 bytes ...re150x150Logo.scale-150_contrast-black.png | Bin 0 -> 1374 bytes ...re150x150Logo.scale-150_contrast-white.png | Bin 0 -> 1302 bytes .../Square150x150Logo.scale-200.png | Bin 0 -> 4165 bytes ...re150x150Logo.scale-200_contrast-black.png | Bin 0 -> 1764 bytes ...re150x150Logo.scale-200_contrast-white.png | Bin 0 -> 1670 bytes .../Square150x150Logo.scale-400.png | Bin 0 -> 9151 bytes ...re150x150Logo.scale-400_contrast-black.png | Bin 0 -> 3117 bytes ...re150x150Logo.scale-400_contrast-white.png | Bin 0 -> 3073 bytes .../images-Can/Square44x44Logo.scale-100.png | Bin 0 -> 1331 bytes ...uare44x44Logo.scale-100_contrast-black.png | Bin 0 -> 667 bytes ...uare44x44Logo.scale-100_contrast-white.png | Bin 0 -> 670 bytes .../images-Can/Square44x44Logo.scale-125.png | Bin 0 -> 1585 bytes ...uare44x44Logo.scale-125_contrast-black.png | Bin 0 -> 775 bytes ...uare44x44Logo.scale-125_contrast-white.png | Bin 0 -> 777 bytes .../images-Can/Square44x44Logo.scale-150.png | Bin 0 -> 1732 bytes ...uare44x44Logo.scale-150_contrast-black.png | Bin 0 -> 841 bytes ...uare44x44Logo.scale-150_contrast-white.png | Bin 0 -> 828 bytes .../images-Can/Square44x44Logo.scale-200.png | Bin 0 -> 2207 bytes ...uare44x44Logo.scale-200_contrast-black.png | Bin 0 -> 1032 bytes ...uare44x44Logo.scale-200_contrast-white.png | Bin 0 -> 1010 bytes .../images-Can/Square44x44Logo.scale-400.png | Bin 0 -> 4821 bytes ...uare44x44Logo.scale-400_contrast-black.png | Bin 0 -> 1803 bytes ...uare44x44Logo.scale-400_contrast-white.png | Bin 0 -> 1726 bytes .../Square44x44Logo.targetsize-16.png | Bin 0 -> 554 bytes ...x44Logo.targetsize-16_altform-unplated.png | Bin 0 -> 554 bytes ...ize-16_altform-unplated_contrast-black.png | Bin 0 -> 404 bytes ...ize-16_altform-unplated_contrast-white.png | Bin 0 -> 485 bytes ...44x44Logo.targetsize-16_contrast-black.png | Bin 0 -> 404 bytes ...44x44Logo.targetsize-16_contrast-white.png | Bin 0 -> 485 bytes .../Square44x44Logo.targetsize-20.png | Bin 0 -> 669 bytes ...x44Logo.targetsize-20_altform-unplated.png | Bin 0 -> 669 bytes ...ize-20_altform-unplated_contrast-black.png | Bin 0 -> 509 bytes ...ize-20_altform-unplated_contrast-white.png | Bin 0 -> 546 bytes ...44x44Logo.targetsize-20_contrast-black.png | Bin 0 -> 509 bytes ...44x44Logo.targetsize-20_contrast-white.png | Bin 0 -> 546 bytes .../Square44x44Logo.targetsize-24.png | Bin 0 -> 834 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 834 bytes ...ize-24_altform-unplated_contrast-black.png | Bin 0 -> 595 bytes ...ize-24_altform-unplated_contrast-white.png | Bin 0 -> 586 bytes ...44x44Logo.targetsize-24_contrast-black.png | Bin 0 -> 595 bytes ...44x44Logo.targetsize-24_contrast-white.png | Bin 0 -> 586 bytes .../Square44x44Logo.targetsize-256.png | Bin 0 -> 9204 bytes ...44Logo.targetsize-256_altform-unplated.png | Bin 0 -> 9204 bytes ...ze-256_altform-unplated_contrast-black.png | Bin 0 -> 4062 bytes ...ze-256_altform-unplated_contrast-white.png | Bin 0 -> 3009 bytes ...4x44Logo.targetsize-256_contrast-black.png | Bin 0 -> 4062 bytes ...4x44Logo.targetsize-256_contrast-white.png | Bin 0 -> 3009 bytes .../Square44x44Logo.targetsize-30.png | Bin 0 -> 1002 bytes ...x44Logo.targetsize-30_altform-unplated.png | Bin 0 -> 1002 bytes ...ize-30_altform-unplated_contrast-black.png | Bin 0 -> 770 bytes ...ize-30_altform-unplated_contrast-white.png | Bin 0 -> 680 bytes ...44x44Logo.targetsize-30_contrast-black.png | Bin 0 -> 770 bytes ...44x44Logo.targetsize-30_contrast-white.png | Bin 0 -> 680 bytes .../Square44x44Logo.targetsize-32.png | Bin 0 -> 1039 bytes ...x44Logo.targetsize-32_altform-unplated.png | Bin 0 -> 1039 bytes ...ize-32_altform-unplated_contrast-black.png | Bin 0 -> 681 bytes ...ize-32_altform-unplated_contrast-white.png | Bin 0 -> 647 bytes ...44x44Logo.targetsize-32_contrast-black.png | Bin 0 -> 681 bytes ...44x44Logo.targetsize-32_contrast-white.png | Bin 0 -> 647 bytes .../Square44x44Logo.targetsize-36.png | Bin 0 -> 1204 bytes ...x44Logo.targetsize-36_altform-unplated.png | Bin 0 -> 1204 bytes ...ize-36_altform-unplated_contrast-black.png | Bin 0 -> 842 bytes ...ize-36_altform-unplated_contrast-white.png | Bin 0 -> 730 bytes ...44x44Logo.targetsize-36_contrast-black.png | Bin 0 -> 842 bytes ...44x44Logo.targetsize-36_contrast-white.png | Bin 0 -> 730 bytes .../Square44x44Logo.targetsize-40.png | Bin 0 -> 1346 bytes ...x44Logo.targetsize-40_altform-unplated.png | Bin 0 -> 1346 bytes ...ize-40_altform-unplated_contrast-black.png | Bin 0 -> 867 bytes ...ize-40_altform-unplated_contrast-white.png | Bin 0 -> 741 bytes ...44x44Logo.targetsize-40_contrast-black.png | Bin 0 -> 867 bytes ...44x44Logo.targetsize-40_contrast-white.png | Bin 0 -> 741 bytes .../Square44x44Logo.targetsize-48.png | Bin 0 -> 1524 bytes ...x44Logo.targetsize-48_altform-unplated.png | Bin 0 -> 1524 bytes ...ize-48_altform-unplated_contrast-black.png | Bin 0 -> 963 bytes ...ize-48_altform-unplated_contrast-white.png | Bin 0 -> 792 bytes ...44x44Logo.targetsize-48_contrast-black.png | Bin 0 -> 963 bytes ...44x44Logo.targetsize-48_contrast-white.png | Bin 0 -> 792 bytes .../Square44x44Logo.targetsize-60.png | Bin 0 -> 2019 bytes ...x44Logo.targetsize-60_altform-unplated.png | Bin 0 -> 2019 bytes ...ize-60_altform-unplated_contrast-black.png | Bin 0 -> 1234 bytes ...ize-60_altform-unplated_contrast-white.png | Bin 0 -> 953 bytes ...44x44Logo.targetsize-60_contrast-black.png | Bin 0 -> 1234 bytes ...44x44Logo.targetsize-60_contrast-white.png | Bin 0 -> 953 bytes .../Square44x44Logo.targetsize-64.png | Bin 0 -> 2141 bytes ...x44Logo.targetsize-64_altform-unplated.png | Bin 0 -> 2141 bytes ...ize-64_altform-unplated_contrast-black.png | Bin 0 -> 1260 bytes ...ize-64_altform-unplated_contrast-white.png | Bin 0 -> 963 bytes ...44x44Logo.targetsize-64_contrast-black.png | Bin 0 -> 1260 bytes ...44x44Logo.targetsize-64_contrast-white.png | Bin 0 -> 963 bytes .../Square44x44Logo.targetsize-72.png | Bin 0 -> 2433 bytes ...x44Logo.targetsize-72_altform-unplated.png | Bin 0 -> 2433 bytes ...ize-72_altform-unplated_contrast-black.png | Bin 0 -> 1322 bytes ...ize-72_altform-unplated_contrast-white.png | Bin 0 -> 1027 bytes ...44x44Logo.targetsize-72_contrast-black.png | Bin 0 -> 1322 bytes ...44x44Logo.targetsize-72_contrast-white.png | Bin 0 -> 1027 bytes .../Square44x44Logo.targetsize-80.png | Bin 0 -> 2671 bytes ...x44Logo.targetsize-80_altform-unplated.png | Bin 0 -> 2671 bytes ...ize-80_altform-unplated_contrast-black.png | Bin 0 -> 1563 bytes ...ize-80_altform-unplated_contrast-white.png | Bin 0 -> 1150 bytes ...44x44Logo.targetsize-80_contrast-black.png | Bin 0 -> 1563 bytes ...44x44Logo.targetsize-80_contrast-white.png | Bin 0 -> 1150 bytes .../Square44x44Logo.targetsize-96.png | Bin 0 -> 3180 bytes ...x44Logo.targetsize-96_altform-unplated.png | Bin 0 -> 3180 bytes ...ize-96_altform-unplated_contrast-black.png | Bin 0 -> 1721 bytes ...ize-96_altform-unplated_contrast-white.png | Bin 0 -> 1298 bytes ...44x44Logo.targetsize-96_contrast-black.png | Bin 0 -> 1721 bytes ...44x44Logo.targetsize-96_contrast-white.png | Bin 0 -> 1298 bytes .../images-Can/StoreLogo.scale-100.png | Bin 0 -> 1496 bytes .../StoreLogo.scale-100_contrast-black.png | Bin 0 -> 780 bytes .../StoreLogo.scale-100_contrast-white.png | Bin 0 -> 777 bytes .../images-Can/StoreLogo.scale-125.png | Bin 0 -> 1797 bytes .../StoreLogo.scale-125_contrast-black.png | Bin 0 -> 877 bytes .../StoreLogo.scale-125_contrast-white.png | Bin 0 -> 878 bytes .../images-Can/StoreLogo.scale-150.png | Bin 0 -> 1957 bytes .../StoreLogo.scale-150_contrast-black.png | Bin 0 -> 979 bytes .../StoreLogo.scale-150_contrast-white.png | Bin 0 -> 975 bytes .../images-Can/StoreLogo.scale-200.png | Bin 0 -> 2745 bytes .../StoreLogo.scale-200_contrast-black.png | Bin 0 -> 1135 bytes .../StoreLogo.scale-200_contrast-white.png | Bin 0 -> 1099 bytes .../images-Can/StoreLogo.scale-400.png | Bin 0 -> 5571 bytes .../StoreLogo.scale-400_contrast-black.png | Bin 0 -> 1906 bytes .../StoreLogo.scale-400_contrast-white.png | Bin 0 -> 1880 bytes .../images-Can/Wide310x150Logo.scale-100.png | Bin 0 -> 1907 bytes ...de310x150Logo.scale-100_contrast-black.png | Bin 0 -> 1053 bytes ...de310x150Logo.scale-100_contrast-white.png | Bin 0 -> 1014 bytes .../images-Can/Wide310x150Logo.scale-125.png | Bin 0 -> 2438 bytes ...de310x150Logo.scale-125_contrast-black.png | Bin 0 -> 1320 bytes ...de310x150Logo.scale-125_contrast-white.png | Bin 0 -> 1275 bytes .../images-Can/Wide310x150Logo.scale-150.png | Bin 0 -> 3430 bytes ...de310x150Logo.scale-150_contrast-black.png | Bin 0 -> 1496 bytes ...de310x150Logo.scale-150_contrast-white.png | Bin 0 -> 1402 bytes .../images-Can/Wide310x150Logo.scale-200.png | Bin 0 -> 4613 bytes ...de310x150Logo.scale-200_contrast-black.png | Bin 0 -> 1940 bytes ...de310x150Logo.scale-200_contrast-white.png | Bin 0 -> 1847 bytes .../images-Can/Wide310x150Logo.scale-400.png | Bin 0 -> 10851 bytes ...de310x150Logo.scale-400_contrast-black.png | Bin 0 -> 3898 bytes ...de310x150Logo.scale-400_contrast-white.png | Bin 0 -> 3848 bytes res/terminal/images-Can/terminal.ico | Bin 0 -> 45274 bytes .../images-Can/terminal_contrast-black.ico | Bin 0 -> 38858 bytes .../images-Can/terminal_contrast-white.ico | Bin 0 -> 39006 bytes .../CascadiaPackage/CascadiaPackage.wapproj | 6 + .../CascadiaPackage/Package-Can.appxmanifest | 151 +++++++ .../Resources/en-US/Resources.resw | 16 + src/cascadia/Remoting/Monarch.h | 3 + .../ShellExtension/OpenTerminalHere.cpp | 2 + .../ShellExtension/OpenTerminalHere.h | 2 + .../Resources/en-US/ContextMenu.resw | 20 + .../TerminalConnection/CTerminalHandoff.h | 2 + .../CascadiaSettingsSerialization.cpp | 2 +- .../WindowsTerminal/WindowsTerminal.rc | 4 + src/features.xml | 3 + src/host/exe/CConsoleHandoff.h | 2 + src/host/proxy/Host.Proxy.vcxproj | 5 + tools/FeatureStagingSchema.xsd | 2 +- tools/Generate-FeatureStagingHeader.ps1 | 2 +- 226 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 res/terminal/Terminal_Can.svg create mode 100644 res/terminal/Terminal_Can_HC.svg create mode 100644 res/terminal/images-Can/LargeTile.scale-100.png create mode 100644 res/terminal/images-Can/LargeTile.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/LargeTile.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/LargeTile.scale-125.png create mode 100644 res/terminal/images-Can/LargeTile.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/LargeTile.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/LargeTile.scale-150.png create mode 100644 res/terminal/images-Can/LargeTile.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/LargeTile.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/LargeTile.scale-200.png create mode 100644 res/terminal/images-Can/LargeTile.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/LargeTile.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/LargeTile.scale-400.png create mode 100644 res/terminal/images-Can/LargeTile.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/LargeTile.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-100.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-125.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-150.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-200.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-400.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/LockScreenLogo.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/SmallTile.scale-100.png create mode 100644 res/terminal/images-Can/SmallTile.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/SmallTile.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/SmallTile.scale-125.png create mode 100644 res/terminal/images-Can/SmallTile.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/SmallTile.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/SmallTile.scale-150.png create mode 100644 res/terminal/images-Can/SmallTile.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/SmallTile.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/SmallTile.scale-200.png create mode 100644 res/terminal/images-Can/SmallTile.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/SmallTile.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/SmallTile.scale-400.png create mode 100644 res/terminal/images-Can/SmallTile.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/SmallTile.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-100.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-125.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-150.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-200.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-400.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/SplashScreen.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-100.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-125.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-150.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-200.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-400.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/Square150x150Logo.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-100.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-125.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-150.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-200.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-400.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-black.png create mode 100644 res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-white.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-100.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-125.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-150.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-200.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-400.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/StoreLogo.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-100.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-black.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-white.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-125.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-black.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-white.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-150.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-black.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-white.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-200.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-black.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-white.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-400.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-black.png create mode 100644 res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-white.png create mode 100644 res/terminal/images-Can/terminal.ico create mode 100644 res/terminal/images-Can/terminal_contrast-black.ico create mode 100644 res/terminal/images-Can/terminal_contrast-white.ico create mode 100644 src/cascadia/CascadiaPackage/Package-Can.appxmanifest diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 98980ac37b6..c177634ba64 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -14,6 +14,7 @@ parameters: values: - Release - Preview + - Canary - Dev - name: buildTerminal displayName: "Build Windows Terminal MSIX" diff --git a/build/rules/Branding.targets b/build/rules/Branding.targets index 0ce12dfca57..57216032d6f 100644 --- a/build/rules/Branding.targets +++ b/build/rules/Branding.targets @@ -1,6 +1,7 @@ + <_WTBrandingPreprocessorToken Condition="'$(WindowsTerminalBranding)'=='Canary'">WT_BRANDING_CANARY <_WTBrandingPreprocessorToken Condition="'$(WindowsTerminalBranding)'=='Preview'">WT_BRANDING_PREVIEW <_WTBrandingPreprocessorToken Condition="'$(WindowsTerminalBranding)'=='Release'">WT_BRANDING_RELEASE <_WTBrandingPreprocessorToken Condition="'$(_WTBrandingPreprocessorToken)'==''">WT_BRANDING_DEV diff --git a/res/terminal/Terminal_Can.svg b/res/terminal/Terminal_Can.svg new file mode 100644 index 00000000000..5e7f491784c --- /dev/null +++ b/res/terminal/Terminal_Can.svg @@ -0,0 +1,407 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/terminal/Terminal_Can_HC.svg b/res/terminal/Terminal_Can_HC.svg new file mode 100644 index 00000000000..c08c3747a5e --- /dev/null +++ b/res/terminal/Terminal_Can_HC.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/terminal/images-Can/LargeTile.scale-100.png b/res/terminal/images-Can/LargeTile.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..2beb03d98e1673275d8b85e4ed4bf51d75b3ff57 GIT binary patch literal 4199 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 GWBvz77nCgk literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-100_contrast-black.png b/res/terminal/images-Can/LargeTile.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..6119d303b4c0879c989f4ba9bae630b6aa495d77 GIT binary patch literal 1779 zcmb_cdsxzG7XDRMRzuD_6^^K5O{uFXdBqEv;{}l%q@)~^95g@@5Lz`D@ItfYbkj5( zqq3TD(L@{6#=#=YG}qB2VZG!H)R|GF45AceQPG9{Yxj@+Z@%X_&pGdV-uImE`{%oS zBpwOc3fT$(00hu(IB&@|W*q}h>b@AKQN<3b?K{uMOU(|*`uUV^}8N*ntiX@bD# zG1LpiL$;2hPhiPSN2KQ*JjB~>CLAzcy8nxehz4Zc8T$!v0&im-b7{$m!lM zIu?&WWhAzc>0oG=H1Khr4*B_IU5WMt;*mm#f?6k zi4J9yW$0E3>wBdiFeEZTHKRFFXxTYA^q)%KV=BQ}H5%ou`OhAWnY(P{&)^z3 zUA$bf`Xp{RW_EE7Aqt{tt|`y5TawM@ zkCbhRAOEWBWi&PY-r6B)WwOKk-#1>yRcvx;CNc`jbs(d&{33-vy!UAyqjA+pI}mdF z?fmoGMp|#})`eWf5FtF{QgT*=uAdVKn z64kF0Zo$>iVg^5}1qY_R{XRpmI5#J|a&$*?{4FR2-qTlSE#EK)FNm;DoUIBv5snZkcDrgwl!M(#~c&w|2+A38aVC%>d-_s&sf^>Z&@58Y^i zGUT$D@UV&~0`40;)QS2=6PbqzEdDeX5+l}Oizs$Kjr4b_vFgHv!ybB<-0u#QukjNO zn>y;q+^pSum2KcZ@J;D&e?^L4-BX-5rN%cf}OI-Vy3# z<0jVj84YT?+TPSAG~a81V>Ay;1YW~*6Z@6W%yUXA&{m73g&6@D&XoOoG&ku?I>a37 zQT_b@36DZasBDNi-hygmr%BJU5T#vqKvqQahP`yKD64? zhkTS0{QKe(DRtmEk=%PZIc!M&SxHsP->a%<9^onTrw#YpzZ7h68s$wd zm=@g_ANu-k-q>d>avRvkrW^8Etrcc5o=MsOQfg@m`@}oJ6J=Po=L>%VfAHk(h7!8& zr2ak}GXi_js=6y0Eq_FmU0GCUu6e1ihLo4gTyB!jrfb^(m+J7tutIp)PT}JU{ms z@2>d?b;EhFjEcf{Fn?yQo7zr9@^(mAVb50bbQH3nVc>P8s(%POUb+D_@wm=>O;tKA z68zAtMw;?@g)}(zxS`s+sMf@_B@1h~LC5QR2zOH^SIHBt?}x__FC#HEeT4*xo1J2k z$oblRJ{B+7^rw7$03gc!WEtM27~h$`%QDe52a%LROv@pK60%5^1o-**`Fr~Wd;9qv z^Ysl42oCl4_4Wx2_3=UYvgrR~$Yc;R(%Jvt5I|G(TMQ1?4Om8c4l6B-1h7~vXa=2{ bO-MUKf-WB=Iy`p!J_%=i1epXZt9kN3?w?d_?h zsjmqDfELyZ?F#@Zj$5I=P3fTcbQ5LTMn>UK0Pr{mvQAW0YGaa@FAe}wECAq2IsmLG ztt$%vkPHKWcVPhFdKUn66AGJr+?0jy!cTaj0icX&3RfzP1g{Vp02pR&g-Xuc$4I5A zmW0K5sJ&I!fxwJj{i?GG0IFeFG|K;{mrE0b>u1S&Y9BVc0xN=JPMBb< zG3U8gL-($G9qfeo?A>c!o1(ib_BWmHRUs=S>Y64d=A~f|G<`Jqz9@9L7piv5i^8fZ z2*amSjcn*}3zj=e#J)$HFI?RabPAeTE;0#Yv-y;u=pK7(?Qe1JW>IL)WIz1W`_i(^ z`i~8p@kn0TrSVjWX#JoNytFVsSHpwc&7;U7c#BQf;~L;!5@{>5A|B*krXfKWX1P-i zkD>5-ot{F8hz$KY{8MOHR(ZVra5X`ctm4;LCjM@oR7xbNXc$!Pc06MmTZl!*xfs4L z9XAC7s|7ChBlwm1hCx$pkGQKdn4FXQ_`fg>+=pOUpb)czZfH#u^E0p)OUjHc82n*= z#9#y8@!8#FEIC>p*m2_{&oy);bA)*yy08lrw&~B*9et!J@3GJpWh*iu*;m@)TTj<>Ad2(3#q?&X z#4)CQ)N12qLABz?HjYJX%aF8G179USvSp9ocv-|Vb^K7RwR|mJ-{$#c#7+md*Aa+9 zWSp2w?McPdhxXGLi5DFd$3^Z<`~tG9dmT)YzLOB@Gd*QPYwR7#q1(B{&d;eGW%TeH z0ZaY5lEo>-QGyS5*i>9JZN=yKaz7E|5}|c(UtL7inse*)!b%W7n~-la_sC%XD7(>V zYGJKgI(8K4UD`{g**_g+{$&xGK#w_f|J;0Bqn#9&2)^$Z4Svpc zfu%{fyqyrg=-3qTxowDz=x1jPHqsvYS}B490^FUcoclcg7_c%hU_`AuPCmA$$oO!G z0j{Xenvr%^T(!W?4PbN2h0IN|9QpN5T|*l&q{gwJqH`&TL5VWr&ng-ZVVC8q zoI7);19SKH`^`Ldnxk>>n|}^8AJ|BI#Fxb?B2tgXPxGi>F zFu3&CTi72EpoqB?+wo-3^G*p9I^B2MY_o?P)!fa(P|oDOLdKL}m*)qCq*ec$^bHai zi|uekTFOQsX&2U$paVy8C83{g7x=BVFO16a-WRBjym|PL@3!)@w zg1j~hNQ66MnpSwLG?vs1+nUej+#K{uW9Q_IAdi71BHMJy@L&me+`Eo}aecAm zKPgg7lPHr#nY0qw>)7NYc^$>uzs1L->|wWzEUrP54Lvorl*&BGBRDC7m=x(sN{mz{ z01kuOS;L&I;c$OjTUUE$S36s4n1d?}=3xs`{{zIuM?^<4{vWW99T-vqdRr6x;-iv4 lVqzo!f*>@SN}-X636aqFL^A8Wo4)cAfc5Z3^NxjG`x`!N-m3rr literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-125.png b/res/terminal/images-Can/LargeTile.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..9859633fe66f7ffcdb8d42b4559b42bc00f998be GIT binary patch literal 5391 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 SYuCk&-ECoN3#l`{`}jYC&%&Aj literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-125_contrast-black.png b/res/terminal/images-Can/LargeTile.scale-125_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..253879e1b319014a50fee09d54f1dbd7bb19302b GIT binary patch literal 2101 zcmcgtc`)1S8vdy*RMm2d(w0i3YKchgqUXd?a?~DFHARDPP$r2ITOEROETsgs6~$0n zX*fh_o#LWe-ME(ET#{ItI8Bub>i#tO2+03x*S1rY<8@+VbSz$Qk-IAH+bao*AW0C5m&5FFf{03Z$y0Lh?O2vW(b z01%A?fOUTWuqXil<*4#rH#E3$6QM0iAPmH_7e4 zpEXS{+{q!>xfJbBef^=@KNyJG;oDI39Wop z`b+K?Y0nhbXaY+-mJuyaYtr37JbTmv3R;?&$y$tF{tM)JeFu zJVCfN{p6jwSE2GhCqsrK2PP&V|9jkjavl?@+FCmtm(qz3+x8~u8ZsXPZ*lw!0ukNB zAQl^`wD49(X#V0~xYn>C4Xg?GRk>fxj1BqD5SmQ;RWBPuGTWTE5Uo6<0vQCH^Dvsy zPG=#BmPHZ77aCYuqP_DJ{uES!)}IK^Pzp6Knk#{=Hc$bW5+Dp^7mgAgM&3U*3tbOW3xV*>1t)vF;*jAqu z3ka;46uXpp;*Z-uT5qPd`_V_>eEi6%fQ5$#H|uemqV0$IZPR!d9x_uHzibd) z?4X>2R65sbHNcO{D@}OF*Icuf8(s?RG7#n+yx#j|0T)wBISYYW7FAdg+AU2#;Nf^M z&7xv!gThSA-*}-&%fEy1fDA%}na`Dn{B_Lq_b1~(sW&k%3pF7&>!(vs^gzkc z#n>vDoODeICn&^5E|nt>&H)3?A^F9FQn3Jy8NM_2U0;_el8<#^}CV(x77%|()L5@}e7c&X2bFs^r9 z`^5%ZTvfzgKq0)#A8&siFCHV;6}q3{{WY&#q~4{@Y_QquwX1C4^Q^Eh<%M1Kj&n0- zez3a7+WlJ7;q@R3Cy*QR$qY+RI3sYbaEMSJsg9{ElW_?AMO4b=54$Ll+qqYuhj zMs5keZYhyzm0J*-SmHN&txpyCwcztL`m*`Vw~qW{)Z2Bu$Mdt-D@Tc)kN7z#Hu!|c z1?yU|^Z{@BH0M?Gm6fD!Zf~#YKi(_S&bOndSJWCN2ruSk)`$Y=%EN(yxnA zY|y$Vepv)l;&00BYo5@e#v{yXc{x40=SYdNaP#|k;zytUSgf#_BS(|k9vyp5sp!*Q zd>x*v)uV4oN@TH}jrZj1XYVJpFrJLdGV)jD>*pm&VYx6Wnn|)}Z*Q`>MPW=5m6hgf zhAAOk*!5btOT4#lB&nmg0yTH1ySG1V7qRJP=)~|prK*y+XUPI+3(z16V9VLFTzX9 zdK&l&@QiA=p_f)OzA?ufgWFVaYx2$>U7X=QM&b%t(W)Y&!$szA{Sv@1)s@4NCiA?h zz9&N<(xTX1p#%m{f-DH6Ag}=_Bji{&pWr&Cj z3=O9Ke?#?FmI!D#`8~lSGMEw*Knen4Vqy@XH^az;fT$ouB#BtYN2`J&0Bh%L+iLBf F@^9(6WFG(k literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-125_contrast-white.png b/res/terminal/images-Can/LargeTile.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..f4007a261cdd62ca240e4b7795a7cca296521418 GIT binary patch literal 2057 zcmcgsc`)1i7XMML&{8hSRZ5AXEmB)!sjac(sh|sLDM~~sF%q$~)lxOpT3X9ZwIURu ziqw`owR`ncib#u82%U>q8uDI3T3fuBJM-qgnfLeo&YU^(`Fzjke9oLdPL>zKRZdo2 z761S_xEt&;07zu)2S`fHl(Z22#7PR}=-~(ejd_Q6LJx|uc9`2`4**Cx1pp7k#am+P z!4d!@KmcGR1OTjG0Dx*-$$M{G@rUG1cUKqy)aK}#ijhp5+jR^8fS30}!VA3kQ%p)@ z;U3P?i=bnNA*UW@mHq&LLrgf#@oExll^s;0fzp)z7JPFYysO}^ds_L5O1Y2J(`zOz zpSAV!$JA=C!D^J(oygfqVMXczqlcZnb%lrzve%+&3`_i>W&J5%9)Fg~ZMpWwTM7PJ zPbQ{-bgwh6eGMMbJu=zMOSPrn;BvbPCijNAy3KfNLx^9*^ZcbR;$!Yv4we>${!w&U zz9GesKpk*5wAbv>->w|j0ic*iY9e#0q08R(sQWE(f zNipN519}Ei(1tfP;3Ntv3t1}JOlw~1s|%9gwi+=LP6y!=JS-Qn^4W1&JQ``|jQiY- z0j6)H@k@zZPRI5hDpKmdO|t&n9op2P^PS%tNT@nH#8!lI{^7usXL||pdy~PI+3PE$ zux`f+32XWp);WW!!r-EuR-0yRNc_crhOAToIhnb=g{~6n8(FZ{G_I}7<6x8--9_ZBf@mmXNG8cm+v z?AVSGf^->sC@V5uHA)qJG##STJyLFm>1e}ct!smH8&7WYM@lh;JMVDM_wl=v+fDU! z+hCej6kg_lKO;+a*w@BC)^oMMu;ga10_V)3bO%*|$^pl+rxuTUHCt`kwIMBv1K$ES zv8xTnpt>Hnus9O8hRrybrcz8+14#xA`5uu2hF-&~I!%8vIs``>CzNnm0lM527%hsmxfL zl1E@P?T&y39;EUGNL6WQVa4ilR%efzQZ4;i)n-(6#U5#?s+>_JHf`d^eg8LG^J2xL zi4Jr$6T8|}&d|T-cj8GoL*ZpZ2-foaVw%Oh@q)Hyiw$As#Kq)Fiwzbn0QFv`&ub*F zk2RnTM^M*-XTEN|pV(!j{pM^&RLorW!ck0iY)aL@JT%*We3Wtq?UAoB+>4|K7aVP= z#`E)J1D-5fd{Yx`@vj;#__c-R$YT>yUHpP$PwNNdp}+sk{uh;B!EbKzyAgRo6qrZ$ zY!}%+w!D*}k_7cdy2~$aTBH&lww*oR)VQ%UxKmlNN}Fbt8gHfuCuR(;AQXJ&`*)B> z$o$OQ<+;tc5nM5^qKzy(RGg9yIXHs6Z+Cx2KcgbqoHxh;51tpi5*n(ROcJ)Iz6?Z> zdz2M2WN@w{^GUh$RI6miDJ?Qx)f^g**x=^_j1n;Ham(nV`)0$H`o3TD?B>X61f8laD+7RCwqwz4Kx-vxV->I-{JQ z#GjAENDu4mhYb(KBCW&Xk>UiHLQKt!AW$Py)2rvsS(`(x&8&HC%Is%&*8jl1L6BCVZ#YSVoLgSFe=y=qtHCuIY5de2a Kz#5!F(*FZRxodg= literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-150.png b/res/terminal/images-Can/LargeTile.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..34bc2a28dabbcee61d453787dfaae47e47ab1d68 GIT binary patch literal 6604 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%0AU8`FR8qIsz*zd4|J@$vEQUGZG6 zB+aUvT0XfYa&vP}wq>}sgXGQj@;`!Oe%*K4a^*g`v>zUaVQmX_)1n>+{1Gs2_Q#wJ zn(A%8j&xxTltO7F{t~Z(ZNGLpLS)<6s9Sz}v(Fwp4M`N!Nbi;o{C}9!xvtKtq(|xSj|qhW{`~ZMymK<^dzP_W0ki=&z7(5#Oc=2t*sJM z3$$s8AXMU&1qlU>dUY!or6MXTpuH1(>C&&SBRy}xdY$`Nn%pmVO(p_B(GR#H^gs}!K3`5A7Qp!wdgwYolcKrPcGlb$?zg4v^D zO>}u07^UmOdZ?1TcjC3$G2dNd4n8A>la%d6Rcn|`a&*G>_U!f+iXnVmsf3uqJo#F~ zZ^DM2v|()bhx!AW0>;oYPHB3*<;dl~96BG}`{1g6GLTfg(8{tvv5_;@*`H^UM}_hx)Y0vzW78x(rFF@=@b=xEVZ`C?m3VLWWCymN z>v8X%PNkv_Nwgo1q6yeUdAz6C>$O~U?N5r|iOK765+HNw6e*qtP z=%oswgL@}D$XArj(w1m6@Eplc{#yHWtvJxG~( zs~fW5km~xK-@E<#7hR3F-J;+}tXFv(5gYWrN*?e%GEe8O`TmfY`gGYI@aa-*b7cZ@ z6RO#mGrCxG`${?zs2Z~$xs*85YQ+aprBi#!%lG`*+IKM^~SF^kcB?G;2Pv!koP zf-^Ncl!}d<2e%QVK$%3&vUweubr{= z_xr6tyWeXT&*)1BA}wmLSzMgPqG-`*wab2f$6HO0;*P$9U40$qm-m@l$oVsz9`rhi zi^!p9CY|S?x#hUIqAlRGca?qxcPj)TRFOa9|Hrf*vghBF_qt9CVKC9Zu`5ccjcMQP zV68X4mzJj{bz+*qtSRTn!CIQG4zIMrim+|RL{kj&MHjxcfWeTh+m6*1z_rWCT~dZ{ z6|b1Qw5|E*??p7;%l-hjGU^w@zezclx5~Kw$ZXgMlrrMk2#x5F5%T z=fTnb_FeughB#wB`v7x+Lw+Bz*F~cw6UEn1ST!nY5q`S&P3Hz=A4lHEAsw;2Xk1tv zpmVpZam3qhX0Zv#lqGZEbq@=_R0d1_7`$sI)tXnG=|5R^O-C!B!+Sw3 z8c@n!nhRFBGGaE;5`pYqJ#Rz1btApxqSkLzNWK809!DzrEe3N%Qevz&>)b zu3~=sva`U2b;oEZRcl9j)wIc+ZH+q+N%PIqpnW(0+v~v-Tm`v)PJ#w*3x-d7M9w!) zt|d~g@l49O${Fw9@6y*fNPb140qhdj3EoM|0JKMa2EJ0&S_?eYI&B4t3n%JELdADo zVyJBzd;SufZ}PvxZ^w>jq!^h&kw#yuH1Y&x`fiKH!()Mw1Z1EZz9ywn6%-zS^o7bgdKpsB6f@#+}MeTI!pMe2EB)9@PS& z-0&Tx%m@a0urIuG5*o5+*S5PO95hUmf7?J~mjX_nY|YZ5%I2agIHO9xT-jWjiK>D| zM(qzHq339tln-xlcePI=ffPe8A?1me&xd>Pu10M3 zOsOq%??oMu=IT~AXZy*Qehm-wz2Num{!-M3urDevT%0PHNO&cKj4h^80+D;U9UUED zRN_$K2+6bTS*`>uE~7d-(!yhluEFxU1zqrOJ*f~}yS!6Ne#4O^yb0iN8FteF=II7|pyd(#0BAr;ib~4ziW>4tO82f`*HY2YQdW^y zRMk>c6guTF@Lvr413i7c!vA{%uY3Q50tUi|Gu#dIf+5_3AAk@Dgo1B?Ux;|B_X W!QMGkopXoVVRqBXsOHA~r~d)D@CyF` literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-150_contrast-black.png b/res/terminal/images-Can/LargeTile.scale-150_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..c1850b2622478533bc8092b0ccecf6080f68f9ce GIT binary patch literal 2358 zcmd5-dr;D87yen6m06owShiYfUP?*S@QPVMLWHKKnTKAt0RR9U3>xVR02;DywoX$WxzURKPVF>Fhj51g;9idQQrH?b-W87a#Q^}_901N= z27m>1==>M}P;CL=<7ohJDFOgU{Pjj3H+5pw8LTG~0BWw9+Nsg{cywR_09a$cnT7zt zsa6NI5-~WG*66xz+O}4D@Jo>Zuuc<$JajUtZ)$*$6-?52@pk1|(K_6LqnFZ8FGuaY zFY7bN)sMzopMiZ|U6Doh!4wh&mig3Wt$ zri;Lf?Y&NwfY8@2907(C5i>ZF3U6ascl1g^X0+#1H$}vawm-n7UPg=S<;6>uKXfe9 zA@Tf`1ltmqw33FFoQ=$$R|MXR5z{VIE;x&X_py5JS12{NJ(yDHjq&fgHx%-oJh6~9 z!V7sGw+4dftF&)eG#;SdkY1S&RG1|3ORi4_yQS?j)~V-HJN(MBFMC$OnTKy-^n-Bx zt|;B~fP71FG4kf`W*chFeZT|%Pi-o{a|=&jWiuLj3RnMMD*jvf(oxAvg#MU$Ps)+6E=?)qa~www@9Tq!FOY!zTJREdqj+WK zaUB0D_j(1I%S9U6QLmX0&2?j+z0pUtD$V-NS0xNMY&U9QQ2wLTC)+ULZ?x^rlAIi~ z*c$5OP^Z)3ij5|+wZQ>lKcl(Dj-2Qk%Gi@Tl-TCIx1(W3?Qgo$8)Hz0cVU|Y79MtK zdmcBJ9gUF=9^3r+Eje_NsE-lv#*3=DFHNQTnzO)gWNQpse8_~Dq#MgdZ|5Kl&3!XW zWz9@>7+MVE`IaKHzq9hkh;u*YHQD48@T}Kh#6>lEO}VsTh`<61)3x~}s$!He04WOl&H#m<~zk8xKwvqiNQh_1N-em*iB*YZ>_E$=c5Dzs=!w~>2Pd-GB2*<7@E z*9AL!<~Bb>7k-V)6SE-n?;fra*C8uJ;D#Xep_f*!fN%>&CdTko3nsVlfK;YZ8bGaLy!d=Xt*v*X?Q0+f2Ucb|xC^GuJbnGF-C5mTRg z&`6ANSBDoV2DMPUY;8(4SeDt%jSu48mso{%%xE|N=}c}P?q#PsBf@agV}{hP%D1e| z(BpUTkB0}^lRhRs3?%xCKE2RrNII+P{|!dlXplfwIYMrem(Tkc7mxH{SC*|)>iIgS z5bv|RjKC!;xne%43HG$eR02%{lR}Q0sk=sI5Waf-1fl1R374v;Sp~_&kh72H{d#>O zBClJ>*8a)LtYnHM5T+dKIIy#$p)PWXl}3siDypKmt+dNhB#!Sxy%s)-_EA!vv}6v0 z_i7lI7aOr_Coj1>c`ugbt5DMuggNANU4wEen>TiK9-OW-o}0=R8ovAAeB6cC`W(8k z@?$W(N%`s#%#e)QmdP~e?VB;(uJax4IcodcuJ1_kr zVO=nqTArDAD>eQk@)J6mhkkLC>fr^{O`bHS(X-00c(kajH!?tR0uA~6A)Ce2|m z%CMr??j`d^_fYE@TbcUu|K?1CG!ATN=~a|=3> zAIhF5qi9G1cbX{G->*n9nQm)qE=((~_4_*6*yKo?Vs={KWm_nzgWL#zXv?pP1�~ zPC>GZlSg09(@)`#@b^aXuLoh4CQ`ixG%sZtbnct=_Y!%K3!t+BE zfiw3&Ztt<8 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-150_contrast-white.png b/res/terminal/images-Can/LargeTile.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..27eb42bf6d9a3cf07c3e0f25a10d04522d3e6917 GIT binary patch literal 2367 zcmd5-dr;C@8~$mz7Jg)=W|>{2v?MjeAVpTQFt2Fl{X%94c-QrYq^K?0+C}XvEd}#} zixwqXXfCK-x6Df_Y@{eyt%_KQrmmQ2F715t&3u1+v;Xcp^Uiag=e*B3@0>Yv&iUh> z&R~!s2mk=Es|)4?0H_JR<+`;hrLxO?qYBoN9I=i7aJNWnIb1`F|=%8>&=DhvSLp8^0>DF7HGmh-*QstNVe?#>thU>EF$tJwNPm!M<-Fn9l!YOL?C z-BO8~DXv&2&53oJwP0qtNE#6U)`49yj=t$bQ_`^f(17ilGYOIiMzL?fcXzO^_!74n zM~GhM)$AG9ro~-)u?5uB6apWPri^qnZm15+)+=mlf{ z2Fz;`ZnE71`r~6RHJ&SX`pXj+H>qxVgJM)OZVsw9W%fry{tobI6h&t;#ZoyoD`dfR z?x-h3S1B*Xzg*$*xvK+h`?I0A75Ub1u}BE;{dTmu{HsMaAok~Dg55D4Z|@TzzgQ@>%&7VDEupQkTE?uMI@@V= z(M>uhuE`9#v7$hJqqR1n_E^B}BrBTE>&smE7W5Tr^JZgS;@YPHqG5ag8x8Mmf^ax) z$9i#9;}9*??I5|o;)Aw zvy`ui&qa)B#70N@@IsPe3j9gdA^#C+-od9s(gvcHmBKtY5i$h&!=YLJf#pu<@i5n3hJr9mfrI1Rq##rqU&~m5LlsD;w_-EwAg!#q5_TV9r^4rMmc zb1v+wH?4xW-Cln)Ul{#g2|1rL;*0Wybg>~-5Y&kg=quj>$3?c$ZQhm01cf;j{gjxC zh){dGMG}0lYB<9)=D;{c(M_7AYYiC%zLFJv4oV}f{8&-AVCy{+GW#r#@T&CVO7&|! za$kNeefSG{R7=E(XRM(PmEYmCSJl-}gQYP{yCc0!{EXzD1^eU9FM7&*nTIMvYhS)b zm$qEuS#I7Wuv&Z$-+L9BF*gM%kyad1N|I5ly>kYVk;^nEC@uUJLcEMYgq^xDY$sTW zErE*kMb7Mp^v)ZjL& zR#e7n&)td+;=QQHQ@_c$yZ12hsR6RN`dAln35LtFw!X0vwt!wZ+e4yqdPdsrCHHNo z+^T!IP#&C^k^I$9?oxX;zfdft*bGbvm-lEFt?=HIA%YkK6YbK!D~3Vek9#D~WPkZ^9Y^Ww;-^>7TmAK>rcbILo0!aOZq0^m^A|pYW~DxV=DZKt z?C{1M_vBgqbkmUoGC-DkqkV5uR%Zj&Gs4|+YVMDY)`{UAUoNzlkwhB>wZyr| zv|>vptB_cypNlP%BU4SMyGL0UBUv?+hdIcJ6QRimWC}C`D=robSxvu&7LM)bMLl+2 zul|qz=)N_Vx~%Q;i@)&NXpEMMnWQ)crbL9N5K#Cef(igQ3~p@!v$24~eXXoe2W?OX ztt?;&6b$AypGE#3LtH{c6p`}(4feO5cdHCrzwO|YKujTrClLTLnQR#qACrs^Pb63- VB#{^jw4tg9aCP#;aE_e%@lUTF&ustz literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-200.png b/res/terminal/images-Can/LargeTile.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..d23621d3c75fa20d4bbe15b056b36a3e1bbd22ca GIT binary patch literal 9248 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-200_contrast-black.png b/res/terminal/images-Can/LargeTile.scale-200_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..5244c120011f6b670021b9a9802df82331edb1b1 GIT binary patch literal 3152 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LargeTile.scale-200_contrast-white.png b/res/terminal/images-Can/LargeTile.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..671be6ca5b45a3c7a8c93e078ce5f060e42e68fb GIT binary patch literal 3108 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-100.png b/res/terminal/images-Can/LockScreenLogo.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..c9003ee24150f658e8478c9f63bfced4ff3cd7cd GIT binary patch literal 834 zcmV-I1HJr-P)4Q_K~zYI?Ul_-R8bhle|PQ~CPRl387nEsuT5akB4X6U(x^m& zHuWD=AmYkJVS)CDt?aPF$z(EAY+Y-;9SVis#pCg9 z0el21iUA$RnFiVpBhXqOs@!p$GT>F3=SBFxnWD-ANj=&2P4u>&KlfJ$_YHMo`koZ*|N(X>a+*{b|IQV?7zMJAKMG)(|5R?FsFFB?0A7hEYtS63HWYciP( z6B83Cr4T}J_sSlRZ|?%T04%P%1ckwwN|W^sN#wN2+J>b1s>#NV;&ep-paY2JdXUX# zNhA`iuC6jRHincEAq0kD5DKcC;`g6+n4H;Te$gSln&sZp73{3$*=vgrQ@g}Rb6*yK zlOn&tb8~YflgZpv`O5*2nzxBPsRH1|+YKJvC}(nJkMj}1x0Nj4w`2i6ptaudnj?fD z7!2l=`)3;9+NBdbe`7H^y~~Q_a{Y1{U%vhTpy&Q6rc?U`c%!|&y;(}x;x)=|N+|{( zZnfQPvhR3nuG9$AIDjo;`@e0lVgzvom!Xe@LwG8n`^aC2rMDXFPTV>S8{ z{2we5lNuNqM5OLuu{09D1_lFyMXRF8bEezi>T%DxJ=4A4@qM26^Ld~40siM%i0CKT zq*klZXf(1bld7szuh(tsM5Md|`0iXgmHmHxmSdK>$Fj)j~F# zb!L)ln`xSq%jF0F0iZ-8LAtL0)daXdxR-DoNBjNWIRO#T@pzNS0+9kH<8d%}ADInog&* zTCGTyW$JV~R`1Q41_A*9fFwz%R4QN?=Hcckih_2#4N(+Ptya{b&E`$- zji6jEqtR%52>+GIWCFk650+)o>-7+gM&WX~psFf{!{K8A!!Qs8;VB#d1VKQz+l9yD zL7`B<;c&oiw?jN0M=Tbz`d)}g0r2*ec$MXH3DYzYi9|4;&*65vp=la6n+?|MHCUEK hDwP5NNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1LNlkv$2787-=2-UbS?mw5WRvOnh#a%U~jDU1@HE{-7R^FwA|)7-TY8;t&mT$}8#!2Q20$DnJ1+T~>9$=_2? z{eE|6^UmeJtKV6EFMqX6<))YV6?OIpBKwmMG9Lc9;Pb>eMYArtTo)JGJ|(L6#+9Se z?-$y5v-1ZwwCuYU%@v{8(b=(My_}?rY^Ky#uKoi&yEm@tm{I6_RrvP*RTpoVOsFpY z$Z)1=`Kv0M1y`9v^K-hHO`hD|cBK07yj8Q)oZR<{oxPoYv2M4nmE^XqOgS4}bW`q2 zTz<2`qtV%IMz3Gh%$@Q@4L16FjLxyn=P{IF%bHSma^Vpc`85+4Pc}Zsc*4Nd|8u8Z zv1hCLI*VnebVJuIob#)G{sHlMZyA|tGxqBNgH5%>HKHUXu_Vl&C^85jt*3dEsk$jwj5OsmAA!3?6|*!sm`Kn;>08-nxGO3D+9 mQW?t2%k?tzvWt@w3sUv+i_&MmvylQSV(@hJb6Mw<&;$Ug3)vX} literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-125.png b/res/terminal/images-Can/LockScreenLogo.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..c82284ab1c036b4407960af5a47d1f62fe9ec611 GIT binary patch literal 1002 zcmVe_4`}H$K}%I;SI7?{ zE|h|(3rV2Fg($5ECRw=4!iXUYR}DxO5g`;3btf)baMck)5(vpan1qPpW@zd#e@^D- zoa3V2>%Ei7Op_EB(ibl0-f!OTd(ZowZw@@c{~cm=b@g>)%uq8@DwWFS^Z9elIE6wX zyS24-x`p1_+}zwEfHcN@4>V0sO5FplHsjQ4wRIuHl@`KgbaeFR`T6<#Rx8oR3=s~8 zPXk#0p=bbda&i&?V~iJc-+k-VN8ns(6t9m(B9V3gS^tCk)T4n=!?hKsP$*>K;c$6k zT5E3=LSR`|%VI~}j0?*RXss!i%Z!YSFgrU-cXu~RDQ`1}6>ju2;{=3Vm+^R2~wZ?HA#>U3z?CkWMKMvQPu)pt0DW;~T z*x1+zi1+mL1loQQ*ZceX!Bzt}j)O6V>FH@UH#gB*Bc)_~e4KbZ-t^2Lbu$Ns3qS~g zZQB@Qn3)a z7l`Ywudk2({(g$ZB8fx-tu+e^3$(Shc?Av|w)%G=L}Ow9?vzr*Vlf5>2T@8Pr6iR~ zvAn#DZQBSTcr#w4w@cH}(SaY0idQNXa=BbUeDKA}7wwUY-$}pQXf*1H)@n7@*VjGK zP$hegu!(f>!L@eAZ|xE5wt4wtn9I+H`SQbaeD?JNlD}2}`1tL!^xypBK&}`V7JFvx|1?*8T@>1qZi+LuV(WE{!;dTA{6AZ&1^#Pvv`CxoF#1XuQP_WJx~xz4#@+ zJivFpD2j*#fM0;8RVkaz2E*ZSZE9-jPrJd+&dz>bUS9r)#bUqP)at;A7dQl_+YMMK z6c`y9X;%TjvMg?HZXA1n+2e$|lFDT)P$Q!y@7cyOE~%IG4*&tybNYhebq^ z$)xR`kB^Uc4Q~0$v@DBkHf!6bX_|YFlZc3AS+cymY@6-c+FI9UE7IgrH$#)jWLMD- z@bmMtXqsk2Q>m2WAg8CNVw$GZ>vg%mzZb(W$;qupEu62Ew0hgQ4S6c zP*s(5I!!zt|MXhf+uNg7t5GZ#nVFekV`GDhiwgiEkqEl3GchsI=zHG}plKSnx3?&Y z($ONFot*Fbp!845d7V%heij!OQ4|G_$AhY>#9}eLUN6C5 zkfEU=o}QkFMxzW54+G>s^?Tdp)Pa*Pvc3a<0r;As!%ER>KmY&$07*qoM6N<$g3R`3 A4*&oF literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-white.png b/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b34489eaae91b2af87a86757b2f52c600596b2 GIT binary patch literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX0wgC|rfC8xmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1O92q^v-g@k7caFQ6cMiKnkC`*RL4Zhdo=M!r%hS{Hr*PTriIcUTK6}}vnoF@HU$Uybldh7^ncIoc z-`*Jh%u}EL{^3u%`S;e_)z;beJBQ?-le%#$fnAp;DEw8|mSyq%@oP3EB==X_CKo>S z*wG=piHrHpt)z;Nx0&TFPX{t{U!C^3XKnc5n8Mg+uNO_OwZ4CvsJq+!(iX;m;}){}zZ;#_oA9`g*X~3?|7;OSkAELt z{$2dk!{Xob_s_V!Hcvjs%v0sWnj=I#aND_AvZrIGp!Q0hVSk>PXjebf@}!RPb(=;EJ|f4FE7{2%*!rL bPAo{(%P&fw{mw=TsEEPS)z4*}Q$iB}_+bPI literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-150.png b/res/terminal/images-Can/LockScreenLogo.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..b32469a3c42b75467be02f899a253f40b4324dcb GIT binary patch literal 1204 zcmV;l1WWsgP)FV0rVQp2mR(|YgS zJ9ADC>pgq*j;r`D2){3L_?_SR&1ZhUbAB@e``Xw4Ok#X|{2Aa3;B4d^jYj`YCX+`u z6Zq!Mn}5b)v9gW)wAM>XsS90QUB3a?z-8d@W^Us9{v=SnN#6(%-pEe~foB&hr_hD zw^LG5g75n%rO5OP8gFMlodj4pWoe^QirU&*8X6iXD=VX=rNuyP6m&h1&CF+IDAS>| zv=pTjzVA~|P(W*I>z+bom8`e7m!Y8{ld=5#d|F#u357y?0wptzLI|XkbaZqOi9`%k zZf-8k&CRPg9i;GvCyzOQu@RX%X1|X%xvMhuUM59q$*ELX{=OLvuKs!;gtPHIe zpuWDIii!%x#>Q}67vJ~k?(Sx4YRUxIX(&6>kbY{<^9YB-R904!N~Q2TkFl{aB9RC) zGc!1jW3st6Q$e7aHX+2iogV_;WuE6zS64?>RTZA+0e3h1;NT#0b930Xjg*q|kjE=0 zmN@cY4taTbfx)v&B8bQ1B$LTi)&mdd>c=lU@+I(@$xwQa)YQ}%7~l6vBog%X^%0B3 z%w`98{i$0#`B)N2084CBhk=v8@vKvpNF?w)56|<6$KzbRdey*4dGC8J*U8?HSBC!K z`E%2}{O$~iI~t|n)Z5ei^2Z$jy00zq!Q}-2CVowFall<0qQI0a4K+DAi4cOy%1ZkC z`psRmrLy-qyXCzd~ zR92k8#yBxCK~GN)3kwUTQKrE&ubCPcU*^nnh2-W4avXt_(DRkchfPO`Ovdq7TFv>_ zia7U449^dIuGNhZFpbf19Fv)}*6SSr$BJyCzu()ew{9!G`zg*_oiP?&#nAO-0E!AF zubwRAqbrMnacp2gYrX2vLkNLoS>~7q9p|-bc=jMKouA>wcczg-aJ)K~rc;M_^YkIc ze@^o8=ZicPvH>_*$9q18I#&4L$mV5aQsDplFq{3zvV~ky!iMOZ^M_Y(U0n Sk4Db`0000?>2K~z|U?bk6#8*voJ@oy4|xyylA1VJ3MNhAn@IaE*)2eT>I z!Kq{m6rJ4M1Q927RB;Ik4h3E6q7^bp7Keaf21zg=h-fedAxbco*GbE1QqO77eD)azD!R~PsYsVkw~QW;NYNkY-}uPqWl-| zqYC&5xXe5tl}eG%=gpJ?kk98yrBbHrfp1o`m@>sHA z(@fQ8vstdMuc@!EHxCjK)1Y$X-CbExP%@b$m&=vY0u%wQtgPVicZqyTyh{*2lu4tNOK()8G%f-b-Wk8ig z6$KSTIh{^PBodVYiHL;5VNn#tfKpYpWbf3oA4Z?gM`L57VeRJThU4Sow;FU~{-(`j zGSb=EX*eA~=H})~aylY^xw1kShr=PEP^e^>qM$+;RaGS%4wn>D5R}PeMA!9-Uu&=3HTNQ91#4wjad482u_HniLAOixe0 z7;>NU^K&jQFWK4Ip{}kDfSQ^bY&IKSua~{OJ-l8oGcz+ZH#a{IdTsjp`dC<40O088 z2!PqySx!z)7#|-;*LAM0u82mXoSvTIa{c)evrSD+1%OX)g}J-Cqp7Kh*49=4L%Ki<5fK;g6>z>4>K~H@zE{}rk^dmS0l9V! U>QdEUKmY&$07*qoM6N<$g2{SzrT_o{ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-white.png b/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d842bbded59bfb39fe63efef96c32a3fc306c2 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8JTOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>{XE)7O>#Ifodx0Z)WcxFQ1s;|fm~#}JR>Z>QONNGD1h zs~7M6vHXIB<5CVMA=eE{o!j(YK2exiveG#)!&u~EUf1Lu7Nu`e+RL;pc5Deac(6&q zYK38ULPmg~*TMjfrQ*UZA9kqBd8X&~KB(Zs&uQPQ-|xPE_xrx{<#QBYDaX&XC>KZ( zNSxAk>Pv~n)Vt?yO+S8X{uX`N+m~GTnQU^qQRl2)cl`OUmnC%z&F$2CxWC>#n3Qi} zwMkm|vf}>+vpVbM6_dCA_wRKwsdiXt6SsQB6swlqfqaj&ye3WJs$#Yc_50Mc>Cp3E z;Z8emJ$Txwe&y=QRwL1mOis>L+6#-eubO&xf@a;cMPXHUe{w8rC=0o^=J(pj8GD4@ zb>}7)Nfq7y*Dxv0KujZ$`Lkb;qx@BM&r=xz#*>yzJ+-d(nf8SAmiM8%K6MJLui!EH z*vy$A+AKbG{>0*A5h~}Zu73Mw>^U#~0Q;gE=A<`ouKz!Gd zO1D{+es2~;-Fl)c!1nCLL)+grDyNkD7%~#(Jn%KB^Rr~B=_uVM@ql5n7}p!76rMwS zmb?v1Kd3i3_3^*|E0*aotYFc8XlESx4j7H9C9V-ADTyViR>?)FK#IZ0z{ptFz(Uu^ zD8$gv%EZFT&`8(7)XKoX>`zu7iiX_$l+3hB+#1{$UNQn|kObKfoS#-wo>-L1P+nfH gmzkGcoSayYs+V7sKKq@G6i^X^r>mdKI;Vst0H<*r3IG5A literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-200.png b/res/terminal/images-Can/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..9dee409a6e0fdf95b7d787c5fc91f000433f9507 GIT binary patch literal 1524 zcmVku+^UcivKi~Jw%nsy{M;>|Pu?|QVFJ633mgQrnX_l1l(x3El*ZBJjc!Y43!vnBl3cV! zLrG_B7zUCg<&4~V#sN@7Wvrq2Vns-j^uO|e9BW`02Bv9Z7zU-KrPS2aP*_-KsgD&J z`#V`V+ij4<1G=tLR#rw+QxhJKhwAETjvhVA?CdNyo6X{pbgPw%FpGx81Ey)B>pDe6 zMKm`zQ&d!hVHlK@lyLa)VO%a3nx+wDfz^;Hwk zWR=qiiSvLEAj>j}qA)Npz=;zl;ymH?dTD8Cp`xN9t}WNeoYVuba!|>#jLl|?h&Xxj zWSl2tS*D?(fy&BCOBiKymDlV65lW(7i0JR{=k)2*aVss$GW+-M-_T^X2uNluNs>i~ zs;YQA9)iK3#S4aEaOu*eoUL*?4=BkRMM#*YiPPz%wzd|h(@7u@V0n3&s;VkX(`0C9 zh>ng9>~?!n0@27un?*pP#^~*_prC*~d-kBJD$C2uI2;ZpCMF1n!;FoM(b?IF-EK#g z?$7-kf>4DbqWd! zsH>}skdMAT3=R%P$crskk|dsaILy1dLKGI5mXKO4qIqbVmefvlZk+*cAxR%vJRp)% zQ50%xYf)7-vd0S%Jv}{Ky?WI`UQB-Im03K6VmG?8W&swY)?W-1W17CWUC1;|ii?XQ z}lsoSaGcvIhDNo$c_-SAPv95*z zKlIKg8@4RrNgNCYxp3ivg}hjKk+8A<2a?4oKzr95PrY-EUw#cU`1@@JF5i8Tm=5W@ z{_)=&`s`-JcHr_NbX&c=8E!Ad+gu4OaV4f&gQ^D+lfzu)3u%}qkH6nW>0b0-2X~3J!G1Fis+t2F&tmyoe-$JTDsgV^BMG-ce4K|w%q9_(AQkF;^ zG_J3&X*QdQ_%21G(a6io%UsVbiXkb`+1Z&0|8<#6CSF}#6*1oLNVbs4WJ((oMG=A^ zlvg`RnZ3QeQ4oZ*r+_kRCrJUaG9+XL$nux?Re+S2(CKtMIyzE1rz9B(kn$2*t(J#} zhf3y@Bx3=Rj7FpE>FN1VqNb)ssf@B@b(DB`c)dU1Pu3m|@ooSK?K zdwV<9*4A)vaFCeqa=9=uF_BbLavN=JZ7pbc00;(y*x%pB=jSJUJ|Ft}`tbbxj6fiO zPTK8ulH}&*CIK)I2+(S^a%5zL zBsn`fO8|U)e5A+YVN+9+Y)!3{SY2Hmc6WC%IXQ_~EC!#?m-EVh=i}o8p->2ZzaJWn z24=Gv01ys`vADR1_4Rf1_V%K=xfuZP{{9ZP+YJDio}R|w;9yccNdW-R>-E^$+R8ih zch1kx;cz&h)9J9hybO!Q0wDgew6L&%>gs9)gTc%T=G7Bf;&eJ`u~--mhxzjIk{JJL zp^J+P*4Ni_e0-cF^J^icsI9F{ta>U|s}=L}^O%{L!PwXsfaDhH>gvMW++5NeEr7o$ z(m#Uk?rs>3#7?XKr|Xnof@ek`3Ld8GzLY} l7oG?JPpLzwsHpt!`~&*JRf*~>{eA!d002ovPDHLkV1mx~w^RTC literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-white.png b/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c646e57cfb0a88d4b377440f0a6a69d6015906f1 GIT binary patch literal 792 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 N;BOe?YO&5S{|77C+eH8X literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-black.png b/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..217ea2d31a00e2aeaa5095b4fe055e688784ecc6 GIT binary patch literal 1721 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 TQs|%ok{|;166S$x*v-EI;{li; literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-100_contrast-black.png b/res/terminal/images-Can/SmallTile.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..ac8380a456625a8acadf2b805437ad26cf5fb29a GIT binary patch literal 805 zcmeAS@N?(olHy`uVBq!ia0vp^?jX#;0wn+MoHPYUv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i!>V3oBOs5n zz$3Dlfq`2Xgc%uT&5-~KvX^-Jy0Sm#5aTxBi7*ORWME+Y?CIhdl5y|tbbpVKK#}(S zl8a?zH5@sPh*@dr6|ZP@5{c;cxX`MdsqV7&*2`1ATZ|U^x=IHYC|+4+mipjW!DT1& zU7yZ3CLN!Z1xAfE* z^=#&NbNLX<2ZoP!M=m+Ph1T^}*fQODXvYvPp>oc9 z-9-;`IqUyz(_^0%{4Djq=w8~`dpp%=Ths#?NcaQEeScUTc3e5}@ZFwQz!afc;u=ws zl30>zm0Xkxq!^40jEr>+EOd>GLJSS9Of0MnjdTr6tqcsz{$%x`Xvob^$xN%nt-*ca zB~U_?1lbUrpH@mmtT}V`<;yxP!WTttDnm{r-UW|hJs0$ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-100_contrast-white.png b/res/terminal/images-Can/SmallTile.scale-100_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..bf658c223a53743f9e097598a4c21e34bda00e7c GIT binary patch literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^?jX#;0wn+MoHPYUv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i!>V3oBOs5n zz$3Dlfq`2Xgc%uT&5-~KvX^-Jy0Sm#5aTxBi7*ORWME)?EZpSMt=`Fn_9j| zkuf^>WoJuF$+wAYmkTXU=|2s(?Q4G?Z11FfzvlOxLW$E)(@rW1U%Vonv)Sp@5=)Wo zhO2CXwKSI}FWROaHP>*eisxxB)24HpqD!h;7TsGT_*aMf>Xy{N*H<(}_MR(K^xymO z^@6~^THK=Mm2b3aevS=1ojc zI&#MO#h+(3*In$xH8%0tBy?0J%-9~oyYTU+q|LrBzS-}8uW~eUAG5v2#OEfJ z%YN<&w{qO`$g2K+bKb?Xw)YCBDDL|4;?BaGipHL&mWEE!-@`sjfO}6`gQ!i^jTFv= z-rompWB%<*-{Fuwm;3ogjU!5uZHW?K)bfCTMypO|m>Wk4Fe#{(xJHzuB$lLFB^RXv zDF!10BV%0y3tc0l5JN*N6ALRtBV7YiD+2?wKUsY!8glbfGSez?Yj9t936ugQK{f>E rrERK(!v>gTe~DWM4fAe}p| literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-125.png b/res/terminal/images-Can/SmallTile.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..e0458423654c0f8b4b70965315524f710d04ed56 GIT binary patch literal 1827 zcmZ`)c|6_qSeHSM@diGgFt6h{Cm@o>Bw9spW$Wsm3*;2Obj@^%A&6m0-VzX||fKvViU03@OT zU?Ug+aK!+i6vyxIvH}3nd{0+jBIqzMFfcqkTwh;bR8*9el@%Hq8Wt8dG&EFIRh5~U z86F-E&M*Egetv!)9v)OGl}skv+1X(*m;r&_muF|ceg%$t&VtR>))usbVr6Bu-LCz& zQR{dFeV%H6=|X;erU|0)cs!5-L}oIXARU9jpwVa`EQn8`P@J8eL3}4CCr3v|S65dT z7Z(zVL?jXk1cHNugT1{y9*+kpK(2s*02>>d;NalbuV1gMtRyEVzj^a!b#--daWOG5 z(c0QNH#c{AdHL-_xu+}zyk?5w4wWm8kr)YKFX zhr4(0-ptI*^z`)PsE%gxKnOHWS+^VZPN zP+neMQc@BT5kd99#syo%2ARhMnqB(NG&;b9!{Jm{R||#0=;-Kdmg!;lac-lcqQ)xq zDk>@>BO}es%qFXKw;pO<_E)qvlJ>TdTWLg02-O`-WEL9`G=hA2veH7MIw|X zUh?4DaEZK_GcH(J0SD@1E7y8M#obcYR0q<_RaO%YV18hHfE@-w`iQ&B}IaJ+H7xk9{_G*U|?W^yxuofy$417v2TGyATFE}DLvS` zs%A3!iGFlT)kN!-$?5_aArj)^NC0j~z@5N_bet271po@*Jy6gVsr;yQ1R5c1vKtYy zCIyv6i&Ne$ody6gB$)tj1n zcyO@$``+H3!{@_8h`9JJ-%f_WbJb=U;jlV7IvC7jD4O0A{TpWF%1UyeU1p^YxnL@C z(@htm8fRS;wH`v)urzt#g=7sOU9`Vl z86AHn5___DSy7mOiRWKy0^~! zCP5~*89KZA^9$|_9DCmP84S&k@ET_n8Mx63OKSBKdcpHP@)~&2Ej2Dv*hFoQOx!48 zyT!xM@s}ozyX3^1hJP9If0X9XA^A-|{-R`dzyE<|+{L}E$BW(DyCo;mjlCOR_P#P?Y_{@{Q5|t_31r!}~!2CsqW%CB_l=USXjPM(6iCIGxwKtuI`sUDnYLL6rQohzOEHz zWMuyHP$7-7aP8BUL+W0}55svcb@lBgCf^LAe|5|)uUMN19}Vgx?A)FC&+bm}Dp#c5 zlDR71m3+AM=MTAB%^xPhQ_YTcZU?Hbum`To>32_Y|K(V2W_u^?=bwYR8<>xTy)e`H zJ)7K$0s#y=P-E4>Y@I7^LBVG1i;pYV0Q&yl8z)d6^WMH=p~v5g7Mn?RY+{JU7#bVNr1w7omtsRB!jk`g!1~-;AsC=?%-|Cn o#!jLqFahvQMMcC!u^99?CMq@|ymZS7208&`qC4TfeejjP0K%slrvLx| literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-125_contrast-black.png b/res/terminal/images-Can/SmallTile.scale-125_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..f511c39342f297c39fae2e46c16d24c0facb60cd GIT binary patch literal 906 zcmeAS@N?(olHy`uVBq!ia0vp^ks!>$0wn)4Ij8_BmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5SkFdh=oI{M;fNz_P881*%gr|#RNXEUlGwm~k6D8W` zU%3#qHOWzjZDG)g4zV?h9@S55hz`#b7jIW$WsRNVGQ~=xqbZubP{2{vrSPNtv;~tF zEl5)KOH#kBv9>hx%kv8olP2wcruTg==f3k_V(*!k``7K+$^Y(v0ncFs`ZtN=(i+|+ zTH*8b>o+I8*!DX8c*`@%_)K!;Utyn7+srPjNk9 z%pMr`%BOOs_EW2cj8i9H-9GQ*%GAo|(e>wjV}5GxmS5MlUlhC_WzTnSppNj6H zd0O|UUf!aT8D{zP%jT51mqTh!<*yc*wyQN@`aF%`7yj8ni(9`oS_In#P2F&1t(|Rf zznPY@$)eXz;U-!op8a2prWCFCUDPDl$F$dGNp0i3sc+JSg&Ec?W{==;kbYZY!L{A# zRI#VM-b~l`Q?6&tpH;HTLW12iSf*$4!XUm6GRbw9UatBTb>p1E-#rX|#oeB>7%L8( zbGqlqxi&_3_l}5|vnIW_S!bWhF`1<~5^aeH^!2!Wt&{wHN72Z5V6sy!ag8WRNi0dVN-jzT zQVd20M#j1Z7P>}8A%=!lCKgtPCb|ZuRt5&4I`LajH00)|WTsW()}T`OoCByq5@bVg rep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt&9mu6{1-oD!M<-w|*2 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-125_contrast-white.png b/res/terminal/images-Can/SmallTile.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..f99d62e07f95fc8d4aae9e32220067d895bd54aa GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0vp^ks!>$0wn)4Ij8_BmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5SkFdh=oI{M;fNz_P881*%n5T0D&S3L1G!}Y);yBy0y_MUf5p6Q&we!b3} zcW)N$xiq_Iwf2WqaKHf2+=sssDya58%KQ{b{R81RkhnNyHw>v zYJt=H+M}Z3r_RQ3tJoZzRwdt)^^j@y>%Wh~b#1(&_sSO;a{ms`TP0NIrxlXQs~IJ@ zs8vLB8v8fRFxBOE0vB&M!T6!?Zpx{@<&o~&_UxaYaw|y z_1iT4?a8^PU(C?+u3A{DrFY?XsB+cHGS48t7@a=v! zLsIS6+^$-5xk^EEZKm(beb2-HKdZa-L1P+nfHmzkGcoSayYs+V7sKKq@G6i^X^r>mdKI;Vst04>RIxBvhE literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-150.png b/res/terminal/images-Can/SmallTile.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..6549d5a635a78ec3519433e84c2168f8d2ed92e2 GIT binary patch literal 1987 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 I=akR{066Eb761SM literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-150_contrast-white.png b/res/terminal/images-Can/SmallTile.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..49732d785a4f9d8423f2bf923d1484506bfa56eb GIT binary patch literal 1026 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-200_contrast-white.png b/res/terminal/images-Can/SmallTile.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..87b9c9a50a74dbcc80a4136409d71343f07f4ab1 GIT binary patch literal 1174 zcmeAS@N?(olHy`uVBq!ia0vp^eIU%i0wjIE{AdPJEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4I8#axTn!?3JYNvoI}&weakbWm!CLFYRP07O~DxGhX&6 z$6Lpxr8-&bT-y>0MiKRfM=Swz7&MJ%D^8X>u_{qga{9u_2cj3gs}-31L+sJNwHXJq zjjg4+7Amt(leJ7cw$u`;qf1!fkwxwc$LLGRK6BoS`_GM0_&HnMxpr%6%EnW(GH3Y6 ztrmG3*FL>XMf}E6i~rNt>xdM~DJ#vMa6J6(y5q?uu0LMgSo30Q_A`!|cP=K2&X>)3 zbbOJ`M&Gkx3sWoCIGz3!(K_wvMy|t8BcGKXjkM;!x!i*D>BdvLcE2va=R9wlm0!;N z=P4JTZ$6*?sOp!?+TT%SqA?xoYO=OQ)IK>Pvi!2S@9t-FST2S5%j`F?-)ViyhGF%3Bm`H@6CF@ z=eS!_f?59SyH`b?`?|WHz1DQRZ_~l<$)DHkdMpx`(N^pH^wFDSS^Ztk=Z~&TpXID_ z@@?K6IjfQv2C|NGPUzWewrKEwcR$&pCnqvG@{sc%rVW>)H+L*&Xkh#P&E}awyQPks z0bB6@-w%@6WGfnDfAlO)UB=k(H-p9C<-II-**qqR!!5g?iIf3T@otzr zSs#St_x!dg|Nl?;t%Fw54;$6;Co#|B-b>G)%K6h`y63#FeRI48=RJ@3dwSh>3B6yQ z?}Tlb3gmqaZT4R+{PXnz`?iD=drdnc&m@U83))OxY4NmPrq!%Dj^VBaBg2UUjm*b? zXRtH)9N%ufRKmde`H~kG5_9j(va%|fp~P|R!otJ8cizln0P;@O*VJFKt)x7$D3zhSyj(9c eFS|H7u^?41zbJk7I~ysWA_h-aKbLh*2~7ZsrR%H! literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-400.png b/res/terminal/images-Can/SmallTile.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..3d97a501f2ec5b26780ebbfd33d00bea6f0520cb GIT binary patch literal 5908 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 F`ya7KFF*hQ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-400_contrast-black.png b/res/terminal/images-Can/SmallTile.scale-400_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..3815ab97e2df33e49eac5d8ae821994bf28ba2fa GIT binary patch literal 2082 zcmb_dYdF+f7ym0%gvyI!LT|_IxDUqo8;mq$=AF#MOyyE8XE3gr7}p|4Nyas#!wixc zH3sFlg`_wSO&pgwhVY6>a?33*N3cm6@7mVswatc8>%AL+qYQuxtz1V)6hL z=~}kjsOJKamOA~@FX6P$1$@qx9D=F^-Gyrxf!$F&ML&>9G8v$tyFH!?aR z=zR*Q)GEmn^7C(Bp91;JNKUjH90mM&eREM@=6)*Uhsgb01oOAt$=!s`}iAP!ToClN=X|EcWr-kNq8pL(Q2c%!&W}? z9j)7t0}Y@GonPg8=x4+~&A!%@m?$IF)TKJOa)&X@Ptc4aX3vWfyFQJ;cF)bmv2>qN zN|wn@hL1AOwEk`&BycS{sm;a)&#t>J>6l|t6hSZlHj>AlG+u)^(uP=;7Wy~-up7H% z7Vt!V-XvYfdB;R@k$r;L zUd^?r6ZxN}VHdBi4EN*9riMXRmILJh{WmHd+rEZI za$h}${zY+{2I)1{T5==k308$+V21%@GO zW0s04IaRt|nxU1hmiE~7*_Ix!n3QzNg3tNufvc!Cl5HX6Bm?oo!-lm@GXFAlTtm*p zDrHTng7H7*hd6(oZ@SCZg_Ij?|4xN06|F81Z-AKY9rxMY=wCNr*l&3#R+9zn;v$AM$u@2M`518q7t$E*+24V+w&;EoYU4SLc7>tS zE$~f&cSL7xuAZF?;vjb3NdF`ALVU>F#Enh1-9(9@Lyj0}Qytx?e!f(H1S!H_8~`(@ znYj@ZG%_N0(!u^5*;=V6Hvb%c30@b}62;l+L7~cqg jASNcpIOs}9B*~ZJZyX*$rZ3w-#7O|g5$8~I+9&yMzdDe5 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SmallTile.scale-400_contrast-white.png b/res/terminal/images-Can/SmallTile.scale-400_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..2549ef9df302f21bdf80bf8f8daae9302570cf21 GIT binary patch literal 2100 zcmb_dYdD+P7XGx!Aj+8Pan~}mZWH%v8qroOBBSn!x}`+wlAuxvN)Mw{YuqwDaSeJo zDwQa$3PV(<+7gUQ6Cx(9ORI<|rI-#?bG|-;$9*X-w6&%57ut#_@x|Lpyvj~7N2 zrV9fAKo#rm>IVP{`d>l`0xG|JeijP?B-+K(1pt~DD%(UT$PG#Eex3l3b_f76uK>Un zC}qk3Ajt**X<{VFYU0pT@}_9B(G+~&4^uz;f>FHQA@CmDrWP-8`?ABjQ?Z&t0TmqDoDy^B02G_S$;8p zMv~f%doLi1y#}q}e{!8Rn2YkYd0`B+LtxJE+&KK!`o(mC8cfJ(FwC}3cF*;KL$#~^ z$W*VEDqA7ec%SyZjuD@s&b9VZep$# z7t4U@Ze9D+FUH#d^Q;Ua?xx|TDY>j=) zd7ASd5K~1B+2|dij@{24`!lkb?aE292Cj;>hsK+77-|ux+hPxxC)3r9vxnexI_ixu zB)j+nv+4p;%#3Ni+H&F=A>jgYe_<)6)~sgQgfaBZge)z8WHp9|vOrfrF~K!0%E79f zz75U0GvIALjtNbqJd|(8520GR4|aWJy4g_}Mk7$3pro7TOpe7ShBP_iCwW;}im93P zHSae>^c{EJxm?;dDP2eDHk|atLm7oiU+?uTBRVIYI=mCL=^Zu4-4)$vCB= zk^xe)Xv0UNie>271rgL}Y<$1onrAtBbH!{G7EkYV4qA)hWjW107$e6?>ys7clU$wF zd5spaBhK8xueWf&9&U;7Ot*C^S+a6E5~!RknKEVFLDE>?NlE)x?=9p?=>p!-D9zFa zjx}5S`w2F8`geq>Af7miU&B(^lR}Ld9$9>TB6NoTK_irU_Q3q{8$wv;?45@je2%dnB73(ZVfq{ z35|l-;WrPyUg=J^MDJKXsn0exZZ_n{hYw8m_C7i=(b{H#m|~~?lL0w`$BLqoDCb5S z+z4KWIN3sLX{gpq_K{%L>swDz5FLGT^Zxq0qWW=nkv7F^p=h{>G9WdPK*y!u zlKqP>l{Hmpq$hPH*uopnV8{#J8)qgYb#r-(cR#bPM?m%T$@y;UY!iOdZ!-`r>sxz~Vva~i#c(Y?ur$UD(=(XX z_vz9`+`0*0X;(UawoJjfZpb!SDPfWJIVAY8N2Or<*CMvtd)=K=0+Mt>@#51L^08^hhKrF%ks8&c@CjZi9f^+2L$$kq!u?gDu?VFw(}x zdp#rNzYOsS=g3j1|J!hk)AK86(D^dKKOu^qLQISVQc_Z^$me5eBqBA^Dj_ktav7xy P76Dkamuut6@T~s;MMaBo literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-100.png b/res/terminal/images-Can/SplashScreen.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1b1f067858af4a1344078bb3fcf2315b7d2a9a GIT binary patch literal 4613 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-100_contrast-black.png b/res/terminal/images-Can/SplashScreen.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..8a1e60299d8e3d2ae7db8efbb8c17870ec84d921 GIT binary patch literal 1940 zcmd5+dsGtU8~!Y{)H%wt-L@`f!8*tOPon%0+RmDNkg09hL=LEe%SunAN{}YoaemX`+V>7KHvBK@t#Ud zz}efn*#ZFU@iEc+09Zo@z`}36rI~r+=R-0F%S=o>1_04{yA`sPIp08u*%uE$;bs7e zzXo8*OchT7a1;)}%QOH`l>oTpSIhTCm=D&Z$Kj#@__@>zY0hl&V-B(afPT9Qi}RCz zQ_bW$Ha;G^PPg9K4(_Qv>--J?>vViH=0Jf)|BP|t*-_}azg~4f4qT~XDfY7ZeaX?u zu{S2N=GP&%CblN7vD;X9LFtI!E6YKvyn^cLYwJDc``WRX3->HBIleno7fye&9=nyE zajrE>%5Yd5RU2eF?yOpN_js3XWiN2 z!Y2V~rA1e!2_lQ2zRd;JeyU2RBzpJ2Cm+OF{l!D&XN)m=BruA3t)1k-S zMM`xJSwjV)r+Jd$Ymv2>=e@}{l}X3m7F#b^_5P|z_uoXVe0^~f}QC=F)u9;_@(R)ZD30_ZD zo5Q~84USIHo7TEXm~@F}?tAZ?`^v(9yl>)gqyKo9I(qoy%!Limfb5L4_N>>tc3h}9 zc5ONF+6_XN9XtF!?!<{BBM~s{Xo!g@U9y7ssCeDkqg6h)r8?Ph*&PmVgR1iDtcKi@ z(o-`7&(+#FZD)~~!>gWLNh&$<7~N8^VdRDM;zRm?(sp4P3h^O`5Tcreo@8c%{!y61 zCUi4f;4#HJyd&}!(Y^afBMUJTHA|JxQI%Hmov`cMpISoTzZCXpD8uRTtrlLlLY}6Q z_(a5eT9cj|7oHgMe$Vc^lD2ktrGel`?_U2HN-MGdlq{#LO7=L@mT9`GG>Yr!N!O_} zCmEA0bc?Ebe8PBY?9a4C?)evUPY)qf#3gp>OOM>&M~#N|=4HdB87S%(9MdrLUHo6& z$m6g&DEg-K4;rBfEt!bXRRmrhiQV#J5qk>Sx$kXC?3PJ2b@pwE6QqM)lKBP0H5_Hi z$(4OMm%rm0MbYb^8`Bb(`!a2FZxMlhIf0`a8d5FwXqvSb1&0w`d&Jwi`upU6Z=bv0 zUfH6Pd6N`q-u`WT%;HC>$H@9@i|ZJ{^RS}F9nu9NATWaDhaC2zGTV?I&_+w@>@#{KLYuW^G1u^N)|xeeStiRMZ2t=V$-@>eu^)aSX_ zagv9Br0)n>IYPhQfzb(1fwHJw7Qc+vJ}QBR{>np~z;hY0b; zSB7!DiO(5%E2@erDYd2#eiaw6X6#~vDXy&IWS$F)+^I%uP=>m2m`&(b&86*P#3+** z>gU=Oc$d17G^i{>J*;6upM}e0oCkZXGO<5Rm1(OXb7OUcxH6gF1RDSG=5YZ1 ziP+tWR+mlW8{Bk}UCC|9nmVURVTWvbPsi|4_4gB36}=3_QGD~u&e8Iki7IcFn%y6u zbi{pX?5X;}H(3`*KqLkz7>yi;X`H#Bt;uo0z$En_4L}E+c#@8j>@RS}O2XRz##ja5 ze*>>Q+hhA4z7q=GnD4ZSjU})%$ZRT#!lar5K*Eth{_rq=B=P_PfeH>o1tI+5At*Q; zi{R${OOVUR$Ue;buppTJ@R3>Iy6Ui>ahS~|GpT^fB%>2O+;kNoU Ncx*y+b5vT%{{jD95hDNq literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-100_contrast-white.png b/res/terminal/images-Can/SplashScreen.scale-100_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..71167720c7b0f66c8354c3679452b90f8cafe44e GIT binary patch literal 1847 zcmd5+do-Q&I{+%Ol$UUFP`(52?ClOfq9FkEGyoPLi2fOX zI3xgH!T_*h0H7Xyr{2pNI#_qy&B+0PN16KOP^K8|97q8`Cw*K%jiAi@T%z zCxvawNZo-e+g1U9$+dz8b~DUG8z;N%YPpQEQy=|;KB=jB3p#9A zL90CaR?sH;aM5X-^O8Vm)I+=gx~cvQb-cOdJ`(@mj5L*i{mp{ziO#A7^tW0#tjUnG zXh2Q>TB+Z^aGSUK2kaHfR}8y@rn7ceq5G<3W-tnZWDDW2c)Kb(OiA_p6t;)4D*gT2*oOovT${hKounwvis*Z;R9_TZTXog*>=;SPOCrC^SoUEYFPwdef z8ChjWYSNg_Uqcv^97QdpfJSuhqnnK$_=ixyk&yFIxCgC-7S8Q!7mto;slubtlWn@S zGuXOO#9+zl=wt#dHg1etS;no&E}ULmD&$h_H*zeSTM|+vcD%fEEJf5kz6YZu|E+)| zsq)tuW6#=#1Q&PPN3ez|HIKcE>tq|~}w!asCK^i?|6V9}O5?`j&q$bNaR z3ooik38X(oo*8~Jm)cbKOZ$h{-ZR{ru81CeU*FjeCwDJgcP*IfarO1(QL5E!K90!v zRHzY@j3`d5?Slt$WKqFshAtnoT(Y&qM=_Zn3LVTf4|>b z4;t5FL%#_5jiVU_MOF7Pt$|*eiF;b550(s#d2DPWW60ob9=R62lk&p*#<`%jLh0l2 z_C@J48o&QrhH2*Q@FhFr60Hc9n1NfcVO4#~zEhUp84JF}S1D6zN9 zIdSwVW||E>M??#5cl#qYA)%1}TJv{9k8PC0juY`RczaIci!oPH#iyVk1<2O~H zo(y6g17c6$VhL9G7y=Z4Ino@3K<-DFn`6z)toH4LUYq4aRr`M&|o CW88oM literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-125.png b/res/terminal/images-Can/SplashScreen.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..04246331697ed73ffbdfef6d6187bbfc07dbb5d8 GIT binary patch literal 6058 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 P(<#%tZKP9u%Q5C(sE8LA literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-125_contrast-black.png b/res/terminal/images-Can/SplashScreen.scale-125_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..9c1add72993630fae2d527354baa63f62ded9f41 GIT binary patch literal 2408 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 Lh&SiZsVn~hMhJSE literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-150.png b/res/terminal/images-Can/SplashScreen.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..27d820c280b94a6e6ae1ac8198382012dab865c3 GIT binary patch literal 7480 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-150_contrast-black.png b/res/terminal/images-Can/SplashScreen.scale-150_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..91f66af5e5dcf8cebbae6d17021ef3bd6b03205d GIT binary patch literal 2777 zcmeHJX;9PG7XPDCC8!h#QczHkC13(!5y}#f7&ars2nq^{7&Zw4CP110YX<~0FaCXfVbxWz?lUAx{2&Iyeqh{={(jO0|0eD?6(CqwM3s&WB`C; zH>OGzidhSSTa$eKJh#qn+og`Q(00g*0?lswVmt!B8=s%JSQC+~^7zA4T>0*rNT!L; zr&~Gv$aplY?5dXU_~vYkrA~w3;*sJUkBqO6{8?U^8cUffj@&1&%wF)510JVpj8 z0pOhV>bQZ(G4}pw{>V(+(R)S%_9m5pSls(W`%2IB#LH(?M_sQm ztuG4cg9*39Nv`fGeNFP@z)(7WHj(Ww)W~p2TNEfK_jCg|m&KhOs=Dn?K`EmiZVRat zkH(-MH>#Z3S1A_%>-S>3Okb1kwbS6J9z9+YA9HRdxF433#}Ga;9N^bylW zB{r#k6N!bIrJRhf|ER?6waIMJ22)&4c}DvHzoT$Ed#(f2BZw&@wB^r}u=`~U^kG{F z17>e_Q=DuJ2>|0eXwqx@U`b^RVQ~bs4T}knwS}B9twe6Ng-n=KBDFu-DGFW&(e6FW zXTS!1Ci2}%mn5ZC=IVpl4B_A(LozJ?m={k61C8?W;)Q@BmN<0y=>Y7021A&C#^R<} z&%a4sC)vNhqorc(N9P0MtgX#_c>sq|MCyiFZ>wSXVctc{C9cO67B~zhyttJdZq+hd zefl-?2oB21WC%myk(4`^yqO#rQdN`gG+Lw{8&61UkOyX1jCQH$G>@9M6)zIanksjcx^N$YjDtYr+b!#naiUKakv* z6Gv_^cx;2gUK41WKZa+drkdKLs$a_$hb)#oDN>a_V#4hA@eWM~#vrSQYsKp&h=bZR z@0QXuw4D^h`kCM{4|r-T5@{s^)ok{)g}!9FaHN#Vfl~Z%M3m zuY9Zz>$X_?h5F`W*NNy8J612zykxoR~2P4VNr1`)S6-pQZ^J1@b6;DE0%hw)V=0yPHXaJ$EjhptdRAJGX#R+P|s5Bkuiw zw&~RpHEVR%t*TR(=H;&sN3~6}>~gW9T`Z&fMc{=EX(rC81}=yp)_Yg^3m$#U9BhK6 zy?4ys+_kLUhNJfoO1;nnW#SHMM3Y>h+jkSYnbA7hJ74=}|Mx6fc#l(U;p;M*Lp)|9mN$?5I1UR`+ zW|vcu(R)8VUiVZ`Ez(7}oXcp(s23iK_0HB|mLG_jOfdM95b|N3ccA@0|#bKa{aRWg?9m>I%COl+-TZQ_a#M7PJirmkK#O90Z?dO|To8wRtQM}>G zy?RvTu~Gy+xOI3yP_(D~_2OjvCAv+R$c75nm5O|Sav}S=ou-Y8jg2AiMm+hQBM%%U zTb=BzO?^`&?DhuvBi_OG3Uz}-T7w>Z-bJ=3I7BvvR3yHBkKDc_xm;zw`HoSgSj~He z`D$Udxsf#0?UXXN(08jow?GRoV`$85&Z_#J-#`+PIQjxOu9GG+(WKYy;!aC;cUucy zmT?_IdY%rYZ67~^;l1TO(PnX5CF8Fa3fjdHI*9f8L)!i$fX*7xE5*?2mTs=8i_zmo zr1Zlop-Dd6K3C{o&SrN|)976Hiw}mJrEe$atGMoPElv8>b`p9!Nr*Df+9tv+sT`044w&sc5sjo%SC9p4->NuI$;5#dRZ&ID2g(&L(c5tih9RMBlHZlYxL?uzeNs$1BLb19KPb3q<6CNYUm0 RcGU-s0AEjkOauB{=HIOo;G+Nl literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-150_contrast-white.png b/res/terminal/images-Can/SplashScreen.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..7afbaf5bd725cc9c502db05202a017baf5225635 GIT binary patch literal 2780 zcmeHIcTm&W7QO+I1q2pBz#wH5BuEQYK&eKMCWazif(QYTz>;7HE#Sf$999(ka~)i-$y}e3|Ew+Z}R&EUN9wu2x?U*p3UFg6zBT($PKn+?wLDY z=YDr>`$J3VQog)ppJhXhyq(gzWBq$4-!?eho8ha3LQK?CbeeO{X}RiKUb*Y{Q6h-( zAjbcG$Ncj1aGKemIhMtmwm&wzT70#vd}E3G^%8A$Xag&{BnGAQ*ZPwcKq|6^4M_&> z4Tnj=^$Z5%^l3un`Z&qY&+pCh;1Wr%APPH1{Sphqh(f8lbvJ(UJ23KfQjX8zkS&n0 zW49^*leW5=JYkowZaj@DoH6i-6VA>A6e?QB2Zc>xS_U69cxs-l#=CieD5glDO)%;@ zH16gFi^`)o@z&|O$k#Z8EsSPJ#aA!RgqWAB4?j=Dwcg6BdyEfAFUxh}-P&uTn?|g1 zM!~9d6eKdKC?ycI7Y(9a|0j_MOQ~;QaLj0Nu?NgBnP&eopyS=4Jo4VSRas+e0K1SP z=HPx%8W`pxK%4geA({qsI*uAo=lojg?&c+7^)Vz)P1v`YUZeKV*k9ECwdB*Z7aeP# z{^Ne+vtmS>C5`=|Ak*kubhKbJ`Xz#pPjS+(lfQe-=`jV-hP0uYN{1e-LAL9!up6@z zBQyri3-rXWIh5XaIQtXYDi6J!s})g7eV5nwy9^)*-$Q%_4(aS%E%SNGeAMEncfA%Q zD;e3#7DYv23Cv6w3l)B;Aj+jyMvauG0C?UOPj~_4O(n_govj}<{0C)$^To28KSn75 zXZRsRITHb{nVxdJAuazj*((Y zM(DzF`Uyl0@OU9D_S(F*zma(RbTvf`DWUJ?A!#ZN4mBf9@s&GnUlecrk%ol=mN-Q7 zZEBx^9*m0caAEEOmpAt!>0t1RuK;^oqkoU;lmskE(dO;Nl98XezMK0C`c1@lCp$Xu zeo;1vABvN8Rv&)P8BRV9m369&qnNqTMvKeu+pb$fW$pVGO}mm$(#p$s&Zu4U&DwY0 zTs%oJfECK@{y}QnvBh{ht$&6F6YtWh!o5)7(b79wM+P)!Iyu9a({{R)Xyh?krmIx1 zfTn3l<=`MtmC9*>;5uJwyF0tL4DwI8E7iP0PitGd0K*&!3Z? zG6vIf#|BfTRVtnw)_jj=)LJIvpfBrlIU=c* zE&phbi@6`eaC=Lx+_I*@d;30l^3e%}ZJt$_rYgo-C5T(P5NJ0uapYK@!L;3ty42u! z4eKZ}`D2ZkS@wo@4VyEOb{;HpGvM!wLbr#XMNTQpS^*{38bD0lZb`3+vHrT z$6O?LbbNE1q7XkgHZf+8Pndg<({wNWbB$kc^eI%h#3C!PyIjUwBt*sH{gqS3d*CVa zCjwcWAr+-7B@?zp{_Sd$Qv4m!@(-UI*=AX9oKfK?7Y7e@7^(2KY&?B*GmW^|(9Kc9Z|WANwvCG1>)x!Q&fxDL)pF-(nB#{@R%@?_8R$HmYA|Awmug{oHUX7}v*`B6vp+64@bf*|yI;{@?!TeXV?ajYBF z<9EL!%B$3YxA=1jw)YR9+#Kzpp#@xgZX<>szdaY#;|IHvS z{z6O?>AxG;HT1oL4EFCt@QIHi5W^EA0g*^FiowO=Bf=9Rjp7rrrHjYmkP>h`c^cK? Hgt_t$=XTh( literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-200.png b/res/terminal/images-Can/SplashScreen.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..9361fe998528b7d55a3a5c92f17cedc61c3cb5c7 GIT binary patch literal 10851 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-200_contrast-white.png b/res/terminal/images-Can/SplashScreen.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa1be7a035c362e727fec7e9e449c05f09a78cb GIT binary patch literal 3848 zcmeHJdo#Rfqw$_Dic+QZD%hRtEK@=tl3u*CqtBUvsiCvwcVqOMfA5Hm^&C7RxVyQr;N;ba zkoe6h??MLJtF*`!#@;Hgy3JKLq{O6baq$#x%ecDO2fEk>*DvXk>RXjF(^B5!xg!t&Nj*Rfr}1g4J}v4e#sJ}CBKfKpsC zYMdvS1*ECcE+qJ<8SYRU%vRKzA#zYk zOaAY$ddBYNz;ZD0uZ@;~`Eu=FG!T_=d1q(N-R!WlV$T`^pmKNoNos zuSY;5Q7QM^+!Z@3VoEW}T`KGjr{T%eF~G8w6@ z)~H~9fQwK~AtH6Q^@9RvA{B4Rytme}^z=1UDIpo9Ee8>8ZB$UbNJgH_BqCFtxd?NY zDufdTioe;BdOjjyWnpPnVVSUivf|9%@y;|e!(MzP*U4!l>e}`vvPRtP)!2JsRQDQ# zAsrO2F=r|_o-T8}t-1H`Ws#_HT z0}U!bFKG_2vO=3cvwp5YE?U2I6O`;`Jo?+4e&zvtz!P(yGEvQ!v~Q z&DC1wqN2Jv>}CbS7we`SYH!tTwf3)EHclhE|7SbfCTN?;pF4BR2FF+EgkHgzpR9cP z!lsY;XnP+sJDUPhG{YPI-eSRYTx&_(`c|jmS9x8m91TUpK^hHdBBbP$(Sa9w--|)4 zMtFlaibaVV`|We>>^SIwyawcTY21gcYfi$$-`Z#{_I+u=RD*%}4w}?j4NTD2z&KPQ z%<)x-D>)R<{$033(_0;fS1{a$P|S8U%#~lmj1hxWm{lkUu4zjjb3X+4hOk=h`#6c} z{>8i1qs(Yj9g(m>%S*+f7MC(*%nxPe;M0QwOOV#Q<3g4Gg0c@$i8q#>_?8rP{7khg zq~fm`_ZOwj{3)IPU?|4K%mgE!Bs?PP3 zJv#qr(`pg;l}u8EHB)M$qgMB_K2EnOJUlLkSzQTyEuLV|jq$9}v3LJ$UR+0ukY%6W zQ}xR%XsC=ln%VIaJt**C*C&wps^$&lkY<5+8AMu1m3M{nl zH4x7@DcT#H^J4B4Y*jWV2KtWPBlz{qybCBRqsK;sNfJqFMW>e^()TydX|kt5)vvJ5 zG^=+6a`?QV$C(_b5JE!S%}5EuMfT?Wnu9P%`BwcCqMEnanH)b6cai=1A(g_N{jDJx z*69n6lJFX70QUiDGN0gUP>?XdL!%m^;Es+@b2wju+=qJ~eSBtAAxRofG@ZYJM*IJE zj-%LFP9@wR2TN`q-7KDKnc#7n%LEPsVI}AGR1EOy&D#B91h{(HKwOU=t^m;q`x#2Y z$_l27+MKVE6GY`WF-t0m_ms?0?dk|4 zxIhrZ6)gKnchd!_uC$Qnj><+`anYc2Y)Rk1Ft0V2yRQ;~fp-8?Kj70TTrP(ml;9O1 zN-_JqEF0{pna*NQZ>hTUT{yrWdpY!J-}~aZ=|UQ5PGlp&6;(yPZ^@3Q4PemiGWN7q z6;a8O=rL4MmLs^@zM8zRY;hwK_MtJ?(u1z=ihFN^6-%}1d(AGOF~T zy^uon}IkXze0S z0?iZGRWo*kf44ci5H<<0J6%qdc(gfpEOGCST(pvZC#>x$eiyPkk|LU1P~gQ72D*>f z2QJnj{_XHYa@$idKeACnq*Yd=Jho zEIp5>#J=Xo?62!N{VaN&@czp5%$oJK#|`A8ZEV#;ZqI6vPEj&jFzhsNYUH|V^jXuT zQBQW7_F-{If=u)4TO$}B(UA`4oZ`mbS_ynrF6+a4=&9g(A=fYS_r|-L0|gZNy&xpx zcU4{N(r(e)9nI08_SXpexVt?r@eqgY33w z7yoGTiRfT!5-k`KV2U+0Gsf;UHZ}Fxv&Y(eueG_UG1kHwi%rxcPyCMpHIf__690b` z_Iy>q{q&*@Dr{V}a5yp~I_?B57{tZJnS@19=%f=-!6uQk(8Bk&I#3AUUA&wt_Ysr- E4LAb%7 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-400.png b/res/terminal/images-Can/SplashScreen.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..623e949335a36427bac014434ff65417c19bdb82 GIT binary patch literal 27860 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/SplashScreen.scale-400_contrast-white.png b/res/terminal/images-Can/SplashScreen.scale-400_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d81ec11271c5e1e2bfd5e2b861bc8d60c99a10 GIT binary patch literal 10012 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%0A@5jD%yM$R1+0l9zdOx`O{(Tu{z#>DF#g zaBy&fmTi_?Jv7XSwer)7v?Qn^<7*lFk|4FnS94Wfu>6FsazZULJ^NBkWP!+Bt)Jb2 zawqH--i*tCpJe*_EebL$XcSabci#@Rvkadla-Q58PFn1mY!P)gC+kl=Sn5nl%H8d1 zEyw1*`_cAd+4w8BQaq^`Rnu*You(SCiX1CoUF_VjUf-{@VK?uBPwDf(7*XbAQH_+E zdi{OCbN?aIi)VQ;;!W%5_5$PJr}%%2N~lt3UQ0}7k)#RK8&P05N-Um2aEc)lKn7S_ zS=*RfIhb2pW6z#-vUPB>vNN}`bF#8>+s0P^!yzd-HlBF-UmR@zc32m!9sFUM2N^y| pCQ>r-WCD0(+8K80YBOeR%sxj+uegTs*Ct)9`ezW|`BDmMTC literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-black.png b/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..44cc29a026777a5c74081abbf43d8a786ca98f3b GIT binary patch literal 954 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm1x)VD1X3*Nj=qiz3>*8o|0J>k`3lJ%LB0$O zRcZ_j4J`}|zkoswFBlj~4Hy_+B``2p&0t^2<1Fxq zEM{QfI|9OtQ?>b|fr9KMp1!W^&pE`n4fwX%nDGKNb$GfshE&{od+W4!NTS5?kNL|2 zZniE6ozc0VvGJN#mtKT)sVYm*?pr%LGeWz~bi00uZ(6$Lg_8n{;>ATAQ7(=2Jb$J^#d8u?w8g|Fyyy5DzGGi! zoSyn(=Iaj?CCgSW(h7{Ky)yOf*4Hn*gI!i_I3+H6V3~>Q60X%s5@CWot2s^!%~jW0 z*!N0AedUBTIfs)VqBC1eow#*-QeuB6g;r>rOZ7SmeEq28rTY5UDY0wU9(=zhp#OjM z2BVC_ZGY}@yfu4V(i(oyWy|yRYlBRauYH}WyQb*K)Y95ip4DFpQ)A;J56_Yc_q}7h z<|c?ydMEZvqUx_}iO-7EZk#{-Cxi9ASe)A=E2F)4E05o9x;RT`xskqPo9Qt|x6g~W z+MKoOKAQAvx6iv}=|#5%@@tys>#nh1d`ouzf6Y~0e5G$tN z-M1p=m(Ei^pjLJB5N8}SuS~M>^c~C&;_gMMA+IhaKDJrh^!v^;^}lbPC^y?YU>DD1 zmouJFck89dHlMtw|Gu-m>A2RhL+VI^iF>)r9Ii*TUykkaxn*$CKe08eaPs0m3+m6U z&bn0ICb6K)En!A+Od9*^#`Rwqj^CVI@=UUYb!*J*=gRXhy77ujxjK>YQgah z&z}O5scMO9L`h0wNvc(HQ7VvPFfuSQ)-|xuH8Kh@G_*3Yurf5!H88a@FbLI&--4nc zH$NpatrE8emBQy7Kn;>08-nxGO3D+9QW?t2%k?tzvWt@w3sUv+i_&MmvylQSV(@hJ Kb6Mw<&;$UIgp7^= literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-white.png b/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..999d17a9f886488dd472e93966553a70583a4e49 GIT binary patch literal 936 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-white.png b/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1e831a82595d07616adf185f5948ba4d9ad4b60d GIT binary patch literal 1152 zcmeAS@N?(olHy`uVBq!ia0vp^dq9|j1xRK%@F)N&mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5SkV71rx(pJ3=GVEo-U3d6}R5r_U-U3lsWb> z{g_J7iD?dwW?mKLs80Ruj`%;(6(Y-^L9y_OwJq+&JMT7}zsWz*M_Z znp?lQBjsJPq4;;!?=i;3^~dkLf4;`=d;Phe|L4_xzrS;Nu`1ARBEX0Ft2^g4EK$?F z(6OKYWkki-n=hX)_1{`nb$QEKb8ntkmwjXV^{nE$jfz&jow$9fjPhySx(O*UZnqbo zUe`OX;TucM>#Qy2yWaeG@Y;Eul_`j^TI3+R;tvHDd6hqQ_xQAIc6{1(@ z;Z>6p?x^^Nl$UsRwv_K_va;J%bPc4m2Hwqy2TYU%k+dMgd9ybgyss^=BN$6e>Y z85RFkMx^#?aln_*^Ntt8ZPmN?ozLdnCv;HUef~$Qru8l#J{?>1B3z(GwQ2fy>-&=v zg=l$AJgO{63sa zx^Hx2qi{{Y8y0zy>WUAV)$5lzZZ}m-w_ogQ74}Kww5iu6@zR$|msrkH^sHLt7kNp@ z_gvrZ`M);>*E~J==~qGa!Ot^$J9lkYxBfT#w$AqdHviVCNALSoTp!+LWfvFMl3cs| z&5hs9@o#1SB~-7woh$z-tNfPT4Boo^v+ur%Wck1R!PAfLbgFaTZ?wJiN%QvWyLr{U zPtR`aPj8+dS`)J2!}Kujy_bz*FT|L>cA4pZEqmgUytC6x;xGAmif^93_0`%XX12Y0 z`mS}i4=+Bw%lYNTzt^_ao;0=0{*$Mj-oIZ-U#u_+7s`(RYjU<1I$dd0UTG zX}okUiCk&K@%@SYwIBc1*7vUeYr7Lx-f-E03 zznA=>d2h9O`uojQUqdgM%7)I3`Wm=*Rd}{dm*c&l57U2{{3*!0 z6x6%&!B@MNoAO-5d5K87KqddG?=yap+BG4NxBDoNt6Jh3QIe8al4_M)lnSI6j0}v7 zbqy?Zjf_GJ4XsQptc*-_4NR>J3~n!u2uIP7o1c=IR*74~s(%k&12ss3YzWRzD=AMb mN@XZ7FW1Y=%Pvk%EJ)SMFG`>N&PEETh{4m<&t;ucLK6Vb^6I_- literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-150.png b/res/terminal/images-Can/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..eeabf03075c8e105c1d48cd7174743e9878167a5 GIT binary patch literal 3150 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 Lt&J!Ko_GHP3RZ}) literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-black.png b/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c2860ac36909645c0458641f65bcb2bdae103d GIT binary patch literal 1374 zcmeAS@N?(olHy`uVBq!ia0vp^4?&oN1xQZWCG`V3oBOs5n zz$3Dlfk8|agc&`9R6YO&*-JcqUD=;=h;bXpxVE~x0xi_>ba4!+xb^mqZ-z{&%<+%; zUc1{I7MxsaqO>hE_>J!hc2>cI=3lOCUcj#Z$W`!_v}iQ9+eUW%do~rpESg>iw@h1f zp}8ZX>f-~)Ak(E{o;BIO)P7cBb!;a>;xltAZv#st|nBD7idsP>5d(N%~ul$ciuY|aS_c(osdS$dK z$1B!h>GELl3y)@AVGUWG-*P)xJm7Iw)D?55(i0q;(_3yDA9^F1_^mBSJU}x@e1lW2 zb?9yH-{!k-{mL#YsusU}zMzkNcU8OdO6j1L+xABXZr)q4V%ywL5i@L+&&(4{JI}Fs zdTdL&<)NEbTvrG`EsyU%ULer>W6^5qJ%1l**o{PTeB+`Nh&z<9G~vS zky4tuzqwfV^kJE&9~-YeURo=EXKT_=uHQ5KVyCRy%AdM( z!Y7e9cg@vHj_n{-QM*^BUhh0|akk!}p0xC^Sr2+A{XBR+Xu4^^*112Ue}Au=RT~qz zaos`x*xE^Ji_cB`cDM5q!~VMq7_K&7?e{qzoo91*huNRR!+vS6YIN4D&1077HI|lG zAoy*;ZU*(_GG}I!LvmNN9hBcV?qz~AtA+iivFxaJV3bPa2YUbcE~Y&<_AD;UwhJi4xQ=zEghvn0KBX+nu^!a2eKIkky zaQ>_*?>vSFMH_7nNhJ3KKCa(=ZPB*+6|rg3@gF(ti_F!lc?2Gq{AaN4UTPfkT*2U^ z?9o~0f3h|l&hh-q{CsVe!yb|4UBUhDbao>zjCBnxbd8Kc3=OSJEUb(ybPY_c z3=CwLjg(O|)EAE-eRWJ7R%T1k0gQ7S`udAVL@UUqSEVnM22eo^}D ScQ#T$MGT&V3oBOs5n zz$3Dlfk8|agc&`9R6YO&*-JcqUD=;=h;bXpxVE~xGB7Yd@pN$vskrs_j(>(ss?4#E z>CT%?G+o(r4k~P`_!7ij6|^EOnmJ6w<%K6}e%v0Gqs~{&E<|-*m^{y>>f4eIu0;V= zD;}Kux72NEuP2EcMC=l@6WHC*vijx3ycP)|!L)vk&F(EXl@Gny3Y6`T-Q5(oHYHl? z=E}dF3-->xw5N{yM*aM6uYTWHZlQGYU#{lHxEXogh5BdYU^-9h9r`=#>c1PV2^zKW z`{h37-md@MxtgLQ*7UR)UwF=&yjF~^J*0L;e#hTmtX;pI7>-&7Z z=k1Q0v95I17mxc4`~~_x9#M=!iOh|R|6aPA8#^fFC}gR`&JU-;SUp;JuSk9T)y8xuc<;p>Fa2#&f9}jW{r=yH-MXjen%=ML z;V@qzc8Fj4RbzEk-`-c9T8GxH?kLwomtdM@_x4rh)ko0a;QhHxV$7_`l<8ROFZVf-QTpx|LgQ6?W=XmzbsYBToe<3ZPK(YORP8g zS9qNKx46uHdr8Fo7lN_ZHuEx1S)#r*`clo#GKa^%94?vO)!4pgMZ1dU@vTC?Ta3*g zG+De{etMz%jZ1qc#C@q;e`DVFl=FK(xc#gteY>|nX)#OsbRN}Zk zd*_``-QO~O?!Q`|HGlo-^Z$?7U47GA5qJG6WufZ}P+HU)0UjzJ4w%usiC?%^Rt_ns+&7^IJCG zy;SIVyd-3j$x5fzuB=Ilo>3ilWh||1Z&fFrZsmR-u{rflEBAJt)5&@<71Q5qFaO%g z?Z3|UU&xzhCjz6FZ#8)3=)3soGQXvNKEEO-+ko`s^!;bxx4rvQ=bT>wkgHna8c~vx zSdwa$T$Bo=7>o>zjCBnxbd8Kc3=OSJEUb(ybPY_c3=CwLjg(O|)E yAE-eRWJ7R%T1k0gQ7S`udAVL@UUqSEVnM22eo^}DcQ#T$MGT&Ubfm@

FgAeoHt}zo5_zKgExqvpZ8^Z(??U7c_g|<3unX2qZE9*F z)((by8M>FF`a$K8vC!fH4u0p1$REZHz!6+?ITWx%!YegyIZ&QRTueSj7VZ8^Z#@0=0&qxBx43^Z0j`g`LQ+Hz<)!|@RL zXOz^R1AIQdy8$&7h zA9<`no5sfqh4jWUXdk`t3~3)>Kb~hlZN6yx@8pkT-}k%y@LgfL-xN+wO|`^rwf~lU w>zltg25Wqbh<}~z*WaHpfIs5H_xUJazSQ@vv;SmL13tncPjnc|6VLbm0SsN74FCWD literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/terminal_contrast-black.ico b/res/terminal/images-Can/terminal_contrast-black.ico new file mode 100644 index 0000000000000000000000000000000000000000..3b2e743758beb3d5d28bbee376d214a4b50e8645 GIT binary patch literal 38858 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/terminal_contrast-white.ico b/res/terminal/images-Can/terminal_contrast-white.ico new file mode 100644 index 0000000000000000000000000000000000000000..b791799c20f876099f3e1307bbd7b2e042d8acdd GIT binary patch literal 39006 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 SagdLD1`mH&IWWXg>->M-*;gq5 literal 0 HcmV?d00001 diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 58be390d1c6..6e43186c761 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -43,6 +43,9 @@ Designer + + Designer + Designer @@ -63,6 +66,9 @@ -Pre + + -Can + diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest new file mode 100644 index 00000000000..f2e697921a9 --- /dev/null +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -0,0 +1,151 @@ + + + + + + + + ms-resource:AppStoreNameCan + Microsoft Corporation + Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.microsoft.windows.terminal.settings + + + + + + {1F9F2BF5-5BC3-4F17-B0E6-912413F1F451} + + + + + + + {051F34EE-C1FD-4B19-AF75-9BA54648434C} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw b/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw index eced69ff25a..f7d5a7010e9 100644 --- a/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw +++ b/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw @@ -125,6 +125,10 @@ Terminal Dev {Locked} + + Terminal Canary + {Locked} + Terminal Preview {Locked} @@ -137,6 +141,10 @@ Windows Terminal Dev {Locked} + + Windows Terminal Canary + {Locked} + Windows Terminal Preview {Locked} @@ -149,6 +157,10 @@ Terminal Dev {Locked} + + Terminal Canary + {Locked} + Terminal Preview {Locked} @@ -161,6 +173,10 @@ The Windows Terminal, but Unofficial {Locked} + + The Windows Terminal (Canary build) + {Locked} + Windows Terminal with a preview of upcoming features {Locked} diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 3b5cc7d9abe..25c2cd1f8d2 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -15,6 +15,7 @@ // // * Release: {06171993-7eb1-4f3e-85f5-8bdd7386cce3} // * Preview: {04221993-7eb1-4f3e-85f5-8bdd7386cce3} +// * Canary: {09222022-7eb1-4f3e-85f5-8bdd7386cce3} // * Dev: {08302020-7eb1-4f3e-85f5-8bdd7386cce3} constexpr GUID Monarch_clsid { @@ -22,6 +23,8 @@ constexpr GUID Monarch_clsid 0x06171993, #elif defined(WT_BRANDING_PREVIEW) 0x04221993, +#elif defined(WT_BRANDING_CANARY) + 0x09222022, #else 0x08302020, #endif diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 450fb347528..f1842c9cf52 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -94,6 +94,8 @@ HRESULT OpenTerminalHere::GetTitle(IShellItemArray* /*psiItemArray*/, RS_(L"ShellExtension_OpenInTerminalMenuItem"); #elif defined(WT_BRANDING_PREVIEW) RS_(L"ShellExtension_OpenInTerminalMenuItem_Preview"); +#elif defined(WT_BRANDING_CANARY) + RS_(L"ShellExtension_OpenInTerminalMenuItem_Canary"); #else RS_(L"ShellExtension_OpenInTerminalMenuItem_Dev"); #endif diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.h b/src/cascadia/ShellExtension/OpenTerminalHere.h index f626d8112c5..de5d0886437 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.h +++ b/src/cascadia/ShellExtension/OpenTerminalHere.h @@ -29,6 +29,8 @@ struct __declspec(uuid("9f156763-7844-4dc4-b2b1-901f640f5155")) #elif defined(WT_BRANDING_PREVIEW) __declspec(uuid("02db545a-3e20-46de-83a5-1329b1e88b6b")) +#elif defined(WT_BRANDING_CANARY) + __declspec(uuid("6119575F-6918-4392-AF16-C2C627AF9416")) #else // DEV __declspec(uuid("52065414-e077-47ec-a3ac-1cc5455e1b54")) #endif diff --git a/src/cascadia/TerminalApp/Resources/en-US/ContextMenu.resw b/src/cascadia/TerminalApp/Resources/en-US/ContextMenu.resw index 68a873569b6..28fc0311647 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/ContextMenu.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/ContextMenu.resw @@ -125,6 +125,10 @@ Terminal Dev {Locked} The dev build will never be seen in multiple languages + + Terminal Canary + {Locked=qps-ploc,qps-ploca,qps-plocm} + Terminal Preview {Locked=qps-ploc,qps-ploca,qps-plocm} @@ -137,6 +141,10 @@ Windows Terminal Dev {Locked} The dev build will never be seen in multiple languages + + Windows Terminal Canary + {Locked=qps-ploc,qps-ploca,qps-plocm}. "Canary" in this context means an unstable or nightly build of a software product, not the bird. + Windows Terminal Preview {Locked=qps-ploc,qps-ploca,qps-plocm} @@ -149,6 +157,10 @@ Terminal Dev {Locked} The dev build will never be seen in multiple languages + + Terminal Canary + {Locked=qps-ploc,qps-ploca,qps-plocm} + Terminal Preview {Locked=qps-ploc,qps-ploca,qps-plocm} @@ -160,6 +172,10 @@ The Windows Terminal, but Unofficial {Locked} The dev build will never be seen in multiple languages + + The Windows Terminal (Canary build) + {Locked} + Windows Terminal with a preview of upcoming features @@ -167,6 +183,10 @@ Open in Terminal (&Dev) {Locked} The dev build will never be seen in multiple languages + + Open in Terminal (&Canary) + This is a menu item that will be displayed in the Windows File Explorer that launches the Canary version of Windows Terminal. Please mark one of the characters to be an accelerator key. + Open in Terminal &Preview This is a menu item that will be displayed in the Windows File Explorer that launches the Preview version of Windows Terminal. Please mark one of the characters to be an accelerator key. diff --git a/src/cascadia/TerminalConnection/CTerminalHandoff.h b/src/cascadia/TerminalConnection/CTerminalHandoff.h index cdeb87f6af9..3d6c3d19876 100644 --- a/src/cascadia/TerminalConnection/CTerminalHandoff.h +++ b/src/cascadia/TerminalConnection/CTerminalHandoff.h @@ -22,6 +22,8 @@ Author(s): #define __CLSID_CTerminalHandoff "E12CFF52-A866-4C77-9A90-F570A7AA2C6B" #elif defined(WT_BRANDING_PREVIEW) #define __CLSID_CTerminalHandoff "86633F1F-6454-40EC-89CE-DA4EBA977EE2" +#elif defined(WT_BRANDING_CANARY) +#define __CLSID_CTerminalHandoff "1706609C-A4CE-4C0D-B7D2-C19BF66398A5" #else #define __CLSID_CTerminalHandoff "051F34EE-C1FD-4B19-AF75-9BA54648434C" #endif diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 3063f549494..5925271fadc 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -837,7 +837,7 @@ try bool releaseSettingExists = false; if (firstTimeSetup && !IsPortableMode()) { -#if defined(WT_BRANDING_PREVIEW) +#if defined(WT_BRANDING_PREVIEW) || defined(WT_BRANDING_CANARY) { try { diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.rc b/src/cascadia/WindowsTerminal/WindowsTerminal.rc index fb03671aafc..d349078a422 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.rc +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.rc @@ -61,6 +61,10 @@ IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\imag IDI_APPICON ICON "..\\..\\..\\res\\terminal\\images-Pre\\terminal.ico" IDI_APPICON_HC_BLACK ICON "..\\..\\..\\res\\terminal\\images-Pre\\terminal_contrast-black.ico" IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\images-Pre\\terminal_contrast-white.ico" +#elif defined(WT_BRANDING_CANARY) +IDI_APPICON ICON "..\\..\\..\\res\\terminal\\images-Can\\terminal.ico" +IDI_APPICON_HC_BLACK ICON "..\\..\\..\\res\\terminal\\images-Can\\terminal_contrast-black.ico" +IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\images-Can\\terminal_contrast-white.ico" #else IDI_APPICON ICON "..\\..\\..\\res\\terminal\\images-Dev\\terminal.ico" IDI_APPICON_HC_BLACK ICON "..\\..\\..\\res\\terminal\\images-Dev\\terminal_contrast-black.ico" diff --git a/src/features.xml b/src/features.xml index 121076d259f..4656ef0ffd5 100644 --- a/src/features.xml +++ b/src/features.xml @@ -121,6 +121,7 @@ Dev + Canary Preview @@ -142,6 +143,7 @@ Dev + Canary Preview @@ -170,6 +172,7 @@ AlwaysDisabled Dev + Canary Preview diff --git a/src/host/exe/CConsoleHandoff.h b/src/host/exe/CConsoleHandoff.h index 67ee7bde11e..095fb728f07 100644 --- a/src/host/exe/CConsoleHandoff.h +++ b/src/host/exe/CConsoleHandoff.h @@ -22,6 +22,8 @@ Author(s): #define __CLSID_CConsoleHandoff "2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69" #elif defined(WT_BRANDING_PREVIEW) #define __CLSID_CConsoleHandoff "06EC847C-C0A5-46B8-92CB-7C92F6E35CD5" +#elif defined(WT_BRANDING_CANARY) +#define __CLSID_CConsoleHandoff "A854D02A-F2FE-44A5-BB24-D03F4CF830D4" #else #define __CLSID_CConsoleHandoff "1F9F2BF5-5BC3-4F17-B0E6-912413F1F451" #endif diff --git a/src/host/proxy/Host.Proxy.vcxproj b/src/host/proxy/Host.Proxy.vcxproj index 942cfd0d2f4..6908c1d75d9 100644 --- a/src/host/proxy/Host.Proxy.vcxproj +++ b/src/host/proxy/Host.Proxy.vcxproj @@ -66,6 +66,11 @@ PROXY_CLSID_IS={0x1833E661,0xCC81,0x4DD0,{0x87,0xC6,0xC2,0xF7,0x4B,0xD3,0x9E,0xFA}};%(PreprocessorDefinitions) + + + PROXY_CLSID_IS={0x1D1852F4,0xADAD,0x42B6,{0x9A,0x43,0x94,0x37,0xAA,0xAD,0x77,0x17}};%(PreprocessorDefinitions) + + PROXY_CLSID_IS={0xDEC4804D,0x56D1,0x4F73,{0x9F,0xBE,0x68,0x28,0xE7,0xC8,0x5C,0x56}};%(PreprocessorDefinitions) diff --git a/tools/FeatureStagingSchema.xsd b/tools/FeatureStagingSchema.xsd index 49d4f7ce3d4..7f0dc9103bd 100644 --- a/tools/FeatureStagingSchema.xsd +++ b/tools/FeatureStagingSchema.xsd @@ -61,7 +61,7 @@ - + diff --git a/tools/Generate-FeatureStagingHeader.ps1 b/tools/Generate-FeatureStagingHeader.ps1 index 8206b7bb10e..cc78685b8b0 100644 --- a/tools/Generate-FeatureStagingHeader.ps1 +++ b/tools/Generate-FeatureStagingHeader.ps1 @@ -11,7 +11,7 @@ Param( [ValidateScript({ Test-Path $_ })] [string]$Path, - [ValidateSet("Dev", "Preview", "Release", "WindowsInbox")] + [ValidateSet("Dev", "Canary", "Preview", "Release", "WindowsInbox")] [string]$Branding = "Dev", [string]$BranchOverride = $Null, From e10b7e4fb9efc418d400741263b19597743eaf07 Mon Sep 17 00:00:00 2001 From: hanpuliu-charles <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 24 Aug 2023 06:45:28 -0500 Subject: [PATCH 34/59] Add --appendCommandLine flag for appending to command (#15822) Added --appendCommandLine flag that when set, appends the command to the preset command in the profile instead of replacing it. Previously, there was no good way to launch wt while running a command appended to the set command in the profile. Some uses include profiles that are set to login or start an application. Additional comments: Looking for a review, and expecting additional changes that needs to be done. For example, I am not really sure on how to include the the option's information in the CallForHelp() screen. Also, would be great if someone could guide me on including tests for this new feature. Thanks! Closes #5528 --------- Co-authored-by: Charles Liu --- src/cascadia/TerminalApp/AppCommandlineArgs.cpp | 8 ++++++++ src/cascadia/TerminalApp/AppCommandlineArgs.h | 2 ++ src/cascadia/TerminalApp/Resources/en-US/Resources.resw | 5 ++++- src/cascadia/TerminalSettingsModel/ActionArgs.h | 3 +++ src/cascadia/TerminalSettingsModel/ActionArgs.idl | 1 + src/cascadia/TerminalSettingsModel/TerminalSettings.cpp | 9 ++++++++- 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index e9b6368ff0b..2fa36a9711f 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -568,6 +568,9 @@ void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubc subcommand.colorSchemeOption = subcommand.subcommand->add_option("--colorScheme", _startingColorScheme, RS_A(L"CmdColorSchemeArgDesc")); + + subcommand.appendCommandLineOption = subcommand.subcommand->add_flag("--appendCommandLine", _appendCommandLineOption, RS_A(L"CmdAppendCommandLineDesc")); + // 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", @@ -654,6 +657,10 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT { args.ColorScheme(winrt::to_hstring(_startingColorScheme)); } + if (*subcommand.appendCommandLineOption) + { + args.AppendCommandLine(_appendCommandLineOption); + } return args; } @@ -699,6 +706,7 @@ void AppCommandlineArgs::_resetStateToDefault() _startingTabColor.clear(); _commandline.clear(); _suppressApplicationTitle = false; + _appendCommandLineOption = false; _splitVertical = false; _splitHorizontal = false; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 31eac78168b..292a3500c42 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -67,6 +67,7 @@ class TerminalApp::AppCommandlineArgs final CLI::Option* tabColorOption; CLI::Option* suppressApplicationTitleOption; CLI::Option* colorSchemeOption; + CLI::Option* appendCommandLineOption; }; struct NewPaneSubcommand : public NewTerminalSubcommand @@ -105,6 +106,7 @@ class TerminalApp::AppCommandlineArgs final // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; + bool _appendCommandLineOption{ false }; bool _splitVertical{ false }; bool _splitHorizontal{ false }; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 80f0a9df574..6834eff5847 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -840,4 +840,7 @@ Run as Administrator This text is displayed on context menu for profile entries in add new tab button. - \ No newline at end of file + + If set, the command will be appended to the profile's default command instead of replacing it. + + diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 87e1ac4fcfe..5633c039469 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -309,6 +309,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARG(Windows::Foundation::IReference, TabColor, nullptr); ACTION_ARG(Windows::Foundation::IReference, ProfileIndex, nullptr); ACTION_ARG(winrt::hstring, Profile, L""); + ACTION_ARG(bool, AppendCommandLine, false); ACTION_ARG(Windows::Foundation::IReference, SuppressApplicationTitle, nullptr); ACTION_ARG(winrt::hstring, ColorScheme); ACTION_ARG(Windows::Foundation::IReference, Elevate, nullptr); @@ -320,6 +321,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static constexpr std::string_view TabColorKey{ "tabColor" }; static constexpr std::string_view ProfileIndexKey{ "index" }; static constexpr std::string_view ProfileKey{ "profile" }; + static constexpr std::string_view AppendCommandLineKey{ "appendCommandLine" }; static constexpr std::string_view SuppressApplicationTitleKey{ "suppressApplicationTitle" }; static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; static constexpr std::string_view ElevateKey{ "elevate" }; @@ -340,6 +342,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation otherAsUs->_TabColor == _TabColor && otherAsUs->_ProfileIndex == _ProfileIndex && otherAsUs->_Profile == _Profile && + otherAsUs->_AppendCommandLine == _AppendCommandLine && otherAsUs->_SuppressApplicationTitle == _SuppressApplicationTitle && otherAsUs->_ColorScheme == _ColorScheme && otherAsUs->_Elevate == _Elevate && diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index d38e3011281..271c9d979d8 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -132,6 +132,7 @@ namespace Microsoft.Terminal.Settings.Model String TabTitle; Windows.Foundation.IReference TabColor; String Profile; // Either a GUID or a profile's name if the GUID isn't a match + Boolean AppendCommandLine; // We use IReference<> to treat some args as nullable where null means // "use the inherited value". See ProfileIndex, diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index aaa151d9cf2..c594d42fb3b 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -129,7 +129,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Override commandline, starting directory if they exist in newTerminalArgs if (!newTerminalArgs.Commandline().empty()) { - defaultSettings.Commandline(newTerminalArgs.Commandline()); + if (!newTerminalArgs.AppendCommandLine()) + { + defaultSettings.Commandline(newTerminalArgs.Commandline()); + } + else + { + defaultSettings.Commandline(defaultSettings.Commandline() + L" " + newTerminalArgs.Commandline()); + } } if (!newTerminalArgs.StartingDirectory().empty()) { From 30eb9eed49c53bb61b257192170dbf57aff0cac6 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 24 Aug 2023 10:31:09 -0500 Subject: [PATCH 35/59] Raise ShortcutActions with the sender (tab, control) context (#15773) This PR's goal is to allow something like a `Tab` to raise a ShortcutAction, by saying "this action should be performed on ME". We've had a whole category of these issues in the past: * #15734 * #15760 * #13579 * #13942 * #13942 * Heck even dating back to #10832 So, this tries to remove a bit of that footgun. This probably isn't the _final_ form of what this refactor might look like, but the code is certainly better than before. Basically, there's a few bits: * `ShortcutActionDispatch.DoAction` now takes a `sender`, which can be _anything_. * Most actions that use a "Get the focused _thing_ then do something to it" are changed to "If there was a sender, let's use that - otherwise, we'll use the focused _thing_". * TerminalTab was largely refactored to use this, instead of making requests to the `TerminalPage` to just do a thing to it. I've got a few TODO!s left, but wanted to get initial feedback. * [x] `TerminalPage::_HandleTogglePaneZoom` * [x] `TerminalPage::_HandleFocusPane` * [x] `TerminalPage::_MoveTab` Closes #15734 --- .../LocalTests_TerminalApp/TabTests.cpp | 10 +- .../TerminalApp/AppActionHandlers.cpp | 136 ++++++++++------- src/cascadia/TerminalApp/AppLogic.cpp | 4 +- .../TerminalApp/ShortcutActionDispatch.cpp | 17 ++- .../TerminalApp/ShortcutActionDispatch.h | 4 +- .../TerminalApp/ShortcutActionDispatch.idl | 3 +- src/cascadia/TerminalApp/TabManagement.cpp | 89 +++-------- src/cascadia/TerminalApp/TerminalPage.cpp | 80 ++++------ src/cascadia/TerminalApp/TerminalPage.h | 13 +- src/cascadia/TerminalApp/TerminalTab.cpp | 139 +++++++++--------- src/cascadia/TerminalApp/TerminalTab.h | 18 +-- src/cascadia/inc/WindowingBehavior.h | 7 + 12 files changed, 248 insertions(+), 272 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 63f9d1ed67a..4a09828c680 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -517,7 +517,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format(L"Duplicate the first pane")); result = RunOnUIThread([&page]() { - page->_SplitPane(SplitDirection::Automatic, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); + page->_SplitPane(nullptr, SplitDirection::Automatic, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); @@ -535,7 +535,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format(L"Duplicate the pane, and don't crash")); result = RunOnUIThread([&page]() { - page->_SplitPane(SplitDirection::Automatic, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); + page->_SplitPane(nullptr, SplitDirection::Automatic, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); @@ -857,7 +857,7 @@ namespace TerminalAppLocalTests // | 1 | 2 | // | | | // ------------------- - page->_SplitPane(SplitDirection::Right, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); + page->_SplitPane(nullptr, SplitDirection::Right, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); secondId = tab->_activePane->Id().value(); }); Sleep(250); @@ -875,7 +875,7 @@ namespace TerminalAppLocalTests // | 3 | | // | | | // ------------------- - page->_SplitPane(SplitDirection::Down, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); + page->_SplitPane(nullptr, SplitDirection::Down, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); // Split again to make the 3rd tab thirdId = tab->_activePane->Id().value(); @@ -895,7 +895,7 @@ namespace TerminalAppLocalTests // | 3 | 4 | // | | | // ------------------- - page->_SplitPane(SplitDirection::Down, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); + page->_SplitPane(nullptr, SplitDirection::Down, 0.5f, page->_MakePane(nullptr, page->_GetFocusedTab(), nullptr)); auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); fourthId = tab->_activePane->Id().value(); }); diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 2fefc40aae4..9e6db17ca29 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -29,6 +29,29 @@ namespace winrt namespace winrt::TerminalApp::implementation { + TermControl TerminalPage::_senderOrActiveControl(const IInspectable& sender) + { + if (sender) + { + if (auto arg{ sender.try_as() }) + { + return arg; + } + } + return _GetActiveControl(); + } + winrt::com_ptr TerminalPage::_senderOrFocusedTab(const IInspectable& sender) + { + if (sender) + { + if (auto tab{ sender.try_as() }) + { + return _GetTerminalTabImpl(tab); + } + } + return _GetFocusedTabImpl(); + } + void TerminalPage::_HandleOpenNewTabDropdown(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -149,7 +172,7 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleSendInput(const IInspectable& /*sender*/, + void TerminalPage::_HandleSendInput(const IInspectable& sender, const ActionEventArgs& args) { if (args == nullptr) @@ -158,7 +181,7 @@ namespace winrt::TerminalApp::implementation } else if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto termControl{ _GetActiveControl() }) + if (const auto termControl{ _senderOrActiveControl(sender) }) { termControl.SendInput(realArgs.Input()); args.Handled(true); @@ -166,10 +189,10 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleCloseOtherPanes(const IInspectable& /*sender*/, + void TerminalPage::_HandleCloseOtherPanes(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto terminalTab{ _GetFocusedTabImpl() }) + if (const auto& terminalTab{ _senderOrFocusedTab(sender) }) { const auto activePane = terminalTab->GetActivePane(); if (terminalTab->GetRootPane() != activePane) @@ -214,7 +237,7 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/, + void TerminalPage::_HandleSplitPane(const IInspectable& sender, const ActionEventArgs& args) { if (args == nullptr) @@ -236,7 +259,11 @@ namespace winrt::TerminalApp::implementation } const auto& duplicateFromTab{ realArgs.SplitMode() == SplitType::Duplicate ? _GetFocusedTab() : nullptr }; - _SplitPane(realArgs.SplitDirection(), + + const auto& terminalTab{ _senderOrFocusedTab(sender) }; + + _SplitPane(terminalTab, + realArgs.SplitDirection(), // This is safe, we're already filtering so the value is (0, 1) ::base::saturated_cast(realArgs.SplitSize()), _MakePane(realArgs.TerminalArgs(), duplicateFromTab)); @@ -251,33 +278,27 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleTogglePaneZoom(const IInspectable& /*sender*/, + void TerminalPage::_HandleTogglePaneZoom(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto terminalTab{ _senderOrFocusedTab(sender) }) { // Don't do anything if there's only one pane. It's already zoomed. - if (activeTab->GetLeafPaneCount() > 1) + if (terminalTab->GetLeafPaneCount() > 1) { - // First thing's first, remove the current content from the UI - // tree. This is important, because we might be leaving zoom, and if - // a pane is zoomed, then it's currently in the UI tree, and should - // be removed before it's re-added in Pane::Restore - _tabContent.Children().Clear(); - // Togging the zoom on the tab will cause the tab to inform us of // the new root Content for this tab. - activeTab->ToggleZoom(); + terminalTab->ToggleZoom(); } } args.Handled(true); } - void TerminalPage::_HandleTogglePaneReadOnly(const IInspectable& /*sender*/, + void TerminalPage::_HandleTogglePaneReadOnly(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { activeTab->TogglePaneReadOnly(); } @@ -285,10 +306,10 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleEnablePaneReadOnly(const IInspectable& /*sender*/, + void TerminalPage::_HandleEnablePaneReadOnly(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { activeTab->SetPaneReadOnly(true); } @@ -296,10 +317,10 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleDisablePaneReadOnly(const IInspectable& /*sender*/, + void TerminalPage::_HandleDisablePaneReadOnly(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { activeTab->SetPaneReadOnly(false); } @@ -529,11 +550,12 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleFind(const IInspectable& /*sender*/, + void TerminalPage::_HandleFind(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { + _SetFocusedTab(*activeTab); _Find(*activeTab); } args.Handled(true); @@ -637,7 +659,7 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleSetTabColor(const IInspectable& /*sender*/, + void TerminalPage::_HandleSetTabColor(const IInspectable& sender, const ActionEventArgs& args) { Windows::Foundation::IReference tabColor; @@ -647,7 +669,7 @@ namespace winrt::TerminalApp::implementation tabColor = realArgs.TabColor(); } - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { if (tabColor) { @@ -661,17 +683,22 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleOpenTabColorPicker(const IInspectable& /*sender*/, + void TerminalPage::_HandleOpenTabColorPicker(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { - activeTab->RequestColorPicker(); + if (!_tabColorPicker) + { + _tabColorPicker = winrt::make(); + } + + activeTab->AttachColorPicker(_tabColorPicker); } args.Handled(true); } - void TerminalPage::_HandleRenameTab(const IInspectable& /*sender*/, + void TerminalPage::_HandleRenameTab(const IInspectable& sender, const ActionEventArgs& args) { std::optional title; @@ -681,7 +708,7 @@ namespace winrt::TerminalApp::implementation title = realArgs.Title(); } - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { if (title.has_value()) { @@ -695,10 +722,10 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleOpenTabRenamer(const IInspectable& /*sender*/, + void TerminalPage::_HandleOpenTabRenamer(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { activeTab->ActivateTabRenamer(); } @@ -807,12 +834,12 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleMoveTab(const IInspectable& /*sender*/, + void TerminalPage::_HandleMoveTab(const IInspectable& sender, const ActionEventArgs& actionArgs) { if (const auto& realArgs = actionArgs.ActionArgs().try_as()) { - auto moved = _MoveTab(realArgs); + auto moved = _MoveTab(_senderOrFocusedTab(sender), realArgs); actionArgs.Handled(moved); } } @@ -1101,6 +1128,16 @@ namespace winrt::TerminalApp::implementation if (const auto& realArgs = args.ActionArgs().try_as()) { const auto paneId = realArgs.Id(); + + // This action handler is not enlightened for _senderOrFocusedTab. + // There's currently no way for an inactive tab to be the sender of a focusPane command. + // If that ever changes, then we'll need to consider how this handler should behave. + // Should it + // * focus the tab that sent the command AND activate the requested pane? + // * or should it just activate the pane in the sender, and leave the focused tab alone? + // + // For now, we'll just focus the pane in the focused tab. + if (const auto activeTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); @@ -1117,10 +1154,10 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } - void TerminalPage::_HandleExportBuffer(const IInspectable& /*sender*/, + void TerminalPage::_HandleExportBuffer(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { if (args) { @@ -1188,10 +1225,10 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleSelectAll(const IInspectable& /*sender*/, + void TerminalPage::_HandleSelectAll(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto& control{ _GetActiveControl() }) + if (const auto& control{ _senderOrActiveControl(sender) }) { control.SelectAll(); args.Handled(true); @@ -1227,30 +1264,30 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleMarkMode(const IInspectable& /*sender*/, + void TerminalPage::_HandleMarkMode(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto& control{ _GetActiveControl() }) + if (const auto& control{ _senderOrActiveControl(sender) }) { control.ToggleMarkMode(); args.Handled(true); } } - void TerminalPage::_HandleToggleBlockSelection(const IInspectable& /*sender*/, + void TerminalPage::_HandleToggleBlockSelection(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto& control{ _GetActiveControl() }) + if (const auto& control{ _senderOrActiveControl(sender) }) { const auto handled = control.ToggleBlockSelection(); args.Handled(handled); } } - void TerminalPage::_HandleSwitchSelectionEndpoint(const IInspectable& /*sender*/, + void TerminalPage::_HandleSwitchSelectionEndpoint(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto& control{ _GetActiveControl() }) + if (const auto& control{ _senderOrActiveControl(sender) }) { const auto handled = control.SwitchSelectionEndpoint(); args.Handled(handled); @@ -1349,10 +1386,10 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleToggleBroadcastInput(const IInspectable& /*sender*/, + void TerminalPage::_HandleToggleBroadcastInput(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { activeTab->ToggleBroadcastInput(); args.Handled(true); @@ -1360,10 +1397,10 @@ namespace winrt::TerminalApp::implementation // If the focused tab wasn't a TerminalTab, then leave handled=false } - void TerminalPage::_HandleRestartConnection(const IInspectable& /*sender*/, + void TerminalPage::_HandleRestartConnection(const IInspectable& sender, const ActionEventArgs& args) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { if (const auto activePane{ activeTab->GetActivePane() }) { @@ -1382,5 +1419,4 @@ namespace winrt::TerminalApp::implementation } args.Handled(true); } - } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index a31246a031c..fb39ebf72fd 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -597,11 +597,11 @@ namespace winrt::TerminalApp::implementation // use as a title though! // // First, check the reserved keywords: - if (parsedTarget == "new") + if (parsedTarget == NewWindow) { return winrt::make(WindowingBehaviorUseNew); } - else if (parsedTarget == "last") + else if (parsedTarget == MostRecentlyUsedWindow) { return winrt::make(WindowingBehaviorUseExisting); } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp index 8b7a15cd312..b44e44976e3 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp @@ -10,11 +10,11 @@ using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::TerminalApp; -#define ACTION_CASE(action) \ - case ShortcutAction::action: \ - { \ - _##action##Handlers(*this, eventArgs); \ - break; \ +#define ACTION_CASE(action) \ + case ShortcutAction::action: \ + { \ + action.raise(sender, eventArgs); \ + break; \ } namespace winrt::TerminalApp::implementation @@ -27,7 +27,8 @@ namespace winrt::TerminalApp::implementation // - actionAndArgs: the ShortcutAction and associated args to raise an event for. // Return Value: // - true if we handled the event was handled, else false. - bool ShortcutActionDispatch::DoAction(const ActionAndArgs& actionAndArgs) + bool ShortcutActionDispatch::DoAction(const winrt::Windows::Foundation::IInspectable& sender, + const ActionAndArgs& actionAndArgs) { if (!actionAndArgs) { @@ -50,4 +51,8 @@ namespace winrt::TerminalApp::implementation return eventArgs.Handled(); } + bool ShortcutActionDispatch::DoAction(const ActionAndArgs& actionAndArgs) + { + return DoAction(nullptr, actionAndArgs); + } } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.h b/src/cascadia/TerminalApp/ShortcutActionDispatch.h index de4f0e3d9af..d43d7fc2421 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.h +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.h @@ -13,7 +13,7 @@ namespace TerminalAppLocalTests class KeyBindingsTests; } -#define DECLARE_ACTION(action) TYPED_EVENT(action, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); +#define DECLARE_ACTION(action) til::typed_event action; namespace winrt::TerminalApp::implementation { @@ -22,6 +22,8 @@ namespace winrt::TerminalApp::implementation ShortcutActionDispatch() = default; bool DoAction(const Microsoft::Terminal::Settings::Model::ActionAndArgs& actionAndArgs); + bool DoAction(const winrt::Windows::Foundation::IInspectable& sender, + const Microsoft::Terminal::Settings::Model::ActionAndArgs& actionAndArgs); #define ON_ALL_ACTIONS(action) DECLARE_ACTION(action); ALL_SHORTCUT_ACTIONS diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl index 74d4bed4e42..2798df77ac3 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl @@ -2,7 +2,7 @@ // Licensed under the MIT license. #include "../TerminalSettingsModel/AllShortcutActions.h" -#define ACTION_EVENT(name) event Windows.Foundation.TypedEventHandler name +#define ACTION_EVENT(name) event Windows.Foundation.TypedEventHandler name namespace TerminalApp { @@ -10,6 +10,7 @@ namespace TerminalApp ShortcutActionDispatch(); Boolean DoAction(Microsoft.Terminal.Settings.Model.ActionAndArgs actionAndArgs); + Boolean DoAction(Object sender, Microsoft.Terminal.Settings.Model.ActionAndArgs actionAndArgs); // When adding a new action, add them to AllShortcutActions.h! #define ON_ALL_ACTIONS(action) ACTION_EVENT(action); diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index ef0093dc30d..4a673536e7d 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -168,61 +168,6 @@ namespace winrt::TerminalApp::implementation } }); - newTabImpl->DuplicateRequested([weakTab, weakThis{ get_weak() }]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab) - { - page->_DuplicateTab(*tab); - } - }); - - newTabImpl->SplitTabRequested([weakTab, weakThis{ get_weak() }]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab) - { - page->_SplitTab(*tab); - } - }); - - newTabImpl->MoveTabToNewWindowRequested([weakTab, weakThis{ get_weak() }]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab) - { - MoveTabArgs args{ hstring{ L"new" }, MoveTabDirection::Forward }; - page->_SetFocusedTab(*tab); - page->_MoveTab(args); - } - }); - - newTabImpl->ExportTabRequested([weakTab, weakThis{ get_weak() }]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab) - { - // Passing empty string as the path to export tab will make it - // prompt for the path - page->_ExportTab(*tab, L""); - } - }); - - newTabImpl->FindRequested([weakTab, weakThis{ get_weak() }]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab) - { - page->_SetFocusedTab(*tab); - page->_Find(*tab); - } - }); - auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().InsertAt(insertPosition, tabViewItem); @@ -370,20 +315,6 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } - // Method Description: - // - Sets the specified tab as the focused tab and splits its active pane - // Arguments: - // - tab: tab to split - void TerminalPage::_SplitTab(TerminalTab& tab) - { - try - { - _SetFocusedTab(tab); - _SplitPane(tab, SplitDirection::Automatic, 0.5f, _MakePane(nullptr, tab)); - } - CATCH_LOG(); - } - // Method Description: // - Exports the content of the Terminal Buffer inside the tab // Arguments: @@ -696,6 +627,21 @@ namespace winrt::TerminalApp::implementation return std::nullopt; } + // Method Description: + // - Returns the index in our list of tabs of the currently focused tab. If + // no tab is currently selected, returns nullopt. + // Return Value: + // - the index of the currently focused tab if there is one, else nullopt + std::optional TerminalPage::_GetTabIndex(const TerminalApp::TabBase& tab) const noexcept + { + uint32_t i; + if (_tabs.IndexOf(tab, i)) + { + return i; + } + return std::nullopt; + } + // Method Description: // - returns the currently focused tab. This might return null, // so make sure to check the result! @@ -751,7 +697,10 @@ namespace winrt::TerminalApp::implementation // sometimes set focus to an incorrect tab after removing some tabs auto weakThis{ get_weak() }; - co_await wil::resume_foreground(_tabView.Dispatcher()); + if (!_tabView.Dispatcher().HasThreadAccess()) + { + co_await winrt::resume_foreground(_tabView.Dispatcher()); + } if (auto page{ weakThis.get() }) { diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index d4ec0666b68..bc55b11a88f 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1158,7 +1158,8 @@ namespace winrt::TerminalApp::implementation } if (altPressed && !debugTap) { - this->_SplitPane(SplitDirection::Automatic, + this->_SplitPane(_GetFocusedTabImpl(), + SplitDirection::Automatic, 0.5f, newPane); } @@ -1695,20 +1696,6 @@ namespace winrt::TerminalApp::implementation } }); - hostingTab.ColorPickerRequested([weakTab, weakThis]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - if (page && tab) - { - if (!page->_tabColorPicker) - { - page->_tabColorPicker = winrt::make(); - } - - tab->AttachColorPicker(page->_tabColorPicker); - } - }); - // Add an event handler for when the terminal or tab wants to set a // progress indicator on the taskbar hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); @@ -2125,8 +2112,13 @@ namespace winrt::TerminalApp::implementation _RequestMoveContentHandlers(*this, *request); } - bool TerminalPage::_MoveTab(MoveTabArgs args) + bool TerminalPage::_MoveTab(winrt::com_ptr tab, MoveTabArgs args) { + if (!tab) + { + return false; + } + // If there was a windowId in the action, try to move it to the // specified window instead of moving it in our tab row. const auto windowId{ args.Window() }; @@ -2139,12 +2131,12 @@ namespace winrt::TerminalApp::implementation return true; } - if (const auto terminalTab{ _GetFocusedTabImpl() }) + if (tab) { - auto startupActions = terminalTab->BuildStartupActions(true); - _DetachTabFromWindow(terminalTab); + auto startupActions = tab->BuildStartupActions(true); + _DetachTabFromWindow(tab); _MoveContent(std::move(startupActions), args.Window(), 0); - _RemoveTab(*terminalTab); + _RemoveTab(*tab); return true; } } @@ -2152,9 +2144,13 @@ namespace winrt::TerminalApp::implementation const auto direction = args.Direction(); if (direction != MoveTabDirection::None) { - if (auto focusedTabIndex = _GetFocusedTabIndex()) + // Use the requested tab, if provided. Otherwise, use the currently + // focused tab. + const auto tabIndex = til::coalesce(_GetTabIndex(*tab), + _GetFocusedTabIndex()); + if (tabIndex) { - const auto currentTabIndex = focusedTabIndex.value(); + const auto currentTabIndex = tabIndex.value(); const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; _TryMoveTab(currentTabIndex, currentTabIndex + delta); } @@ -2231,19 +2227,20 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Split the focused pane either horizontally or vertically, and place the - // given pane accordingly in the tree + // - Split the focused pane of the given tab, either horizontally or vertically, and place the + // given pane accordingly // Arguments: + // - tab: The tab that is going to be split. // - newPane: the pane to add to our tree of panes // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the // new pane should be split from its parent. // - splitSize: the size of the split - void TerminalPage::_SplitPane(const SplitDirection splitDirection, + void TerminalPage::_SplitPane(const winrt::com_ptr& tab, + const SplitDirection splitDirection, const float splitSize, std::shared_ptr newPane) { - const auto focusedTab{ _GetFocusedTabImpl() }; - + auto activeTab = tab; // Clever hack for a crash in startup, with multiple sub-commands. Say // you have the following commandline: // @@ -2259,38 +2256,19 @@ namespace winrt::TerminalApp::implementation // Instead, let's just promote this first split to be a tab instead. // Crash avoided, and we don't need to worry about inserting a new-tab // command in at the start. - if (!focusedTab) + if (!tab) { if (_tabs.Size() == 0) { _CreateNewTabFromPane(newPane); + return; } else { - // The focused tab isn't a terminal tab - return; + activeTab = _GetFocusedTabImpl(); } } - else - { - _SplitPane(*focusedTab, splitDirection, splitSize, newPane); - } - } - // Method Description: - // - Split the focused pane of the given tab, either horizontally or vertically, and place the - // given pane accordingly - // Arguments: - // - tab: The tab that is going to be split. - // - newPane: the pane to add to our tree of panes - // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the - // new pane should be split from its parent. - // - splitSize: the size of the split - void TerminalPage::_SplitPane(TerminalTab& tab, - const SplitDirection splitDirection, - const float splitSize, - std::shared_ptr newPane) - { // If the caller is calling us with the return value of _MakePane // directly, it's possible that nullptr was returned, if the connections // was supposed to be launched in an elevated window. In that case, do @@ -2303,14 +2281,14 @@ namespace winrt::TerminalApp::implementation const auto contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; - const auto realSplitType = tab.PreCalculateCanSplit(splitDirection, splitSize, availableSpace); + const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); if (!realSplitType) { return; } _UnZoomIfNeeded(); - tab.SplitPane(*realSplitType, splitSize, newPane); + activeTab->SplitPane(*realSplitType, splitSize, newPane); // After GH#6586, the control will no longer focus itself // automatically when it's finished being laid out. Manually focus diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2c0439f9d98..1c684c59936 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -333,7 +333,6 @@ namespace winrt::TerminalApp::implementation void _DuplicateFocusedTab(); void _DuplicateTab(const TerminalTab& tab); - void _SplitTab(TerminalTab& tab); winrt::fire_and_forget _ExportTab(const TerminalTab& tab, winrt::hstring filepath); winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab); @@ -355,7 +354,7 @@ namespace winrt::TerminalApp::implementation bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); - bool _MoveTab(const Microsoft::Terminal::Settings::Model::MoveTabArgs args); + bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); template bool _ApplyToActiveControls(F f) @@ -379,6 +378,7 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; + std::optional _GetTabIndex(const TerminalApp::TabBase& tab) const noexcept; TerminalApp::TabBase _GetFocusedTab() const noexcept; winrt::com_ptr _GetFocusedTabImpl() const noexcept; TerminalApp::TabBase _GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept; @@ -392,10 +392,7 @@ namespace winrt::TerminalApp::implementation void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); - void _SplitPane(const Microsoft::Terminal::Settings::Model::SplitDirection splitType, - const float splitSize, - std::shared_ptr newPane); - void _SplitPane(TerminalTab& tab, + void _SplitPane(const winrt::com_ptr& tab, const Microsoft::Terminal::Settings::Model::SplitDirection splitType, const float splitSize, std::shared_ptr newPane); @@ -541,6 +538,10 @@ namespace winrt::TerminalApp::implementation void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args); void _PopulateContextMenu(const IInspectable& 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); + winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp #define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index ad90c6b1d7b..1b881029b56 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -9,6 +9,7 @@ #include "Utils.h" #include "ColorHelper.h" #include "AppLogic.h" +#include "../inc/WindowingBehavior.h" using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -1263,23 +1264,20 @@ namespace winrt::TerminalApp::implementation // "Color..." Controls::MenuFlyoutItem chooseColorMenuItem; - Controls::FontIcon colorPickSymbol; - colorPickSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); - colorPickSymbol.Glyph(L"\xE790"); + { + Controls::FontIcon colorPickSymbol; + colorPickSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + colorPickSymbol.Glyph(L"\xE790"); - chooseColorMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->RequestColorPicker(); - } - }); - chooseColorMenuItem.Text(RS_(L"TabColorChoose")); - chooseColorMenuItem.Icon(colorPickSymbol); + chooseColorMenuItem.Click({ get_weak(), &TerminalTab::_chooseColorClicked }); + chooseColorMenuItem.Text(RS_(L"TabColorChoose")); + chooseColorMenuItem.Icon(colorPickSymbol); - const auto chooseColorToolTip = RS_(L"ChooseColorToolTip"); + const auto chooseColorToolTip = RS_(L"ChooseColorToolTip"); - WUX::Controls::ToolTipService::SetToolTip(chooseColorMenuItem, box_value(chooseColorToolTip)); - Automation::AutomationProperties::SetHelpText(chooseColorMenuItem, chooseColorToolTip); + WUX::Controls::ToolTipService::SetToolTip(chooseColorMenuItem, box_value(chooseColorToolTip)); + Automation::AutomationProperties::SetHelpText(chooseColorMenuItem, chooseColorToolTip); + } Controls::MenuFlyoutItem renameTabMenuItem; { @@ -1288,12 +1286,7 @@ namespace winrt::TerminalApp::implementation renameTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); renameTabSymbol.Glyph(L"\xE8AC"); // Rename - renameTabMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->ActivateTabRenamer(); - } - }); + renameTabMenuItem.Click({ get_weak(), &TerminalTab::_renameTabClicked }); renameTabMenuItem.Text(RS_(L"RenameTabText")); renameTabMenuItem.Icon(renameTabSymbol); @@ -1310,12 +1303,7 @@ namespace winrt::TerminalApp::implementation duplicateTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); duplicateTabSymbol.Glyph(L"\xF5ED"); - duplicateTabMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->_DuplicateRequestedHandlers(); - } - }); + duplicateTabMenuItem.Click({ get_weak(), &TerminalTab::_duplicateTabClicked }); duplicateTabMenuItem.Text(RS_(L"DuplicateTabText")); duplicateTabMenuItem.Icon(duplicateTabSymbol); @@ -1332,12 +1320,7 @@ namespace winrt::TerminalApp::implementation splitTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); splitTabSymbol.Glyph(L"\xF246"); // ViewDashboard - splitTabMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->_SplitTabRequestedHandlers(); - } - }); + splitTabMenuItem.Click({ get_weak(), &TerminalTab::_splitTabClicked }); splitTabMenuItem.Text(RS_(L"SplitTabText")); splitTabMenuItem.Icon(splitTabSymbol); @@ -1349,17 +1332,12 @@ namespace winrt::TerminalApp::implementation Controls::MenuFlyoutItem moveTabToNewWindowMenuItem; { - // "Move Tab to New Window Tab" + // "Move Tab to New Window" Controls::FontIcon moveTabToNewWindowTabSymbol; moveTabToNewWindowTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); moveTabToNewWindowTabSymbol.Glyph(L"\xE8A7"); - moveTabToNewWindowMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->_MoveTabToNewWindowRequestedHandlers(); - } - }); + moveTabToNewWindowMenuItem.Click({ get_weak(), &TerminalTab::_moveTabToNewWindowClicked }); moveTabToNewWindowMenuItem.Text(RS_(L"MoveTabToNewWindowText")); moveTabToNewWindowMenuItem.Icon(moveTabToNewWindowTabSymbol); @@ -1372,12 +1350,7 @@ namespace winrt::TerminalApp::implementation Controls::MenuFlyoutItem closePaneMenuItem = _closePaneMenuItem; { // "Close Pane" - closePaneMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->ClosePane(); - } - }); + closePaneMenuItem.Click({ get_weak(), &TerminalTab::_closePaneClicked }); closePaneMenuItem.Text(RS_(L"ClosePaneText")); const auto closePaneToolTip = RS_(L"ClosePaneToolTip"); @@ -1393,12 +1366,7 @@ namespace winrt::TerminalApp::implementation exportTabSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); exportTabSymbol.Glyph(L"\xE74E"); // Save - exportTabMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->_ExportTabRequestedHandlers(); - } - }); + exportTabMenuItem.Click({ get_weak(), &TerminalTab::_exportTextClicked }); exportTabMenuItem.Text(RS_(L"ExportTabText")); exportTabMenuItem.Icon(exportTabSymbol); @@ -1415,12 +1383,7 @@ namespace winrt::TerminalApp::implementation findSymbol.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); findSymbol.Glyph(L"\xF78B"); // SearchMedium - findMenuItem.Click([weakThis](auto&&, auto&&) { - if (auto tab{ weakThis.get() }) - { - tab->_FindRequestedHandlers(); - } - }); + findMenuItem.Click({ get_weak(), &TerminalTab::_findClicked }); findMenuItem.Text(RS_(L"FindText")); findMenuItem.Icon(findSymbol); @@ -1542,20 +1505,6 @@ namespace winrt::TerminalApp::implementation return terminalBrush; } - // Method Description: - // - Send an event to request for the color picker - // - The listener should attach the color picker via AttachColorPicker() - // Arguments: - // - - // Return Value: - // - - void TerminalTab::RequestColorPicker() - { - ASSERT_UI_THREAD(); - - _ColorPickerRequestedHandlers(); - } - // - Get the total number of leaf panes in this tab. This will be the number // of actual controls hosted by this tab. // Arguments: @@ -1858,4 +1807,52 @@ namespace winrt::TerminalApp::implementation } }); } + + void TerminalTab::_chooseColorClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + _dispatch.DoAction(*this, { ShortcutAction::OpenTabColorPicker, nullptr }); + } + void TerminalTab::_renameTabClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ActivateTabRenamer(); + } + void TerminalTab::_duplicateTabClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ActionAndArgs actionAndArgs{ ShortcutAction::DuplicateTab, nullptr }; + _dispatch.DoAction(*this, actionAndArgs); + } + void TerminalTab::_splitTabClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ActionAndArgs actionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate } }; + _dispatch.DoAction(*this, actionAndArgs); + } + void TerminalTab::_closePaneClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ClosePane(); + } + void TerminalTab::_exportTextClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ActionAndArgs actionAndArgs{}; + actionAndArgs.Action(ShortcutAction::ExportBuffer); + _dispatch.DoAction(*this, actionAndArgs); + } + void TerminalTab::_moveTabToNewWindowClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + MoveTabArgs args{ winrt::to_hstring(NewWindow), MoveTabDirection::Forward }; + ActionAndArgs actionAndArgs{ ShortcutAction::MoveTab, args }; + _dispatch.DoAction(*this, actionAndArgs); + } + void TerminalTab::_findClicked(const winrt::Windows::Foundation::IInspectable& /* sender */, + const winrt::Windows::UI::Xaml::RoutedEventArgs& /* args */) + { + ActionAndArgs actionAndArgs{ ShortcutAction::Find, nullptr }; + _dispatch.DoAction(*this, actionAndArgs); + } } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index d7c1cede718..72ec8d8bac2 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -71,7 +71,6 @@ namespace winrt::TerminalApp::implementation virtual std::optional GetTabColor() override; void SetRuntimeTabColor(const winrt::Windows::UI::Color& color); void ResetRuntimeTabColor(); - void RequestColorPicker(); void UpdateZoom(std::shared_ptr newFocus); void ToggleZoom(); @@ -99,12 +98,6 @@ namespace winrt::TerminalApp::implementation WINRT_CALLBACK(ActivePaneChanged, winrt::delegate<>); WINRT_CALLBACK(TabRaiseVisualBell, winrt::delegate<>); - WINRT_CALLBACK(DuplicateRequested, winrt::delegate<>); - WINRT_CALLBACK(SplitTabRequested, winrt::delegate<>); - WINRT_CALLBACK(MoveTabToNewWindowRequested, winrt::delegate<>); - WINRT_CALLBACK(FindRequested, winrt::delegate<>); - WINRT_CALLBACK(ExportTabRequested, winrt::delegate<>); - WINRT_CALLBACK(ColorPickerRequested, winrt::delegate<>); TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: @@ -154,8 +147,6 @@ namespace winrt::TerminalApp::implementation bool _inRename{ false }; winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _tabRenameBoxLayoutUpdatedRevoker; - winrt::TerminalApp::ShortcutActionDispatch _dispatch; - void _Setup(); std::optional _bellIndicatorTimer; @@ -186,6 +177,15 @@ namespace winrt::TerminalApp::implementation void _addBroadcastHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control, ControlEventTokens& events); + void _chooseColorClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _renameTabClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _duplicateTabClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _splitTabClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _closePaneClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _exportTextClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _moveTabToNewWindowClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void _findClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + friend class ::TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h index 348b2b51ab6..c00a33e9ce0 100644 --- a/src/cascadia/inc/WindowingBehavior.h +++ b/src/cascadia/inc/WindowingBehavior.h @@ -12,3 +12,10 @@ inline constexpr int32_t WindowingBehaviorUseName{ -4 }; inline constexpr int32_t WindowingBehaviorUseNone{ -5 }; inline constexpr std::wstring_view QuakeWindowName{ L"_quake" }; + +// Magic names for magic windowing behaviors. These are reserved names, in place +// of window names. "new" can also be used in MoveTab / MovePane actions. +// * new: to use a new window, always +// * last: use the most recent window +inline constexpr std::string_view NewWindow{ "new" }; +inline constexpr std::string_view MostRecentlyUsedWindow{ "last" }; From 10ea38c9a7790b062292607d19c2d630474691a3 Mon Sep 17 00:00:00 2001 From: John HU Date: Thu, 24 Aug 2023 08:32:36 -0700 Subject: [PATCH 36/59] README: Explain why 1.17/1.18 don't winget (#15840) We chose to link to the Terminal issue rather than the WinGet one, because ours has more contextual information. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f43556bb03d..f0e06e1d145 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ winget install --id Microsoft.WindowsTerminal -e ``` > **Note**\ -> Due to a dependency issue, Terminal's current versions cannot be installed via the Windows Package Manager CLI. To install the stable release 1.17 or later, or the Preview release 1.18 or later, please use an alternative installation method. +> Due to [a dependency issue](https://github.com/microsoft/terminal/issues/15663), Terminal's current versions cannot be installed via the Windows Package Manager CLI. To install the stable release 1.17 or later, or the Preview release 1.18 or later, please use an alternative installation method. #### Via Chocolatey (unofficial) From b024efb3b7049f55a91379bd8bb7590fcb0ccb74 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 24 Aug 2023 14:11:42 -0500 Subject: [PATCH 37/59] Disambiguate the test job artifact based on attempt number (#15877) Closes #15876 --- build/pipelines/ci.yml | 2 ++ build/pipelines/templates-v2/job-test-project.yml | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build/pipelines/ci.yml b/build/pipelines/ci.yml index 3a142688443..4ad72566a15 100644 --- a/build/pipelines/ci.yml +++ b/build/pipelines/ci.yml @@ -97,6 +97,8 @@ stages: - template: ./templates-v2/job-test-project.yml parameters: platform: ${{ platform }} + # The tests might be run more than once; log one artifact per attempt. + outputArtifactStem: -$(System.JobAttempt) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - stage: CodeIndexer diff --git a/build/pipelines/templates-v2/job-test-project.yml b/build/pipelines/templates-v2/job-test-project.yml index 78f5fa4db6a..1cd8e2bef67 100644 --- a/build/pipelines/templates-v2/job-test-project.yml +++ b/build/pipelines/templates-v2/job-test-project.yml @@ -2,7 +2,8 @@ parameters: configuration: 'Release' platform: '' testLogPath: '$(Build.BinariesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\testsOnBuildMachine.wtl' - artifactStem: '' + inputArtifactStem: '' + outputArtifactStem: '' jobs: - job: Test${{ parameters.platform }}${{ parameters.configuration }} @@ -37,7 +38,7 @@ jobs: - task: DownloadPipelineArtifact@2 displayName: Download artifacts inputs: - artifactName: build-${{ parameters.platform }}-$(BuildConfiguration)${{ parameters.artifactStem }} + artifactName: build-${{ parameters.platform }}-$(BuildConfiguration)${{ parameters.inputArtifactStem }} downloadPath: $(Terminal.BinDir) - task: PowerShell@2 @@ -94,5 +95,5 @@ jobs: flattenFolders: true - publish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test-logs' - artifact: test-logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + artifact: test-logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.outputArtifactStem }} condition: always() From 7a055019079e20a7d2d64e147392db5fe125334f Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 24 Aug 2023 14:13:49 -0500 Subject: [PATCH 38/59] A first couple Suggestion UI nits (#15848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the active checkboxes in #15845. I'll leave that open till we get to the endgame, I'm sure more will show up. Closes: - [x] Accessibility tags all have `CommandPalette_` strings 🤣 - [x] useCommandline should leave the cursor at the _end_ of the input, not at the start - [x] useCommandline, when bottom-up, should leave the _last_ list item selected, not the first. - [x] ^ Probably applies to any changes to the filter text when bottom up. --- .../Resources/en-US/Resources.resw | 20 ++++++++ .../TerminalApp/SuggestionsControl.cpp | 46 +++++++++---------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 6834eff5847..4d1fd13f202 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -590,6 +590,26 @@ Enter a wt commandline to run {Locked="wt"} + + More options for "{}" + This text will be read aloud using assistive technologies when the user selects a command that has additional options. The {} will be expanded to the name of the command containing more options. + + + Type a command name... + + + No matching commands + + + Suggestions menu + + + More options + + + Suggestions found: {0} + {0} will be replaced with a number. + Crimson diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp index d70c9a6bc4d..742e8222015 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.cpp +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -584,7 +584,7 @@ namespace winrt::TerminalApp::implementation automationPeer.RaiseNotificationEvent( Automation::Peers::AutomationNotificationKind::ActionCompleted, Automation::Peers::AutomationNotificationProcessing::CurrentThenMostRecent, - fmt::format(std::wstring_view{ RS_(L"CommandPalette_NestedCommandAnnouncement") }, ParentCommandName()), + fmt::format(std::wstring_view{ RS_(L"SuggestionsControl_NestedCommandAnnouncement") }, ParentCommandName()), L"SuggestionsControlNestingLevelChanged" /* unique name for this notification category */); } } @@ -725,10 +725,12 @@ namespace winrt::TerminalApp::implementation // here will ensure that we can check this case appropriately. _lastFilterTextWasEmpty = _searchBox().Text().empty(); + const auto lastSelectedIndex = _filteredActionsView().SelectedIndex(); + _updateFilteredActions(); // In the command line mode we want the user to explicitly select the command - _filteredActionsView().SelectedIndex(0); + _filteredActionsView().SelectedIndex(std::min(lastSelectedIndex, _filteredActionsView().Items().Size() - 1)); const auto currentNeedleHasResults{ _filteredActions.Size() > 0 }; _noMatchesText().Visibility(currentNeedleHasResults ? Visibility::Collapsed : Visibility::Visible); @@ -738,7 +740,7 @@ namespace winrt::TerminalApp::implementation Automation::Peers::AutomationNotificationKind::ActionCompleted, Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, currentNeedleHasResults ? - winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"CommandPalette_MatchesAvailable") }, _filteredActions.Size()) } : + winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"SuggestionsControl_MatchesAvailable") }, _filteredActions.Size()) } : NoMatchesText(), // what to announce if results were found L"SuggestionsControlResultAnnouncement" /* unique name for this group of notifications */); } @@ -781,9 +783,6 @@ namespace winrt::TerminalApp::implementation void SuggestionsControl::_switchToMode() { - const auto currentlyVisible{ Visibility() == Visibility::Visible }; - - auto modeAnnouncementResourceKey{ USES_RESOURCE(L"CommandPaletteModeAnnouncement_ActionMode") }; ParsedCommandLineText(L""); _searchBox().Text(L""); _searchBox().Select(_searchBox().Text().size(), 0); @@ -795,23 +794,9 @@ namespace winrt::TerminalApp::implementation // guarantees that the correct text is shown for the mode // whenever _switchToMode is called. - SearchBoxPlaceholderText(RS_(L"CommandPalette_SearchBox/PlaceholderText")); - NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text")); - ControlName(RS_(L"CommandPaletteControlName")); - // modeAnnouncementResourceKey is already set to _ActionMode - // We did this above to deduce the type (and make it easier on ourselves later). - - if (currentlyVisible) - { - if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(_searchBox()) }) - { - automationPeer.RaiseNotificationEvent( - Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::CurrentThenMostRecent, - GetLibraryResourceString(modeAnnouncementResourceKey), - L"SuggestionsControlModeSwitch" /* unique ID for this notification */); - } - } + SearchBoxPlaceholderText(RS_(L"SuggestionsControl_SearchBox/PlaceholderText")); + NoMatchesText(RS_(L"SuggestionsControl_NoMatchesText/Text")); + ControlName(RS_(L"SuggestionsControlName")); // The smooth remove/add animations that happen during // UpdateFilteredActions don't work very well when switching between @@ -1004,7 +989,7 @@ namespace winrt::TerminalApp::implementation if (dataTemplate == _itemTemplateSelector.NestedItemTemplate()) { - const auto helpText = winrt::box_value(RS_(L"CommandPalette_MoreOptions/[using:Windows.UI.Xaml.Automation]AutomationProperties/HelpText")); + const auto helpText = winrt::box_value(RS_(L"SuggestionsControl_MoreOptions/[using:Windows.UI.Xaml.Automation]AutomationProperties/HelpText")); listViewItem.SetValue(Automation::AutomationProperties::HelpTextProperty(), helpText); } @@ -1104,6 +1089,19 @@ namespace winrt::TerminalApp::implementation Margin(newMargin); _searchBox().Text(filter); + + // If we're in bottom-up mode, make sure to re-select the _last_ item in + // the list, so that it's like we're starting with the most recent one + // selected. + if (_direction == TerminalApp::SuggestionsDirection::BottomUp) + { + const auto last = _filteredActionsView().Items().Size() - 1; + _filteredActionsView().SelectedIndex(last); + } + // Move the cursor to the very last position, so it starts immediately + // after the text. This is apparently done by starting a 0-wide + // selection starting at the end of the string. + _searchBox().Select(filter.size(), 0); } } From 0f61b5f97d649b74af90877e971f7fd035dd5535 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 24 Aug 2023 14:53:03 -0500 Subject: [PATCH 39/59] Remove the FontSizeChanged event from TermControl (#15867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I originally just wanted to close #1104, but then discovered that hey, this event wasn't even used anymore. Excerpts of Teams convo: * [Snap to character grid when resizing window by mcpiroman · Pull Request #3181 · microsoft/terminal (github.com)](https://github.com/microsoft/terminal/pull/3181/files#diff-d7ca72e0d5652fee837c06532efa614191bd5c41b18aa4d3ee6711f40138f04c) added it to Tab.cpp * where it was added * which called `pane->Relayout` which I don't even REMEMBER * By [Add functionality to open the Settings UI tab through openSettings by leonMSFT · Pull Request #7802 · microsoft/terminal (github.com)](https://github.com/microsoft/terminal/pull/7802/files#diff-83d260047bed34d3d9d5a12ac62008b65bd6dc5f3b9642905a007c3efce27efd), there was seemingly no FontSizeChanged in Tab.cpp (when it got moved to terminaltab.cpp) > `Pane::Relayout` functionally did nothing because sizing was switched to `star` sizing at some point in the past, so it was just deleted. From [Misc pane refactoring by Rosefield · Pull Request #11373 · microsoft/terminal](https://github.com/microsoft/terminal/pull/11373/files#r736900998) So, great. We can kill part of it, and convert the rest to a `TypedEvent`, and get rid of `DECLARE_` / `DEFINE_`. `ScrollPositionChangedEventArgs` was ALSO apparently already promoted to a typed event, so kill that too. --- src/cascadia/TerminalControl/ControlCore.cpp | 11 ++--- src/cascadia/TerminalControl/ControlCore.h | 4 +- src/cascadia/TerminalControl/ControlCore.idl | 2 +- src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 16 ++++++ src/cascadia/TerminalControl/EventArgs.idl | 10 ++-- src/cascadia/TerminalControl/TermControl.cpp | 17 ++----- src/cascadia/TerminalControl/TermControl.h | 7 +-- src/cascadia/TerminalControl/TermControl.idl | 1 - src/cascadia/inc/cppwinrt_utils.h | 52 -------------------- 10 files changed, 39 insertions(+), 82 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 87e98c0a1b7..2fb25987178 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -241,7 +241,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _setupDispatcherAndCallbacks(); const auto actualNewSize = _actualFont.GetSize(); // Bubble this up, so our new control knows how big we want the font. - _FontSizeChangedHandlers(actualNewSize.width, actualNewSize.height, true); + _FontSizeChangedHandlers(*this, winrt::make(actualNewSize.width, actualNewSize.height)); // The renderer will be re-enabled in Initialize @@ -344,7 +344,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Initialize our font with the renderer // We don't have to care about DPI. We'll get a change message immediately if it's not 96 // and react accordingly. - _updateFont(true); + _updateFont(); const til::size windowSize{ til::math::rounding, windowWidth, windowHeight }; @@ -897,9 +897,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // appropriately call _doResizeUnderLock after this method is called. // - The write lock should be held when calling this method. // Arguments: - // - initialUpdate: whether this font update should be considered as being - // concerned with initialization process. Value forwarded to event handler. - void ControlCore::_updateFont(const bool initialUpdate) + // + void ControlCore::_updateFont() { const auto newDpi = static_cast(lrint(_compositionScale * USER_DEFAULT_SCREEN_DPI)); @@ -947,7 +946,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto actualNewSize = _actualFont.GetSize(); - _FontSizeChangedHandlers(actualNewSize.width, actualNewSize.height, initialUpdate); + _FontSizeChangedHandlers(*this, winrt::make(actualNewSize.width, actualNewSize.height)); } // Method Description: diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 5f9f23a3e89..0851f746056 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -248,7 +248,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // -------------------------------- WinRT Events --------------------------------- // clang-format off - WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); + TYPED_EVENT(FontSizeChanged, IInspectable, Control::FontSizeChangedArgs); TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs); TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs); @@ -340,7 +340,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _setupDispatcherAndCallbacks(); bool _setFontSizeUnderLock(float fontSize); - void _updateFont(const bool initialUpdate = false); + void _updateFont(); void _refreshSizeUnderLock(); void _updateSelectionUI(); bool _shouldTryUpdateSelection(const WORD vkey); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 8157346dcd1..af898ff3d5a 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -165,7 +165,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ShowWindowChanged; // These events are always called from the UI thread (bugs aside) - event FontSizeChangedEventArgs FontSizeChanged; + event Windows.Foundation.TypedEventHandler FontSizeChanged; event Windows.Foundation.TypedEventHandler ScrollPositionChanged; event Windows.Foundation.TypedEventHandler CursorPositionChanged; event Windows.Foundation.TypedEventHandler ConnectionStateChanged; diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 36a9121957b..93e147feaa8 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "EventArgs.h" +#include "FontSizeChangedArgs.g.cpp" #include "TitleChangedEventArgs.g.cpp" #include "CopyToClipboardEventArgs.g.cpp" #include "ContextMenuRequestedEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 20cb222e008..9d3f3e2a3f6 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -3,6 +3,7 @@ #pragma once +#include "FontSizeChangedArgs.g.h" #include "TitleChangedEventArgs.g.h" #include "CopyToClipboardEventArgs.g.h" #include "ContextMenuRequestedEventArgs.g.h" @@ -22,6 +23,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation { + + struct FontSizeChangedArgs : public FontSizeChangedArgsT + { + public: + FontSizeChangedArgs(int32_t width, + int32_t height) : + Width(width), + Height(height) + { + } + + til::property Width; + til::property Height; + }; + struct TitleChangedEventArgs : public TitleChangedEventArgsT { public: diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 2f3c6adfefa..75e1a20211c 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -3,9 +3,6 @@ namespace Microsoft.Terminal.Control { - delegate void FontSizeChangedEventArgs(Int32 width, Int32 height, Boolean isInitialChange); - delegate void ScrollPositionChangedEventArgs(Int32 viewTop, Int32 viewHeight, Int32 bufferLength); - [flags] enum CopyFormat { @@ -14,6 +11,13 @@ namespace Microsoft.Terminal.Control All = 0xffffffff }; + + runtimeclass FontSizeChangedArgs + { + Int32 Width { get; }; + Int32 Height { get; }; + } + runtimeclass CopyToClipboardEventArgs { String Text { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 2bce8ecc0d0..550d13eae30 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3287,16 +3287,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation return posInDIPs + marginsInDips; } - void TermControl::_coreFontSizeChanged(const int fontWidth, - const int fontHeight, - const bool isInitialChange) + void TermControl::_coreFontSizeChanged(const IInspectable& /*sender*/, + const Control::FontSizeChangedArgs& args) { // scale the selection markers to be the size of a cell - auto scaleMarker = [fontWidth, fontHeight, dpiScale{ SwapChainPanel().CompositionScaleX() }](const Windows::UI::Xaml::Shapes::Path& shape) { + auto scaleMarker = [args, dpiScale{ SwapChainPanel().CompositionScaleX() }](const Windows::UI::Xaml::Shapes::Path& shape) { // The selection markers were designed to be 5x14 in size, // so use those dimensions below for the scaling - const auto scaleX = fontWidth / 5.0 / dpiScale; - const auto scaleY = fontHeight / 14.0 / dpiScale; + const auto scaleX = args.Width() / 5.0 / dpiScale; + const auto scaleY = args.Height() / 14.0 / dpiScale; Windows::UI::Xaml::Media::ScaleTransform transform; transform.ScaleX(scaleX); @@ -3308,12 +3307,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation }; scaleMarker(SelectionStartMarker()); scaleMarker(SelectionEndMarker()); - - // Don't try to inspect the core here. The Core is raising this while - // it's holding its write lock. If the handlers calls back to some - // method on the TermControl on the same thread, and that _method_ calls - // to ControlCore, we might be in danger of deadlocking. - _FontSizeChangedHandlers(fontWidth, fontHeight, isInitialChange); } void TermControl::_coreRaisedNotice(const IInspectable& /*sender*/, diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 6b05e7e9a83..895f46128e4 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -160,10 +160,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ITerminalConnection Connection(); void Connection(const TerminalConnection::ITerminalConnection& connection); - WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); // -------------------------------- WinRT Events --------------------------------- // clang-format off - WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); // UNDER NO CIRCUMSTANCES SHOULD YOU ADD A (PROJECTED_)FORWARDED_TYPED_EVENT HERE // Those attach the handler to the core directly, and will explode if @@ -354,9 +353,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args); winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args); - void _coreFontSizeChanged(const int fontWidth, - const int fontHeight, - const bool isInitialChange); + void _coreFontSizeChanged(const IInspectable& s, const Control::FontSizeChangedArgs& args); winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 63495109325..f01a11104ef 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -40,7 +40,6 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Control.IControlSettings Settings { get; }; - event FontSizeChangedEventArgs FontSizeChanged; event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index 142730610c1..7ca1943d4b4 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -17,58 +17,6 @@ Revision History: #pragma once -// This is a helper macro to make declaring events easier. -// This will declare the event handler and the methods for adding and removing a -// handler callback from the event -#define DECLARE_EVENT(name, eventHandler, args) \ -public: \ - winrt::event_token name(const args& handler); \ - void name(const winrt::event_token& token) noexcept; \ - \ -protected: \ - winrt::event eventHandler; - -// This is a helper macro for defining the body of events. -// Winrt events need a method for adding a callback to the event and removing -// the callback. This macro will define them both for you, because they -// don't really vary from event to event. -#define DEFINE_EVENT(className, name, eventHandler, args) \ - winrt::event_token className::name(const args& handler) \ - { \ - return eventHandler.add(handler); \ - } \ - void className::name(const winrt::event_token& token) noexcept \ - { \ - eventHandler.remove(token); \ - } - -// This is a helper macro to make declaring events easier. -// This will declare the event handler and the methods for adding and removing a -// handler callback from the event. -// Use this if you have a Windows.Foundation.TypedEventHandler -#define DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(name, eventHandler, sender, args) \ -public: \ - winrt::event_token name(const Windows::Foundation::TypedEventHandler& handler); \ - void name(const winrt::event_token& token) noexcept; \ - \ -private: \ - winrt::event> eventHandler; - -// This is a helper macro for defining the body of events. -// Winrt events need a method for adding a callback to the event and removing -// the callback. This macro will define them both for you, because they -// don't really vary from event to event. -// Use this if you have a Windows.Foundation.TypedEventHandler -#define DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(className, name, eventHandler, sender, args) \ - winrt::event_token className::name(const Windows::Foundation::TypedEventHandler& handler) \ - { \ - return eventHandler.add(handler); \ - } \ - void className::name(const winrt::event_token& token) noexcept \ - { \ - eventHandler.remove(token); \ - } - // 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 From 5651f087702d97f8c5e48db6cd4199aed8a04479 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Thu, 24 Aug 2023 17:15:54 -0500 Subject: [PATCH 40/59] Add a Nightly build pipeline for the Canary branding (#15869) To make this happen, I moved most of `release.yml` into a shared _pipeline_ template (which is larger than a steps or jobs template). Most of the diffs are due to that move. If you compare main:build/pipelines/release.yml against dev/duhowett/nightly-build:build/pipelines/templates-v2/pipeline-full-release-build.yml, you will see that the changes are much more minimal than they look. I also added a parameter to configure how long symbols will be kept. It defaults to 36530 days (which is the default for the PublishSymbols task! Yes, 100 years!) but nightly builds will get 15 days. --- build/pipelines/nightly.yml | 23 ++ build/pipelines/release.yml | 154 ++------------ .../templates-v2/job-build-project.yml | 1 - .../templates-v2/job-publish-symbols.yml | 5 + .../pipeline-full-release-build.yml | 196 ++++++++++++++++++ 5 files changed, 240 insertions(+), 139 deletions(-) create mode 100644 build/pipelines/nightly.yml create mode 100644 build/pipelines/templates-v2/pipeline-full-release-build.yml diff --git a/build/pipelines/nightly.yml b/build/pipelines/nightly.yml new file mode 100644 index 00000000000..025a643509f --- /dev/null +++ b/build/pipelines/nightly.yml @@ -0,0 +1,23 @@ +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/release.yml b/build/pipelines/release.yml index c177634ba64..e016270e87f 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -1,11 +1,7 @@ -# This build should never run as CI or against a pull request. trigger: none pr: none -pool: - name: SHINE-INT-S # By default, send jobs to the small agent pool. - demands: ImageOverride -equals SHINE-VS17-Latest - +# Expose all of these parameters for user configuration. parameters: - name: branding displayName: "Branding (Build Type)" @@ -70,138 +66,20 @@ parameters: type: boolean default: false -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 - name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) -resources: - repositories: - - repository: self - type: git - ref: main - -stages: - - stage: Build - displayName: Build - dependsOn: [] - jobs: - - template: ./templates-v2/job-build-project.yml - parameters: - pool: - name: SHINE-INT-L # Run the compilation on the large agent pool, rather than the default small one. - demands: ImageOverride -equals SHINE-VS17-Latest - branding: ${{ parameters.branding }} - buildTerminal: ${{ parameters.buildTerminal }} - buildConPTY: ${{ parameters.buildConPTY }} - buildWPF: ${{ parameters.buildWPF }} - pgoBuildMode: ${{ parameters.pgoBuildMode }} - buildConfigurations: ${{ parameters.buildConfigurations }} - buildPlatforms: ${{ parameters.buildPlatforms }} - generateSbom: ${{ parameters.generateSbom }} - 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: ./templates-v2/steps-fetch-and-prepare-localizations.yml - parameters: - includePseudoLoc: true - - - ${{ if eq(parameters.buildWPF, true) }}: - # Add an Any CPU build flavor for the WPF control bits - - template: ./templates-v2/job-build-project.yml - parameters: - # This job is allowed to run on the default small pool. - jobName: BuildWPF - branding: ${{ parameters.branding }} - buildTerminal: false - buildWPFDotNetComponents: true - buildConfigurations: ${{ parameters.buildConfigurations }} - buildPlatforms: - - Any CPU - generateSbom: ${{ parameters.generateSbom }} - 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: ./templates-v2/job-merge-msix-into-bundle.yml - parameters: - jobName: Bundle - branding: ${{ parameters.branding }} - buildConfigurations: ${{ parameters.buildConfigurations }} - buildPlatforms: ${{ parameters.buildPlatforms }} - generateSbom: ${{ parameters.generateSbom }} - codeSign: ${{ parameters.codeSign }} - - - ${{ if eq(parameters.buildConPTY, true) }}: - - template: ./templates-v2/job-package-conpty.yml - parameters: - buildConfigurations: ${{ parameters.buildConfigurations }} - buildPlatforms: ${{ parameters.buildPlatforms }} - generateSbom: ${{ parameters.generateSbom }} - codeSign: ${{ parameters.codeSign }} - - - ${{ if eq(parameters.buildWPF, true) }}: - - template: ./templates-v2/job-build-package-wpf.yml - parameters: - buildConfigurations: ${{ parameters.buildConfigurations }} - buildPlatforms: ${{ parameters.buildPlatforms }} - generateSbom: ${{ parameters.generateSbom }} - codeSign: ${{ parameters.codeSign }} - - - stage: Publish - displayName: Publish - dependsOn: [Build, Package] - jobs: - # We only support the vpack for Release builds that include Terminal - - ${{ if and(containsValue(parameters.buildConfigurations, 'Release'), parameters.buildTerminal, parameters.publishVpackToWindows) }}: - - template: ./templates-v2/job-submit-windows-vpack.yml - parameters: - buildConfiguration: Release - generateSbom: ${{ parameters.generateSbom }} - - - template: ./templates-v2/job-publish-symbols.yml - parameters: - includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} - -... +extends: + template: templates-v2/pipeline-full-release-build.yml + parameters: + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + codeSign: ${{ parameters.codeSign }} + generateSbom: ${{ parameters.generateSbom }} + terminalInternalPackageVersion: ${{ parameters.terminalInternalPackageVersion }} + publishSymbolsToPublic: ${{ parameters.publishSymbolsToPublic }} + publishVpackToWindows: ${{ parameters.publishVpackToWindows }} diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index 5dcc8d50cf8..eda45842bbe 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -2,7 +2,6 @@ parameters: - name: branding type: string default: Dev - values: [Release, Preview, Dev] - name: additionalBuildOptions type: string default: '' diff --git a/build/pipelines/templates-v2/job-publish-symbols.yml b/build/pipelines/templates-v2/job-publish-symbols.yml index 6c37f0f4cbd..65663885602 100644 --- a/build/pipelines/templates-v2/job-publish-symbols.yml +++ b/build/pipelines/templates-v2/job-publish-symbols.yml @@ -14,6 +14,9 @@ parameters: - name: jobName type: string default: PublishSymbols + - name: symbolExpiryTime + type: string + default: 36530 # This is the default from PublishSymbols@2 jobs: - job: ${{ parameters.jobName }} @@ -55,6 +58,7 @@ jobs: SymbolServerType: 'TeamServices' SymbolsProduct: 'Windows Terminal Converged Symbols' SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' + SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} env: LIB: $(Build.SourcesDirectory) @@ -71,6 +75,7 @@ jobs: SymbolServerType: 'TeamServices' SymbolsProduct: 'Windows Terminal Converged Symbols' SymbolsVersion: '$(XES_APPXMANIFESTVERSION)' + SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} # 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. diff --git a/build/pipelines/templates-v2/pipeline-full-release-build.yml b/build/pipelines/templates-v2/pipeline-full-release-build.yml new file mode 100644 index 00000000000..42c5c6250d8 --- /dev/null +++ b/build/pipelines/templates-v2/pipeline-full-release-build.yml @@ -0,0 +1,196 @@ +# This build should never run as CI or against a pull request. +trigger: none + +parameters: + - name: branding + type: string + default: Release + - 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: generateSbom + 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: pool + type: object + default: + name: SHINE-INT-S # By default, send jobs to the small agent pool. + 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 + +resources: + repositories: + - repository: self + type: git + ref: main + +stages: + - stage: Build + displayName: Build + pool: ${{ parameters.pool }} + dependsOn: [] + jobs: + - template: ./job-build-project.yml + parameters: + pool: + name: SHINE-INT-L # Run the compilation on the large agent pool, rather than the default small one. + demands: ImageOverride -equals SHINE-VS17-Latest + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + 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: ./steps-fetch-and-prepare-localizations.yml + parameters: + includePseudoLoc: true + + - ${{ if eq(parameters.buildWPF, true) }}: + # Add an Any CPU build flavor for the WPF control bits + - template: ./job-build-project.yml + parameters: + # This job is allowed to run on the default small pool. + jobName: BuildWPF + branding: ${{ parameters.branding }} + buildTerminal: false + buildWPFDotNetComponents: true + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: + - Any CPU + generateSbom: ${{ parameters.generateSbom }} + 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 + pool: ${{ parameters.pool }} + dependsOn: [Build] + jobs: + - ${{ if eq(parameters.buildTerminal, true) }}: + - template: ./job-merge-msix-into-bundle.yml + parameters: + jobName: Bundle + branding: ${{ parameters.branding }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildConPTY, true) }}: + - template: ./job-package-conpty.yml + parameters: + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildWPF, true) }}: + - template: ./job-build-package-wpf.yml + parameters: + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: ${{ parameters.generateSbom }} + codeSign: ${{ parameters.codeSign }} + + - stage: Publish + displayName: Publish + pool: ${{ parameters.pool }} + dependsOn: [Build, Package] + jobs: + # We only support the vpack for Release builds that include Terminal + - ${{ if and(containsValue(parameters.buildConfigurations, 'Release'), parameters.buildTerminal, parameters.publishVpackToWindows) }}: + - template: ./job-submit-windows-vpack.yml + parameters: + buildConfiguration: Release + generateSbom: ${{ parameters.generateSbom }} + + - template: ./job-publish-symbols.yml + parameters: + includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} + symbolExpiryTime: ${{ parameters.symbolExpiryTime }} + +... From cd80f3c76496c7664ed710907d7d473f634191bc Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 25 Aug 2023 00:56:40 +0200 Subject: [PATCH 41/59] Use ICU for text search (#15858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ultimate goal of this PR was to use ICU for text search to * Improve Unicode support Previously we used `towlower` and only supported BMP glphs. * Improve search performance (10-100x) This allows us to search for all results in the entire text buffer at once without having to do so asynchronously. Unfortunately, this required some significant changes too: * ICU's search facilities operate on text positions which we need to be mapped back to buffer coordinates. This required the introduction of `CharToColumnMapper` to implement sort of a reverse-`_charOffsets` mapping. It turns text (character) positions back into coordinates. * Previously search restarted every time you clicked the search button. It used the current selection as the starting position for the new search. But since ICU's `uregex` cannot search backwards we're required to accumulate all results in a vector first and so we need to cache that vector in between searches. * We need to know when the cached vector became invalid and so we have to track any changes made to `TextBuffer`. The way this commit solves it is by splitting `GetRowByOffset` into `GetRowByOffset` for `const ROW` access and `GetMutableRowByOffset` which increments a mutation counter on each call. The `Search` instance can then compare its cached mutation count against the previous mutation count. Finally, this commit makes 2 semi-unrelated changes: * URL search now also uses ICU, since it's closely related to regular text search anyways. This significantly improves performance at large window sizes. * A few minor issues in `UiaTracing` were fixed. In particular 2 functions which passed strings as `wstring` by copy are now using `wstring_view` and `TraceLoggingCountedWideString`. Related to #6319 and #8000 ## Validation Steps Performed * Search upward/downward in conhost ✅ * Search upward/downward in WT ✅ * Searching for any of ß, ẞ, ss or SS matches any of the other ✅ * Searching for any of Σ, σ, or ς matches any of the other ✅ --- .github/actions/spelling/allow/apis.txt | 2 + .github/actions/spelling/expect/expect.txt | 56 +-- src/buffer/out/Row.cpp | 210 ++++++++--- src/buffer/out/Row.hpp | 44 ++- src/buffer/out/UTextAdapter.cpp | 329 +++++++++++++++++ src/buffer/out/UTextAdapter.h | 17 + src/buffer/out/lib/bufferout.vcxproj | 2 + src/buffer/out/search.cpp | 339 +++--------------- src/buffer/out/search.h | 72 +--- src/buffer/out/sources.inc | 3 +- src/buffer/out/textBuffer.cpp | 291 +++++---------- src/buffer/out/textBuffer.hpp | 21 +- src/buffer/out/ut_textbuffer/ReflowTests.cpp | 2 +- src/cascadia/TerminalControl/ControlCore.cpp | 38 +- src/cascadia/TerminalControl/ControlCore.h | 7 +- src/cascadia/TerminalControl/ControlCore.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 1 + src/cascadia/TerminalCore/Terminal.cpp | 134 ++++++- src/cascadia/TerminalCore/Terminal.hpp | 3 +- src/cascadia/TerminalCore/TerminalApi.cpp | 2 - .../TerminalCore/TerminalSelection.cpp | 15 +- src/host/_stream.cpp | 4 +- src/host/renderData.cpp | 13 - src/host/renderData.hpp | 1 - src/host/selection.cpp | 18 +- src/host/selectionInput.cpp | 7 +- src/host/telemetry.cpp | 38 -- src/host/telemetry.hpp | 6 - src/host/ut_host/ScreenBufferTests.cpp | 2 +- src/host/ut_host/SearchTests.cpp | 90 ++--- src/host/ut_host/TextBufferTests.cpp | 36 +- src/host/ut_host/VtIoTests.cpp | 4 - src/inc/test/CommonState.hpp | 2 +- src/inc/til/at.h | 1 + src/interactivity/win32/find.cpp | 55 ++- .../UiaTextRangeTests.cpp | 4 +- src/renderer/inc/IRenderData.hpp | 1 - src/terminal/adapter/adaptDispatch.cpp | 8 +- src/types/UiaTextRangeBase.cpp | 58 ++- src/types/UiaTextRangeBase.hpp | 14 +- src/types/UiaTracing.cpp | 20 +- src/types/UiaTracing.h | 16 +- 42 files changed, 1035 insertions(+), 952 deletions(-) create mode 100644 src/buffer/out/UTextAdapter.cpp create mode 100644 src/buffer/out/UTextAdapter.h diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 08b1c6bcadb..71b2922ca03 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -52,6 +52,7 @@ futex GETDESKWALLPAPER GETHIGHCONTRAST GETMOUSEHOVERTIME +GETTEXTLENGTH Hashtable HIGHCONTRASTON HIGHCONTRASTW @@ -186,6 +187,7 @@ snprintf spsc sregex SRWLOC +srwlock SRWLOCK STDCPP STDMETHOD diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 626777f6bdb..2e552912cfa 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -7,7 +7,6 @@ ABCF abgr abi ABORTIFHUNG -ACCESSTOKEN acidev ACIOSS ACover @@ -117,10 +116,8 @@ binplace binplaced bitcoin bitcrazed -bitflag bitmask BITOPERATION -bitsets BKCOLOR BKGND Bksp @@ -149,12 +146,12 @@ bufferout buffersize buflen buildtransitive -BUILDURI burriter BValue bytebuffer cac cacafire +CALLCONV capslock CARETBLINKINGENABLED CARRIAGERETURN @@ -198,7 +195,6 @@ CHT Cic cielab Cielab -Clcompile CLE cleartype CLICKACTIVE @@ -229,7 +225,6 @@ codepage codepath codepoints coinit -COLLECTIONURI colorizing COLORMATRIX COLORREFs @@ -307,7 +302,6 @@ coordnew COPYCOLOR CORESYSTEM cotaskmem -countof CPG cpinfo CPINFOEX @@ -315,7 +309,6 @@ CPLINFO cplusplus CPPCORECHECK cppcorecheckrules -cpprest cpprestsdk cppwinrt CProc @@ -382,7 +375,6 @@ dai DATABLOCK DBatch dbcs -DBCSCHAR DBCSFONT dbg DBGALL @@ -504,7 +496,6 @@ devicecode Dext DFactory DFF -dhandler dialogbox directio DIRECTX @@ -522,7 +513,6 @@ dllmain DLLVERSIONINFO DLOAD DLOOK -dmp DONTCARE doskey dotnet @@ -600,7 +590,6 @@ eplace EPres EQU ERASEBKGND -etcoreapp ETW EUDC EVENTID @@ -642,7 +631,6 @@ FGs FILEDESCRIPTION FILESUBTYPE FILESYSPATH -fileurl FILEW FILLATTR FILLCONSOLEOUTPUT @@ -824,7 +812,6 @@ HIWORD HKCU hkey hkl -HKLM hlocal hlsl HMB @@ -832,7 +819,6 @@ HMK hmod hmodule hmon -homeglyphs homoglyph HORZ hostable @@ -920,6 +906,7 @@ INSERTMODE INTERACTIVITYBASE INTERCEPTCOPYPASTE INTERNALNAME +Interner intsafe INVALIDARG INVALIDATERECT @@ -941,7 +928,6 @@ IUI IUnknown ivalid IWIC -IXMP IXP jconcpp JOBOBJECT @@ -965,7 +951,6 @@ kernelbasestaging KEYBDINPUT keychord keydown -keyevent KEYFIRST KEYLAST Keymapping @@ -1012,7 +997,6 @@ LINEWRAP LINKERRCAP LINKERROR linputfile -listproperties listptr listptrsize lld @@ -1131,7 +1115,6 @@ MIIM milli mincore mindbogglingly -minimizeall minkernel MINMAXINFO minwin @@ -1318,7 +1301,7 @@ onecoreuuid ONECOREWINDOWS onehalf oneseq -ONLCR +OOM openbash opencode opencon @@ -1328,13 +1311,6 @@ openps openvt ORIGINALFILENAME osc -OSCBG -OSCCT -OSCFG -OSCRCC -OSCSCB -OSCSCC -OSCWT OSDEPENDSROOT OSG OSGENG @@ -1453,7 +1429,6 @@ PPEB ppf ppguid ppidl -pplx PPROC ppropvar ppsi @@ -1467,8 +1442,8 @@ prc prealigned prect prefast +preflighting prefs -preinstalled prepopulated presorted PREVENTPINNING @@ -1481,7 +1456,6 @@ prioritization processenv processhost PROCESSINFOCLASS -procs PROPERTYID PROPERTYKEY PROPERTYVAL @@ -1496,7 +1470,6 @@ propvariant propvarutil psa PSECURITY -pseudocode pseudoconsole pseudoterminal psh @@ -1776,7 +1749,6 @@ SND SOLIDBOX Solutiondir somefile -SOURCEBRANCH sourced spammy SRCCODEPAGE @@ -1828,7 +1800,6 @@ SUBLANG subresource subsystemconsole subsystemwindows -suiteless swapchain swapchainpanel swappable @@ -1873,7 +1844,6 @@ tcommands Tdd TDelegated TDP -TEAMPROJECT tearoff Teb Techo @@ -1885,23 +1855,18 @@ terminalrenderdata TERMINALSCROLLING terminfo TEs -testbuildplatform testcon testd -testdlls testenv testlab testlist testmd -testmode testname -testnameprefix TESTNULL testpass testpasses testtestabc testtesttesttesttest -testtimeout TEXCOORD texel TExpected @@ -1929,7 +1894,6 @@ TJson TLambda TLDP TLEN -Tlgdata TMAE TMPF TMult @@ -1989,11 +1953,14 @@ UAC uap uapadmin UAX +UBool ucd uch +UChars udk UDM uer +UError uget uia UIACCESS @@ -2023,13 +1990,14 @@ unknwn UNORM unparseable unregistering -untests untextured untimes UPDATEDISPLAY UPDOWN UPKEY UPSS +uregex +URegular usebackq USECALLBACK USECOLOR @@ -2051,6 +2019,9 @@ USESIZE USESTDHANDLES usp USRDLL +utext +UText +UTEXT utr UVWX UVWXY @@ -2134,7 +2105,6 @@ WDDMCONSOLECONTEXT wdm webpage websites -websockets wekyb wex wextest @@ -2162,7 +2132,6 @@ windbg WINDEF windll WINDOWALPHA -Windowbuffer windowdpiapi WINDOWEDGE windowext @@ -2306,7 +2275,6 @@ xunit xutr XVIRTUALSCREEN XWalk -xxyyzz yact YCast YCENTER diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 760f8cf501f..6c1c136e947 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -10,8 +10,19 @@ #include "textBuffer.hpp" #include "../../types/inc/GlyphWidth.hpp" +// It would be nice to add checked array access in the future, but it's a little annoying to do so without impacting +// performance (including Debug performance). Other languages are a little bit more ergonomic there than C++. +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). + extern "C" int __isa_available; +constexpr auto clamp(auto value, auto lo, auto hi) +{ + return value < lo ? lo : (value > hi ? hi : value); +} + // The STL is missing a std::iota_n analogue for std::iota, so I made my own. template constexpr OutIt iota_n(OutIt dest, Diff count, T val) @@ -71,6 +82,86 @@ constexpr OutIt copy_n_small(InIt first, Diff count, OutIt dest) return dest; } +CharToColumnMapper::CharToColumnMapper(const wchar_t* chars, const uint16_t* charOffsets, ptrdiff_t lastCharOffset, til::CoordType currentColumn) noexcept : + _chars{ chars }, + _charOffsets{ charOffsets }, + _lastCharOffset{ lastCharOffset }, + _currentColumn{ currentColumn } +{ +} + +// If given a position (`offset`) inside the ROW's text, this function will return the corresponding column. +// This function in particular returns the glyph's first column. +til::CoordType CharToColumnMapper::GetLeadingColumnAt(ptrdiff_t offset) noexcept +{ + offset = clamp(offset, 0, _lastCharOffset); + + auto col = _currentColumn; + const auto currentOffset = _charOffsets[col]; + + // Goal: Move the _currentColumn cursor to a cell which contains the given target offset. + // Depending on where the target offset is we have to either search forward or backward. + if (offset < currentOffset) + { + // Backward search. + // Goal: Find the first preceding column where the offset is <= the target offset. This results in the first + // cell that contains our target offset, even if that offset is in the middle of a long grapheme. + // + // We abuse the fact that the trailing half of wide glyphs is marked with CharOffsetsTrailer to our advantage. + // Since they're >0x8000, the `offset < _charOffsets[col]` check will always be true and ensure we iterate over them. + // + // Since _charOffsets cannot contain negative values and because offset has been + // clamped to be positive we naturally exit when reaching the first column. + for (; offset < _charOffsets[col - 1]; --col) + { + } + } + else if (offset > currentOffset) + { + // Forward search. + // Goal: Find the first subsequent column where the offset is > the target offset. + // We stop 1 column before that however so that the next loop works correctly. + // It's the inverse of the loop above. + // + // Since offset has been clamped to be at most 1 less than the maximum + // _charOffsets value the loop naturally exits before hitting the end. + for (; offset >= (_charOffsets[col + 1] & CharOffsetsMask); ++col) + { + } + // Now that we found the cell that definitely includes this char offset, + // we have to iterate back to the cell's starting column. + for (; WI_IsFlagSet(_charOffsets[col], CharOffsetsTrailer); --col) + { + } + } + + _currentColumn = col; + return col; +} + +// If given a position (`offset`) inside the ROW's text, 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(ptrdiff_t offset) noexcept +{ + auto col = GetLeadingColumnAt(offset); + // This loop is a little redundant with the forward search loop in GetLeadingColumnAt() + // but it's realistically not worth caring about this. This code is not a bottleneck. + for (; WI_IsFlagSet(_charOffsets[col + 1], CharOffsetsTrailer); ++col) + { + } + return col; +} + +til::CoordType CharToColumnMapper::GetLeadingColumnAt(const wchar_t* str) noexcept +{ + return GetLeadingColumnAt(str - _chars); +} + +til::CoordType CharToColumnMapper::GetTrailingColumnAt(const wchar_t* str) noexcept +{ + return GetTrailingColumnAt(str - _chars); +} + // Routine Description: // - constructor // Arguments: @@ -118,10 +209,17 @@ LineRendition ROW::GetLineRendition() const noexcept return _lineRendition; } -uint16_t ROW::GetLineWidth() const noexcept +// Returns the index 1 past the last (technically) valid column in the row. +// The interplay between the old console and newer VT APIs which support line renditions is +// still unclear so it might be necessary to add two kinds of this function in the future. +// Console APIs treat the buffer as a large NxM matrix after all. +til::CoordType ROW::GetReadableColumnCount() const noexcept { - const auto scale = _lineRendition != LineRendition::SingleWidth ? 1 : 0; - return _columnCount >> scale; + if (_lineRendition == LineRendition::SingleWidth) [[likely]] + { + return _columnCount - _doubleBytePadded; + } + return (_columnCount - (_doubleBytePadded << 1)) >> 1; } // Routine Description: @@ -287,26 +385,6 @@ til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept return _adjustForward(_clampedColumn(column + 1)); } -uint16_t ROW::_adjustBackward(uint16_t column) const noexcept -{ - // Safety: This is a little bit more dangerous. The first column is supposed - // to never be a trailer and so this loop should exit if column == 0. - for (; _uncheckedIsTrailer(column); --column) - { - } - return column; -} - -uint16_t ROW::_adjustForward(uint16_t column) const noexcept -{ - // Safety: This is a little bit more dangerous. The last column is supposed - // to never be a trailer and so this loop should exit if column == _columnCount. - for (; _uncheckedIsTrailer(column); ++column) - { - } - return column; -} - // Routine Description: // - clears char data in column in row // Arguments: @@ -841,12 +919,6 @@ uint16_t ROW::size() const noexcept return _columnCount; } -til::CoordType ROW::LineRenditionColumns() const noexcept -{ - const auto scale = _lineRendition != LineRendition::SingleWidth ? 1 : 0; - return _columnCount >> scale; -} - til::CoordType ROW::MeasureLeft() const noexcept { const auto text = GetText(); @@ -945,20 +1017,31 @@ DbcsAttribute ROW::DbcsAttrAt(til::CoordType column) const noexcept std::wstring_view ROW::GetText() const noexcept { - return { _chars.data(), _charSize() }; + const auto width = size_t{ til::at(_charOffsets, GetReadableColumnCount()) } & CharOffsetsMask; + return { _chars.data(), width }; } std::wstring_view ROW::GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept { const til::CoordType columns = _columnCount; - const auto colBeg = std::max(0, std::min(columns, columnBegin)); - const auto colEnd = std::max(colBeg, std::min(columns, columnEnd)); + const auto colBeg = clamp(columnBegin, 0, columns); + const auto colEnd = clamp(columnEnd, colBeg, columns); const size_t chBeg = _uncheckedCharOffset(gsl::narrow_cast(colBeg)); const size_t chEnd = _uncheckedCharOffset(gsl::narrow_cast(colEnd)); #pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). return { _chars.data() + chBeg, chEnd - chBeg }; } +til::CoordType ROW::GetLeadingColumnAtCharOffset(const ptrdiff_t offset) const noexcept +{ + return _createCharToColumnMapper(offset).GetLeadingColumnAt(offset); +} + +til::CoordType ROW::GetTrailingColumnAtCharOffset(const ptrdiff_t offset) const noexcept +{ + return _createCharToColumnMapper(offset).GetTrailingColumnAt(offset); +} + DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept { const auto col = _clampedColumn(column); @@ -982,43 +1065,80 @@ DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_v template constexpr uint16_t ROW::_clampedUint16(T v) noexcept { - return static_cast(std::max(T{ 0 }, std::min(T{ 65535 }, v))); + return static_cast(clamp(v, 0, 65535)); } template constexpr uint16_t ROW::_clampedColumn(T v) const noexcept { - return static_cast(std::max(T{ 0 }, std::min(_columnCount - 1u, v))); + return static_cast(clamp(v, 0, _columnCount - 1)); } template constexpr uint16_t ROW::_clampedColumnInclusive(T v) const noexcept { - return static_cast(std::max(T{ 0 }, std::min(_columnCount, v))); + return static_cast(clamp(v, 0, _columnCount)); } -// Safety: off must be [0, _charSize()]. -wchar_t ROW::_uncheckedChar(size_t off) const noexcept +uint16_t ROW::_charSize() const noexcept { - return til::at(_chars, off); + // Safety: _charOffsets is an array of `_columnCount + 1` entries. + return _charOffsets[_columnCount]; } -uint16_t ROW::_charSize() const noexcept +// Safety: off must be [0, _charSize()]. +template +wchar_t ROW::_uncheckedChar(T off) const noexcept { - // Safety: _charOffsets is an array of `_columnCount + 1` entries. - return til::at(_charOffsets, _columnCount); + return _chars[off]; } // Safety: col must be [0, _columnCount]. -uint16_t ROW::_uncheckedCharOffset(size_t col) const noexcept +template +uint16_t ROW::_uncheckedCharOffset(T col) const noexcept { assert(col < _charOffsets.size()); - return til::at(_charOffsets, col) & CharOffsetsMask; + return _charOffsets[col] & CharOffsetsMask; } // Safety: col must be [0, _columnCount]. -bool ROW::_uncheckedIsTrailer(size_t col) const noexcept +template +bool ROW::_uncheckedIsTrailer(T col) const noexcept { assert(col < _charOffsets.size()); - return WI_IsFlagSet(til::at(_charOffsets, col), CharOffsetsTrailer); + return WI_IsFlagSet(_charOffsets[col], CharOffsetsTrailer); +} + +template +T ROW::_adjustBackward(T column) const noexcept +{ + // Safety: This is a little bit more dangerous. The first column is supposed + // to never be a trailer and so this loop should exit if column == 0. + for (; _uncheckedIsTrailer(column); --column) + { + } + return column; +} + +template +T ROW::_adjustForward(T column) const noexcept +{ + // Safety: This is a little bit more dangerous. The last column is supposed + // to never be a trailer and so this loop should exit if column == _columnCount. + for (; _uncheckedIsTrailer(column); ++column) + { + } + return column; +} + +// Creates a CharToColumnMapper given an offset into _chars.data(). +// In other words, for a 120 column ROW with just ASCII text, the offset should be [0,120). +CharToColumnMapper ROW::_createCharToColumnMapper(ptrdiff_t offset) const noexcept +{ + const auto charsSize = _charSize(); + const auto lastChar = gsl::narrow_cast(charsSize - 1); + // We can sort of guess what column belongs to what offset because BMP glyphs are very common and + // UTF-16 stores them in 1 char. In other words, usually a ROW will have N chars for N columns. + const auto guessedColumn = gsl::narrow_cast(clamp(offset, 0, _columnCount)); + return CharToColumnMapper{ _chars.data(), _charOffsets.data(), lastChar, guessedColumn }; } diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index c19f343f235..586aa12b95b 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -65,6 +65,28 @@ struct RowCopyTextFromState til::CoordType sourceColumnEnd = 0; // OUT }; +// This structure is basically an inverse of ROW::_charOffsets. If you have a pointer +// into a ROW's text this class can tell you what cell that pointer belongs to. +struct CharToColumnMapper +{ + CharToColumnMapper(const wchar_t* chars, const uint16_t* charOffsets, ptrdiff_t lastCharOffset, til::CoordType currentColumn) noexcept; + + til::CoordType GetLeadingColumnAt(ptrdiff_t offset) noexcept; + til::CoordType GetTrailingColumnAt(ptrdiff_t offset) noexcept; + til::CoordType GetLeadingColumnAt(const wchar_t* str) noexcept; + til::CoordType GetTrailingColumnAt(const wchar_t* str) noexcept; + +private: + // See ROW and its members with identical name. + static constexpr uint16_t CharOffsetsTrailer = 0x8000; + static constexpr uint16_t CharOffsetsMask = 0x7fff; + + const wchar_t* _chars; + const uint16_t* _charOffsets; + ptrdiff_t _lastCharOffset; + til::CoordType _currentColumn; +}; + class ROW final { public: @@ -106,7 +128,7 @@ class ROW final bool WasDoubleBytePadded() const noexcept; void SetLineRendition(const LineRendition lineRendition) noexcept; LineRendition GetLineRendition() const noexcept; - uint16_t GetLineWidth() const noexcept; + til::CoordType GetReadableColumnCount() const noexcept; void Reset(const TextAttribute& attr) noexcept; void TransferAttributes(const til::small_rle& attr, til::CoordType newWidth); @@ -128,7 +150,6 @@ class ROW final TextAttribute GetAttrByColumn(til::CoordType column) const; std::vector GetHyperlinks() const; uint16_t size() const noexcept; - til::CoordType LineRenditionColumns() const noexcept; til::CoordType MeasureLeft() const noexcept; til::CoordType MeasureRight() const noexcept; bool ContainsText() const noexcept; @@ -136,6 +157,8 @@ class ROW final DbcsAttribute DbcsAttrAt(til::CoordType column) const noexcept; std::wstring_view GetText() const noexcept; std::wstring_view GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept; + til::CoordType GetLeadingColumnAtCharOffset(ptrdiff_t offset) const noexcept; + til::CoordType GetTrailingColumnAtCharOffset(ptrdiff_t offset) const noexcept; DelimiterClass DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept; auto AttrBegin() const noexcept { return _attr.begin(); } @@ -206,16 +229,21 @@ class ROW final template constexpr uint16_t _clampedColumnInclusive(T v) const noexcept; - uint16_t _adjustBackward(uint16_t column) const noexcept; - uint16_t _adjustForward(uint16_t column) const noexcept; - - wchar_t _uncheckedChar(size_t off) const noexcept; uint16_t _charSize() const noexcept; - uint16_t _uncheckedCharOffset(size_t col) const noexcept; - bool _uncheckedIsTrailer(size_t col) const noexcept; + template + wchar_t _uncheckedChar(T off) const noexcept; + template + uint16_t _uncheckedCharOffset(T col) const noexcept; + template + bool _uncheckedIsTrailer(T col) const noexcept; + template + T _adjustBackward(T column) const noexcept; + template + T _adjustForward(T column) const noexcept; void _init() noexcept; void _resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDirty, uint16_t chEndDirtyOld); + CharToColumnMapper _createCharToColumnMapper(ptrdiff_t offset) const noexcept; // These fields are a bit "wasteful", but it makes all this a bit more robust against // programming errors during initial development (which is when this comment was written). diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp new file mode 100644 index 00000000000..7dec8d2dc04 --- /dev/null +++ b/src/buffer/out/UTextAdapter.cpp @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "UTextAdapter.h" + +#include "textBuffer.hpp" + +struct RowRange +{ + til::CoordType begin; + til::CoordType end; +}; + +constexpr size_t& accessLength(UText* ut) noexcept +{ + return *std::bit_cast(&ut->p); +} + +constexpr RowRange& accessRowRange(UText* ut) noexcept +{ + return *std::bit_cast(&ut->a); +} + +constexpr til::CoordType& accessCurrentRow(UText* ut) noexcept +{ + return ut->b; +} + +// An excerpt from the ICU documentation: +// +// Clone a UText. Much like opening a UText where the source text is itself another UText. +// +// A shallow clone replicates only the UText data structures; it does not make +// a copy of the underlying text. Shallow clones can be used as an efficient way to +// have multiple iterators active in a single text string that is not being modified. +// +// A shallow clone operation must not fail except for truly exceptional conditions such +// as memory allocation failures. +// +// @param dest A UText struct to be filled in with the result of the clone operation, +// or NULL if the clone function should heap-allocate a new UText struct. +// @param src The UText to be cloned. +// @param deep true to request a deep clone, false for a shallow clone. +// @param status Errors are returned here. For deep clones, U_UNSUPPORTED_ERROR should +// be returned if the text provider is unable to clone the original text. +// @return The newly created clone, or NULL if the clone operation failed. +static UText* U_CALLCONV utextClone(UText* dest, const UText* src, UBool deep, UErrorCode* status) noexcept +{ + __assume(status != nullptr); + + if (deep) + { + *status = U_UNSUPPORTED_ERROR; + return dest; + } + + dest = utext_setup(dest, 0, status); + if (*status <= U_ZERO_ERROR) + { + memcpy(dest, src, sizeof(UText)); + } + + return dest; +} + +// An excerpt from the ICU documentation: +// +// Gets the length of the text. +// +// @param ut the UText to get the length of. +// @return the length, in the native units of the original text string. +static int64_t U_CALLCONV utextNativeLength(UText* ut) noexcept +try +{ + auto length = accessLength(ut); + + if (!length) + { + const auto& textBuffer = *static_cast(ut->context); + const auto range = accessRowRange(ut); + + for (til::CoordType y = range.begin; y < range.end; ++y) + { + length += textBuffer.GetRowByOffset(y).GetText().size(); + } + + accessLength(ut) = length; + } + + return gsl::narrow_cast(length); +} +catch (...) +{ + return 0; +} + +// An excerpt from the ICU documentation: +// +// Get the description of the text chunk containing the text at a requested native index. +// The UText's iteration position will be left at the requested index. +// If the index is out of bounds, the iteration position will be left +// at the start or end of the string, as appropriate. +// +// @param ut the UText being accessed. +// @param nativeIndex Requested index of the text to be accessed. +// @param forward If true, then the returned chunk must contain text starting from the index, so that start<=index(ut->context); + const auto range = accessRowRange(ut); + auto start = ut->chunkNativeStart; + auto limit = ut->chunkNativeLimit; + auto y = accessCurrentRow(ut); + std::wstring_view text; + + if (neededIndex < start || neededIndex >= limit) + { + if (neededIndex < start) + { + do + { + --y; + if (y < range.begin) + { + return false; + } + + text = textBuffer.GetRowByOffset(y).GetText(); + limit = start; + start -= text.size(); + } while (neededIndex < start); + } + else + { + do + { + ++y; + if (y >= range.end) + { + return false; + } + + text = textBuffer.GetRowByOffset(y).GetText(); + start = limit; + limit += text.size(); + } while (neededIndex >= limit); + } + + accessCurrentRow(ut) = y; + ut->chunkNativeStart = start; + ut->chunkNativeLimit = limit; + ut->chunkLength = gsl::narrow_cast(text.size()); +#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). + ut->chunkContents = reinterpret_cast(text.data()); + ut->nativeIndexingLimit = ut->chunkLength; + } + + auto offset = gsl::narrow_cast(nativeIndex - start); + + // Don't leave the offset on a trailing surrogate pair. See U16_SET_CP_START. + // This assumes that the TextBuffer contains valid UTF-16 which may theoretically not be the case. + if (offset > 0 && offset < ut->chunkLength && U16_IS_TRAIL(til::at(ut->chunkContents, offset))) + { + offset--; + } + + ut->chunkOffset = offset; + return true; +} +catch (...) +{ + return false; +} + +// An excerpt from the ICU documentation: +// +// Extract text from a UText into a UChar buffer. +// The size (number of 16 bit UChars) in the data to be extracted is returned. +// The full amount is returned, even when the specified buffer size is smaller. +// The extracted string must be NUL-terminated if there is sufficient space in the destination buffer. +// +// @param ut the UText from which to extract data. +// @param nativeStart the native index of the first character to extract. +// @param nativeLimit the native string index of the position following the last character to extract. +// @param dest the UChar (UTF-16) buffer into which the extracted text is placed +// @param destCapacity The size, in UChars, of the destination buffer. May be zero for precomputing the required size. +// @param status receives any error status. If U_BUFFER_OVERFLOW_ERROR: Returns number of UChars for preflighting. +// @return Number of UChars in the data. Does not include a trailing NUL. +// +// NOTE: utextExtract's correctness hasn't been verified yet. The code remains, just incase its functionality is needed in the future. +#pragma warning(suppress : 4505) // 'utextExtract': unreferenced function with internal linkage has been removed +static int32_t U_CALLCONV utextExtract(UText* ut, int64_t nativeStart, int64_t nativeLimit, char16_t* dest, int32_t destCapacity, UErrorCode* status) noexcept +try +{ + __assume(status != nullptr); + + if (*status > U_ZERO_ERROR) + { + return 0; + } + if (destCapacity < 0 || (dest == nullptr && destCapacity > 0) || nativeStart > nativeLimit) + { + *status = U_ILLEGAL_ARGUMENT_ERROR; + return 0; + } + + if (!utextAccess(ut, nativeStart, true)) + { + return 0; + } + + nativeLimit = std::min(ut->chunkNativeLimit, nativeLimit); + + if (destCapacity <= 0) + { + return gsl::narrow_cast(nativeLimit - nativeStart); + } + + const auto& textBuffer = *static_cast(ut->context); + const auto y = accessCurrentRow(ut); + const auto offset = ut->chunkNativeStart - nativeStart; + const auto text = textBuffer.GetRowByOffset(y).GetText().substr(gsl::narrow_cast(std::max(0, offset))); + const auto destCapacitySizeT = gsl::narrow_cast(destCapacity); + const auto length = std::min(destCapacitySizeT, text.size()); + + memcpy(dest, text.data(), length * sizeof(char16_t)); + + if (length < destCapacitySizeT) + { +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + dest[length] = 0; + } + + return gsl::narrow_cast(length); +} +catch (...) +{ + // The only thing that can fail is GetRowByOffset() which in turn can only fail when VirtualAlloc() fails. + *status = U_MEMORY_ALLOCATION_ERROR; + return 0; +} + +static constexpr UTextFuncs utextFuncs{ + .tableSize = sizeof(UTextFuncs), + .clone = utextClone, + .nativeLength = utextNativeLength, + .access = utextAccess, +}; + +// Creates a UText from the given TextBuffer that spans rows [rowBeg,RowEnd). +UText Microsoft::Console::ICU::UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept +{ +#pragma warning(suppress : 26477) // Use 'nullptr' rather than 0 or NULL (es.47). + UText ut = UTEXT_INITIALIZER; + ut.providerProperties = (1 << UTEXT_PROVIDER_LENGTH_IS_EXPENSIVE) | (1 << UTEXT_PROVIDER_STABLE_CHUNKS); + ut.pFuncs = &utextFuncs; + ut.context = &textBuffer; + accessCurrentRow(&ut) = rowBeg - 1; // the utextAccess() below will advance this by 1. + accessRowRange(&ut) = { rowBeg, rowEnd }; + + utextAccess(&ut, 0, true); + return ut; +} + +Microsoft::Console::ICU::unique_uregex Microsoft::Console::ICU::CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept +{ +#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). + const auto re = uregex_open(reinterpret_cast(pattern.data()), gsl::narrow_cast(pattern.size()), flags, nullptr, status); + // ICU describes the time unit as being dependent on CPU performance and "typically [in] the order of milliseconds", + // but this claim seems highly outdated already. On my CPU from 2021, a limit of 4096 equals roughly 600ms. + uregex_setTimeLimit(re, 4096, status); + uregex_setStackLimit(re, 4 * 1024 * 1024, status); + return unique_uregex{ re }; +} + +// Returns an inclusive point range given a text start and end position. +// This function is designed to be used with uregex_start64/uregex_end64. +til::point_span Microsoft::Console::ICU::BufferRangeFromMatch(UText* ut, URegularExpression* re) +{ + UErrorCode status = U_ZERO_ERROR; + const auto nativeIndexBeg = uregex_start64(re, 0, &status); + auto nativeIndexEnd = uregex_end64(re, 0, &status); + + // The parameters are given as a half-open [beg,end) range, but the point_span we return in closed [beg,end]. + nativeIndexEnd--; + + const auto& textBuffer = *static_cast(ut->context); + til::point_span ret; + + if (utextAccess(ut, nativeIndexBeg, true)) + { + const auto y = accessCurrentRow(ut); + ret.start.x = textBuffer.GetRowByOffset(y).GetLeadingColumnAtCharOffset(ut->chunkOffset); + ret.start.y = y; + } + else + { + ret.start.y = accessRowRange(ut).begin; + } + + if (utextAccess(ut, nativeIndexEnd, true)) + { + const auto y = accessCurrentRow(ut); + ret.end.x = textBuffer.GetRowByOffset(y).GetTrailingColumnAtCharOffset(ut->chunkOffset); + ret.end.y = y; + } + else + { + ret.end = ret.start; + } + + return ret; +} diff --git a/src/buffer/out/UTextAdapter.h b/src/buffer/out/UTextAdapter.h new file mode 100644 index 00000000000..c8c325143ef --- /dev/null +++ b/src/buffer/out/UTextAdapter.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +class TextBuffer; + +namespace Microsoft::Console::ICU +{ + using unique_uregex = wistd::unique_ptr>; + + UText UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept; + unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept; + til::point_span BufferRangeFromMatch(UText* ut, URegularExpression* re); +} diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index f7962921c2d..20385eff096 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -26,6 +26,7 @@ Create + @@ -44,6 +45,7 @@ + diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index d060f55b7de..0186110f6cf 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -2,342 +2,103 @@ // Licensed under the MIT license. #include "precomp.h" - #include "search.h" -#include - #include "textBuffer.hpp" -#include "../types/inc/GlyphWidth.hpp" using namespace Microsoft::Console::Types; -// Routine Description: -// - Constructs a Search object. -// - Make a Search object then call .FindNext() to locate items. -// - Once you've found something, you can perform actions like .Select() or .Color() -// Arguments: -// - textBuffer - The screen text buffer to search through (the "haystack") -// - renderData - The IRenderData type reference, it is for providing selection methods -// - str - The search term you want to find (the "needle") -// - direction - The direction to search (upward or downward) -// - sensitivity - Whether or not you care about case -Search::Search(Microsoft::Console::Render::IRenderData& renderData, - const std::wstring_view str, - const Direction direction, - const Sensitivity sensitivity) : - _direction(direction), - _sensitivity(sensitivity), - _needle(s_CreateNeedleFromString(str)), - _renderData(renderData), - _coordAnchor(s_GetInitialAnchor(renderData, direction)) -{ - _coordNext = _coordAnchor; -} - -// Routine Description: -// - Constructs a Search object. -// - Make a Search object then call .FindNext() to locate items. -// - Once you've found something, you can perform actions like .Select() or .Color() -// Arguments: -// - textBuffer - The screen text buffer to search through (the "haystack") -// - renderData - The IRenderData type reference, it is for providing selection methods -// - str - The search term you want to find (the "needle") -// - direction - The direction to search (upward or downward) -// - sensitivity - Whether or not you care about case -// - anchor - starting search location in screenInfo -Search::Search(Microsoft::Console::Render::IRenderData& renderData, - const std::wstring_view str, - const Direction direction, - const Sensitivity sensitivity, - const til::point anchor) : - _direction(direction), - _sensitivity(sensitivity), - _needle(s_CreateNeedleFromString(str)), - _coordAnchor(anchor), - _renderData(renderData) +bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive) { - _coordNext = _coordAnchor; -} + const auto& textBuffer = renderData.GetTextBuffer(); + const auto lastMutationId = textBuffer.GetLastMutationId(); -// Routine Description -// - Locates the next instance of the search term within the screen buffer. -// Arguments: -// - - Uses internal state from constructor -// Return Value: -// - True if we found another item. False if we've reached the end of the buffer. -// - NOTE: You can FindNext() again after False to go around the buffer again. -bool Search::FindNext() -{ - if (_reachedEnd) + if (_needle == needle && + _reverse == reverse && + _caseInsensitive == caseInsensitive && + _lastMutationId == lastMutationId) { - _reachedEnd = false; return false; } - do - { - if (_FindNeedleInHaystackAt(_coordNext, _coordSelStart, _coordSelEnd)) - { - _UpdateNextPosition(); - _reachedEnd = _coordNext == _coordAnchor; - return true; - } - else - { - _UpdateNextPosition(); - } - - } while (_coordNext != _coordAnchor); + _renderData = &renderData; + _needle = needle; + _reverse = reverse; + _caseInsensitive = caseInsensitive; + _lastMutationId = lastMutationId; - return false; -} + _results = textBuffer.SearchText(needle, caseInsensitive); + _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; + _step = reverse ? -1 : 1; -// Routine Description: -// - Takes the found word and selects it in the screen buffer -void Search::Select() const -{ - // Convert buffer selection offsets into the equivalent screen coordinates - // required by SelectNewRegion, taking line renditions into account. - const auto& textBuffer = _renderData.GetTextBuffer(); - const auto selStart = textBuffer.BufferToScreenPosition(_coordSelStart); - const auto selEnd = textBuffer.BufferToScreenPosition(_coordSelEnd); - _renderData.SelectNewRegion(selStart, selEnd); + return true; } -// Routine Description: -// - Applies the supplied TextAttribute to the current search result. -// Arguments: -// - attr - The attribute to apply to the result -void Search::Color(const TextAttribute attr) const +void Search::MovePastCurrentSelection() { - // Only select if we've found something. - if (_coordSelEnd >= _coordSelStart) + if (_renderData->IsSelectionActive()) { - _renderData.ColorSelection(_coordSelStart, _coordSelEnd, attr); + MovePastPoint(_renderData->GetTextBuffer().ScreenToBufferPosition(_renderData->GetSelectionAnchor())); } } -// Routine Description: -// - gets start and end position of text sound by search. only guaranteed to have valid data if FindNext has -// been called and returned true. -// Return Value: -// - pair containing [start, end] coord positions of text found by search -std::pair Search::GetFoundLocation() const noexcept +void Search::MovePastPoint(const til::point anchor) noexcept { - return { _coordSelStart, _coordSelEnd }; -} - -// Routine Description: -// - Finds the anchor position where we will start searches from. -// - This position will represent the "wrap around" point in the buffer or where -// we reach the end of our search. -// - If the screen buffer given already has a selection in it, it will be used to determine the anchor. -// - Otherwise, we will choose one of the ends of the screen buffer depending on direction. -// Arguments: -// - renderData - The reference to the IRenderData interface type object -// - direction - The intended direction of the search -// Return Value: -// - Coordinate to start the search from. -til::point Search::s_GetInitialAnchor(const Microsoft::Console::Render::IRenderData& renderData, const Direction direction) -{ - const auto& textBuffer = renderData.GetTextBuffer(); - const auto textBufferEndPosition = renderData.GetTextBufferEndPosition(); - if (renderData.IsSelectionActive()) + if (_results.empty()) { - // Convert the screen position of the selection anchor into an equivalent - // buffer position to start searching, taking line rendition into account. - auto anchor = textBuffer.ScreenToBufferPosition(renderData.GetSelectionAnchor()); - - if (direction == Direction::Forward) - { - textBuffer.GetSize().IncrementInBoundsCircular(anchor); - } - else - { - textBuffer.GetSize().DecrementInBoundsCircular(anchor); - // If the selection starts at (0, 0), we need to make sure - // it does not exceed the text buffer end position - anchor.x = std::min(textBufferEndPosition.x, anchor.x); - anchor.y = std::min(textBufferEndPosition.y, anchor.y); - } - return anchor; + return; } - else - { - if (direction == Direction::Forward) - { - return { 0, 0 }; - } - else - { - return textBufferEndPosition; - } - } -} -// Routine Description: -// - Attempts to compare the search term (the needle) to the screen buffer (the haystack) -// at the given coordinate position of the screen buffer. -// - Performs one comparison. Call again with new positions to check other spots. -// Arguments: -// - pos - The position in the haystack (screen buffer) to compare -// - start - If we found it, this is filled with the coordinate of the first character of the needle. -// - end - If we found it, this is filled with the coordinate of the last character of the needle. -// Return Value: -// - True if we found it. False if not. -bool Search::_FindNeedleInHaystackAt(const til::point pos, til::point& start, til::point& end) const -{ - start = {}; - end = {}; - - auto bufferPos = pos; + const auto count = gsl::narrow_cast(_results.size()); + const auto highestIndex = count - 1; + auto index = _reverse ? highestIndex : 0; - for (const auto& needleChars : _needle) + if (_reverse) { - // Haystack is the buffer. Needle is the string we were given. - const auto hayIter = _renderData.GetTextBuffer().GetTextDataAt(bufferPos); - const auto hayChars = *hayIter; - - // If we didn't match at any point of the needle, return false. - if (!_CompareChars(hayChars, needleChars)) + for (; index >= 0 && til::at(_results, index).start >= anchor; --index) { - return false; } - - _IncrementCoord(bufferPos); } - - _DecrementCoord(bufferPos); - - // If we made it the whole way through the needle, then it was in the haystack. - // Fill out the span that we found the result at and return true. - start = pos; - end = bufferPos; - - return true; -} - -// Routine Description: -// - Provides an abstraction for comparing two spans of text. -// - Internally handles case sensitivity based on object construction. -// Arguments: -// - one - String view representing the first string of text -// - two - String view representing the second string of text -// Return Value: -// - True if they are the same. False otherwise. -bool Search::_CompareChars(const std::wstring_view one, const std::wstring_view two) const noexcept -{ - if (one.size() != two.size()) - { - return false; - } - - for (size_t i = 0; i < one.size(); i++) + else { - if (_ApplySensitivity(one.at(i)) != _ApplySensitivity(two.at(i))) + for (; index <= highestIndex && til::at(_results, index).start <= anchor; ++index) { - return false; } } - return true; + _index = (index + count) % count; } -// Routine Description: -// - Provides an abstraction for conditionally applying case sensitivity -// based on object construction -// Arguments: -// - wch - Character to adjust if necessary -// Return Value: -// - Adjusted value (or not). -wchar_t Search::_ApplySensitivity(const wchar_t wch) const noexcept +void Search::FindNext() noexcept { - if (_sensitivity == Sensitivity::CaseInsensitive) - { - return ::towlower(wch); - } - else - { - return wch; - } + const auto count = gsl::narrow_cast(_results.size()); + _index = (_index + _step + count) % count; } -// Routine Description: -// - Helper to increment a coordinate in respect to the associated screen buffer -// Arguments -// - coord - Updated by function to increment one position (will wrap X and Y direction) -void Search::_IncrementCoord(til::point& coord) const noexcept +const til::point_span* Search::GetCurrent() const noexcept { - _renderData.GetTextBuffer().GetSize().IncrementInBoundsCircular(coord); -} - -// Routine Description: -// - Helper to decrement a coordinate in respect to the associated screen buffer -// Arguments -// - coord - Updated by function to decrement one position (will wrap X and Y direction) -void Search::_DecrementCoord(til::point& coord) const noexcept -{ - _renderData.GetTextBuffer().GetSize().DecrementInBoundsCircular(coord); -} - -// Routine Description: -// - Helper to update the coordinate position to the next point to be searched -// Return Value: -// - True if we haven't reached the end of the buffer. False otherwise. -void Search::_UpdateNextPosition() -{ - if (_direction == Direction::Forward) - { - _IncrementCoord(_coordNext); - } - else if (_direction == Direction::Backward) - { - _DecrementCoord(_coordNext); - } - else + const auto index = gsl::narrow_cast(_index); + if (index < _results.size()) { - THROW_HR(E_NOTIMPL); - } - - // To reduce wrap-around time, if the next position is larger than - // the end position of the written text - // We put the next position to: - // Forward: (0, 0) - // Backward: the position of the end of the text buffer - const auto bufferEndPosition = _renderData.GetTextBufferEndPosition(); - - if (_coordNext.y > bufferEndPosition.y || - (_coordNext.y == bufferEndPosition.y && _coordNext.x > bufferEndPosition.x)) - { - if (_direction == Direction::Forward) - { - _coordNext = {}; - } - else - { - _coordNext = bufferEndPosition; - } + return &til::at(_results, index); } + return nullptr; } // Routine Description: -// - Creates a "needle" of the correct format for comparison to the screen buffer text data -// that we can use for our search -// Arguments: -// - wstr - String that will be our search term -// Return Value: -// - Structured text data for comparison to screen buffer text data. -std::vector Search::s_CreateNeedleFromString(const std::wstring_view wstr) +// - Takes the found word and selects it in the screen buffer +bool Search::SelectCurrent() const { - std::vector cells; - for (const auto& chars : til::utf16_iterator{ wstr }) + if (const auto s = GetCurrent()) { - if (IsGlyphFullWidth(chars)) - { - cells.emplace_back(chars); - } - cells.emplace_back(chars); + // Convert buffer selection offsets into the equivalent screen coordinates + // required by SelectNewRegion, taking line renditions into account. + const auto& textBuffer = _renderData->GetTextBuffer(); + const auto selStart = textBuffer.BufferToScreenPosition(s->start); + const auto selEnd = textBuffer.BufferToScreenPosition(s->end); + _renderData->SelectNewRegion(selStart, selEnd); + return true; } - return cells; + + return false; } diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index 68f9f82bfd9..a337552d59a 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -17,70 +17,32 @@ Revision History: #pragma once -#include "TextAttribute.hpp" #include "textBuffer.hpp" #include "../renderer/inc/IRenderData.hpp" -// This used to be in find.h. -#define SEARCH_STRING_LENGTH (80) - class Search final { public: - enum class Direction - { - Forward, - Backward - }; - - enum class Sensitivity - { - CaseInsensitive, - CaseSensitive - }; - - Search(Microsoft::Console::Render::IRenderData& renderData, - const std::wstring_view str, - const Direction dir, - const Sensitivity sensitivity); + Search() = default; - Search(Microsoft::Console::Render::IRenderData& renderData, - const std::wstring_view str, - const Direction dir, - const Sensitivity sensitivity, - const til::point anchor); + bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive); - bool FindNext(); - void Select() const; - void Color(const TextAttribute attr) const; + void MovePastCurrentSelection(); + void MovePastPoint(til::point anchor) noexcept; + void FindNext() noexcept; - std::pair GetFoundLocation() const noexcept; + const til::point_span* GetCurrent() const noexcept; + bool SelectCurrent() const; private: - wchar_t _ApplySensitivity(const wchar_t wch) const noexcept; - bool _FindNeedleInHaystackAt(const til::point pos, til::point& start, til::point& end) const; - bool _CompareChars(const std::wstring_view one, const std::wstring_view two) const noexcept; - void _UpdateNextPosition(); - - void _IncrementCoord(til::point& coord) const noexcept; - void _DecrementCoord(til::point& coord) const noexcept; - - static til::point s_GetInitialAnchor(const Microsoft::Console::Render::IRenderData& renderData, const Direction dir); - - static std::vector s_CreateNeedleFromString(const std::wstring_view wstr); - - bool _reachedEnd = false; - til::point _coordNext; - til::point _coordSelStart; - til::point _coordSelEnd; - - const til::point _coordAnchor; - const std::vector _needle; - const Direction _direction; - const Sensitivity _sensitivity; - Microsoft::Console::Render::IRenderData& _renderData; - -#ifdef UNIT_TESTING - friend class SearchTests; -#endif + // _renderData is a pointer so that Search() is constexpr default constructable. + Microsoft::Console::Render::IRenderData* _renderData = nullptr; + std::wstring_view _needle; + bool _reverse = false; + bool _caseInsensitive = false; + uint64_t _lastMutationId = 0; + + std::vector _results; + ptrdiff_t _index = 0; + ptrdiff_t _step = 0; }; diff --git a/src/buffer/out/sources.inc b/src/buffer/out/sources.inc index af3d278749c..6611cc0aa5e 100644 --- a/src/buffer/out/sources.inc +++ b/src/buffer/out/sources.inc @@ -40,7 +40,8 @@ SOURCES= \ ..\textBuffer.cpp \ ..\textBufferCellIterator.cpp \ ..\textBufferTextIterator.cpp \ - ..\search.cpp \ + ..\search.cpp \ + ..\UTextAdapter.cpp \ INCLUDES= \ $(INCLUDES); \ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index d120b2e4a98..f5bfdeda3ce 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -8,16 +8,31 @@ #include #include +#include "UTextAdapter.h" +#include "../../types/inc/GlyphWidth.hpp" #include "../renderer/base/renderer.hpp" -#include "../types/inc/utils.hpp" #include "../types/inc/convert.hpp" -#include "../../types/inc/GlyphWidth.hpp" +#include "../types/inc/utils.hpp" using namespace Microsoft::Console; using namespace Microsoft::Console::Types; using PointTree = interval_tree::IntervalTree; +constexpr bool allWhitespace(const std::wstring_view& text) noexcept +{ + for (const auto ch : text) + { + if (ch != L' ') + { + return false; + } + } + return true; +} + +static std::atomic s_lastMutationIdInitialValue; + // Routine Description: // - Creates a new instance of TextBuffer // Arguments: @@ -36,6 +51,9 @@ TextBuffer::TextBuffer(til::size screenBufferSize, Microsoft::Console::Render::Renderer& renderer) : _renderer{ renderer }, _currentAttributes{ defaultAttributes }, + // This way every TextBuffer will start with a ""unique"" _lastMutationId + // and so it'll compare unequal with the counter of other TextBuffers. + _lastMutationId{ s_lastMutationIdInitialValue.fetch_add(0x100000000) }, _cursor{ cursorSize, *this }, _isActiveBuffer{ isActiveBuffer } { @@ -166,6 +184,23 @@ ROW& TextBuffer::_getRowByOffsetDirect(size_t offset) return *reinterpret_cast(row); } +ROW& TextBuffer::_getRow(til::CoordType y) const +{ + // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. + auto offset = (_firstRow + y) % _height; + + // Support negative wrap around. This way an index of -1 will + // wrap to _rowCount-1 and make implementing scrolling easier. + if (offset < 0) + { + offset += _height; + } + + // We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow(). +#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3). + return const_cast(this)->_getRowByOffsetDirect(gsl::narrow_cast(offset) + 1); +} + // Returns the "user-visible" index of the last committed row, which can be used // to short-circuit some algorithms that try to scan the entire buffer. // Returns 0 if no rows are committed in. @@ -183,27 +218,15 @@ til::CoordType TextBuffer::_estimateOffsetOfLastCommittedRow() const noexcept // (what corresponds to the top row of the screen buffer). const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const { - // The const_cast is safe because "const" never had any meaning in C++ in the first place. -#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3). - return const_cast(this)->GetRowByOffset(index); + return _getRow(index); } // Retrieves a row from the buffer by its offset from the first row of the text buffer // (what corresponds to the top row of the screen buffer). -ROW& TextBuffer::GetRowByOffset(const til::CoordType index) +ROW& TextBuffer::GetMutableRowByOffset(const til::CoordType index) { - // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. - auto offset = (_firstRow + index) % _height; - - // Support negative wrap around. This way an index of -1 will - // wrap to _rowCount-1 and make implementing scrolling easier. - if (offset < 0) - { - offset += _height; - } - - // We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow(). - return _getRowByOffsetDirect(gsl::narrow_cast(offset) + 1); + _lastMutationId++; + return _getRow(index); } // Returns a row filled with whitespace and the current attributes, for you to freely use. @@ -345,91 +368,6 @@ TextBufferCellIterator TextBuffer::GetCellDataAt(const til::point at, const View return TextBufferCellIterator(*this, at, limit); } -//Routine Description: -// - Corrects and enforces consistent double byte character state (KAttrs line) within a row of the text buffer. -// - This will take the given double byte information and check that it will be consistent when inserted into the buffer -// at the current cursor position. -// - It will correct the buffer (by erasing the character prior to the cursor) if necessary to make a consistent state. -//Arguments: -// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer -//Return Value: -// - True if it is valid to insert a character with the given double byte attributes. False otherwise. -bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute) -{ - // To figure out if the sequence is valid, we have to look at the character that comes before the current one - const auto coordPrevPosition = _GetPreviousFromCursor(); - auto& prevRow = GetRowByOffset(coordPrevPosition.y); - DbcsAttribute prevDbcsAttr = DbcsAttribute::Single; - try - { - prevDbcsAttr = prevRow.DbcsAttrAt(coordPrevPosition.x); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - return false; - } - - auto fValidSequence = true; // Valid until proven otherwise - auto fCorrectableByErase = false; // Can't be corrected until proven otherwise - - // Here's the matrix of valid items: - // N = None (single byte) - // L = Lead (leading byte of double byte sequence - // T = Trail (trailing byte of double byte sequence - // Prev Curr Result - // N N OK. - // N L OK. - // N T Fail, uncorrectable. Trailing byte must have had leading before it. - // L N Fail, OK with erase. Lead needs trailing pair. Can erase lead to correct. - // L L Fail, OK with erase. Lead needs trailing pair. Can erase prev lead to correct. - // L T OK. - // T N OK. - // T L OK. - // T T Fail, uncorrectable. New trailing byte must have had leading before it. - - // Check for only failing portions of the matrix: - if (prevDbcsAttr == DbcsAttribute::Single && dbcsAttribute == DbcsAttribute::Trailing) - { - // N, T failing case (uncorrectable) - fValidSequence = false; - } - else if (prevDbcsAttr == DbcsAttribute::Leading) - { - if (dbcsAttribute == DbcsAttribute::Single || dbcsAttribute == DbcsAttribute::Leading) - { - // L, N and L, L failing cases (correctable) - fValidSequence = false; - fCorrectableByErase = true; - } - } - else if (prevDbcsAttr == DbcsAttribute::Trailing && dbcsAttribute == DbcsAttribute::Trailing) - { - // T, T failing case (uncorrectable) - fValidSequence = false; - } - - // If it's correctable by erase, erase the previous character - if (fCorrectableByErase) - { - // Erase previous character into an N type. - try - { - prevRow.ClearCell(coordPrevPosition.x); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - return false; - } - - // Sequence is now N N or N L, which are both okay. Set sequence back to valid. - fValidSequence = true; - } - - return fValidSequence; -} - //Routine Description: // - Call before inserting a character into the buffer. // - This will ensure a consistent double byte state (KAttrs line) within the text buffer @@ -453,7 +391,7 @@ void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute if (cursorPosition.x == lineWidth - 1) { // set that we're wrapping for double byte reasons - auto& row = GetRowByOffset(cursorPosition.y); + auto& row = GetMutableRowByOffset(cursorPosition.y); row.SetDoubleBytePadded(true); // then move the cursor forward and onto the next row @@ -482,7 +420,7 @@ size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) // You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit. void TextBuffer::Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state) { - auto& r = GetRowByOffset(row); + auto& r = GetMutableRowByOffset(row); r.ReplaceText(state); r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes); TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 })); @@ -535,7 +473,7 @@ void TextBuffer::FillRect(const til::rect& rect, const std::wstring_view& fill, for (auto y = rect.top; y < rect.bottom; ++y) { - auto& r = GetRowByOffset(y); + auto& r = GetMutableRowByOffset(y); r.CopyTextFrom(state); r.ReplaceAttributes(rect.left, rect.right, attributes); TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, y, state.columnEndDirty, y + 1 })); @@ -616,7 +554,7 @@ OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, } // Get the row and write the cells - auto& row = GetRowByOffset(target.y); + auto& row = GetMutableRowByOffset(target.y); const auto newIt = row.WriteCells(givenIt, target.x, wrap, limitRight); // Take the cell distance written and notify that it needs to be repainted. @@ -648,7 +586,7 @@ void TextBuffer::InsertCharacter(const std::wstring_view chars, const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal. // Get the row associated with the given logical position - auto& Row = GetRowByOffset(iRow); + auto& Row = GetMutableRowByOffset(iRow); // Store character and double byte data switch (dbcsAttribute) @@ -708,7 +646,7 @@ void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) const auto uiCurrentRowOffset = GetCursor().GetPosition().y; // Set the wrap status as appropriate - GetRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet); + GetMutableRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet); } //Routine Description: @@ -784,7 +722,7 @@ void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes) _PruneHyperlinks(); // Second, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. - GetRowByOffset(0).Reset(fillAttributes); + GetMutableRowByOffset(0).Reset(fillAttributes); { // Now proceed to increment. // Incrementing it will cause the next line down to become the new "top" of the window (the new "0" in logical coordinates) @@ -955,7 +893,7 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size, for (; y != end; y += step) { - GetRowByOffset(y + delta).CopyFrom(GetRowByOffset(y)); + GetMutableRowByOffset(y + delta).CopyFrom(GetRowByOffset(y)); } } @@ -969,6 +907,11 @@ const Cursor& TextBuffer::GetCursor() const noexcept return _cursor; } +uint64_t TextBuffer::GetLastMutationId() const noexcept +{ + return _lastMutationId; +} + const TextAttribute& TextBuffer::GetCurrentAttributes() const noexcept { return _currentAttributes; @@ -981,14 +924,14 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no void TextBuffer::SetWrapForced(const til::CoordType y, bool wrap) { - GetRowByOffset(y).SetWrapForced(wrap); + GetMutableRowByOffset(y).SetWrapForced(wrap); } void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes) { const auto cursorPosition = GetCursor().GetPosition(); const auto rowIndex = cursorPosition.y; - auto& row = GetRowByOffset(rowIndex); + auto& row = GetMutableRowByOffset(rowIndex); if (row.GetLineRendition() != lineRendition) { row.SetLineRendition(lineRendition); @@ -1013,7 +956,7 @@ void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const ti { for (auto row = startRow; row < endRow; row++) { - GetRowByOffset(row).SetLineRendition(LineRendition::SingleWidth); + GetMutableRowByOffset(row).SetLineRendition(LineRendition::SingleWidth); } } @@ -1090,7 +1033,7 @@ void TextBuffer::Reset() noexcept for (; dstRow < copyableRows; ++dstRow, ++srcRow) { - newBuffer.GetRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); + newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); } // NOTE: Keep this in sync with _reserve(). @@ -2446,7 +2389,7 @@ try const auto newBufferPos = newCursor.GetPosition(); if (newBufferPos.x == 0) { - auto& newRow = newBuffer.GetRowByOffset(newBufferPos.y); + auto& newRow = newBuffer.GetMutableRowByOffset(newBufferPos.y); newRow.SetLineRendition(row.GetLineRendition()); } @@ -2516,7 +2459,7 @@ try // 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.GetRowByOffset(newRowY); + 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 @@ -2631,7 +2574,7 @@ try // 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.GetRowByOffset(newRowY); + auto& newRow = newBuffer.GetMutableRowByOffset(newRowY); const auto newWidth = newBuffer.GetLineWidth(newRowY); newRow.TransferAttributes(row.Attributes(), newWidth); @@ -2641,7 +2584,6 @@ try // Finish copying remaining parameters from the old text buffer to the new one newBuffer.CopyProperties(oldBuffer); newBuffer.CopyHyperlinkMaps(oldBuffer); - newBuffer.CopyPatterns(oldBuffer); // If we found where to put the cursor while placing characters into the buffer, // just put the cursor there. Otherwise we have to advance manually. @@ -2803,104 +2745,45 @@ void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other) _currentHyperlinkId = other._currentHyperlinkId; } -// Method Description: -// - Adds a regex pattern we should search for -// - The searching does not happen here, we only search when asked to by TerminalCore -// Arguments: -// - The regex pattern -// Return value: -// - An ID that the caller should associate with the given pattern -const size_t TextBuffer::AddPatternRecognizer(const std::wstring_view regexString) -{ - ++_currentPatternId; - _idsAndPatterns.emplace(std::make_pair(_currentPatternId, regexString)); - return _currentPatternId; -} - -// Method Description: -// - Clears the patterns we know of and resets the pattern ID counter -void TextBuffer::ClearPatternRecognizers() noexcept -{ - _idsAndPatterns.clear(); - _currentPatternId = 0; -} - -// Method Description: -// - Copies the patterns the other buffer knows about into this one -// Arguments: -// - The other buffer -void TextBuffer::CopyPatterns(const TextBuffer& OtherBuffer) +// Searches through the entire (committed) text buffer for `needle` and returns the coordinates in absolute coordinates. +// The end coordinates of the returned ranges are considered inclusive. +std::vector TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive) const { - _idsAndPatterns = OtherBuffer._idsAndPatterns; - _currentPatternId = OtherBuffer._currentPatternId; + return SearchText(needle, caseInsensitive, 0, til::CoordTypeMax); } -// Method Description: -// - Finds patterns within the requested region of the text buffer -// Arguments: -// - The firstRow to start searching from -// - The lastRow to search -// Return value: -// - An interval tree containing the patterns found -PointTree TextBuffer::GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const +// Searches through the given rows [rowBeg,rowEnd) for `needle` and returns the coordinates in absolute coordinates. +// While the end coordinates of the returned ranges are considered inclusive, the [rowBeg,rowEnd) range is half-open. +std::vector TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive, til::CoordType rowBeg, til::CoordType rowEnd) const { - PointTree::interval_vector intervals; + rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1); - std::wstring concatAll; - const auto rowSize = GetRowByOffset(0).size(); - concatAll.reserve(gsl::narrow_cast(rowSize) * gsl::narrow_cast(lastRow - firstRow + 1)); + std::vector results; - // to deal with text that spans multiple lines, we will first concatenate - // all the text into one string and find the patterns in that string - for (til::CoordType i = firstRow; i <= lastRow; ++i) + // All whitespace strings would match the not-yet-written parts of the TextBuffer which would be weird. + if (allWhitespace(needle) || rowBeg >= rowEnd) { - auto& row = GetRowByOffset(i); - concatAll += row.GetText(); + return results; } - // for each pattern we know of, iterate through the string - for (const auto& idAndPattern : _idsAndPatterns) - { - std::wregex regexObj{ idAndPattern.second }; + auto text = ICU::UTextFromTextBuffer(*this, rowBeg, rowEnd); - // search through the run with our regex object - auto words_begin = std::wsregex_iterator(concatAll.begin(), concatAll.end(), regexObj); - auto words_end = std::wsregex_iterator(); + uint32_t flags = UREGEX_LITERAL; + WI_SetFlagIf(flags, UREGEX_CASE_INSENSITIVE, caseInsensitive); - til::CoordType lenUpToThis = 0; - for (auto i = words_begin; i != words_end; ++i) + UErrorCode status = U_ZERO_ERROR; + const auto re = ICU::CreateRegex(needle, flags, &status); + uregex_setUText(re.get(), &text, &status); + + if (uregex_find(re.get(), -1, &status)) + { + do { - // record the locations - - // when we find a match, the prefix is text that is between this - // match and the previous match, so we use the size of the prefix - // along with the size of the match to determine the locations - til::CoordType prefixSize = 0; - for (const auto str = i->prefix().str(); const auto& glyph : til::utf16_iterator{ str }) - { - prefixSize += IsGlyphFullWidth(glyph) ? 2 : 1; - } - const auto start = lenUpToThis + prefixSize; - til::CoordType matchSize = 0; - for (const auto str = i->str(); const auto& glyph : til::utf16_iterator{ str }) - { - matchSize += IsGlyphFullWidth(glyph) ? 2 : 1; - } - const auto end = start + matchSize; - lenUpToThis = end; - - const til::point startCoord{ start % rowSize, start / rowSize }; - const til::point endCoord{ end % rowSize, end / rowSize }; - - // store the intervals - // NOTE: these intervals are relative to the VIEWPORT not the buffer - // Keeping these relative to the viewport for now because its the renderer - // that actually uses these locations and the renderer works relative to - // the viewport - intervals.push_back(PointTree::interval(startCoord, endCoord, idAndPattern.first)); - } + results.emplace_back(ICU::BufferRangeFromMatch(&text, re.get())); + } while (uregex_findNext(re.get(), &status)); } - PointTree result(std::move(intervals)); - return result; + + return results; } const std::vector& TextBuffer::GetMarks() const noexcept diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index d4cf6f74214..a57474b6b81 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -59,6 +59,8 @@ filling in the last row, and updating the screen. #include "../buffer/out/textBufferCellIterator.hpp" #include "../buffer/out/textBufferTextIterator.hpp" +struct URegularExpression; + namespace Microsoft::Console::Render { class Renderer; @@ -122,7 +124,7 @@ class TextBuffer final ROW& GetScratchpadRow(); ROW& GetScratchpadRow(const TextAttribute& attributes); const ROW& GetRowByOffset(til::CoordType index) const; - ROW& GetRowByOffset(til::CoordType index); + ROW& GetMutableRowByOffset(til::CoordType index); TextBufferCellIterator GetCellDataAt(const til::point at) const; TextBufferCellIterator GetCellLineDataAt(const til::point at) const; @@ -164,6 +166,7 @@ class TextBuffer final Cursor& GetCursor() noexcept; const Cursor& GetCursor() const noexcept; + uint64_t GetLastMutationId() const noexcept; const til::CoordType GetFirstRowIndex() const noexcept; const Microsoft::Console::Types::Viewport GetSize() const noexcept; @@ -262,10 +265,8 @@ class TextBuffer final const std::optional lastCharacterViewport, std::optional> positionInfo); - const size_t AddPatternRecognizer(const std::wstring_view regexString); - void ClearPatternRecognizers() noexcept; - void CopyPatterns(const TextBuffer& OtherBuffer); - interval_tree::IntervalTree GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const; + 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; const std::vector& GetMarks() const noexcept; void ClearMarksInRange(const til::point start, const til::point end); @@ -285,6 +286,7 @@ class TextBuffer final void _construct(const std::byte* until) noexcept; void _destroy() const noexcept; ROW& _getRowByOffsetDirect(size_t offset); + ROW& _getRow(til::CoordType y) const; til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept; void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept; @@ -293,7 +295,6 @@ class TextBuffer final void _AdjustWrapOnCurrentRow(const bool fSet); // Assist with maintaining proper buffer state for Double Byte character sequences void _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute); - bool _AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute); void _ExpandTextRow(til::inclusive_rect& selectionRow) const; DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const; til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const; @@ -311,9 +312,6 @@ class TextBuffer final std::unordered_map _hyperlinkCustomIdMap; uint16_t _currentHyperlinkId = 1; - std::unordered_map _idsAndPatterns; - size_t _currentPatternId = 0; - // This block describes the state of the underlying virtual memory buffer that holds all ROWs, text and attributes. // Initially memory is only allocated with MEM_RESERVE to reduce the private working set of conhost. // ROWs are laid out like this in memory: @@ -373,12 +371,11 @@ class TextBuffer final TextAttribute _currentAttributes; til::CoordType _firstRow = 0; // indexes top row (not necessarily 0) + uint64_t _lastMutationId = 0; Cursor _cursor; - - bool _isActiveBuffer = false; - std::vector _marks; + bool _isActiveBuffer = false; #ifdef UNIT_TESTING friend class TextBufferTests; diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index 2d4a5c4a1e7..3c99a9fb583 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -739,7 +739,7 @@ class ReflowTests til::CoordType y = 0; for (const auto& testRow : testBuffer.rows) { - auto& row{ buffer->GetRowByOffset(y) }; + auto& row{ buffer->GetMutableRowByOffset(y) }; row.SetWrapForced(testRow.wrap); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 2fb25987178..c487a81960f 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1538,42 +1538,38 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - caseSensitive: boolean that represents if the current search is case sensitive // Return Value: // - - void ControlCore::Search(const winrt::hstring& text, - const bool goForward, - const bool caseSensitive) + void ControlCore::Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive) { - if (text.size() == 0) + auto lock = _terminal->LockForWriting(); + + if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive)) { - return; + _searcher.MovePastCurrentSelection(); + } + else + { + _searcher.FindNext(); } - const auto direction = goForward ? - Search::Direction::Forward : - Search::Direction::Backward; - - const auto sensitivity = caseSensitive ? - Search::Sensitivity::CaseSensitive : - Search::Sensitivity::CaseInsensitive; - - ::Search search(*GetRenderData(), text.c_str(), direction, sensitivity); - auto lock = _terminal->LockForWriting(); - const auto foundMatch{ search.FindNext() }; + const auto foundMatch = _searcher.SelectCurrent(); if (foundMatch) { - _terminal->SetBlockSelection(false); - search.Select(); - // this is used for search, // DO NOT call _updateSelectionUI() here. // We don't want to show the markers so manually tell it to clear it. + _terminal->SetBlockSelection(false); _renderer->TriggerSelection(); _UpdateSelectionMarkersHandlers(*this, winrt::make(true)); } // Raise a FoundMatch event, which the control will use to notify // narrator if there was any results in the buffer - auto foundResults = winrt::make_self(foundMatch); - _FoundMatchHandlers(*this, *foundResults); + _FoundMatchHandlers(*this, winrt::make(foundMatch)); + } + + void ControlCore::ClearSearch() + { + _searcher = {}; } void ControlCore::Close() diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 0851f746056..37058ebfbd3 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -205,9 +205,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetSelectionAnchor(const til::point position); void SetEndSelectionPoint(const til::point position); - void Search(const winrt::hstring& text, - const bool goForward, - const bool caseSensitive); + void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); + void ClearSearch(); void LeftClickOnTerminal(const til::point terminalPosition, const int numberOfClicks, @@ -305,6 +304,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::unique_ptr<::Microsoft::Console::Render::IRenderEngine> _renderEngine{ nullptr }; std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer{ nullptr }; + ::Search _searcher; + winrt::handle _lastSwapChainHandle{ nullptr }; FontInfoDesired _desiredFont; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index af898ff3d5a..1b9f67d8d9c 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -128,6 +128,7 @@ namespace Microsoft.Terminal.Control void ResumeRendering(); void BlinkAttributeTick(); void Search(String text, Boolean goForward, Boolean caseSensitive); + void ClearSearch(); Microsoft.Terminal.Core.Color BackgroundColor { get; }; SelectionData SelectionInfo { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 550d13eae30..88a8db66b00 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -414,6 +414,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) { + _core.ClearSearch(); _searchBox->Visibility(Visibility::Collapsed); // Set focus back to terminal control diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 75161e5c7f0..c4e6edc2685 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -9,7 +9,9 @@ #include "../../types/inc/utils.hpp" #include "../../types/inc/colorTable.hpp" #include "../../buffer/out/search.h" +#include "../../buffer/out/UTextAdapter.h" +#include #include using namespace winrt::Microsoft::Terminal::Core; @@ -111,7 +113,6 @@ void Terminal::UpdateSettings(ICoreSettings settings) if (_mainBuffer) { // Clear the patterns first - _mainBuffer->ClearPatternRecognizers(); _detectURLs = settings.DetectURLs(); _updateUrlDetection(); } @@ -569,7 +570,7 @@ std::wstring Terminal::GetHyperlinkAtBufferPosition(const til::point bufferPos) { // Hyperlink is outside of the current view. // We need to find if there's a pattern at that location. - const auto patterns = _activeBuffer().GetPatterns(bufferPos.y, bufferPos.y); + const auto patterns = _getPatterns(bufferPos.y, bufferPos.y); // NOTE: patterns is stored with top y-position being 0, // so we need to cleverly set the y-pos to 0. @@ -1208,9 +1209,8 @@ bool Terminal::IsCursorBlinkingAllowed() const noexcept // - INVARIANT: this function can only be called if the caller has the writing lock on the terminal void Terminal::UpdatePatternsUnderLock() { - auto oldTree = _patternIntervalTree; - _patternIntervalTree = _activeBuffer().GetPatterns(_VisibleStartIndex(), _VisibleEndIndex()); - _InvalidatePatternTree(oldTree); + _InvalidatePatternTree(_patternIntervalTree); + _patternIntervalTree = _getPatterns(_VisibleStartIndex(), _VisibleEndIndex()); _InvalidatePatternTree(_patternIntervalTree); } @@ -1337,9 +1337,6 @@ void Terminal::_updateUrlDetection() { if (_detectURLs) { - // Add regex pattern recognizers to the buffer - // For now, we only add the URI regex pattern - _hyperlinkPatternId = _activeBuffer().AddPatternRecognizer(linkPattern); UpdatePatternsUnderLock(); } else @@ -1348,6 +1345,103 @@ void Terminal::_updateUrlDetection() } } +struct URegularExpressionInterner +{ + // Interns (caches) URegularExpression instances so that they can be reused. This method is thread-safe. + // uregex_open is not terribly expensive at ~10us/op, but it's also much more expensive than uregex_clone + // at ~400ns/op and would effectively double the time it takes to scan the viewport for patterns. + // + // An alternative approach would be to not make this method thread-safe and give each + // Terminal instance its own cache. I'm not sure which approach would have been better. + ICU::unique_uregex Intern(const std::wstring_view& pattern) + { + UErrorCode status = U_ZERO_ERROR; + + { + const auto guard = _lock.lock_shared(); + if (const auto it = _cache.find(pattern); it != _cache.end()) + { + return ICU::unique_uregex{ uregex_clone(it->second.re.get(), &status) }; + } + } + + // Even if the URegularExpression creation failed, we'll insert it into the cache, because there's no point in retrying. + // (Apart from OOM but in that case this application will crash anyways in 3.. 2.. 1..) + auto re = ICU::CreateRegex(pattern, 0, &status); + ICU::unique_uregex clone{ uregex_clone(re.get(), &status) }; + std::wstring key{ pattern }; + + const auto guard = _lock.lock_exclusive(); + + _cache.insert_or_assign(std::move(key), CacheValue{ std::move(re), _totalInsertions }); + _totalInsertions++; + + // If the cache is full remove the oldest element (oldest = lowest generation, just like with humans). + if (_cache.size() > cacheSizeLimit) + { + _cache.erase(std::min_element(_cache.begin(), _cache.end(), [](const auto& it, const auto& smallest) { + return it.second.generation < smallest.second.generation; + })); + } + + return clone; + } + +private: + struct CacheValue + { + ICU::unique_uregex re; + size_t generation = 0; + }; + + struct CacheKeyHasher + { + using is_transparent = void; + + std::size_t operator()(const std::wstring_view& str) const noexcept + { + return til::hash(str); + } + }; + + static constexpr size_t cacheSizeLimit = 128; + wil::srwlock _lock; + std::unordered_map> _cache; + size_t _totalInsertions = 0; +}; + +static URegularExpressionInterner uregexInterner; + +PointTree Terminal::_getPatterns(til::CoordType beg, til::CoordType end) const +{ + static constexpr std::array patterns{ + LR"(\b(?:https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])", + }; + + auto text = ICU::UTextFromTextBuffer(_activeBuffer(), beg, end + 1); + UErrorCode status = U_ZERO_ERROR; + PointTree::interval_vector intervals; + + for (size_t i = 0; i < patterns.size(); ++i) + { + const auto re = uregexInterner.Intern(patterns.at(i)); + uregex_setUText(re.get(), &text, &status); + + if (uregex_find(re.get(), -1, &status)) + { + do + { + auto range = ICU::BufferRangeFromMatch(&text, re.get()); + // PointTree uses half-open ranges. + range.end.x++; + intervals.push_back(PointTree::interval(range.start, range.end, 0)); + } while (uregex_findNext(re.get(), &status)); + } + } + + return PointTree{ std::move(intervals) }; +} + // NOTE: This is the version of AddMark that comes from the UI. The VT api call into this too. void Terminal::AddMark(const ScrollMark& mark, const til::point& start, @@ -1463,31 +1557,37 @@ std::wstring_view Terminal::CurrentCommand() const void Terminal::ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode) { + const auto colorSelection = [this](const til::point coordStart, const til::point coordEnd, const TextAttribute& attr) { + auto& textBuffer = _activeBuffer(); + const auto spanLength = textBuffer.SpanLength(coordStart, coordEnd); + textBuffer.Write(OutputCellIterator(attr, spanLength), coordStart); + }; + for (const auto [start, end] : _GetSelectionSpans()) { try { if (matchMode == winrt::Microsoft::Terminal::Core::MatchMode::None) { - ColorSelection(start, end, attr); + colorSelection(start, end, attr); } else if (matchMode == winrt::Microsoft::Terminal::Core::MatchMode::All) { - const auto textBuffer = _activeBuffer().GetPlainText(start, end); - std::wstring_view text{ textBuffer }; + const auto& textBuffer = _activeBuffer(); + const auto text = textBuffer.GetPlainText(start, end); + std::wstring_view textView{ text }; if (IsBlockSelection()) { - text = Utils::TrimPaste(text); + textView = Utils::TrimPaste(textView); } - if (!text.empty()) + if (!textView.empty()) { - Search search(*this, text, Search::Direction::Forward, Search::Sensitivity::CaseInsensitive, { 0, 0 }); - - while (search.FindNext()) + const auto hits = textBuffer.SearchText(textView, true); + for (const auto& s : hits) { - search.Color(attr); + colorSelection(s.start, s.end, attr); } } } diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 8e63819bdb6..ac5a18ec91f 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -17,7 +17,6 @@ #include -inline constexpr std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" }; inline constexpr size_t TaskbarMinProgress{ 10 }; // You have to forward decl the ICoreSettings here, instead of including the header. @@ -217,7 +216,6 @@ class Microsoft::Terminal::Core::Terminal final : const til::point GetSelectionAnchor() const noexcept override; const til::point GetSelectionEnd() const noexcept override; const std::wstring_view GetConsoleTitle() const noexcept override; - void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute) override; const bool IsUiaDataInitialized() const noexcept override; #pragma endregion @@ -449,6 +447,7 @@ class Microsoft::Terminal::Core::Terminal final : bool _inAltBuffer() const noexcept; TextBuffer& _activeBuffer() const noexcept; void _updateUrlDetection(); + interval_tree::IntervalTree _getPatterns(til::CoordType beg, til::CoordType end) const; #pragma region TextSelection // These methods are defined in TerminalSelection.cpp diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index e19f158adf8..4c31cbc4bbc 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -193,7 +193,6 @@ void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs) const auto cursorSize = _mainBuffer->GetCursor().GetSize(); ClearSelection(); - _mainBuffer->ClearPatternRecognizers(); // Create a new alt buffer _altBuffer = std::make_unique(_altBufferSize, @@ -272,7 +271,6 @@ void Terminal::UseMainScreenBuffer() } // update all the hyperlinks on the screen - _mainBuffer->ClearPatternRecognizers(); _updateUrlDetection(); // GH#3321: Make sure we let the TerminalInput know that we switched diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index 018bf0623c6..5f89c533896 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -495,7 +495,7 @@ void Terminal::SelectHyperlink(const SearchDirection dir) const til::point bufferEnd{ bufferSize.RightInclusive(), ViewEndIndex() }; while (!result && bufferSize.IsInBounds(searchStart) && bufferSize.IsInBounds(searchEnd) && searchStart <= searchEnd && bufferStart <= searchStart && searchEnd <= bufferEnd) { - auto patterns = _activeBuffer().GetPatterns(searchStart.y, searchEnd.y); + auto patterns = _getPatterns(searchStart.y, searchEnd.y); resultList = patterns.findContained(convertToSearchArea(searchStart), convertToSearchArea(searchEnd)); result = extractResultFromList(resultList); if (!result) @@ -887,16 +887,3 @@ void Terminal::_ScrollToPoint(const til::point pos) _activeBuffer().TriggerScroll(); } } - -// Method Description: -// - apply the TextAttribute "attr" to the active buffer -// Arguments: -// - coordStart - where to begin applying attr -// - coordEnd - where to end applying attr (inclusive) -// - attr - the text attributes to apply -void Terminal::ColorSelection(const til::point coordStart, const til::point coordEnd, const TextAttribute attr) -{ - const auto spanLength = _activeBuffer().SpanLength(coordStart, coordEnd); - - _activeBuffer().Write(OutputCellIterator(attr, spanLength), coordStart); -} diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index d85d4e105f8..cc858f74bef 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -247,7 +247,7 @@ try AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); const auto y = cursor.GetPosition().y; - auto& row = textBuffer.GetRowByOffset(y); + auto& row = textBuffer.GetMutableRowByOffset(y); pos.x = textBuffer.GetSize().RightExclusive(); pos.y = y; @@ -408,7 +408,7 @@ try pos.x = 0; } - textBuffer.GetRowByOffset(pos.y).SetWrapForced(false); + textBuffer.GetMutableRowByOffset(pos.y).SetWrapForced(false); pos.y = pos.y + 1; AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); continue; diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index 1f1dadda001..d327c0b9d61 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -418,16 +418,3 @@ const til::point RenderData::GetSelectionEnd() const noexcept return { x_pos, y_pos }; } - -// Routine Description: -// - Given two points in the buffer space, color the selection between the two with the given attribute. -// - This will create an internal selection rectangle covering the two points, assume a line selection, -// and use the first point as the anchor for the selection (as if the mouse click started at that point) -// Arguments: -// - coordSelectionStart - Anchor point (start of selection) for the region to be colored -// - coordSelectionEnd - Other point referencing the rectangle inscribing the selection area -// - attr - Color to apply to region. -void RenderData::ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr) -{ - Selection::Instance().ColorSelection(coordSelectionStart, coordSelectionEnd, attr); -} diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 4d796812a81..5e649353cd7 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -56,6 +56,5 @@ class RenderData final : void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; const til::point GetSelectionAnchor() const noexcept override; const til::point GetSelectionEnd() const noexcept override; - void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr) override; const bool IsUiaDataInitialized() const noexcept override { return true; } }; diff --git a/src/host/selection.cpp b/src/host/selection.cpp index 88b00fd45de..97145a6013a 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -104,7 +104,10 @@ void Selection::_SetSelectionVisibility(const bool fMakeVisible) _PaintSelection(); } - LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); + if (const auto window = ServiceLocator::LocateConsoleWindow()) + { + LOG_IF_FAILED(window->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } } // Routine Description: @@ -269,12 +272,14 @@ void Selection::ExtendSelection(_In_ til::point coordBufferPos) _PaintSelection(); // Fire off an event to let accessibility apps know the selection has changed. - auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); - if (pNotifier) + if (const auto pNotifier = ServiceLocator::LocateAccessibilityNotifier()) { pNotifier->NotifyConsoleCaretEvent(IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection, PACKCOORD(coordBufferPos)); } - LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); + if (const auto window = ServiceLocator::LocateConsoleWindow()) + { + LOG_IF_FAILED(window->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } } // Routine Description: @@ -366,7 +371,10 @@ void Selection::ClearSelection(const bool fStartingNewSelection) { _CancelMarkSelection(); } - LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); + if (const auto window = ServiceLocator::LocateConsoleWindow()) + { + LOG_IF_FAILED(window->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } _dwSelectionFlags = 0; diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp index d85c5835bd0..69d40b9ae79 100644 --- a/src/host/selectionInput.cpp +++ b/src/host/selectionInput.cpp @@ -708,10 +708,11 @@ bool Selection::_HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo) Telemetry::Instance().LogColorSelectionUsed(); - Search search(gci.renderData, str, Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); - while (search.FindNext()) + const auto& textBuffer = gci.renderData.GetTextBuffer(); + const auto hits = textBuffer.SearchText(str, true); + for (const auto& s : hits) { - search.Color(selectionAttr); + ColorSelection(s.start, s.end, selectionAttr); } } } diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp index c8318f118a0..21cff15e815 100644 --- a/src/host/telemetry.cpp +++ b/src/host/telemetry.cpp @@ -21,10 +21,6 @@ TRACELOGGING_DEFINE_PROVIDER(g_hConhostV2EventTraceProvider, // Disable 4351 so we can initialize the arrays to 0 without a warning. #pragma warning(disable : 4351) Telemetry::Telemetry() : - _fpFindStringLengthAverage(0), - _fpDirectionDownAverage(0), - _fpMatchCaseAverage(0), - _uiFindNextClickedTotal(0), _uiColorSelectionUsed(0), _tStartedAt(0), _wchProcessFileNames(), @@ -177,40 +173,6 @@ void Telemetry::LogApiCall(const ApiCall api) _rguiTimesApiUsed[api]++; } -// Log usage of the Find Dialog. -void Telemetry::LogFindDialogNextClicked(const unsigned int uiStringLength, const bool fDirectionDown, const bool fMatchCase) -{ - // Don't send telemetry for every time it's used, as this will help reduce the load on our servers. - // Instead just create a running average of the string length, the direction down radio - // button, and match case checkbox. - _fpFindStringLengthAverage = ((_fpFindStringLengthAverage * _uiFindNextClickedTotal + uiStringLength) / (_uiFindNextClickedTotal + 1)); - _fpDirectionDownAverage = ((_fpDirectionDownAverage * _uiFindNextClickedTotal + (fDirectionDown ? 1 : 0)) / (_uiFindNextClickedTotal + 1)); - _fpMatchCaseAverage = ((_fpMatchCaseAverage * _uiFindNextClickedTotal + (fMatchCase ? 1 : 0)) / (_uiFindNextClickedTotal + 1)); - _uiFindNextClickedTotal++; -} - -// Find dialog was closed, now send out the telemetry. -void Telemetry::FindDialogClosed() -{ - // clang-format off -#pragma prefast(suppress: __WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") - // clang-format on - TraceLoggingWriteTagged(_activity, - "FindDialogUsed", - TraceLoggingValue(_fpFindStringLengthAverage, "StringLengthAverage"), - TraceLoggingValue(_fpDirectionDownAverage, "DirectionDownAverage"), - TraceLoggingValue(_fpMatchCaseAverage, "MatchCaseAverage"), - TraceLoggingValue(_uiFindNextClickedTotal, "FindNextButtonClickedTotal"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - // Get ready for the next time the dialog is used. - _fpFindStringLengthAverage = 0; - _fpDirectionDownAverage = 0; - _fpMatchCaseAverage = 0; - _uiFindNextClickedTotal = 0; -} - // Tries to find the process name amongst our previous process names by doing a binary search. // The main difference between this and the standard bsearch library call, is that if this // can't find the string, it returns the position the new string should be inserted at. This saves diff --git a/src/host/telemetry.hpp b/src/host/telemetry.hpp index 45cbea796a4..4cb2560a3b5 100644 --- a/src/host/telemetry.hpp +++ b/src/host/telemetry.hpp @@ -44,9 +44,7 @@ class Telemetry void LogQuickEditPasteRawUsed(); void LogColorSelectionUsed(); - void LogFindDialogNextClicked(const unsigned int iStringLength, const bool fDirectionDown, const bool fMatchCase); void LogProcessConnected(const HANDLE hProcess); - void FindDialogClosed(); void WriteFinalTraceLog(); void LogRipMessage(_In_z_ const char* pszMessage, ...) const; @@ -138,10 +136,6 @@ class Telemetry TraceLoggingActivity _activity; - float _fpFindStringLengthAverage; - float _fpDirectionDownAverage; - float _fpMatchCaseAverage; - unsigned int _uiFindNextClickedTotal; unsigned int _uiColorSelectionUsed; time_t _tStartedAt; WCHAR const* const c_pwszBashExeName = L"bash.exe"; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index b26801be0a1..1dd058f057d 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -3650,7 +3650,7 @@ void _FillLine(til::point position, T fillContent, TextAttribute fillAttr) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); - auto& row = si.GetTextBuffer().GetRowByOffset(position.y); + auto& row = si.GetTextBuffer().GetMutableRowByOffset(position.y); row.WriteCells({ fillContent, fillAttr }, position.x, false); } diff --git a/src/host/ut_host/SearchTests.cpp b/src/host/ut_host/SearchTests.cpp index faa3935e518..5a2e4b9bd6d 100644 --- a/src/host/ut_host/SearchTests.cpp +++ b/src/host/ut_host/SearchTests.cpp @@ -53,109 +53,109 @@ class SearchTests TEST_METHOD_CLEANUP(MethodCleanup) { m_state->CleanupNewTextBufferInfo(); - + Selection::Instance().ClearSelection(); return true; } - void DoFoundChecks(Search& s, til::point& coordStartExpected, til::CoordType lineDelta) + static void DoFoundChecks(Search& s, til::point coordStartExpected, til::CoordType lineDelta) { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto coordEndExpected = coordStartExpected; coordEndExpected.x += 1; - VERIFY_IS_TRUE(s.FindNext()); - VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); - VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + VERIFY_IS_TRUE(s.SelectCurrent()); + VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); + VERIFY_ARE_EQUAL(coordEndExpected, gci.renderData.GetSelectionEnd()); coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - VERIFY_IS_TRUE(s.FindNext()); - VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); - VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + s.FindNext(); + + VERIFY_IS_TRUE(s.SelectCurrent()); + VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); + VERIFY_ARE_EQUAL(coordEndExpected, gci.renderData.GetSelectionEnd()); coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - VERIFY_IS_TRUE(s.FindNext()); - VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); - VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + s.FindNext(); + + VERIFY_IS_TRUE(s.SelectCurrent()); + VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); + VERIFY_ARE_EQUAL(coordEndExpected, gci.renderData.GetSelectionEnd()); coordStartExpected.y += lineDelta; coordEndExpected.y += lineDelta; - VERIFY_IS_TRUE(s.FindNext()); - VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); - VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + s.FindNext(); - VERIFY_IS_FALSE(s.FindNext()); + VERIFY_IS_TRUE(s.SelectCurrent()); + VERIFY_ARE_EQUAL(coordStartExpected, gci.renderData.GetSelectionAnchor()); + VERIFY_ARE_EQUAL(coordEndExpected, gci.renderData.GetSelectionEnd()); } TEST_METHOD(ForwardCaseSensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - til::point coordStartExpected; - Search s(gci.renderData, L"AB", Search::Direction::Forward, Search::Sensitivity::CaseSensitive); - DoFoundChecks(s, coordStartExpected, 1); + Search s; + s.ResetIfStale(gci.renderData, L"AB", false, false); + DoFoundChecks(s, {}, 1); } TEST_METHOD(ForwardCaseSensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 2, 0 }; - Search s(gci.renderData, L"\x304b", Search::Direction::Forward, Search::Sensitivity::CaseSensitive); - DoFoundChecks(s, coordStartExpected, 1); + Search s; + s.ResetIfStale(gci.renderData, L"\x304b", false, false); + DoFoundChecks(s, { 2, 0 }, 1); } TEST_METHOD(ForwardCaseInsensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - til::point coordStartExpected; - Search s(gci.renderData, L"ab", Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); - DoFoundChecks(s, coordStartExpected, 1); + Search s; + s.ResetIfStale(gci.renderData, L"ab", false, true); + DoFoundChecks(s, {}, 1); } TEST_METHOD(ForwardCaseInsensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 2, 0 }; - Search s(gci.renderData, L"\x304b", Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); - DoFoundChecks(s, coordStartExpected, 1); + Search s; + s.ResetIfStale(gci.renderData, L"\x304b", false, true); + DoFoundChecks(s, { 2, 0 }, 1); } TEST_METHOD(BackwardCaseSensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 0, 3 }; - Search s(gci.renderData, L"AB", Search::Direction::Backward, Search::Sensitivity::CaseSensitive); - DoFoundChecks(s, coordStartExpected, -1); + Search s; + s.ResetIfStale(gci.renderData, L"AB", true, false); + DoFoundChecks(s, { 0, 3 }, -1); } TEST_METHOD(BackwardCaseSensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 2, 3 }; - Search s(gci.renderData, L"\x304b", Search::Direction::Backward, Search::Sensitivity::CaseSensitive); - DoFoundChecks(s, coordStartExpected, -1); + Search s; + s.ResetIfStale(gci.renderData, L"\x304b", true, false); + DoFoundChecks(s, { 2, 3 }, -1); } TEST_METHOD(BackwardCaseInsensitive) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 0, 3 }; - Search s(gci.renderData, L"ab", Search::Direction::Backward, Search::Sensitivity::CaseInsensitive); - DoFoundChecks(s, coordStartExpected, -1); + Search s; + s.ResetIfStale(gci.renderData, L"ab", true, true); + DoFoundChecks(s, { 0, 3 }, -1); } TEST_METHOD(BackwardCaseInsensitiveJapanese) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - til::point coordStartExpected = { 2, 3 }; - Search s(gci.renderData, L"\x304b", Search::Direction::Backward, Search::Sensitivity::CaseInsensitive); - DoFoundChecks(s, coordStartExpected, -1); + Search s; + s.ResetIfStale(gci.renderData, L"\x304b", true, true); + DoFoundChecks(s, { 2, 3 }, -1); } }; diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index bcb4cec4a99..44781d38a72 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -188,7 +188,7 @@ void TextBufferTests::TestWrapFlag() { auto& textBuffer = GetTbi(); - auto& Row = textBuffer.GetRowByOffset(0); + auto& Row = textBuffer.GetMutableRowByOffset(0); // no wrap by default VERIFY_IS_FALSE(Row.WasWrapForced()); @@ -278,7 +278,7 @@ void TextBufferTests::TestDoubleBytePadFlag() { auto& textBuffer = GetTbi(); - auto& Row = textBuffer.GetRowByOffset(0); + auto& Row = textBuffer.GetMutableRowByOffset(0); // no padding by default VERIFY_IS_FALSE(Row.WasDoubleBytePadded()); @@ -300,7 +300,7 @@ void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString, { auto& textBuffer = GetTbi(); - auto& row = textBuffer.GetRowByOffset(0); + auto& row = textBuffer.GetMutableRowByOffset(0); // copy string into buffer for (til::CoordType i = 0; i < cLength; ++i) @@ -571,7 +571,7 @@ void TextBufferTests::TestSetWrapOnCurrentRow() auto sCurrentRow = textBuffer.GetCursor().GetPosition().y; - auto& Row = textBuffer.GetRowByOffset(sCurrentRow); + auto& Row = textBuffer.GetMutableRowByOffset(sCurrentRow); Log::Comment(L"Testing off to on"); @@ -622,7 +622,7 @@ void TextBufferTests::TestIncrementCircularBuffer() textBuffer._firstRow = iRowToTestIndex; // fill first row with some stuff - auto& FirstRow = textBuffer.GetRowByOffset(0); + auto& FirstRow = textBuffer.GetMutableRowByOffset(0); FirstRow.ReplaceCharacters(0, 1, { L"A" }); // ensure it does say that it contains text @@ -1847,7 +1847,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() // This is the negative squared latin capital letter B emoji: 🅱 // It's encoded in UTF-16, as needed by the buffer. const auto bButton = L"\xD83C\xDD71"; - _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, bButton); + _buffer->GetMutableRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, bButton); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); @@ -1888,7 +1888,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() // This is the fire emoji: 🔥 // It's encoded in UTF-16, as needed by the buffer. const auto fire = L"\xD83D\xDD25"; - _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, fire); + _buffer->GetMutableRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, fire); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); @@ -1923,7 +1923,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() // This is the eggplant emoji: 🍆 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF46"; - _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); + _buffer->GetMutableRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); @@ -1953,7 +1953,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() // This is the peach emoji: 🍑 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF51"; - _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); + _buffer->GetMutableRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); @@ -1993,7 +1993,7 @@ void TextBufferTests::TestOverwriteChars() UINT cursorSize = 12; TextAttribute attr{ 0x7f }; TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; - auto& row = buffer.GetRowByOffset(0); + auto& row = buffer.GetMutableRowByOffset(0); // scientist emoji U+1F9D1 U+200D U+1F52C #define complex1 L"\U0001F9D1\U0000200D\U0001F52C" @@ -2009,17 +2009,17 @@ void TextBufferTests::TestOverwriteChars() // Test overwriting wide chars with wide chars slightly shifted left/right. row.ReplaceCharacters(1, 2, complex1); row.ReplaceCharacters(7, 2, complex1); - VERIFY_ARE_EQUAL(L" " complex1 L" " complex1 L" ", row.GetText()); + VERIFY_ARE_EQUAL(L" " complex1 L" " complex1, row.GetText()); // Test overwriting wide chars with wide chars. row.ReplaceCharacters(1, 2, complex2); row.ReplaceCharacters(7, 2, complex2); - VERIFY_ARE_EQUAL(L" " complex2 L" " complex2 L" ", row.GetText()); + VERIFY_ARE_EQUAL(L" " complex2 L" " complex2, row.GetText()); // Test overwriting wide chars with narrow chars. row.ReplaceCharacters(1, 1, simple); row.ReplaceCharacters(8, 1, simple); - VERIFY_ARE_EQUAL(L" " simple L" " simple L" ", row.GetText()); + VERIFY_ARE_EQUAL(L" " simple L" " simple, row.GetText()); // Test clearing narrow/wide chars. row.ReplaceCharacters(0, 1, simple); @@ -2049,7 +2049,7 @@ void TextBufferTests::TestRowReplaceText() static constexpr UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; - auto& row = buffer.GetRowByOffset(0); + auto& row = buffer.GetMutableRowByOffset(0); #define complex L"\U0001F41B" @@ -2755,14 +2755,14 @@ void TextBufferTests::HyperlinkTrim() const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); - _buffer->GetRowByOffset(pos.y).SetAttrToEnd(pos.x, newAttr); + _buffer->GetMutableRowByOffset(pos.y).SetAttrToEnd(pos.x, newAttr); _buffer->AddHyperlinkToMap(url, id); // Set a different hyperlink id somewhere else in the buffer const til::point otherPos{ 70, 5 }; const auto otherId = _buffer->GetHyperlinkId(otherUrl, otherCustomId); newAttr.SetHyperlinkId(otherId); - _buffer->GetRowByOffset(otherPos.y).SetAttrToEnd(otherPos.x, newAttr); + _buffer->GetMutableRowByOffset(otherPos.y).SetAttrToEnd(otherPos.x, newAttr); _buffer->AddHyperlinkToMap(otherUrl, otherId); // Increment the circular buffer @@ -2799,12 +2799,12 @@ void TextBufferTests::NoHyperlinkTrim() const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); - _buffer->GetRowByOffset(pos.y).SetAttrToEnd(pos.x, newAttr); + _buffer->GetMutableRowByOffset(pos.y).SetAttrToEnd(pos.x, newAttr); _buffer->AddHyperlinkToMap(url, id); // Set the same hyperlink id somewhere else in the buffer const til::point otherPos{ 70, 5 }; - _buffer->GetRowByOffset(otherPos.y).SetAttrToEnd(otherPos.x, newAttr); + _buffer->GetMutableRowByOffset(otherPos.y).SetAttrToEnd(otherPos.x, newAttr); // Increment the circular buffer _buffer->IncrementCircularBuffer(); diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 329dd3f46ec..902f711771d 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -373,10 +373,6 @@ class MockRenderData : public IRenderData return {}; } - void ColorSelection(const til::point /*coordSelectionStart*/, const til::point /*coordSelectionEnd*/, const TextAttribute /*attr*/) - { - } - const bool IsUiaDataInitialized() const noexcept { return true; diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp index 9a0e1272b7d..1803acb0ae8 100644 --- a/src/inc/test/CommonState.hpp +++ b/src/inc/test/CommonState.hpp @@ -237,7 +237,7 @@ class CommonState for (til::CoordType iRow = 0; iRow < cRowsToFill; iRow++) { - ROW& row = textBuffer.GetRowByOffset(iRow); + ROW& row = textBuffer.GetMutableRowByOffset(iRow); FillRow(&row, iRow & 1); } diff --git a/src/inc/til/at.h b/src/inc/til/at.h index 9b7c804e839..bb1596a4d69 100644 --- a/src/inc/til/at.h +++ b/src/inc/til/at.h @@ -16,6 +16,7 @@ namespace til template constexpr auto at(T&& cont, const I i) noexcept -> decltype(auto) { +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). #pragma warning(suppress : 26482) // Suppress bounds.2 check for indexing with constant expressions #pragma warning(suppress : 26446) // Suppress bounds.4 check for subscript operator. #pragma warning(suppress : 26445) // Suppress lifetime check for a reference to std::span or std::string_view diff --git a/src/interactivity/win32/find.cpp b/src/interactivity/win32/find.cpp index 896c5885a1b..93c4d55a8b4 100644 --- a/src/interactivity/win32/find.cpp +++ b/src/interactivity/win32/find.cpp @@ -22,17 +22,18 @@ INT_PTR CALLBACK FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM l auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // This bool is used to track which option - up or down - was used to perform the last search. That way, the next time the // find dialog is opened, it will default to the last used option. - static auto fFindSearchUp = true; + static auto reverse = true; + static auto caseInsensitive = true; static std::wstring lastFindString; + static Search searcher; - WCHAR szBuf[SEARCH_STRING_LENGTH + 1]; switch (Message) { case WM_INITDIALOG: SetWindowLongPtrW(hWnd, DWLP_USER, lParam); - SendDlgItemMessageW(hWnd, ID_CONSOLE_FINDSTR, EM_LIMITTEXT, ARRAYSIZE(szBuf) - 1, 0); - CheckRadioButton(hWnd, ID_CONSOLE_FINDUP, ID_CONSOLE_FINDDOWN, (fFindSearchUp ? ID_CONSOLE_FINDUP : ID_CONSOLE_FINDDOWN)); - SetDlgItemText(hWnd, ID_CONSOLE_FINDSTR, lastFindString.c_str()); + CheckRadioButton(hWnd, ID_CONSOLE_FINDUP, ID_CONSOLE_FINDDOWN, (reverse ? ID_CONSOLE_FINDUP : ID_CONSOLE_FINDDOWN)); + CheckDlgButton(hWnd, ID_CONSOLE_FINDCASE, !caseInsensitive); + SetDlgItemTextW(hWnd, ID_CONSOLE_FINDSTR, lastFindString.c_str()); return TRUE; case WM_COMMAND: { @@ -40,44 +41,40 @@ INT_PTR CALLBACK FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM l { case IDOK: { - const auto StringLength = (USHORT)GetDlgItemTextW(hWnd, ID_CONSOLE_FINDSTR, szBuf, ARRAYSIZE(szBuf)); - if (StringLength == 0) - { - lastFindString.clear(); - break; - } - const auto IgnoreCase = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDCASE) == 0; - const auto Reverse = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDDOWN) == 0; - fFindSearchUp = !!Reverse; - auto& ScreenInfo = gci.GetActiveOutputBuffer(); + auto length = SendDlgItemMessageW(hWnd, ID_CONSOLE_FINDSTR, WM_GETTEXTLENGTH, 0, 0); + lastFindString.resize(length); + length = GetDlgItemTextW(hWnd, ID_CONSOLE_FINDSTR, lastFindString.data(), gsl::narrow_cast(length + 1)); + lastFindString.resize(length); + + caseInsensitive = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDCASE) == 0; + reverse = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDDOWN) == 0; - std::wstring wstr(szBuf, StringLength); - lastFindString = wstr; LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - Search search(gci.renderData, - wstr, - Reverse ? Search::Direction::Backward : Search::Direction::Forward, - IgnoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive); - - if (search.FindNext()) + if (searcher.ResetIfStale(gci.renderData, lastFindString, reverse, caseInsensitive)) { - Telemetry::Instance().LogFindDialogNextClicked(StringLength, (Reverse != 0), (IgnoreCase == 0)); - search.Select(); - return TRUE; + searcher.MovePastCurrentSelection(); } else { - // The string wasn't found. - ScreenInfo.SendNotifyBeep(); + searcher.FindNext(); } + + if (searcher.SelectCurrent()) + { + return TRUE; + } + + std::ignore = gci.GetActiveOutputBuffer().SendNotifyBeep(); break; } case IDCANCEL: - Telemetry::Instance().FindDialogClosed(); EndDialog(hWnd, 0); + searcher = Search{}; return TRUE; + default: + break; } break; } diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 8278bfa952a..34b4e20caef 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -361,7 +361,7 @@ class UiaTextRangeTests for (auto i = 0; i < _pTextBuffer->TotalRowCount() / 2; ++i) { const std::wstring_view glyph{ i % 2 == 0 ? L" " : L"X" }; - auto& row = _pTextBuffer->GetRowByOffset(i); + auto& row = _pTextBuffer->GetMutableRowByOffset(i); const auto width = row.size(); for (uint16_t x = 0; x < width; ++x) @@ -489,7 +489,7 @@ class UiaTextRangeTests // Let's start by filling the text buffer with something useful: for (auto i = 0; i < _pTextBuffer->TotalRowCount(); ++i) { - auto& row = _pTextBuffer->GetRowByOffset(i); + auto& row = _pTextBuffer->GetMutableRowByOffset(i); const auto width = row.size(); for (uint16_t x = 0; x < width; ++x) diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index e45dc388ac7..69e0dc2a0e6 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -73,7 +73,6 @@ namespace Microsoft::Console::Render virtual void SelectNewRegion(const til::point coordStart, const til::point coordEnd) = 0; virtual const til::point GetSelectionAnchor() const noexcept = 0; virtual const til::point GetSelectionEnd() const noexcept = 0; - virtual void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr) = 0; virtual const bool IsUiaDataInitialized() const noexcept = 0; }; } diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 92af043e4f6..d49a5b816b7 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -894,7 +894,7 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& { for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetRowByOffset(row); + auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); for (auto col = eraseRect.left; col < eraseRect.right; col++) { // Only unprotected cells are affected. @@ -996,7 +996,7 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec { for (auto row = changeRect.top; row < changeRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetRowByOffset(row); + auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); for (auto col = changeRect.left; col < changeRect.right; col++) { auto attr = rowBuffer.GetAttrByColumn(col); @@ -2407,7 +2407,7 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // If the line was forced to wrap, set the wrap status. // When explicitly moving down a row, clear the wrap status. - textBuffer.GetRowByOffset(currentPosition.y).SetWrapForced(wrapForced); + textBuffer.GetMutableRowByOffset(currentPosition.y).SetWrapForced(wrapForced); // If a carriage return was requested, we move to the leftmost column or // the left margin, depending on whether we started within the margins. @@ -2453,7 +2453,7 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c else { const auto eraseAttributes = _GetEraseAttributes(textBuffer); - textBuffer.GetRowByOffset(newPosition.y).Reset(eraseAttributes); + textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes); } } else diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index da6352358e8..321b86756fb 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -3,8 +3,7 @@ #include "precomp.h" #include "UiaTextRangeBase.hpp" -#include "ScreenInfoUiaProviderBase.h" -#include "../buffer/out/search.h" + #include "UiaTracing.h" using namespace Microsoft::Console::Types; @@ -450,7 +449,7 @@ try // Technically, we'll truncate early if there's an embedded null in the BSTR. // But we're probably fine in this circumstance. - const std::wstring queryFontName{ val.bstrVal }; + const std::wstring_view queryFontName{ val.bstrVal, SysStringLen(val.bstrVal) }; if (queryFontName == _pData->GetFontInfo().GetFaceName()) { Clone(ppRetVal); @@ -608,45 +607,36 @@ try }); RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized()); - const std::wstring queryText{ text, SysStringLen(text) }; - const auto bufferSize = _getOptimizedBufferSize(); - const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive; + const std::wstring_view queryText{ text, SysStringLen(text) }; + auto exclusiveBegin = _start; - auto searchDirection = Search::Direction::Forward; - auto searchAnchor = _start; - if (searchBackward) - { - searchDirection = Search::Direction::Backward; + // MovePastPoint() moves *past* the given point. + // -> We need to turn [_beg,_end) into (_beg,_end). + exclusiveBegin.x--; - // we need to convert the end to inclusive - // because Search operates with an inclusive til::point - searchAnchor = _end; - bufferSize.DecrementInBounds(searchAnchor, true); - } + _searcher.ResetIfStale(*_pData, queryText, searchBackward, ignoreCase); + _searcher.MovePastPoint(searchBackward ? _end : exclusiveBegin); - Search searcher{ *_pData, queryText, searchDirection, sensitivity, searchAnchor }; + til::point hitBeg{ til::CoordTypeMax, til::CoordTypeMax }; + til::point hitEnd{ til::CoordTypeMin, til::CoordTypeMin }; - if (searcher.FindNext()) + if (const auto hit = _searcher.GetCurrent()) { - const auto foundLocation = searcher.GetFoundLocation(); - const auto start = foundLocation.first; - + hitBeg = hit->start; + hitEnd = hit->end; // we need to increment the position of end because it's exclusive - auto end = foundLocation.second; - bufferSize.IncrementInBounds(end, true); - - // make sure what was found is within the bounds of the current range - if ((searchDirection == Search::Direction::Forward && end < _end) || - (searchDirection == Search::Direction::Backward && start > _start)) - { - RETURN_IF_FAILED(Clone(ppRetVal)); - auto& range = static_cast(**ppRetVal); - range._start = start; - range._end = end; + _pData->GetTextBuffer().GetSize().IncrementInBounds(hitEnd, true); + } - UiaTracing::TextRange::FindText(*this, queryText, searchBackward, ignoreCase, range); - } + if (hitBeg >= _start && hitEnd <= _end) + { + RETURN_IF_FAILED(Clone(ppRetVal)); + auto& range = static_cast(**ppRetVal); + range._start = hitBeg; + range._end = hitEnd; + UiaTracing::TextRange::FindText(*this, queryText, searchBackward, ignoreCase, range); } + return S_OK; } CATCH_RETURN(); diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index c69ff5b4a73..eec30a3dcbd 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -18,16 +18,11 @@ Author(s): #pragma once -#include "inc/viewport.hpp" -#include "../buffer/out/textBuffer.hpp" -#include "../renderer/inc/IRenderData.hpp" -#include "unicode.hpp" -#include "IUiaTraceable.h" - #include -#include -#include -#include + +#include "IUiaTraceable.h" +#include "unicode.hpp" +#include "../buffer/out/search.h" #ifdef UNIT_TESTING class UiaTextRangeTests; @@ -126,6 +121,7 @@ namespace Microsoft::Console::Types IRawElementProviderSimple* _pProvider{ nullptr }; std::wstring _wordDelimiters{}; + ::Search _searcher; virtual void _TranslatePointToScreen(til::point* clientPoint) const = 0; virtual void _TranslatePointFromScreen(til::point* screenPoint) const = 0; diff --git a/src/types/UiaTracing.cpp b/src/types/UiaTracing.cpp index 9a04103448e..dae05a073b9 100644 --- a/src/types/UiaTracing.cpp +++ b/src/types/UiaTracing.cpp @@ -63,14 +63,14 @@ UiaTracing::~UiaTracing() noexcept TraceLoggingUnregister(g_UiaProviderTraceProvider); } -inline std::wstring UiaTracing::_getValue(const ScreenInfoUiaProviderBase& siup) noexcept +std::wstring UiaTracing::_getValue(const ScreenInfoUiaProviderBase& siup) noexcept { std::wstringstream stream; stream << "_id: " << siup.GetId(); return stream.str(); } -inline std::wstring UiaTracing::_getValue(const UiaTextRangeBase& utr) noexcept +std::wstring UiaTracing::_getValue(const UiaTextRangeBase& utr) noexcept try { const auto start = utr.GetEndpoint(TextPatternRangeEndpoint_Start); @@ -90,7 +90,7 @@ catch (...) return {}; } -inline std::wstring UiaTracing::_getValue(const TextPatternRangeEndpoint endpoint) noexcept +std::wstring UiaTracing::_getValue(const TextPatternRangeEndpoint endpoint) noexcept { switch (endpoint) { @@ -103,7 +103,7 @@ inline std::wstring UiaTracing::_getValue(const TextPatternRangeEndpoint endpoin } } -inline std::wstring UiaTracing::_getValue(const TextUnit unit) noexcept +std::wstring UiaTracing::_getValue(const TextUnit unit) noexcept { switch (unit) { @@ -126,7 +126,7 @@ inline std::wstring UiaTracing::_getValue(const TextUnit unit) noexcept } } -inline std::wstring UiaTracing::_getValue(const VARIANT val) noexcept +std::wstring UiaTracing::_getValue(const VARIANT val) noexcept { // This is not a comprehensive conversion of VARIANT result to string // We're only including the one's we need at this time. @@ -148,7 +148,7 @@ inline std::wstring UiaTracing::_getValue(const VARIANT val) noexcept } } -inline std::wstring UiaTracing::_getValue(const AttributeType attrType) noexcept +std::wstring UiaTracing::_getValue(const AttributeType attrType) noexcept { switch (attrType) { @@ -263,7 +263,7 @@ void UiaTracing::TextRange::FindAttribute(const UiaTextRangeBase& utr, TEXTATTRI } } -void UiaTracing::TextRange::FindText(const UiaTextRangeBase& base, std::wstring text, bool searchBackward, bool ignoreCase, const UiaTextRangeBase& result) noexcept +void UiaTracing::TextRange::FindText(const UiaTextRangeBase& base, const std::wstring_view& text, bool searchBackward, bool ignoreCase, const UiaTextRangeBase& result) noexcept { EnsureRegistration(); if (TraceLoggingProviderEnabled(g_UiaProviderTraceProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) @@ -272,7 +272,7 @@ void UiaTracing::TextRange::FindText(const UiaTextRangeBase& base, std::wstring g_UiaProviderTraceProvider, "UiaTextRange::FindText", TraceLoggingValue(_getValue(base).c_str(), "base"), - TraceLoggingValue(text.c_str(), "text"), + TraceLoggingCountedWideString(text.data(), (ULONG)text.size(), "text"), TraceLoggingValue(searchBackward, "searchBackward"), TraceLoggingValue(ignoreCase, "ignoreCase"), TraceLoggingValue(_getValue(result).c_str(), "result"), @@ -326,7 +326,7 @@ void UiaTracing::TextRange::GetEnclosingElement(const UiaTextRangeBase& utr) noe } } -void UiaTracing::TextRange::GetText(const UiaTextRangeBase& utr, int maxLength, std::wstring result) noexcept +void UiaTracing::TextRange::GetText(const UiaTextRangeBase& utr, int maxLength, const std::wstring_view& result) noexcept { EnsureRegistration(); if (TraceLoggingProviderEnabled(g_UiaProviderTraceProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) @@ -336,7 +336,7 @@ void UiaTracing::TextRange::GetText(const UiaTextRangeBase& utr, int maxLength, "UiaTextRange::GetText", TraceLoggingValue(_getValue(utr).c_str(), "base"), TraceLoggingValue(maxLength, "maxLength"), - TraceLoggingValue(result.c_str(), "result"), + TraceLoggingCountedWideString(result.data(), (ULONG)result.size(), "result"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } diff --git a/src/types/UiaTracing.h b/src/types/UiaTracing.h index f9752caf480..58c95231a10 100644 --- a/src/types/UiaTracing.h +++ b/src/types/UiaTracing.h @@ -46,11 +46,11 @@ namespace Microsoft::Console::Types static void CompareEndpoints(const UiaTextRangeBase& base, const TextPatternRangeEndpoint endpoint, const UiaTextRangeBase& other, TextPatternRangeEndpoint otherEndpoint, int result) noexcept; static void ExpandToEnclosingUnit(TextUnit unit, const UiaTextRangeBase& result) noexcept; static void FindAttribute(const UiaTextRangeBase& base, TEXTATTRIBUTEID attributeId, VARIANT val, BOOL searchBackwards, const UiaTextRangeBase& result, AttributeType attrType = AttributeType::Standard) noexcept; - static void FindText(const UiaTextRangeBase& base, std::wstring text, bool searchBackward, bool ignoreCase, const UiaTextRangeBase& result) noexcept; + static void FindText(const UiaTextRangeBase& base, const std::wstring_view& text, bool searchBackward, bool ignoreCase, const UiaTextRangeBase& result) noexcept; static void GetAttributeValue(const UiaTextRangeBase& base, TEXTATTRIBUTEID id, VARIANT result, AttributeType attrType = AttributeType::Standard) noexcept; static void GetBoundingRectangles(const UiaTextRangeBase& base) noexcept; static void GetEnclosingElement(const UiaTextRangeBase& base) noexcept; - static void GetText(const UiaTextRangeBase& base, int maxLength, std::wstring result) noexcept; + static void GetText(const UiaTextRangeBase& base, int maxLength, const std::wstring_view& result) noexcept; static void Move(TextUnit unit, int count, int resultCount, const UiaTextRangeBase& result) noexcept; static void MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count, int resultCount, const UiaTextRangeBase& result) noexcept; static void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, const UiaTextRangeBase& other, TextPatternRangeEndpoint otherEndpoint, const UiaTextRangeBase& result) noexcept; @@ -104,13 +104,13 @@ namespace Microsoft::Console::Types UiaTracing& operator=(const UiaTracing&) = delete; UiaTracing& operator=(UiaTracing&&) = delete; - static inline std::wstring _getValue(const ScreenInfoUiaProviderBase& siup) noexcept; - static inline std::wstring _getValue(const UiaTextRangeBase& utr) noexcept; - static inline std::wstring _getValue(const TextPatternRangeEndpoint endpoint) noexcept; - static inline std::wstring _getValue(const TextUnit unit) noexcept; + static std::wstring _getValue(const ScreenInfoUiaProviderBase& siup) noexcept; + static std::wstring _getValue(const UiaTextRangeBase& utr) noexcept; + static std::wstring _getValue(const TextPatternRangeEndpoint endpoint) noexcept; + static std::wstring _getValue(const TextUnit unit) noexcept; - static inline std::wstring _getValue(const AttributeType attrType) noexcept; - static inline std::wstring _getValue(const VARIANT val) noexcept; + static std::wstring _getValue(const AttributeType attrType) noexcept; + static std::wstring _getValue(const VARIANT val) noexcept; // these are used to assign IDs to new UiaTextRanges and ScreenInfoUiaProviders respectively static IdType _utrId; From 2fb4a7fa91201eab1fe9667dea2073bc158188bc Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Fri, 25 Aug 2023 09:38:41 -0700 Subject: [PATCH 42/59] Make screen reader announce successful MovePane and MoveTab actions (#15771) Uses the `RaiseNotificationEvent()` API from UIA automation peers to announce successful `MovePane` and `MoveTab` actions. The announcements are localized in the resw file. Closes #15159 Based on #13575 --- .../TerminalApp/AppActionHandlers.cpp | 4 +- .../Resources/en-US/Resources.resw | 28 ++++++++ src/cascadia/TerminalApp/TabManagement.cpp | 9 +++ src/cascadia/TerminalApp/TerminalPage.cpp | 65 ++++++++++++++++++- src/cascadia/TerminalApp/TerminalPage.h | 1 + 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 9e6db17ca29..9d7d835597d 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -232,7 +232,7 @@ namespace winrt::TerminalApp::implementation } else if (const auto& realArgs = args.ActionArgs().try_as()) { - auto moved = _MovePane(realArgs); + const auto moved = _MovePane(realArgs); args.Handled(moved); } } @@ -839,7 +839,7 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = actionArgs.ActionArgs().try_as()) { - auto moved = _MoveTab(_senderOrFocusedTab(sender), realArgs); + const auto moved = _MoveTab(_senderOrFocusedTab(sender), realArgs); actionArgs.Handled(moved); } } diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 4d1fd13f202..df28d046daf 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -860,6 +860,34 @@ Run as Administrator This text is displayed on context menu for profile entries in add new tab button. + + Active pane moved to "{0}" tab + {Locked="{0}"}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. + + + "{0}" tab moved to "{1}" window + {Locked="{0}"}{Locked="{1}"}This text is read out by screen readers upon a successful tab movement. {0} is the name of the tab. {1} is the name of the window the tab was moved to. + + + "{0}" tab moved to new window + {Locked="{0}"}This text is read out by screen readers upon a successful tab movement. {0} is the name of the tab. + + + "{0}" tab moved to position "{1}" + {Locked="{0}"}{Locked="{1}"}This text is read out by screen readers upon a successful tab movement. {0} is the name of the tab. {1} is the new tab index in the tab row. + + + 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. + + + Active pane moved to new window + This text is read out by screen readers upon a successful pane movement to a new window. + + + Active pane moved to new tab + This text is read out by screen readers upon a successful pane movement to a new tab within the existing window. + If set, the command will be appended to the profile's default command instead of replacing it. diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 4a673536e7d..0d8bcbee26f 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -1044,6 +1044,15 @@ namespace winrt::TerminalApp::implementation _tabView.TabItems().RemoveAt(currentTabIndex); _tabView.TabItems().InsertAt(newTabIndex, tabViewItem); _tabView.SelectedItem(tabViewItem); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = tab.Title(); + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + fmt::format(std::wstring_view{ RS_(L"TerminalPage_TabMovedAnnouncement_Direction") }, tabTitle, newTabIndex + 1), + L"TerminalPageMoveTabWithDirection" /* unique name for this notification category */); + } } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index bc55b11a88f..024ae18c10a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -287,6 +287,11 @@ namespace winrt::TerminalApp::implementation ShowSetAsDefaultInfoBar(); } + Windows::UI::Xaml::Automation::Peers::AutomationPeer TerminalPage::OnCreateAutomationPeer() + { + return Automation::Peers::FrameworkElementAutomationPeer(*this); + } + // Method Description: // - This is a bit of trickiness: If we're running unelevated, and the user // passed in only --elevate actions, the we don't _actually_ want to @@ -2025,10 +2030,31 @@ 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), args.Window(), args.TabIndex()); + _MoveContent(std::move(startupActions.args), windowId, tabIdx); focusedTab->DetachPane(); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewWindow"), + L"TerminalPageMovePaneToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + fmt::format(std::wstring_view{ RS_(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow") }, tabTitle, windowId), + L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); + } + } return true; } } @@ -2054,11 +2080,27 @@ namespace winrt::TerminalApp::implementation auto pane = focusedTab->DetachPane(); targetTab->AttachPane(pane); _SetFocusedTab(*targetTab); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = targetTab->Title(); + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + fmt::format(std::wstring_view{ RS_(L"TerminalPage_PaneMovedAnnouncement_ExistingTab") }, tabTitle), + L"TerminalPageMovePaneToExistingTab" /* unique name for this notification category */); + } } else { auto pane = focusedTab->DetachPane(); _CreateNewTabFromPane(pane); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewTab"), + L"TerminalPageMovePaneToNewTab" /* unique name for this notification category */); + } } return true; @@ -2135,8 +2177,26 @@ namespace winrt::TerminalApp::implementation { auto startupActions = tab->BuildStartupActions(true); _DetachTabFromWindow(tab); - _MoveContent(std::move(startupActions), args.Window(), 0); + _MoveContent(std::move(startupActions), windowId, 0); _RemoveTab(*tab); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = tab->Title(); + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + fmt::format(std::wstring_view{ RS_(L"TerminalPage_TabMovedAnnouncement_NewWindow") }, tabTitle), + L"TerminalPageMoveTabToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + fmt::format(std::wstring_view{ RS_(L"TerminalPage_TabMovedAnnouncement_Default") }, tabTitle, windowId), + L"TerminalPageMoveTabToExistingWindow" /* unique name for this notification category */); + } + } return true; } } @@ -5097,5 +5157,4 @@ namespace winrt::TerminalApp::implementation return profileMenuItemFlyout; } - } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 1c684c59936..fd63f7cc674 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -101,6 +101,7 @@ namespace winrt::TerminalApp::implementation void SetSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings, bool needRefreshUI); void Create(); + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); From 821ae3af2d311350fbfaa4c77af09858488681e9 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 25 Aug 2023 20:25:39 +0200 Subject: [PATCH 43/59] Rewrite COOKED_READ_DATA (#15783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This massive refactoring has two goals: * Enable us to go beyond UCS-2 support for input editing * Bring clarity into `COOKED_READ_DATA`'s inner workings Unfortunately, over time, knowledge about its exact operation was lost. While the new code is still complex it reduces the amount of code by 4x which will make preserving knowledge hopefully significantly easier. The new implementation is simpler and slower than the old one in a way, because every time the input line is modified it's rewritten to the text buffer from scratch. This however massively simplifies the underlying algorithm and the amount of state that needs to be tracked and results in a significant reduction in code size. It also makes it more robust, because there's less code now that can be incorrect. This "optimization laziness" can be afforded due the recent >10x improvements to `TextBuffer`'s text ingestion performance. For short inputs (<1000 characters) I still expect this implementation to outperform the conhost from the past. It has received one optimization already however: While reading text from the `InputBuffer` we'll now defer writing into the `TextBuffer` until we've stopped reading. This improves the overhead of pasting text from O(n^2) to O(n), which is immediately noticeable for inputs >100kB. Resizing the text buffer still ends up corrupting the input line however, which unfortunately cannot be fixed in `COOKED_READ_DATA`. The issue occurs due to bugs in `TextBuffer::Reflow` itself, as it misplaces the cursor if the prompt is on the last line of the buffer. Closes #1377 Closes #1503 Closes #4628 Closes #4975 Closes #5033 Closes #8008 This commit is required to fix #797 ## Validation Steps Performed * ASCII input ✅ * Chinese input (中文維基百科) ❔ * Resizing the window properly wraps/unwraps wide glyphs ❌ Broken due to `TextBuffer::Reflow` bugs * Surrogate pair input (🙂) ❔ * Resizing the window properly wraps/unwraps surrogate pairs ❌ Broken due to `TextBuffer::Reflow` bugs * In cmd.exe * Create 2 file: "a😊b.txt" and "a😟b.txt" * Press tab: Autocompletes "a😊b.txt" ✅ * Navigate the cursor right past the "a" * Press tab twice: Autocompletes "a😟b.txt" ✅ * Backspace deletes preceding glyphs ✅ * Ctrl+Backspace deletes preceding words ✅ * Escape clears input ✅ * Home navigates to start ✅ * Ctrl+Home deletes text between cursor and start ✅ * End navigates to end ✅ * Ctrl+End deletes text between cursor and end ✅ * Left navigates over previous code points ✅ * Ctrl+Left navigates to previous word-starts ✅ * Right and F1 navigate over next code points ✅ * Pressing right at the end of input copies characters from the previous command ✅ * Ctrl+Right navigates to next word-ends ✅ * Insert toggles overwrite mode ✅ * Delete deletes next code point ✅ * Up and F5 cycle through history ✅ * Doesn't crash with no history ✅ * Stops at first entry ✅ * Down cycles through history ✅ * Doesn't crash with no history ✅ * Stops at last entry ✅ * PageUp retrieves the oldest command ✅ * PageDown retrieves the newest command ✅ * F2 starts "copy to char" prompt ✅ * Escape dismisses prompt ✅ * Typing a character copies text from the previous command up until that character into the current buffer (acts identical to F3, but with automatic character search) ✅ * F3 copies the previous command into the current buffer, starting at the current cursor position, for as many characters as possible ✅ * Doesn't erase trailing text if the current buffer is longer than the previous command ✅ * Puts the cursor at the end of the copied text ✅ * F4 starts "copy from char" prompt ✅ * Escape dismisses prompt ✅ * Erases text between the current cursor position and the first instance of a given char (but not including it) ✅ * F6 inserts Ctrl+Z ✅ * F7 without modifiers starts "command list" prompt ✅ * Escape dismisses prompt ✅ * Minimum size of 40x10 characters ✅ * Width expands to fit the widest history command ✅ * Height expands up to 20 rows with longer histories ✅ * F9 starts "command number" prompt ✅ * Left/Right paste replace the buffer with the given command ✅ * And put cursor at the end of the buffer ✅ * Up/Down navigate selection through history ✅ * Stops at start/end with <10 entries ✅ * Stops at start/end with >20 entries ✅ * Wide text rendering during pagination with >20 entries ✅ * Shift+Up/Down moves history items around ✅ * Home navigates to first entry ✅ * End navigates to last entry ✅ * PageUp navigates by 20 items at a time or to first ✅ * PageDown navigates by 20 items at a time or to last ✅ * Alt+F7 clears command history ✅ * F8 cycles through commands that start with the same text as the current buffer up until the current cursor position ✅ * Doesn't crash with no history ✅ * F9 starts "command number" prompt ✅ * Escape dismisses prompt ✅ * Ignores non-ASCII-decimal characters ✅ * Allows entering between 1 and 5 digits ✅ * Pressing Enter fetches the given command from the history ✅ * Alt+F10 clears doskey aliases ✅ --- .github/actions/spelling/expect/alphabet.txt | 1 + .github/actions/spelling/expect/expect.txt | 13 +- src/buffer/out/textBuffer.cpp | 58 + src/buffer/out/textBuffer.hpp | 2 + .../ConptyRoundtripTests.cpp | 24 +- src/host/CommandListPopup.cpp | 549 ----- src/host/CommandListPopup.hpp | 50 - src/host/CommandNumberPopup.cpp | 195 -- src/host/CommandNumberPopup.hpp | 45 - src/host/CopyFromCharPopup.cpp | 63 - src/host/CopyFromCharPopup.hpp | 29 - src/host/CopyToCharPopup.cpp | 84 - src/host/CopyToCharPopup.hpp | 32 - src/host/_stream.cpp | 352 +--- src/host/_stream.h | 67 +- src/host/alias.cpp | 90 +- src/host/alias.h | 17 +- src/host/cmdline.cpp | 1237 +---------- src/host/cmdline.h | 87 +- src/host/consoleInformation.cpp | 5 + src/host/ft_fuzzer/fuzzmain.cpp | 19 +- src/host/getset.cpp | 14 - src/host/history.cpp | 60 +- src/host/history.h | 9 +- src/host/host-common.vcxitems | 10 - src/host/input.cpp | 4 +- src/host/input.h | 5 - src/host/lib/hostlib.vcxproj.filters | 30 - src/host/misc.cpp | 188 +- src/host/misc.h | 21 - src/host/popup.cpp | 317 --- src/host/popup.h | 82 - src/host/readDataCooked.cpp | 1835 +++++++++-------- src/host/readDataCooked.hpp | 261 ++- src/host/screenInfo.cpp | 103 +- src/host/screenInfo.hpp | 6 +- src/host/scrolling.cpp | 2 +- src/host/selectionInput.cpp | 58 +- src/host/server.h | 18 +- src/host/sources.inc | 5 - src/host/stream.cpp | 89 +- src/host/stream.h | 14 - src/host/tracing.cpp | 7 +- src/host/tracing.hpp | 2 +- src/host/ut_host/AliasTests.cpp | 250 +-- src/host/ut_host/CommandLineTests.cpp | 539 ----- src/host/ut_host/CommandListPopupTests.cpp | 538 ----- src/host/ut_host/CommandNumberPopupTests.cpp | 297 --- src/host/ut_host/CopyFromCharPopupTests.cpp | 165 -- src/host/ut_host/CopyToCharPopupTests.cpp | 243 --- src/host/ut_host/Host.UnitTests.vcxproj | 6 - .../ut_host/Host.UnitTests.vcxproj.filters | 18 - src/host/ut_host/PopupTestHelper.hpp | 84 - src/host/ut_host/ScreenBufferTests.cpp | 20 +- src/host/ut_host/SelectionTests.cpp | 84 - src/host/ut_host/TextBufferTests.cpp | 52 +- src/host/ut_host/sources | 5 - src/interactivity/win32/menu.cpp | 6 - src/interactivity/win32/windowio.cpp | 2 +- 59 files changed, 1410 insertions(+), 7058 deletions(-) delete mode 100644 src/host/CommandListPopup.cpp delete mode 100644 src/host/CommandListPopup.hpp delete mode 100644 src/host/CommandNumberPopup.cpp delete mode 100644 src/host/CommandNumberPopup.hpp delete mode 100644 src/host/CopyFromCharPopup.cpp delete mode 100644 src/host/CopyFromCharPopup.hpp delete mode 100644 src/host/CopyToCharPopup.cpp delete mode 100644 src/host/CopyToCharPopup.hpp delete mode 100644 src/host/popup.cpp delete mode 100644 src/host/popup.h delete mode 100644 src/host/ut_host/CommandLineTests.cpp delete mode 100644 src/host/ut_host/CommandListPopupTests.cpp delete mode 100644 src/host/ut_host/CommandNumberPopupTests.cpp delete mode 100644 src/host/ut_host/CopyFromCharPopupTests.cpp delete mode 100644 src/host/ut_host/CopyToCharPopupTests.cpp delete mode 100644 src/host/ut_host/PopupTestHelper.hpp diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt index b2c7597eee8..97180b72115 100644 --- a/.github/actions/spelling/expect/alphabet.txt +++ b/.github/actions/spelling/expect/alphabet.txt @@ -21,6 +21,7 @@ BBBBCCCCC BBGGRR efg EFG +efgh EFGh KLMNOQQQQQQQQQQ QQQQQQQQQQABCDEFGHIJ diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 2e552912cfa..2826e125a02 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -17,7 +17,6 @@ ADDALIAS ADDREF ADDSTRING ADDTOOL -AEnd AFew AFill AFX @@ -679,7 +678,7 @@ FSINFOCLASS fte Ftm Fullscreens -fullwidth +Fullwidth FUNCTIONCALL fuzzer fuzzmain @@ -923,7 +922,6 @@ itermcolors ITerminal itf Ith -itoa IUI IUnknown ivalid @@ -1102,6 +1100,7 @@ MDs MEASUREITEM megamix memallocator +meme MENUCHAR MENUCONTROL MENUDROPALIGNMENT @@ -1588,7 +1587,6 @@ rgrc rgs rgui rgw -rgwch RIGHTALIGN RIGHTBUTTON riid @@ -1783,7 +1781,6 @@ STDMETHODCALLTYPE STDMETHODIMP STGM stl -stoutapot Stri Stringable STRINGTABLE @@ -1838,7 +1835,6 @@ TBM tchar TCHFORMAT TCI -tcome tcommandline tcommands Tdd @@ -1865,8 +1861,7 @@ testname TESTNULL testpass testpasses -testtestabc -testtesttesttesttest +testtimeout TEXCOORD texel TExpected @@ -2083,7 +2078,6 @@ vtseq vtterm vttest VWX -waaay waitable WANSUNG WANTARROWS @@ -2201,7 +2195,6 @@ wprp wprpi wregex writeback -writechar WRITECONSOLE WRITECONSOLEINPUT WRITECONSOLEOUTPUT diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index f5bfdeda3ce..84336eaf03e 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -416,6 +416,64 @@ size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) return til::utf16_iterate_prev(chars, position); } +// Pretend as if `position` is a regular cursor in the TextBuffer. +// This function will then pretend as if you pressed the left/right arrow +// keys `distance` amount of times (negative = left, positive = right). +til::point TextBuffer::NavigateCursor(til::point position, til::CoordType distance) const +{ + const til::CoordType maxX = _width - 1; + const til::CoordType maxY = _height - 1; + auto x = std::clamp(position.x, 0, maxX); + auto y = std::clamp(position.y, 0, maxY); + auto row = &GetRowByOffset(y); + + if (distance < 0) + { + do + { + if (x > 0) + { + x = row->NavigateToPrevious(x); + } + else if (y <= 0) + { + break; + } + else + { + --y; + row = &GetRowByOffset(y); + x = row->GetReadableColumnCount() - 1; + } + } while (++distance != 0); + } + else if (distance > 0) + { + auto rowWidth = row->GetReadableColumnCount(); + + do + { + if (x < rowWidth) + { + x = row->NavigateToNext(x); + } + else if (y >= maxY) + { + break; + } + else + { + ++y; + row = &GetRowByOffset(y); + rowWidth = row->GetReadableColumnCount(); + x = 0; + } + } while (--distance != 0); + } + + return { x, y }; +} + // This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row. // You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit. void TextBuffer::Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state) diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index a57474b6b81..360c74aab8c 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -138,6 +138,8 @@ class TextBuffer final static size_t GraphemeNext(const std::wstring_view& chars, size_t position) noexcept; static size_t GraphemePrev(const std::wstring_view& chars, size_t position) noexcept; + til::point NavigateCursor(til::point position, til::CoordType distance) const; + // Text insertion functions void Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state); void FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes); diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 76065f93c95..3c5136b1910 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -21,7 +21,6 @@ #include "../host/readDataCooked.hpp" #include "../host/output.h" #include "../host/_stream.h" // For WriteCharsLegacy -#include "../host/cmdline.h" // For WC_INTERACTIVE #include "test/CommonState.hpp" #include "../cascadia/TerminalCore/Terminal.hpp" @@ -3165,20 +3164,6 @@ void ConptyRoundtripTests::NewLinesAtBottomWithBackground() verifyBuffer(*termTb, term->_mutableViewport.ToExclusive()); } -void doWriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view string, DWORD flags = 0) -{ - auto dwNumBytes = string.size() * sizeof(wchar_t); - VERIFY_NT_SUCCESS(WriteCharsLegacy(screenInfo, - string.data(), - string.data(), - string.data(), - &dwNumBytes, - nullptr, - screenInfo.GetTextBuffer().GetCursor().GetPosition().x, - flags, - nullptr)); -} - void ConptyRoundtripTests::WrapNewLineAtBottom() { // The actual bug case is @@ -3220,11 +3205,6 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() return; } - // I've tested this with 0x0, 0x4, 0x80, 0x84, and 0-8, and none of these - // flags seem to make a difference. So we're just assuming 0 here, so we - // don't test a bunch of redundant cases. - const auto writeCharsLegacyMode = 0; - // This test was originally written for // https://github.com/microsoft/terminal/issues/5691 // @@ -3263,7 +3243,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, writeCharsLegacyMode); + WriteCharsLegacy(si, str, false, nullptr); } }; @@ -3421,7 +3401,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, WC_INTERACTIVE); + WriteCharsLegacy(si, str, true, nullptr); } }; diff --git a/src/host/CommandListPopup.cpp b/src/host/CommandListPopup.cpp deleted file mode 100644 index b530ca2760d..00000000000 --- a/src/host/CommandListPopup.cpp +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "CommandListPopup.hpp" -#include "stream.h" -#include "_stream.h" -#include "cmdline.h" -#include "misc.h" -#include "_output.h" -#include "dbcs.h" -#include "../types/inc/GlyphWidth.hpp" - -#include "../interactivity/inc/ServiceLocator.hpp" - -static constexpr size_t COMMAND_NUMBER_SIZE = 8; // size of command number buffer - -// Routine Description: -// - Calculates what the proposed size of the popup should be, based on the commands in the history -// Arguments: -// - history - the history to look through to measure command sizes -// Return Value: -// - the proposed size of the popup with the history list taken into account -static til::size calculatePopupSize(const CommandHistory& history) -{ - // this is the historical size of the popup, so it is now used as a minimum - const til::size minSize = { 40, 10 }; - - // padding is for the command number listing before a command is printed to the window. - // ex: |10: echo blah - // ^^^^ <- these are the cells that are being accounted for by padding - const size_t padding = 4; - - // find the widest command history item and use it for the width - size_t width = minSize.width; - for (CommandHistory::Index i = 0; i < history.GetNumberOfCommands(); ++i) - { - const auto& historyItem = history.GetNth(i); - width = std::max(width, historyItem.size() + padding); - } - if (width > SHRT_MAX) - { - width = SHRT_MAX; - } - - // calculate height, it can range up to 20 rows - auto height = std::clamp(gsl::narrow(history.GetNumberOfCommands()), minSize.height, 20); - - return { gsl::narrow_cast(width), height }; -} - -CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) : - Popup(screenInfo, calculatePopupSize(history)), - _history{ history }, - _currentCommand{ std::min(history.LastDisplayed, history.GetNumberOfCommands() - 1) } -{ - FAIL_FAST_IF(_currentCommand < 0); - _setBottomIndex(); -} - -[[nodiscard]] NTSTATUS CommandListPopup::_handlePopupKeys(COOKED_READ_DATA& cookedReadData, - const wchar_t wch, - const DWORD modifiers) noexcept -{ - try - { - CommandHistory::Index Index = 0; - const auto shiftPressed = WI_IsFlagSet(modifiers, SHIFT_PRESSED); - switch (wch) - { - case VK_F9: - { - const auto hr = CommandLine::Instance().StartCommandNumberPopup(cookedReadData); - if (S_FALSE == hr) - { - // If we couldn't make the popup, break and go around to read another input character. - break; - } - else - { - return hr; - } - } - case VK_ESCAPE: - CommandLine::Instance().EndCurrentPopup(); - return CONSOLE_STATUS_WAIT_NO_BLOCK; - case VK_UP: - if (shiftPressed) - { - return _swapUp(cookedReadData); - } - else - { - _update(-1); - } - break; - case VK_DOWN: - if (shiftPressed) - { - return _swapDown(cookedReadData); - } - else - { - _update(1); - } - break; - case VK_END: - // Move waaay forward, UpdateCommandListPopup() can handle it. - _update(cookedReadData.History().GetNumberOfCommands()); - break; - case VK_HOME: - // Move waaay back, UpdateCommandListPopup() can handle it. - _update(-cookedReadData.History().GetNumberOfCommands()); - break; - case VK_PRIOR: - _update(-Height()); - break; - case VK_NEXT: - _update(Height()); - break; - case VK_DELETE: - return _deleteSelection(cookedReadData); - case VK_LEFT: - case VK_RIGHT: - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, Index); - return CONSOLE_STATUS_WAIT_NO_BLOCK; - default: - break; - } - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_setBottomIndex() -{ - if (_currentCommand < _history.GetNumberOfCommands() - Height()) - { - _bottomIndex = std::max(_currentCommand, Height() - 1); - } - else - { - _bottomIndex = _history.GetNumberOfCommands() - 1; - } -} - -[[nodiscard]] NTSTATUS CommandListPopup::_deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - history.Remove(_currentCommand); - _setBottomIndex(); - - if (history.GetNumberOfCommands() == 0) - { - // close the popup - return CONSOLE_STATUS_READ_COMPLETE; - } - else if (_currentCommand >= history.GetNumberOfCommands()) - { - _currentCommand = history.GetNumberOfCommands() - 1; - _bottomIndex = _currentCommand; - } - - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item up in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapUp(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == 0) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand - 1); - _update(-1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item down in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapDown(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == history.GetNumberOfCommands() - 1) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand + 1); - _update(1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) -{ - CommandHistory::Index Index = 0; - auto Status = STATUS_SUCCESS; - DWORD LineCount = 1; - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, Index); - cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status); - // complete read - if (cookedReadData.IsEchoInput()) - { - // check for alias - cookedReadData.ProcessAliases(LineCount); - } - - Status = STATUS_SUCCESS; - size_t NumBytes; - if (cookedReadData.BytesRead() > cookedReadData.UserBufferSize() || LineCount > 1) - { - if (LineCount > 1) - { - const wchar_t* Tmp; - for (Tmp = cookedReadData.BufferStartPtr(); *Tmp != UNICODE_LINEFEED; Tmp++) - { - FAIL_FAST_IF(!(Tmp < (cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()))); - } - NumBytes = (Tmp - cookedReadData.BufferStartPtr() + 1) * sizeof(*Tmp); - } - else - { - NumBytes = cookedReadData.UserBufferSize(); - } - - // Copy what we can fit into the user buffer - const auto bytesWritten = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); - - // Store all of the remaining as pending until the next read operation. - cookedReadData.SavePendingInput(NumBytes / sizeof(wchar_t), LineCount > 1); - NumBytes = bytesWritten; - } - else - { - NumBytes = cookedReadData.BytesRead(); - NumBytes = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); - } - - cookedReadData.SetReportedByteCount(NumBytes); -} - -void CommandListPopup::_cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch) -{ - CommandHistory::Index Index = 0; - if (cookedReadData.History().FindMatchingCommand({ &wch, 1 }, - _currentCommand, - Index, - CommandHistory::MatchOptions::JustLooking)) - { - _update(Index - _currentCommand, true); - } -} - -// Routine Description: -// - This routine handles the command list popup. It returns when we're out of input or the user has selected a command line. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandListPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - - for (;;) - { - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - if (popupKeys) - { - Status = _handlePopupKeys(cookedReadData, wch, modifiers); - if (Status != STATUS_SUCCESS) - { - return Status; - } - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - return CONSOLE_STATUS_READ_COMPLETE; - } - else - { - // cycle through commands that start with the letter of the key pressed - _cycleSelectionToMatchingCommands(cookedReadData, wch); - } - } -} - -void CommandListPopup::_DrawContent() -{ - _drawList(); -} - -// Routine Description: -// - Draws a list of commands for the user to choose from -void CommandListPopup::_drawList() -{ - // draw empty popup - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - size_t lStringLength = Width(); - for (til::CoordType i = 0; i < Height(); ++i) - { - const OutputCellIterator spaces(UNICODE_SPACE, _attributes, lStringLength); - const auto result = _screenInfo.Write(spaces, WriteCoord); - lStringLength = result.GetCellDistance(spaces); - WriteCoord.y += 1; - } - - auto api = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().api; - - WriteCoord.y = _region.top + 1; - auto i = std::max(_bottomIndex - Height() + 1, 0); - for (; i <= _bottomIndex; i++) - { - CHAR CommandNumber[COMMAND_NUMBER_SIZE]; - // Write command number to screen. - if (0 != _itoa_s(i, CommandNumber, ARRAYSIZE(CommandNumber), 10)) - { - return; - } - - auto CommandNumberPtr = CommandNumber; - - size_t CommandNumberLength; - if (FAILED(StringCchLengthA(CommandNumberPtr, ARRAYSIZE(CommandNumber), &CommandNumberLength))) - { - return; - } - __assume_bound(CommandNumberLength); - - if (CommandNumberLength + 1 >= ARRAYSIZE(CommandNumber)) - { - return; - } - - CommandNumber[CommandNumberLength] = ':'; - CommandNumber[CommandNumberLength + 1] = ' '; - CommandNumberLength += 2; - if (CommandNumberLength > static_cast(Width())) - { - CommandNumberLength = static_cast(Width()); - } - - WriteCoord.x = _region.left + 1; - - LOG_IF_FAILED(api->WriteConsoleOutputCharacterAImpl(_screenInfo, - { CommandNumberPtr, CommandNumberLength }, - WriteCoord, - CommandNumberLength)); - - // write command to screen - auto command = _history.GetNth(i); - lStringLength = command.size(); - { - auto lTmpStringLength = lStringLength; - auto lPopupLength = static_cast(Width() - CommandNumberLength); - auto lpStr = command.data(); - while (lTmpStringLength--) - { - if (IsGlyphFullWidth(*lpStr++)) - { - lPopupLength -= 2; - } - else - { - lPopupLength--; - } - - if (lPopupLength <= 0) - { - lStringLength -= lTmpStringLength; - if (lPopupLength < 0) - { - lStringLength--; - } - - break; - } - } - } - - WriteCoord.x = gsl::narrow(WriteCoord.x + CommandNumberLength); - size_t used; - LOG_IF_FAILED(api->WriteConsoleOutputCharacterWImpl(_screenInfo, - { command.data(), lStringLength }, - WriteCoord, - used)); - - // write attributes to screen - if (i == _currentCommand) - { - WriteCoord.x = _region.left + 1; - // inverted attributes - lStringLength = Width(); - auto inverted = _attributes; - inverted.Invert(); - - const OutputCellIterator it(inverted, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - - lStringLength = done.GetCellDistance(it); - } - - WriteCoord.y += 1; - } -} - -// Routine Description: -// - For popup lists, will adjust the position of the highlighted item and -// possibly scroll the list if necessary. -// Arguments: -// - originalDelta - The number of lines to move up or down -// - wrap - Down past the bottom or up past the top should wrap the command list -void CommandListPopup::_update(const CommandHistory::Index originalDelta, const bool wrap) -{ - auto delta = originalDelta; - if (delta == 0) - { - return; - } - const auto Size = Height(); - - auto CurCmdNum = _currentCommand; - CommandHistory::Index NewCmdNum = CurCmdNum + delta; - - if (wrap) - { - // Modulo the number of commands to "circle" around if we went off the end. - NewCmdNum %= _history.GetNumberOfCommands(); - } - else - { - if (NewCmdNum >= _history.GetNumberOfCommands()) - { - NewCmdNum = _history.GetNumberOfCommands() - 1; - } - else if (NewCmdNum < 0) - { - NewCmdNum = 0; - } - } - delta = NewCmdNum - CurCmdNum; - - auto Scroll = false; - // determine amount to scroll, if any - if (NewCmdNum <= _bottomIndex - Size) - { - _bottomIndex += delta; - if (_bottomIndex < Size - 1) - { - _bottomIndex = Size - 1; - } - Scroll = true; - } - else if (NewCmdNum > _bottomIndex) - { - _bottomIndex += delta; - if (_bottomIndex >= _history.GetNumberOfCommands()) - { - _bottomIndex = _history.GetNumberOfCommands() - 1; - } - Scroll = true; - } - - // write commands to popup - if (Scroll) - { - _currentCommand = NewCmdNum; - _drawList(); - } - else - { - _updateHighlight(_currentCommand, NewCmdNum); - _currentCommand = NewCmdNum; - } -} - -// Routine Description: -// - Adjusts the highlighted line in a list of commands -// Arguments: -// - OldCurrentCommand - The previous command highlighted -// - NewCurrentCommand - The new command to be highlighted. -void CommandListPopup::_updateHighlight(const CommandHistory::Index OldCurrentCommand, const CommandHistory::Index NewCurrentCommand) -{ - til::CoordType TopIndex; - if (_bottomIndex < Height()) - { - TopIndex = 0; - } - else - { - TopIndex = _bottomIndex - Height() + 1; - } - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - size_t lStringLength = Width(); - - WriteCoord.y = _region.top + 1 + OldCurrentCommand - TopIndex; - - const OutputCellIterator it(_attributes, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - lStringLength = done.GetCellDistance(it); - - // highlight new command - WriteCoord.y = _region.top + 1 + NewCurrentCommand - TopIndex; - - // inverted attributes - auto inverted = _attributes; - inverted.Invert(); - const OutputCellIterator itAttr(inverted, lStringLength); - const auto doneAttr = _screenInfo.Write(itAttr, WriteCoord); - lStringLength = done.GetCellDistance(itAttr); -} diff --git a/src/host/CommandListPopup.hpp b/src/host/CommandListPopup.hpp deleted file mode 100644 index 59bef4c3946..00000000000 --- a/src/host/CommandListPopup.hpp +++ /dev/null @@ -1,50 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CommandListPopup.hpp - -Abstract: -- Popup used for use command list input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CommandListPopup : public Popup -{ -public: - CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - void _drawList(); - void _update(const CommandHistory::Index delta, const bool wrap = false); - void _updateHighlight(const CommandHistory::Index oldCommand, const CommandHistory::Index newCommand); - - void _handleReturn(COOKED_READ_DATA& cookedReadData); - void _cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch); - void _setBottomIndex(); - [[nodiscard]] NTSTATUS _handlePopupKeys(COOKED_READ_DATA& cookedReadData, const wchar_t wch, const DWORD modifiers) noexcept; - [[nodiscard]] NTSTATUS _deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapUp(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapDown(COOKED_READ_DATA& cookedReadData) noexcept; - - CommandHistory::Index _currentCommand; - CommandHistory::Index _bottomIndex; // number of command displayed on last line of popup - const CommandHistory& _history; - -#ifdef UNIT_TESTING - friend class CommandListPopupTests; -#endif -}; diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp deleted file mode 100644 index 6e990439d70..00000000000 --- a/src/host/CommandNumberPopup.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CommandNumberPopup.hpp" - -#include "stream.h" -#include "_stream.h" -#include "cmdline.h" -#include "resource.h" - -#include "../interactivity/inc/ServiceLocator.hpp" - -// 5 digit number for command history -static constexpr size_t COMMAND_NUMBER_LENGTH = 5; - -static constexpr size_t COMMAND_NUMBER_PROMPT_LENGTH = 22; - -CommandNumberPopup::CommandNumberPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COMMAND_NUMBER_PROMPT_LENGTH + COMMAND_NUMBER_LENGTH, 1 }) -{ - _userInput.reserve(COMMAND_NUMBER_LENGTH); -} - -// Routine Description: -// - handles numerical user input -// Arguments: -// - cookedReadData - read data to operate on -// - wch - digit to handle -void CommandNumberPopup::_handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept -{ - if (_userInput.size() < COMMAND_NUMBER_LENGTH) - { - auto CharsToWrite = sizeof(wchar_t); - const auto realAttributes = cookedReadData.ScreenInfo().GetAttributes(); - cookedReadData.ScreenInfo().SetAttributes(_attributes); - size_t NumSpaces; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - _userInput.data(), - _userInput.data() + _userInput.size(), - &wch, - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - cookedReadData.ScreenInfo().SetAttributes(realAttributes); - try - { - _push(wch); - } - CATCH_LOG(); - } -} - -// Routine Description: -// - handles backspace user input. removes a digit from the user input -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (_userInput.size() > 0) - { - auto CharsToWrite = sizeof(WCHAR); - const auto backspace = UNICODE_BACKSPACE; - const auto realAttributes = cookedReadData.ScreenInfo().GetAttributes(); - cookedReadData.ScreenInfo().SetAttributes(_attributes); - size_t NumSpaces; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - _userInput.data(), - _userInput.data() + _userInput.size(), - &backspace, - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - cookedReadData.ScreenInfo().SetAttributes(realAttributes); - _pop(); - } -} - -// Routine Description: -// - handles escape user input. cancels the popup -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleEscape(COOKED_READ_DATA& cookedReadData) noexcept -{ - CommandLine::Instance().EndAllPopups(); - - // Note that cookedReadData's OriginalCursorPosition is the position before ANY text was entered on the edit line. - // We want to use the position before the cursor was moved for this popup handler specifically, which may - // be *anywhere* in the edit line and will be synchronized with the pointers in the cookedReadData - // structure (BufPtr, etc.) - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.BeforeDialogCursorPosition(), TRUE)); -} - -// Routine Description: -// - handles return user input. sets the prompt to the history item indicated -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) noexcept -{ - const auto commandNumber = gsl::narrow(std::min(_parse(), cookedReadData.History().GetNumberOfCommands() - 1)); - - CommandLine::Instance().EndAllPopups(); - SetCurrentCommandLine(cookedReadData, commandNumber); -} - -// Routine Description: -// - This routine handles the command number selection popup. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandNumberPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - for (;;) - { - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - if (std::iswdigit(wch)) - { - _handleNumber(cookedReadData, wch); - } - else if (wch == UNICODE_BACKSPACE) - { - _handleBackspace(cookedReadData); - } - else if (wch == VK_ESCAPE) - { - _handleEscape(cookedReadData); - break; - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - break; - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CommandNumberPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF9); -} - -// Routine Description: -// - adds single digit number to the popup's number buffer -// Arguments: -// - wch - char of the number to add. must be in the range [L'0', L'9'] -// Note: will throw if wch is out of range -void CommandNumberPopup::_push(const wchar_t wch) -{ - THROW_HR_IF(E_INVALIDARG, !std::iswdigit(wch)); - if (_userInput.size() < COMMAND_NUMBER_LENGTH) - { - _userInput += wch; - } -} - -// Routine Description: -// - removes the last number added to the number buffer -void CommandNumberPopup::_pop() noexcept -{ - if (!_userInput.empty()) - { - _userInput.pop_back(); - } -} - -// Routine Description: -// - get numerical value for the data stored in the number buffer -// Return Value: -// - parsed integer representing the string value found in the number buffer -int CommandNumberPopup::_parse() const noexcept -{ - try - { - return std::stoi(_userInput); - } - catch (...) - { - return 0; - } -} diff --git a/src/host/CommandNumberPopup.hpp b/src/host/CommandNumberPopup.hpp deleted file mode 100644 index 3d0c0c22d81..00000000000 --- a/src/host/CommandNumberPopup.hpp +++ /dev/null @@ -1,45 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CommandNumberPopup.hpp - -Abstract: -- Popup used for use command number input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CommandNumberPopup final : public Popup -{ -public: - explicit CommandNumberPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - std::wstring _userInput; - - void _handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept; - void _handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept; - void _handleEscape(COOKED_READ_DATA& cookedReadData) noexcept; - void _handleReturn(COOKED_READ_DATA& cookedReadData) noexcept; - - void _push(const wchar_t wch); - void _pop() noexcept; - int _parse() const noexcept; - -#ifdef UNIT_TESTING - friend class CommandNumberPopupTests; -#endif -}; diff --git a/src/host/CopyFromCharPopup.cpp b/src/host/CopyFromCharPopup.cpp deleted file mode 100644 index 9b64938225a..00000000000 --- a/src/host/CopyFromCharPopup.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CopyFromCharPopup.hpp" - -#include "_stream.h" -#include "resource.h" - -static constexpr size_t COPY_FROM_CHAR_PROMPT_LENGTH = 28; - -CopyFromCharPopup::CopyFromCharPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COPY_FROM_CHAR_PROMPT_LENGTH + 2, 1 }) -{ -} - -// Routine Description: -// - This routine handles the delete from cursor to char popup. It returns when we're out of input or the user has entered a char. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CopyFromCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - // get user input - auto Char = UNICODE_NULL; - auto PopupKeys = false; - DWORD modifiers = 0; - auto Status = _getUserInput(cookedReadData, PopupKeys, modifiers, Char); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (PopupKeys && Char == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - const auto span = cookedReadData.SpanAtPointer(); - const auto foundLocation = std::find(std::next(span.begin()), span.end(), Char); - if (foundLocation == span.end()) - { - // char not found, delete everything to the right of the cursor - CommandLine::Instance().DeletePromptAfterCursor(cookedReadData); - } - else - { - // char was found, delete everything between the cursor and it - const auto difference = std::distance(span.begin(), foundLocation); - for (unsigned int i = 0; i < gsl::narrow(difference); ++i) - { - CommandLine::Instance().DeleteFromRightOfCursor(cookedReadData); - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyFromCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF4); -} diff --git a/src/host/CopyFromCharPopup.hpp b/src/host/CopyFromCharPopup.hpp deleted file mode 100644 index ee40f09be11..00000000000 --- a/src/host/CopyFromCharPopup.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CopyFromCharPopup.hpp - -Abstract: -- Popup used for use copying from char input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CopyFromCharPopup final : public Popup -{ -public: - explicit CopyFromCharPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; -}; diff --git a/src/host/CopyToCharPopup.cpp b/src/host/CopyToCharPopup.cpp deleted file mode 100644 index 35fc69405ad..00000000000 --- a/src/host/CopyToCharPopup.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CopyToCharPopup.hpp" - -#include "stream.h" -#include "_stream.h" -#include "resource.h" - -static constexpr size_t COPY_TO_CHAR_PROMPT_LENGTH = 26; - -CopyToCharPopup::CopyToCharPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COPY_TO_CHAR_PROMPT_LENGTH + 2, 1 }) -{ -} - -// Routine Description: -// - copies text from the previous command into the current prompt line, up to but not including the first -// instance of wch after the current cookedReadData's cursor position. if wch is not found, nothing is copied. -// Arguments: -// - cookedReadData - the read data to operate on -// - LastCommand - the most recent command run -// - wch - the wchar to copy up to -void CopyToCharPopup::_copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch) -{ - // make sure that there it is possible to copy any found text over - if (cookedReadData.InsertionPoint() >= LastCommand.size()) - { - return; - } - - const auto searchStart = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint() + 1); - auto location = std::find(searchStart, LastCommand.cend(), wch); - - // didn't find wch so copy nothing - if (location == LastCommand.cend()) - { - return; - } - - const auto startIt = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint()); - const auto endIt = location; - - cookedReadData.Write({ &*startIt, gsl::narrow(std::distance(startIt, endIt)) }); -} - -// Routine Description: -// - This routine handles the delete char popup. It returns when we're out of input or the user has entered a char. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CopyToCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto wch = UNICODE_NULL; - auto popupKey = false; - DWORD modifiers = 0; - auto Status = _getUserInput(cookedReadData, popupKey, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (popupKey && wch == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - // copy up to specified char - const auto lastCommand = cookedReadData.History().GetLastCommand(); - if (!lastCommand.empty()) - { - _copyToChar(cookedReadData, lastCommand, wch); - } - - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyToCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF2); -} diff --git a/src/host/CopyToCharPopup.hpp b/src/host/CopyToCharPopup.hpp deleted file mode 100644 index a1c490d3b3d..00000000000 --- a/src/host/CopyToCharPopup.hpp +++ /dev/null @@ -1,32 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CopyToCharPopup.hpp - -Abstract: -- Popup used for use copying to char input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CopyToCharPopup final : public Popup -{ -public: - CopyToCharPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - void _copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch); -}; diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index cc858f74bef..5ff5c68f5db 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -39,7 +39,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine; // - coordCursor - New location of cursor. // - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge // Return Value: -void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY) +static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const bool interactive, _Inout_opt_ til::CoordType* psScrollY) { const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); if (coordCursor.x < 0) @@ -77,7 +77,7 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC { *psScrollY += bufferSize.height - coordCursor.y - 1; } - coordCursor.y += bufferSize.height - coordCursor.y - 1; + coordCursor.y = bufferSize.height - 1; } const auto cursorMovedPastViewport = coordCursor.y > screenInfo.GetViewport().BottomInclusive(); @@ -91,21 +91,19 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC LOG_IF_FAILED(screenInfo.SetViewportOrigin(false, WindowOrigin, true)); } - if (fKeepCursorVisible) + if (interactive) { screenInfo.MakeCursorVisible(coordCursor); } - LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible)); + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, interactive)); } // As the name implies, this writes text without processing its control characters. -static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const DWORD dwFlags, til::CoordType* const psScrollY, const std::wstring_view& text) +static void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY) { - const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE); const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing(); auto& textBuffer = screenInfo.GetTextBuffer(); - size_t numSpaces = 0; RowWriteState state{ .text = text, @@ -120,8 +118,6 @@ static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const textBuffer.Write(cursorPosition.y, textBuffer.GetCurrentAttributes(), state); cursorPosition.x = state.columnEnd; - numSpaces += gsl::narrow_cast(state.columnEnd - state.columnBegin); - if (wrapAtEOL && state.columnEnd >= state.columnLimit) { textBuffer.SetWrapForced(cursorPosition.y, true); @@ -132,50 +128,22 @@ static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y); } - AdjustCursorPosition(screenInfo, cursorPosition, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, cursorPosition, interactive, psScrollY); } - - return numSpaces; } -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") -// WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[nodiscard]] NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY) -try +// This routine writes a string to the screen while handling control characters. +// `interactive` exists for COOKED_READ_DATA which uses it to transform control characters into visible text like "^X". +// Similarly, `psScrollY` is also used by it to track whether the underlying buffer circled. It requires this information to know where the input line moved to. +void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY) { static constexpr wchar_t tabSpaces[8]{ L' ', L' ', L' ', L' ', L' ', L' ', L' ', L' ' }; auto& textBuffer = screenInfo.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); - const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE); const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); - auto it = pwchRealUnicode; - const auto end = it + *pcb / sizeof(wchar_t); - size_t numSpaces = 0; + auto it = text.begin(); + const auto end = text.end(); // In VT mode, when you have a 120-column terminal you can write 120 columns without the cursor wrapping. // Whenever the cursor is in that 120th column IsDelayedEOLWrap() will return true. I'm not sure why the VT parts @@ -192,7 +160,7 @@ try { pos.x = 0; pos.y++; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); } } @@ -200,7 +168,7 @@ try // If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed. if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, end }); + _writeCharsLegacyUnprocessed(screenInfo, { it, end }, interactive, psScrollY); it = end; } @@ -209,7 +177,7 @@ try const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); }); if (nextControlChar != it) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, nextControlChar }); + _writeCharsLegacyUnprocessed(screenInfo, { it, nextControlChar }, interactive, psScrollY); it = nextControlChar; } @@ -218,14 +186,14 @@ try switch (*it) { case UNICODE_NULL: - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE)) + if (interactive) { break; } - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], 1 }); + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], 1 }, interactive, psScrollY); continue; case UNICODE_BELL: - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE)) + if (interactive) { break; } @@ -233,171 +201,20 @@ try continue; case UNICODE_BACKSPACE: { + // Backspace handling for interactive mode should happen in COOKED_READ_DATA + // where it has full control over the text and can delete it directly. + // Otherwise handling backspacing tabs/whitespace can turn up complex and bug-prone. + assert(!interactive); auto pos = cursor.GetPosition(); - - if (WI_IsFlagClear(dwFlags, WC_INTERACTIVE)) - { - pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x); - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - continue; - } - - const auto moveUp = [&]() { - pos.x = -1; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - - const auto y = cursor.GetPosition().y; - auto& row = textBuffer.GetMutableRowByOffset(y); - - pos.x = textBuffer.GetSize().RightExclusive(); - pos.y = y; - - if (row.WasDoubleBytePadded()) - { - pos.x--; - numSpaces--; - } - - row.SetWrapForced(false); - row.SetDoubleBytePadded(false); - }; - - // We have to move up early because the tab handling code below needs to be on - // the row of the tab already, so that we can call GetText() for precedingText. - if (pos.x == 0 && pos.y != 0) - { - moveUp(); - } - - til::CoordType glyphCount = 1; - - if (pwchBuffer != pwchBufferBackupLimit) - { - const auto lastChar = pwchBuffer[-1]; - - // Deleting tabs is a bit tricky, because they have a variable width between 1 and 8 spaces, - // are stored as whitespace but are technically distinct from whitespace. - if (lastChar == UNICODE_TAB) - { - const auto precedingText = textBuffer.GetRowByOffset(pos.y).GetText(pos.x - 8, pos.x); - - // First, we measure the amount of spaces that precede the cursor in the text buffer, - // which is generally the amount of spaces that we end up deleting. We do it this way, - // because we don't know what kind of complex mix of wide/narrow glyphs precede the tab. - // Basically, by asking the text buffer we get the size information of the preceding text. - if (precedingText.size() >= 2 && precedingText.back() == L' ') - { - auto textIt = precedingText.rbegin() + 1; - const auto textEnd = precedingText.rend(); - - for (; textIt != textEnd && *textIt == L' '; ++textIt) - { - glyphCount++; - } - } - - // But there's a problem: When you print " \t" it should delete 6 spaces and not 8. - // In other words, we shouldn't delete any actual preceding whitespaces. We can ask - // the "backup" buffer (= preceding text in the commandline) for this information. - // - // backupEnd points to the character immediately preceding the tab (LastChar). - const auto backupEnd = pwchBuffer - 1; - // backupLimit points to how far back we need to search. Even if we have 9000 characters in our command line, - // we'll only need to check a total of 8 whitespaces. "pwchBuffer - pwchBufferBackupLimit" will - // always be at least 1 because that's the \t character in the backup buffer. In other words, - // backupLimit will at a minimum be equal to backupEnd, or precede it by 7 more characters. - const auto backupLimit = pwchBuffer - std::min(8, pwchBuffer - pwchBufferBackupLimit); - // Now count how many spaces precede the \t character. "backupEnd - backupBeg" will be the amount. - auto backupBeg = backupEnd; - for (; backupBeg != backupLimit && backupBeg[-1] == L' '; --backupBeg, --glyphCount) - { - } - - // There's one final problem: A prompt like... - // fputs("foo: ", stdout); - // fgets(buffer, stdin); - // ...has a trailing whitespace in front of our pwchBufferBackupLimit which we should not backspace over. - // sOriginalXPosition stores the start of the prompt at the pwchBufferBackupLimit. - if (backupBeg == pwchBufferBackupLimit) - { - glyphCount = pos.x - sOriginalXPosition; - } - - // Now that we finally know how many columns precede the cursor we can - // subtract the previously determined amount of ' ' from the '\t'. - glyphCount -= gsl::narrow_cast(backupEnd - backupBeg); - - // Can the above code leave glyphCount <= 0? Let's just not find out! - glyphCount = std::max(1, glyphCount); - } - // Control chars in interactive mode were previously written out - // as ^X for instance, so now we also need to delete 2 glyphs. - else if (IS_CONTROL_CHAR(lastChar)) - { - glyphCount = 2; - } - } - - for (;;) - { - // We've already moved up if the cursor was in the first column so - // we need to start off with overwriting the text with whitespace. - // It wouldn't make sense to check the cursor position again already. - { - const auto previousColumn = pos.x; - pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(previousColumn); - - RowWriteState state{ - .text = { &tabSpaces[0], 8 }, - .columnBegin = pos.x, - .columnLimit = previousColumn, - }; - textBuffer.Write(pos.y, textBuffer.GetCurrentAttributes(), state); - numSpaces -= previousColumn - pos.x; - } - - // The cursor movement logic is a little different for the last iteration, so we exit early here. - glyphCount--; - if (glyphCount <= 0) - { - break; - } - - // Otherwise, in case we need to delete 2 or more glyphs, we need to ensure we properly wrap lines back up. - if (pos.x == 0 && pos.y != 0) - { - moveUp(); - } - } - - // After the last iteration the cursor might now be in the first column after a line - // that was previously padded with a whitespace in the last column due to a wide glyph. - // Now that the wide glyph is presumably gone, we can move up a line. - if (pos.x == 0 && pos.y != 0 && textBuffer.GetRowByOffset(pos.y - 1).WasDoubleBytePadded()) - { - moveUp(); - } - else - { - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - } - - // Notify accessibility to read the backspaced character. - // See GH:12735, MSFT:31748387 - if (screenInfo.HasAccessibilityEventing()) - { - if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow()) - { - LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); - } - } + pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } case UNICODE_TAB: { const auto pos = cursor.GetPosition(); - const auto tabCount = gsl::narrow_cast(NUMBER_OF_SPACES_IN_TAB(pos.x)); - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], tabCount }); + const auto tabCount = gsl::narrow_cast(8 - (pos.x & 7)); + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], tabCount }, interactive, psScrollY); continue; } case UNICODE_LINEFEED: @@ -410,24 +227,25 @@ try textBuffer.GetMutableRowByOffset(pos.y).SetWrapForced(false); pos.y = pos.y + 1; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } case UNICODE_CARRIAGERETURN: { auto pos = cursor.GetPosition(); pos.x = 0; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } default: break; } - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE) && IS_CONTROL_CHAR(*it)) + // In the interactive mode we replace C0 control characters (0x00-0x1f) with ASCII representations like ^C (= 0x03). + if (interactive && *it < L' ') { const wchar_t wchs[2]{ L'^', static_cast(*it + L'@') }; - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wchs[0], 2 }); + _writeCharsLegacyUnprocessed(screenInfo, { &wchs[0], 2 }, interactive, psScrollY); } else { @@ -439,76 +257,12 @@ try const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1); if (result == 1) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wch, 1 }); + _writeCharsLegacyUnprocessed(screenInfo, { &wch, 1 }, interactive, psScrollY); } } } } - - if (pcSpaces) - { - *pcSpaces = numSpaces; - } - - return S_OK; } -NT_CATCH_RETURN() - -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") -// WC_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[nodiscard]] NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY) -try -{ - if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) - { - return WriteCharsLegacy(screenInfo, - pwchBufferBackupLimit, - pwchBuffer, - pwchRealUnicode, - pcb, - pcSpaces, - sOriginalXPosition, - dwFlags, - psScrollY); - } - - auto& machine = screenInfo.GetStateMachine(); - const auto cch = *pcb / sizeof(WCHAR); - - machine.ProcessString({ pwchRealUnicode, cch }); - - if (nullptr != pcSpaces) - { - *pcSpaces = 0; - } - - return STATUS_SUCCESS; -} -NT_CATCH_RETURN() // Routine Description: // - Takes the given text and inserts it into the given screen buffer. @@ -530,23 +284,16 @@ NT_CATCH_RETURN() SCREEN_INFORMATION& screenInfo, bool requiresVtQuirk, std::unique_ptr& waiter) +try { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (WI_IsAnyFlagSet(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING))) { - try - { - waiter = std::make_unique(screenInfo, - pwchBuffer, - *pcbBuffer, - gci.OutputCP, - requiresVtQuirk); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - + waiter = std::make_unique(screenInfo, + pwchBuffer, + *pcbBuffer, + gci.OutputCP, + requiresVtQuirk); return CONSOLE_STATUS_WAIT; } @@ -563,17 +310,20 @@ NT_CATCH_RETURN() restoreVtQuirk.release(); } - const auto& textBuffer = screenInfo.GetTextBuffer(); - return WriteChars(screenInfo, - pwchBuffer, - pwchBuffer, - pwchBuffer, - pcbBuffer, - nullptr, - textBuffer.GetCursor().GetPosition().x, - 0, - nullptr); + const std::wstring_view str{ pwchBuffer, *pcbBuffer / sizeof(WCHAR) }; + + if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) + { + WriteCharsLegacy(screenInfo, str, false, nullptr); + } + else + { + screenInfo.GetStateMachine().ProcessString(str); + } + + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // Routine Description: // - This method performs the actual work of attempting to write to the console, converting data types as necessary diff --git a/src/host/_stream.h b/src/host/_stream.h index 58351c86cc1..794591a03f0 100644 --- a/src/host/_stream.h +++ b/src/host/_stream.h @@ -17,76 +17,13 @@ Revision History: #pragma once -#include "../server/IWaitRoutine.h" #include "writeData.hpp" -/*++ -Routine Description: - This routine updates the cursor position. Its input is the non-special - cased new location of the cursor. For example, if the cursor were being - moved one space backwards from the left edge of the screen, the X - coordinate would be -1. This routine would set the X coordinate to - the right edge of the screen and decrement the Y coordinate by one. - -Arguments: - pScreenInfo - Pointer to screen buffer information structure. - coordCursor - New location of cursor. - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge - -Return Value: ---*/ -void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY); - -/*++ -Routine Description: - This routine writes a string to the screen, processing any embedded - unicode characters. The string is also copied to the input buffer, if - the output mode is line mode. - -Arguments: - ScreenInfo - Pointer to screen buffer information structure. - lpBufferBackupLimit - Pointer to beginning of buffer. - lpBuffer - Pointer to buffer to copy string to. assumed to be at least - as long as lpRealUnicodeString. This pointer is updated to point to the - next position in the buffer. - lpRealUnicodeString - Pointer to string to write. - NumBytes - On input, number of bytes to write. On output, number of - bytes written. - NumSpaces - On output, the number of spaces consumed by the written characters. - dwFlags - - WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") - WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge - -Return Value: - -Note: - This routine does not process tabs and backspace properly. That code - will be implemented as part of the line editing services. ---*/ -[[nodiscard]] NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY); - -// The new entry point for WriteChars to act as an intercept in case we place a Virtual Terminal processor in the way. -[[nodiscard]] NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY); +void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, bool interactive, til::CoordType* psScrollY); // NOTE: console lock must be held when calling this routine // String has been translated to unicode at this point. -[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PCWCHAR pwchBuffer, +[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(pcbBuffer) const wchar_t* pwchBuffer, _Inout_ size_t* const pcbBuffer, SCREEN_INFORMATION& screenInfo, bool requiresVtQuirk, diff --git a/src/host/alias.cpp b/src/host/alias.cpp index 85fc3d0875b..a82fa198761 100644 --- a/src/host/alias.cpp +++ b/src/host/alias.cpp @@ -817,32 +817,19 @@ void Alias::s_ClearCmdExeAliases() CATCH_RETURN(); } -// Routine Description: -// - Trims trailing \r\n off of a string -// Arguments: -// - str - String to trim -void Alias::s_TrimTrailingCrLf(std::wstring& str) -{ - const auto trailingCrLfPos = str.find_last_of(UNICODE_CARRIAGERETURN); - if (std::wstring::npos != trailingCrLfPos) - { - str.erase(trailingCrLfPos); - } -} - // Routine Description: // - Tokenizes a string into a collection using space as a separator // Arguments: // - str - String to tokenize // Return Value: // - Collection of tokenized strings -std::deque Alias::s_Tokenize(const std::wstring& str) +std::deque Alias::s_Tokenize(const std::wstring_view str) { std::deque result; size_t prevIndex = 0; auto spaceIndex = str.find(L' '); - while (std::wstring::npos != spaceIndex) + while (std::wstring_view::npos != spaceIndex) { const auto length = spaceIndex - prevIndex; @@ -867,11 +854,11 @@ std::deque Alias::s_Tokenize(const std::wstring& str) // - str - String to split into just args // Return Value: // - Only the arguments part of the string or empty if there are no arguments. -std::wstring Alias::s_GetArgString(const std::wstring& str) +std::wstring Alias::s_GetArgString(const std::wstring_view str) { std::wstring result; auto firstSpace = str.find_first_of(L' '); - if (std::wstring::npos != firstSpace) + if (std::wstring_view::npos != firstSpace) { firstSpace++; if (firstSpace < str.size()) @@ -1126,16 +1113,8 @@ size_t Alias::s_ReplaceMacros(std::wstring& str, // - If we found a matching alias, this will be the processed data // and lineCount is updated to the new number of lines. // - If we didn't match and process an alias, return an empty string. -std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, - const std::wstring& exeName, - size_t& lineCount) +std::wstring Alias::s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount) { - // Copy source text into a local for manipulation. - auto sourceCopy = sourceText; - - // Trim trailing \r\n off of sourceCopy if it has one. - s_TrimTrailingCrLf(sourceCopy); - // Check if we have an EXE in the list that matches the request first. auto exeIter = g_aliasData.find(exeName); if (exeIter == g_aliasData.end()) @@ -1152,7 +1131,7 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, } // Tokenize the text by spaces - const auto tokens = s_Tokenize(sourceCopy); + const auto tokens = s_Tokenize(sourceText); // If there are no tokens, return an empty string if (tokens.size() == 0) @@ -1169,14 +1148,14 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, return std::wstring(); } - const auto target = aliasIter->second; + const auto& target = aliasIter->second; if (target.size() == 0) { return std::wstring(); } // Get the string of all parameters as a shorthand for $* later. - const auto allParams = s_GetArgString(sourceCopy); + const auto allParams = s_GetArgString(sourceText); // The final text will be the target but with macros replaced. auto finalText = target; @@ -1185,59 +1164,6 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, return finalText; } -// Routine Description: -// - This routine matches the input string with an alias and copies the alias to the input buffer. -// Arguments: -// - pwchSource - string to match -// - cbSource - length of pwchSource in bytes -// - pwchTarget - where to store matched string -// - cbTargetSize - on input, contains size of pwchTarget. -// - cbTargetWritten - On output, contains length of alias stored in pwchTarget. -// - pwchExe - Name of exe that command is associated with to find related aliases -// - cbExe - Length in bytes of exe name -// - LineCount - aliases can contain multiple commands. $T is the command separator -// Return Value: -// - None. It will just maintain the source as the target if we can't match an alias. -void Alias::s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PCWCH pwchSource, - _In_ size_t cbSource, - _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, - _In_ const size_t cbTargetSize, - size_t& cbTargetWritten, - const std::wstring& exeName, - DWORD& lines) -{ - try - { - std::wstring sourceText(pwchSource, cbSource / sizeof(WCHAR)); - size_t lineCount = lines; - - const auto targetText = s_MatchAndCopyAlias(sourceText, exeName, lineCount); - - // Only return data if the reply was non-empty (we had a match). - if (!targetText.empty()) - { - const auto cchTargetSize = cbTargetSize / sizeof(wchar_t); - - // If the target text will fit in the result buffer, fill out the results. - if (targetText.size() <= cchTargetSize) - { - // Non-null terminated copy into memory space - std::copy_n(targetText.data(), targetText.size(), pwchTarget); - - // Return bytes copied. - cbTargetWritten = gsl::narrow(targetText.size() * sizeof(wchar_t)); - - // Return lines info. - lines = gsl::narrow(lineCount); - } - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } -} - #ifdef UNIT_TESTING void Alias::s_TestAddAlias(std::wstring& exe, std::wstring& alias, diff --git a/src/host/alias.h b/src/host/alias.h index 1a745c601f7..9c8bc38d534 100644 --- a/src/host/alias.h +++ b/src/host/alias.h @@ -16,22 +16,11 @@ class Alias public: static void s_ClearCmdExeAliases(); - static void s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PCWCH pwchSource, - _In_ size_t cbSource, - _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, - _In_ const size_t cbTargetSize, - size_t& cbTargetWritten, - const std::wstring& exeName, - DWORD& lines); - - static std::wstring s_MatchAndCopyAlias(const std::wstring& sourceText, - const std::wstring& exeName, - size_t& lineCount); + static std::wstring s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount); private: - static void s_TrimTrailingCrLf(std::wstring& str); - static std::deque s_Tokenize(const std::wstring& str); - static std::wstring s_GetArgString(const std::wstring& str); + static std::deque s_Tokenize(const std::wstring_view str); + static std::wstring s_GetArgString(const std::wstring_view str); static size_t s_ReplaceMacros(std::wstring& str, const std::deque& tokens, const std::wstring& fullArgString); diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp index 1e1587e37fc..c523ad9ab3c 100644 --- a/src/host/cmdline.cpp +++ b/src/host/cmdline.cpp @@ -2,25 +2,7 @@ // Licensed under the MIT license. #include "precomp.h" - #include "cmdline.h" -#include "popup.h" -#include "CommandNumberPopup.hpp" -#include "CommandListPopup.hpp" -#include "CopyFromCharPopup.hpp" -#include "CopyToCharPopup.hpp" - -#include "_output.h" -#include "output.h" -#include "stream.h" -#include "_stream.h" -#include "dbcs.h" -#include "handle.h" -#include "misc.h" -#include "../types/inc/convert.hpp" -#include "srvinit.h" - -#include "ApiRoutines.h" #include "../interactivity/inc/ServiceLocator.hpp" @@ -29,7 +11,7 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Routine Description: // - Detects Word delimiters -bool IsWordDelim(const wchar_t wch) +bool IsWordDelim(const wchar_t wch) noexcept { // the space character is always a word delimiter. Do not add it to the WordDelimiters global because // that contains the user configurable word delimiters only. @@ -41,1219 +23,24 @@ bool IsWordDelim(const wchar_t wch) return std::ranges::find(delimiters, wch) != delimiters.end(); } -bool IsWordDelim(const std::wstring_view charData) +bool IsWordDelim(const std::wstring_view& charData) noexcept { return charData.size() == 1 && IsWordDelim(charData.front()); } -CommandLine::CommandLine() : - _isVisible{ true } +// Returns a truthy value for delimiters and 0 otherwise. +// The distinction between whitespace and other delimiters allows us to +// implement Windows' inconsistent, but classic, word-wise navigation. +int DelimiterClass(wchar_t wch) noexcept { -} - -CommandLine::~CommandLine() = default; - -CommandLine& CommandLine::Instance() -{ - static CommandLine c; - return c; -} - -bool CommandLine::IsEditLineEmpty() -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - if (!gci.HasPendingCookedRead()) - { - // If the cooked read data pointer is null, there is no edit line data and therefore it's empty. - return true; - } - else if (0 == gci.CookedReadData().VisibleCharCount()) - { - // If we had a valid pointer, but there are no visible characters for the edit line, then it's empty. - // Someone started editing and back spaced the whole line out so it exists, but has no data. - return true; - } - else + if (wch == L' ') { - return false; + return 1; } -} - -void CommandLine::Hide(const bool fUpdateFields) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (!IsEditLineEmpty()) - { - DeleteCommandLine(gci.CookedReadData(), fUpdateFields); - } - _isVisible = false; -} - -void CommandLine::Show() -{ - _isVisible = true; - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (!IsEditLineEmpty()) - { - RedrawCommandLine(gci.CookedReadData()); - } -} - -// Routine Description: -// - Returns true if the commandline is currently being displayed. This is false -// after Hide() is called, and before Show() is called again. -// Return Value: -// - true if the commandline should be displayed. Does not take into account -// the echo state of the input. This is only controlled by calls to Hide/Show -bool CommandLine::IsVisible() const noexcept -{ - return _isVisible; -} - -// Routine Description: -// - checks for the presence of a popup -// Return Value: -// - true if popup is present -bool CommandLine::HasPopup() const noexcept -{ - return !_popups.empty(); -} - -// Routine Description: -// - gets the topmost popup -// Arguments: -// Return Value: -// - ref to the topmost popup -Popup& CommandLine::GetPopup() const -{ - return *_popups.front(); -} - -// Routine Description: -// - stops the current popup -void CommandLine::EndCurrentPopup() -{ - if (!_popups.empty()) - { - _popups.front()->End(); - _popups.pop_front(); - } -} - -// Routine Description: -// - stops all popups -void CommandLine::EndAllPopups() -{ - while (!_popups.empty()) - { - _popups.front()->End(); - _popups.pop_front(); - } -} - -void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields) -{ - auto CharsToWrite = cookedReadData.VisibleCharCount(); - auto coordOriginalCursor = cookedReadData.OriginalCursorPosition(); - const auto coordBufferSize = cookedReadData.ScreenInfo().GetBufferSize().Dimensions(); - - // catch the case where the current command has scrolled off the top of the screen. - if (coordOriginalCursor.y < 0) - { - CharsToWrite += coordBufferSize.width * coordOriginalCursor.y; - CharsToWrite += cookedReadData.OriginalCursorPosition().x; // account for prompt - cookedReadData.OriginalCursorPosition().x = 0; - cookedReadData.OriginalCursorPosition().y = 0; - coordOriginalCursor.x = 0; - coordOriginalCursor.y = 0; - } - - if (!CheckBisectStringW(cookedReadData.BufferStartPtr(), - CharsToWrite, - coordBufferSize.width - cookedReadData.OriginalCursorPosition().x)) - { - CharsToWrite++; - } - - try - { - cookedReadData.ScreenInfo().Write(OutputCellIterator(UNICODE_SPACE, CharsToWrite), coordOriginalCursor); - } - CATCH_LOG(); - - if (fUpdateFields) - { - cookedReadData.Erase(); - } - - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.OriginalCursorPosition(), true)); -} - -void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.IsEchoInput()) - { - // Draw the command line - cookedReadData.OriginalCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - til::CoordType ScrollY = 0; -#pragma prefast(suppress : 28931, "Status is not unused. It's used in debug assertions.") - auto Status = WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY); - FAIL_FAST_IF_NTSTATUS_FAILED(Status); - - cookedReadData.OriginalCursorPosition().y += ScrollY; - - // Move the cursor back to the right position - auto CursorPosition = cookedReadData.OriginalCursorPosition(); - CursorPosition.x += RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint(), - cookedReadData.ScreenInfo().GetBufferSize().Width() - cookedReadData.OriginalCursorPosition().x)) - { - CursorPosition.x++; - } - AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr); - } -} - -// Routine Description: -// - This routine copies the commandline specified by Index into the cooked read buffer -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index) // index, not command number -{ - DeleteCommandLine(cookedReadData, TRUE); - FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - - const auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); -} - -// Routine Description: -// - This routine handles the command list popup. It puts up the popup, then calls ProcessCommandListInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -[[nodiscard]] NTSTATUS CommandLine::_startCommandListPopup(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && - cookedReadData.History().GetNumberOfCommands()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo(), - cookedReadData.History())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "delete up to this char" popup. It puts up the popup, then calls ProcessCopyFromCharInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -[[nodiscard]] NTSTATUS CommandLine::_startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData) -{ - // Delete the current command from cursor position to the - // letter specified by the user. The user is prompted via - // popup to enter a character. - if (cookedReadData.HasHistory()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "copy up to this char" popup. It puts up the popup, then calls ProcessCopyToCharInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -// - S_FALSE - if we couldn't make a popup because we had no commands -[[nodiscard]] NTSTATUS CommandLine::_startCopyToCharPopup(COOKED_READ_DATA& cookedReadData) -{ - // copy the previous command to the current command, up to but - // not including the character specified by the user. the user - // is prompted via popup to enter a character. - if (cookedReadData.HasHistory()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "enter command number" popup. It puts up the popup, then calls ProcessCommandNumberInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -// - S_FALSE - if we couldn't make a popup because we had no commands or it wouldn't fit. -[[nodiscard]] HRESULT CommandLine::StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && - cookedReadData.History().GetNumberOfCommands() && - cookedReadData.ScreenInfo().GetBufferSize().Width() >= Popup::MINIMUM_COMMAND_PROMPT_SIZE + 2) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - - // Save the original cursor position in case the user cancels out of the dialog - cookedReadData.BeforeDialogCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - // Move the cursor into the dialog so the user can type multiple characters for the command number - const auto CursorPosition = popup.GetCursorPosition(); - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(CursorPosition, TRUE)); - - // Transfer control to the handler routine - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - Process virtual key code and updates the prompt line with the next history element in the direction -// specified by wch -// Arguments: -// - cookedReadData - The cooked read data to operate on -// - searchDirection - Direction in history to search -// Note: -// - May throw exceptions -void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData, - const CommandHistory::SearchDirection searchDirection) -{ - // for doskey compatibility, buffer isn't circular. don't do anything if attempting - // to cycle history past the bounds of the history buffer - if (!cookedReadData.HasHistory()) - { - return; - } - else if (searchDirection == CommandHistory::SearchDirection::Previous && cookedReadData.History().AtFirstCommand()) - { - return; - } - else if (searchDirection == CommandHistory::SearchDirection::Next && cookedReadData.History().AtLastCommand()) - { - return; - } - - DeleteCommandLine(cookedReadData, true); - THROW_IF_FAILED(cookedReadData.History().Retrieve(searchDirection, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - const auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); -} - -// Routine Description: -// - Sets the text on the prompt to the oldest run command in the cookedReadData's history -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Note: -// - May throw exceptions -void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) - { - DeleteCommandLine(cookedReadData, true); - const short commandNumber = 0; - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); - } -} - -// Routine Description: -// - Sets the text on the prompt the most recently run command in cookedReadData's history -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Note: -// - May throw exceptions -void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData) -{ - DeleteCommandLine(cookedReadData, true); - if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) - { - const auto commandNumber = (SHORT)(cookedReadData.History().GetNumberOfCommands() - 1); - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); - } -} - -// Routine Description: -// - Deletes all prompt text to the right of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - DeleteCommandLine(cookedReadData, false); - cookedReadData.BytesRead() = cookedReadData.InsertionPoint() * sizeof(WCHAR); - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } -} - -// Routine Description: -// - Deletes all user input on the prompt to the left of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - DeleteCommandLine(cookedReadData, false); - cookedReadData.BytesRead() -= cookedReadData.InsertionPoint() * sizeof(WCHAR); - cookedReadData.InsertionPoint() = 0; - memmove(cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BytesRead()); - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } - return cookedReadData.OriginalCursorPosition(); -} - -// Routine Description: -// - Moves the cursor to the end of the prompt text -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept -{ - cookedReadData.InsertionPoint() = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + cookedReadData.InsertionPoint()); - til::point cursorPosition; - cursorPosition.x = gsl::narrow(cookedReadData.OriginalCursorPosition().x + cookedReadData.VisibleCharCount()); - cursorPosition.y = cookedReadData.OriginalCursorPosition().y; - - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint(), - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - cursorPosition.x++; - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the start of the user input on the prompt -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept -{ - cookedReadData.InsertionPoint() = 0; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); - return cookedReadData.OriginalCursorPosition(); -} - -// Routine Description: -// - Moves the cursor left by a word -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - New cursor position -til::point CommandLine::_moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept -{ - PWCHAR LastWord; - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) - { - // A bit better word skipping. - LastWord = cookedReadData.BufferCurrentPtr() - 1; - if (LastWord != cookedReadData.BufferStartPtr()) - { - if (*LastWord == L' ') - { - // Skip spaces, until the non-space character is found. - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (*LastWord != L' ') - { - break; - } - } - } - if (LastWord != cookedReadData.BufferStartPtr()) - { - if (IsWordDelim(*LastWord)) - { - // Skip WORD_DELIMs until space or non WORD_DELIM is found. - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (*LastWord == L' ' || !IsWordDelim(*LastWord)) - { - break; - } - } - } - else - { - // Skip the regular words - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (IsWordDelim(*LastWord)) - { - break; - } - } - } - } - FAIL_FAST_IF(!(LastWord >= cookedReadData.BufferStartPtr())); - if (LastWord != cookedReadData.BufferStartPtr()) - { - // LastWord is currently pointing to the last character - // of the previous word, unless it backed up to the beginning - // of the buffer. - // Let's increment LastWord so that it points to the expected - // insertion point. - ++LastWord; - } - cookedReadData.SetBufferCurrentPtr(LastWord); - } - cookedReadData.InsertionPoint() = (cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); - cursorPosition = cookedReadData.OriginalCursorPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves cursor left by a glyph -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - New cursor position -til::point CommandLine::_moveCursorLeft(COOKED_READ_DATA& cookedReadData) -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) - { - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() - 1); - cookedReadData.InsertionPoint()--; - cursorPosition.x = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().x; - cursorPosition.y = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().y; - cursorPosition.x = cursorPosition.x - - RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 2, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - if ((cursorPosition.x == -2) || (cursorPosition.x == -1)) - { - cursorPosition.x--; - } - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the right by a word -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) - { - auto NextWord = cookedReadData.BufferCurrentPtr(); - - // A bit better word skipping. - auto BufLast = cookedReadData.BufferStartPtr() + cookedReadData.BytesRead() / sizeof(WCHAR); - - FAIL_FAST_IF(!(NextWord < BufLast)); - if (*NextWord == L' ') - { - // If the current character is space, skip to the next non-space character. - while (++NextWord < BufLast) - { - if (*NextWord != L' ') - { - break; - } - } - } - else - { - // Skip the body part. - auto fStartFromDelim = IsWordDelim(*NextWord); - - while (++NextWord < BufLast) - { - if (fStartFromDelim != IsWordDelim(*NextWord)) - { - break; - } - } - - // Skip the space block. - for (; NextWord < BufLast; NextWord++) - { - if (*NextWord != L' ') - { - break; - } - } - } - - cookedReadData.SetBufferCurrentPtr(NextWord); - cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); - cursorPosition = cookedReadData.OriginalCursorPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the right by a glyph -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - // If not at the end of the line, move cursor position right. - if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) - { - cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 2, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - // Snap cursorPosition.x to sScreenBufferSizeX if it is at the edge of the screen - if (cursorPosition.x == (sScreenBufferSizeX - 1)) - cursorPosition.x = sScreenBufferSizeX; - } - - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); - cookedReadData.InsertionPoint()++; - } - // if at the end of the line, copy a character from the same position in the last command - else if (cookedReadData.HasHistory()) - { - size_t NumSpaces; - const auto LastCommand = cookedReadData.History().GetLastCommand(); - if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) - { - *cookedReadData.BufferCurrentPtr() = LastCommand[cookedReadData.InsertionPoint()]; - cookedReadData.BytesRead() += sizeof(WCHAR); - cookedReadData.InsertionPoint()++; - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - auto CharsToWrite = sizeof(WCHAR); - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - // update reported cursor position - if (ScrollY != 0) - { - cursorPosition.x = 0; - cursorPosition.y += ScrollY; - } - else - { - cursorPosition.x += 1; - } - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); - } - } - return cursorPosition; -} - -// Routine Description: -// - Place a ctrl-z in the current command line -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept -{ - size_t NumSpaces = 0; - - *cookedReadData.BufferCurrentPtr() = (WCHAR)0x1a; // ctrl-z - cookedReadData.BytesRead() += sizeof(WCHAR); - cookedReadData.InsertionPoint()++; - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - auto CharsToWrite = sizeof(WCHAR); - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); -} - -// Routine Description: -// - Empties the command history for cookedReadData -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (cookedReadData.HasHistory()) - { - cookedReadData.History().Empty(); - cookedReadData.History().Flags |= CommandHistory::CLE_ALLOCATED; - } -} - -// Routine Description: -// - Copy the remainder of the previous command to the current command. -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (cookedReadData.HasHistory()) - { - size_t NumSpaces, cchCount; - - const auto LastCommand = cookedReadData.History().GetLastCommand(); - if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) - { - cchCount = LastCommand.size() - cookedReadData.InsertionPoint(); - const auto bufferSpan = cookedReadData.SpanAtPointer(); - std::copy_n(LastCommand.cbegin() + cookedReadData.InsertionPoint(), cchCount, bufferSpan.begin()); - cookedReadData.InsertionPoint() += cchCount; - cchCount *= sizeof(WCHAR); - cookedReadData.BytesRead() = std::max(LastCommand.size() * sizeof(wchar_t), cookedReadData.BytesRead()); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cchCount, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + cchCount / sizeof(WCHAR)); - } - } -} - -// Routine Description: -// - Cycles through the stored commands that start with the characters in the current command. -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData) -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.HasHistory()) - { - CommandHistory::Index index; - if (cookedReadData.History().FindMatchingCommand({ cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() }, - cookedReadData.History().LastDisplayed, - index, - CommandHistory::MatchOptions::None)) - { - // save cursor position - const auto CurrentPos = cookedReadData.InsertionPoint(); - - DeleteCommandLine(cookedReadData, true); - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(index, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cursorPosition.y += ScrollY; - } - - // restore cursor position - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CurrentPos); - cookedReadData.InsertionPoint() = CurrentPos; - FAIL_FAST_IF_NTSTATUS_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cursorPosition, true)); - } - } - return cursorPosition; -} - -// Routine Description: -// - Deletes a glyph from the right side of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - // save cursor position - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - if (!cookedReadData.AtEol()) - { - // Delete commandline. - // clang-format off -#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast is getting confused here") - // clang-format on - DeleteCommandLine(cookedReadData, false); - - // Delete char. - cookedReadData.BytesRead() -= sizeof(WCHAR); - memmove(cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr() + 1, - cookedReadData.BytesRead() - (cookedReadData.InsertionPoint() * sizeof(WCHAR))); - - { - auto buf = (PWCHAR)((PBYTE)cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()); - *buf = (WCHAR)' '; - } - - // Write commandline. - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } - - // restore cursor position - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// TODO: [MSFT:4586207] Clean up this mess -- needs helpers. http://osgvsowi/4586207 -// Routine Description: -// - This routine process command line editing keys. -// Return Value: -// - CONSOLE_STATUS_WAIT - CommandListPopup ran out of input -// - CONSOLE_STATUS_READ_COMPLETE - user hit in CommandListPopup -// - STATUS_SUCCESS - everything's cool -[[nodiscard]] NTSTATUS CommandLine::ProcessCommandLine(COOKED_READ_DATA& cookedReadData, - _In_ WCHAR wch, - const DWORD dwKeyState) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - NTSTATUS Status; - - const auto altPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); - const auto ctrlPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); - auto UpdateCursorPosition = false; - switch (wch) - { - case VK_ESCAPE: - DeleteCommandLine(cookedReadData, true); - break; - case VK_DOWN: - try - { - _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_UP: - case VK_F5: - try - { - _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_PRIOR: - try - { - _setPromptToOldestCommand(cookedReadData); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_NEXT: - try - { - _setPromptToNewestCommand(cookedReadData); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_END: - if (ctrlPressed) - { - DeletePromptAfterCursor(cookedReadData); - } - else - { - cursorPosition = _moveCursorToEndOfPrompt(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_HOME: - if (ctrlPressed) - { - cursorPosition = _deletePromptBeforeCursor(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorToStartOfPrompt(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_LEFT: - if (ctrlPressed) - { - cursorPosition = _moveCursorLeftByWord(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorLeft(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_F1: - { - // we don't need to check for end of buffer here because we've - // already done it. - cursorPosition = _moveCursorRight(cookedReadData); - UpdateCursorPosition = true; - break; - } - case VK_RIGHT: - // we don't need to check for end of buffer here because we've - // already done it. - if (ctrlPressed) - { - cursorPosition = _moveCursorRightByWord(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorRight(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_F2: - { - Status = _startCopyToCharPopup(cookedReadData); - if (S_FALSE == Status) - { - // We couldn't make the popup, so loop around and read the next character. - break; - } - else - { - return Status; - } - } - case VK_F3: - _fillPromptWithPreviousCommandFragment(cookedReadData); - break; - case VK_F4: - { - Status = _startCopyFromCharPopup(cookedReadData); - if (S_FALSE == Status) - { - // We couldn't display a popup. Go around a loop behind. - break; - } - else - { - return Status; - } - } - case VK_F6: - { - _insertCtrlZ(cookedReadData); - break; - } - case VK_F7: - if (!ctrlPressed && !altPressed) - { - Status = _startCommandListPopup(cookedReadData); - } - else if (altPressed) - { - _deleteCommandHistory(cookedReadData); - } - break; - - case VK_F8: - try - { - cursorPosition = _cycleMatchingCommandHistoryToPrompt(cookedReadData); - UpdateCursorPosition = true; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_F9: - { - Status = StartCommandNumberPopup(cookedReadData); - if (S_FALSE == Status) - { - // If we couldn't make the popup, break and go around to read another input character. - break; - } - else - { - return Status; - } - } - case VK_F10: - // Alt+F10 clears the aliases for specifically cmd.exe. - if (altPressed) - { - Alias::s_ClearCmdExeAliases(); - } - break; - case VK_INSERT: - cookedReadData.SetInsertMode(!cookedReadData.IsInsertMode()); - cookedReadData.ScreenInfo().SetCursorDBMode(cookedReadData.IsInsertMode() != gci.GetInsertMode()); - break; - case VK_DELETE: - cursorPosition = DeleteFromRightOfCursor(cookedReadData); - UpdateCursorPosition = true; - break; - default: - FAIL_FAST_HR(E_NOTIMPL); - break; - } - - if (UpdateCursorPosition && cookedReadData.IsEchoInput()) + const auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; + if (std::find(delimiters.begin(), delimiters.end(), wch) != delimiters.end()) { - AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr); + return 2; } - - return STATUS_SUCCESS; + return 0; } diff --git a/src/host/cmdline.h b/src/host/cmdline.h index e3e9511ff29..89cdf19b5ca 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -3,90 +3,9 @@ #pragma once -#include "input.h" #include "screenInfo.hpp" -#include "server.h" - -#include "history.h" -#include "alias.h" -#include "readDataCooked.hpp" -#include "popup.h" - -class CommandLine -{ -public: - ~CommandLine(); - - static CommandLine& Instance(); - - static bool IsEditLineEmpty(); - void Hide(const bool fUpdateFields); - void Show(); - bool IsVisible() const noexcept; - - [[nodiscard]] NTSTATUS ProcessCommandLine(COOKED_READ_DATA& cookedReadData, - _In_ WCHAR wch, - const DWORD dwKeyState); - - [[nodiscard]] HRESULT StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData); - - bool HasPopup() const noexcept; - Popup& GetPopup() const; - - void EndCurrentPopup(); - void EndAllPopups(); - - void DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept; - til::point DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept; - -protected: - CommandLine(); - - // delete these because we don't want to accidentally get copies of the singleton - CommandLine(const CommandLine&) = delete; - CommandLine& operator=(const CommandLine&) = delete; - - [[nodiscard]] NTSTATUS _startCommandListPopup(COOKED_READ_DATA& cookedReadData); - [[nodiscard]] NTSTATUS _startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData); - [[nodiscard]] NTSTATUS _startCopyToCharPopup(COOKED_READ_DATA& cookedReadData); - - void _processHistoryCycling(COOKED_READ_DATA& cookedReadData, const CommandHistory::SearchDirection searchDirection); - void _setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData); - void _setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData); - til::point _deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorLeft(COOKED_READ_DATA& cookedReadData); - til::point _moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept; - void _insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept; - void _deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept; - void _fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData); - -#ifdef UNIT_TESTING - friend class CommandLineTests; - friend class CommandNumberPopupTests; -#endif - -private: - std::deque> _popups; - bool _isVisible; -}; - -void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields); - -void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); - -// Values for WriteChars(), WriteCharsLegacy() dwFlags -#define WC_INTERACTIVE 0x01 -#define WC_KEEP_CURSOR_VISIBLE 0x02 // Word delimiters -bool IsWordDelim(const wchar_t wch); -bool IsWordDelim(const std::wstring_view charData); - -bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...); - -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index); +bool IsWordDelim(wchar_t wch) noexcept; +bool IsWordDelim(const std::wstring_view& charData) noexcept; +int DelimiterClass(wchar_t wch) noexcept; diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 0b5b6db272e..4722cebe2b9 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -130,6 +130,11 @@ bool CONSOLE_INFORMATION::HasPendingCookedRead() const noexcept return _cookedReadData != nullptr; } +bool CONSOLE_INFORMATION::HasPendingPopup() const noexcept +{ + return _cookedReadData && _cookedReadData->PresentingPopup(); +} + const COOKED_READ_DATA& CONSOLE_INFORMATION::CookedReadData() const noexcept { return *_cookedReadData; diff --git a/src/host/ft_fuzzer/fuzzmain.cpp b/src/host/ft_fuzzer/fuzzmain.cpp index 4b8f04dea6e..cd3708d59ed 100644 --- a/src/host/ft_fuzzer/fuzzmain.cpp +++ b/src/host/ft_fuzzer/fuzzmain.cpp @@ -3,15 +3,13 @@ #include "precomp.h" +#include + #include "../ConsoleArguments.hpp" #include "../srvinit.h" -#include "../../server/Entrypoints.h" +#include "../_stream.h" #include "../../interactivity/inc/ServiceLocator.hpp" -#include "../../server/DeviceHandle.h" #include "../../server/IoThread.h" -#include "../_stream.h" -#include "../getset.h" -#include struct NullDeviceComm : public IDeviceComm { @@ -132,17 +130,8 @@ extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data, const auto u16String{ til::u8u16(std::string_view{ reinterpret_cast(data), size }) }; til::CoordType scrollY{}; - auto sizeInBytes{ u16String.size() * 2 }; gci.LockConsole(); auto u = wil::scope_exit([&]() { gci.UnlockConsole(); }); - (void)WriteCharsLegacy(gci.GetActiveOutputBuffer(), - u16String.data(), - u16String.data(), - u16String.data(), - &sizeInBytes, - nullptr, - 0, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &scrollY); + WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, true, &scrollY); return 0; } diff --git a/src/host/getset.cpp b/src/host/getset.cpp index f2cd485842f..5c2954f4fa4 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -598,13 +598,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont const auto requestedBufferSize = til::wrap_coord_size(data.dwSize); if (requestedBufferSize != coordScreenBufferSize) { - auto& commandLine = CommandLine::Instance(); - - commandLine.Hide(FALSE); - LOG_IF_FAILED(context.ResizeScreenBuffer(requestedBufferSize, TRUE)); - - commandLine.Show(); } const auto newBufferSize = context.GetBufferSize().Dimensions(); @@ -655,15 +649,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont if (NewSize.width != context.GetViewport().Width() || NewSize.height != context.GetViewport().Height()) { - // GH#1856 - make sure to hide the commandline _before_ we execute - // the resize, and the re-display it after the resize. If we leave - // it displayed, we'll crash during the resize when we try to figure - // out if the bounds of the old commandline fit within the new - // window (it might not). - auto& commandLine = CommandLine::Instance(); - commandLine.Hide(FALSE); context.SetViewportSize(&NewSize); - commandLine.Show(); const auto pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) diff --git a/src/host/history.cpp b/src/host/history.cpp index dfe1624987a..245e906695f 100644 --- a/src/host/history.cpp +++ b/src/host/history.cpp @@ -167,47 +167,9 @@ const std::vector& CommandHistory::GetCommands() const noexcept return _commands; } -[[nodiscard]] HRESULT CommandHistory::RetrieveNth(const Index index, std::span buffer, size_t& commandSize) +std::wstring_view CommandHistory::Retrieve(const SearchDirection searchDirection) { - LastDisplayed = index; - - try - { - const auto& cmd = _commands.at(index); - if (cmd.size() > buffer.size()) - { - commandSize = buffer.size(); // room for CRLF? - } - else - { - commandSize = cmd.size(); - } - - std::copy_n(cmd.cbegin(), commandSize, buffer.begin()); - - commandSize *= sizeof(wchar_t); - - return S_OK; - } - CATCH_RETURN(); -} - -[[nodiscard]] HRESULT CommandHistory::Retrieve(const SearchDirection searchDirection, - const std::span buffer, - size_t& commandSize) -{ - FAIL_FAST_IF(!(WI_IsFlagSet(Flags, CLE_ALLOCATED))); - - if (_commands.size() == 0) - { - return E_FAIL; - } - - if (_commands.size() == 1) - { - LastDisplayed = 0; - } - else if (searchDirection == SearchDirection::Previous) + if (searchDirection == SearchDirection::Previous) { // if this is the first time for this read that a command has // been retrieved, return the current command. otherwise, return @@ -218,15 +180,27 @@ const std::vector& CommandHistory::GetCommands() const noexcept } else { - _Prev(LastDisplayed); + LastDisplayed--; } } else { - _Next(LastDisplayed); + LastDisplayed++; + } + + return RetrieveNth(LastDisplayed); +} + +std::wstring_view CommandHistory::RetrieveNth(Index index) +{ + if (_commands.empty()) + { + LastDisplayed = 0; + return {}; } - return RetrieveNth(LastDisplayed, buffer, commandSize); + LastDisplayed = std::clamp(index, 0, GetNumberOfCommands() - 1); + return _commands.at(LastDisplayed); } std::wstring_view CommandHistory::GetLastCommand() const diff --git a/src/host/history.h b/src/host/history.h index 943f9475e21..172be22abfd 100644 --- a/src/host/history.h +++ b/src/host/history.h @@ -43,13 +43,8 @@ class CommandHistory [[nodiscard]] HRESULT Add(const std::wstring_view command, const bool suppressDuplicates); - [[nodiscard]] HRESULT Retrieve(const SearchDirection searchDirection, - const std::span buffer, - size_t& commandSize); - - [[nodiscard]] HRESULT RetrieveNth(const Index index, - const std::span buffer, - size_t& commandSize); + std::wstring_view Retrieve(const SearchDirection searchDirection); + std::wstring_view RetrieveNth(Index index); Index GetNumberOfCommands() const; std::wstring_view GetNth(Index index) const; diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems index 3ff022e82cf..ee482e31a93 100644 --- a/src/host/host-common.vcxitems +++ b/src/host/host-common.vcxitems @@ -3,10 +3,6 @@ - - - - @@ -29,7 +25,6 @@ - Create @@ -64,10 +59,6 @@ - - - - @@ -90,7 +81,6 @@ - diff --git a/src/host/input.cpp b/src/host/input.cpp index d48192daabb..ea13a0c0ef5 100644 --- a/src/host/input.cpp +++ b/src/host/input.cpp @@ -119,7 +119,7 @@ void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak) if (keyEvent.wVirtualKeyCode == 'C' && IsInProcessedInputMode()) { HandleCtrlEvent(CTRL_C_EVENT); - if (gci.PopupCount == 0) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlC); } @@ -135,7 +135,7 @@ void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak) { gci.pInputBuffer->Flush(); HandleCtrlEvent(CTRL_BREAK_EVENT); - if (gci.PopupCount == 0) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlBreak); } diff --git a/src/host/input.h b/src/host/input.h index 7e4509f6a1f..73f22dff777 100644 --- a/src/host/input.h +++ b/src/host/input.h @@ -58,11 +58,6 @@ class INPUT_KEY_INFO ULONG _ulControlKeyState; }; -#define TAB_SIZE 8 -#define TAB_MASK (TAB_SIZE - 1) -// WHY IS THIS NOT POSITION % TAB_SIZE?! -#define NUMBER_OF_SPACES_IN_TAB(POSITION) (TAB_SIZE - ((POSITION)&TAB_MASK)) - // these values are related to GetKeyboardState #define KEY_PRESSED 0x8000 #define KEY_TOGGLED 0x01 diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index fb42149b626..c108f2d1411 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -159,21 +159,6 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files @@ -329,21 +314,6 @@ Header Files - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - Header Files diff --git a/src/host/misc.cpp b/src/host/misc.cpp index 531ec2a9a63..329328f5672 100644 --- a/src/host/misc.cpp +++ b/src/host/misc.cpp @@ -47,140 +47,6 @@ void SetConsoleCPInfo(const BOOL fOutput) } } -// Routine Description: -// - This routine check bisected on Unicode string end. -// Arguments: -// - pwchBuffer - Pointer to Unicode string buffer. -// - cWords - Number of Unicode string. -// - cBytes - Number of bisect position by byte counts. -// Return Value: -// - TRUE - Bisected character. -// - FALSE - Correctly. -BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes) noexcept -{ - while (cWords && cBytes) - { - if (IsGlyphFullWidth(*pwchBuffer)) - { - if (cBytes < 2) - { - return TRUE; - } - else - { - cWords--; - cBytes -= 2; - pwchBuffer++; - } - } - else - { - cWords--; - cBytes--; - pwchBuffer++; - } - } - - return FALSE; -} - -// Routine Description: -// - This routine check bisected on Unicode string end. -// Arguments: -// - ScreenInfo - reference to screen information structure. -// - pwchBuffer - Pointer to Unicode string buffer. -// - cWords - Number of Unicode string. -// - cBytes - Number of bisect position by byte counts. -// - fPrintableControlChars - TRUE if control characters are being expanded (to ^X) -// Return Value: -// - TRUE - Bisected character. -// - FALSE - Correctly. -BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, - _In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes, - _In_ til::CoordType sOriginalXPosition, - _In_ BOOL fPrintableControlChars) -{ - if (WI_IsFlagSet(ScreenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) - { - while (cWords && cBytes) - { - const auto Char = *pwchBuffer; - if (Char >= UNICODE_SPACE) - { - if (IsGlyphFullWidth(Char)) - { - if (cBytes < 2) - { - return TRUE; - } - else - { - cWords--; - cBytes -= 2; - pwchBuffer++; - sOriginalXPosition += 2; - } - } - else - { - cWords--; - cBytes--; - pwchBuffer++; - sOriginalXPosition++; - } - } - else - { - cWords--; - pwchBuffer++; - switch (Char) - { - case UNICODE_BELL: - if (fPrintableControlChars) - goto CtrlChar; - break; - case UNICODE_BACKSPACE: - case UNICODE_LINEFEED: - case UNICODE_CARRIAGERETURN: - break; - case UNICODE_TAB: - { - size_t TabSize = NUMBER_OF_SPACES_IN_TAB(sOriginalXPosition); - sOriginalXPosition = (til::CoordType)(sOriginalXPosition + TabSize); - if (cBytes < TabSize) - return TRUE; - cBytes -= TabSize; - break; - } - default: - if (fPrintableControlChars) - { - CtrlChar: - if (cBytes < 2) - return TRUE; - cBytes -= 2; - sOriginalXPosition += 2; - } - else - { - cBytes--; - sOriginalXPosition++; - } - } - } - } - return FALSE; - } - else - { - return CheckBisectStringW(pwchBuffer, cWords, cBytes); - } -} - // Routine Description: // - Converts unicode characters to ANSI given a destination codepage // Arguments: @@ -205,18 +71,6 @@ int ConvertToOem(const UINT uiCodePage, return LOG_IF_WIN32_BOOL_FALSE(WideCharToMultiByte(uiCodePage, 0, pwchSource, cchSource, pchTarget, cchTarget, nullptr, nullptr)); } -// Data in the output buffer is the true unicode value. -int ConvertInputToUnicode(const UINT uiCodePage, - _In_reads_(cchSource) const CHAR* const pchSource, - const UINT cchSource, - _Out_writes_(cchTarget) WCHAR* const pwchTarget, - const UINT cchTarget) noexcept -{ - DBGCHARS(("ConvertInputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); - - return MultiByteToWideChar(uiCodePage, 0, pchSource, cchSource, pwchTarget, cchTarget); -} - // Output data is always translated via the ansi codepage so glyph translation works. int ConvertOutputToUnicode(_In_ UINT uiCodePage, _In_reads_(cchSource) const CHAR* const pchSource, @@ -226,46 +80,6 @@ int ConvertOutputToUnicode(_In_ UINT uiCodePage, { FAIL_FAST_IF(!(cchTarget > 0)); pwchTarget[0] = L'\0'; - DBGCHARS(("ConvertOutputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); - - if (DoBuffersOverlap(reinterpret_cast(pchSource), - cchSource * sizeof(CHAR), - reinterpret_cast(pwchTarget), - cchTarget * sizeof(WCHAR))) - { - try - { - // buffers overlap so we need to copy one - std::string copyData(pchSource, cchSource); - return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, copyData.data(), cchSource, pwchTarget, cchTarget); - } - catch (...) - { - return 0; - } - } - else - { - return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); - } -} - -// Routine Description: -// - checks if two buffers overlap -// Arguments: -// - pBufferA - pointer to start of first buffer -// - cbBufferA - size of first buffer, in bytes -// - pBufferB - pointer to start of second buffer -// - cbBufferB - size of second buffer, in bytes -// Return Value: -// - true if buffers overlap, false otherwise -bool DoBuffersOverlap(const BYTE* const pBufferA, - const UINT cbBufferA, - const BYTE* const pBufferB, - const UINT cbBufferB) noexcept -{ - const auto pBufferAEnd = pBufferA + cbBufferA; - const auto pBufferBEnd = pBufferB + cbBufferB; - return (pBufferA <= pBufferB && pBufferAEnd >= pBufferB) || (pBufferB <= pBufferA && pBufferBEnd >= pBufferA); + return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); } diff --git a/src/host/misc.h b/src/host/misc.h index ca90ac23ac8..38f9bce3f46 100644 --- a/src/host/misc.h +++ b/src/host/misc.h @@ -28,35 +28,14 @@ WCHAR CharToWchar(_In_reads_(cch) const char* const pch, const UINT cch); void SetConsoleCPInfo(const BOOL fOutput); -BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes) noexcept; -BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, - _In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes, - _In_ til::CoordType sOriginalXPosition, - _In_ BOOL fPrintableControlChars); - int ConvertToOem(const UINT uiCodePage, _In_reads_(cchSource) const WCHAR* const pwchSource, const UINT cchSource, _Out_writes_(cchTarget) CHAR* const pchTarget, const UINT cchTarget) noexcept; -int ConvertInputToUnicode(const UINT uiCodePage, - _In_reads_(cchSource) const CHAR* const pchSource, - const UINT cchSource, - _Out_writes_(cchTarget) WCHAR* const pwchTarget, - const UINT cchTarget) noexcept; - int ConvertOutputToUnicode(_In_ UINT uiCodePage, _In_reads_(cchSource) const CHAR* const pchSource, _In_ UINT cchSource, _Out_writes_(cchTarget) WCHAR* pwchTarget, _In_ UINT cchTarget) noexcept; - -bool DoBuffersOverlap(const BYTE* const pBufferA, - const UINT cbBufferA, - const BYTE* const pBufferB, - const UINT cbBufferB) noexcept; diff --git a/src/host/popup.cpp b/src/host/popup.cpp deleted file mode 100644 index b91376eb679..00000000000 --- a/src/host/popup.cpp +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "popup.h" - -#include "_output.h" -#include "output.h" - -#include "dbcs.h" -#include "srvinit.h" -#include "stream.h" - -#include "resource.h" - -#include "utils.hpp" - -#include "../interactivity/inc/ServiceLocator.hpp" - -#pragma hdrstop - -using namespace Microsoft::Console::Types; -using Microsoft::Console::Interactivity::ServiceLocator; -// Routine Description: -// - Creates an object representing an interactive popup overlay during cooked mode command line editing. -// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) -// Arguments: -// - screenInfo - Reference to screen on which the popup should be drawn/overlaid. -// - proposedSize - Suggested size of the popup. May be adjusted based on screen size. -Popup::Popup(SCREEN_INFORMATION& screenInfo, const til::size proposedSize) : - _screenInfo(screenInfo), - _userInputFunction(&Popup::_getUserInputInternal) -{ - _attributes = screenInfo.GetPopupAttributes(); - - const auto size = _CalculateSize(screenInfo, proposedSize); - const auto origin = _CalculateOrigin(screenInfo, size); - - _region.left = origin.x; - _region.top = origin.y; - _region.right = origin.x + size.width - 1; - _region.bottom = origin.y + size.height - 1; - - _oldScreenSize = screenInfo.GetBufferSize().Dimensions(); - - til::inclusive_rect TargetRect; - TargetRect.left = 0; - TargetRect.top = _region.top; - TargetRect.right = _oldScreenSize.width - 1; - TargetRect.bottom = _region.bottom; - - // copy the data into the backup buffer - _oldContents = std::move(screenInfo.ReadRect(Viewport::FromInclusive(TargetRect))); - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto countWas = gci.PopupCount.fetch_add(1ui16); - if (0 == countWas) - { - // If this is the first popup to be shown, stop the cursor from appearing/blinking - screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(true); - } -} - -// Routine Description: -// - Cleans up a popup object -// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) -Popup::~Popup() -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto countWas = gci.PopupCount.fetch_sub(1); - if (1 == countWas) - { - // Notify we're done showing popups. - gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsPopupShown(false); - } -} - -void Popup::Draw() -{ - _DrawBorder(); - _DrawContent(); -} - -// Routine Description: -// - Draws the outlines of the popup area in the screen buffer -void Popup::_DrawBorder() -{ - // fill attributes of top line - til::point WriteCoord; - WriteCoord.x = _region.left; - WriteCoord.y = _region.top; - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - // draw upper left corner - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_RIGHT, 1), WriteCoord); - - // draw upper bar - WriteCoord.x += 1; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord); - - // draw upper right corner - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_LEFT, 1), WriteCoord); - - for (til::CoordType i = 0; i < Height(); i++) - { - WriteCoord.y += 1; - WriteCoord.x = _region.left; - - // fill attributes - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord); - - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord); - } - - // Draw bottom line. - // Fill attributes of top line. - WriteCoord.x = _region.left; - WriteCoord.y = _region.bottom; - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - // Draw bottom left corner. - WriteCoord.x = _region.left; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_RIGHT, 1), WriteCoord); - - // Draw lower bar. - WriteCoord.x += 1; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord); - - // draw lower right corner - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_LEFT, 1), WriteCoord); -} - -// Routine Description: -// - Draws prompt information in the popup area to tell the user what to enter. -// Arguments: -// - id - Resource ID for string to display to user -void Popup::_DrawPrompt(const UINT id) -{ - auto text = _LoadString(id); - - // Draw empty popup. - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - auto lStringLength = Width(); - for (til::CoordType i = 0; i < Height(); i++) - { - const OutputCellIterator it(UNICODE_SPACE, _attributes, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - lStringLength = done.GetCellDistance(it); - - WriteCoord.y += 1; - } - - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - - // write prompt to screen - lStringLength = gsl::narrow(text.size()); - if (lStringLength > Width()) - { - text = text.substr(0, Width()); - } - - size_t used; - LOG_IF_FAILED(ServiceLocator::LocateGlobals().api->WriteConsoleOutputCharacterWImpl(_screenInfo, - text, - WriteCoord, - used)); -} - -// Routine Description: -// - Cleans up a popup by restoring the stored buffer information to the region of -// the screen that the popup was covering and frees resources. -void Popup::End() -{ - // restore previous contents to screen - - til::inclusive_rect SourceRect; - SourceRect.left = 0; - SourceRect.top = _region.top; - SourceRect.right = _oldScreenSize.width - 1; - SourceRect.bottom = _region.bottom; - - const auto sourceViewport = Viewport::FromInclusive(SourceRect); - - _screenInfo.WriteRect(_oldContents, sourceViewport.Origin()); -} - -// Routine Description: -// - Helper to calculate the size of the popup. -// Arguments: -// - screenInfo - Screen buffer we will be drawing into -// - proposedSize - The suggested size of the popup that may need to be adjusted to fit -// Return Value: -// - Coordinate size that the popup should consume in the screen buffer -til::size Popup::_CalculateSize(const SCREEN_INFORMATION& screenInfo, const til::size proposedSize) -{ - // determine popup dimensions - auto size = proposedSize; - size.width += 2; // add borders - size.height += 2; // add borders - - const auto viewportSize = screenInfo.GetViewport().Dimensions(); - - size.width = std::min(size.width, viewportSize.width); - size.height = std::min(size.height, viewportSize.height); - - // make sure there's enough room for the popup borders - THROW_HR_IF(E_NOT_SUFFICIENT_BUFFER, size.width < 2 || size.height < 2); - - return size; -} - -// Routine Description: -// - Helper to calculate the origin point (within the screen buffer) for the popup -// Arguments: -// - screenInfo - Screen buffer we will be drawing into -// - size - The size that the popup will consume -// Return Value: -// - Coordinate position of the origin point of the popup -til::point Popup::_CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const til::size size) -{ - const auto viewport = screenInfo.GetViewport(); - - // determine origin. center popup on window - til::point origin; - origin.x = (viewport.Width() - size.width) / 2 + viewport.Left(); - origin.y = (viewport.Height() - size.height) / 2 + viewport.Top(); - return origin; -} - -// Routine Description: -// - Helper to return the width of the popup in columns -// Return Value: -// - Width of popup inside attached screen buffer. -til::CoordType Popup::Width() const noexcept -{ - return _region.right - _region.left - 1; -} - -// Routine Description: -// - Helper to return the height of the popup in columns -// Return Value: -// - Height of popup inside attached screen buffer. -til::CoordType Popup::Height() const noexcept -{ - return _region.bottom - _region.top - 1; -} - -// Routine Description: -// - Helper to get the position on top of some types of popup dialogs where -// we should overlay the cursor for user input. -// Return Value: -// - Coordinate location on the popup where the cursor should be placed. -til::point Popup::GetCursorPosition() const noexcept -{ - til::point CursorPosition; - CursorPosition.x = _region.right - MINIMUM_COMMAND_PROMPT_SIZE; - CursorPosition.y = _region.top + 1; - return CursorPosition; -} - -// Routine Description: -// - changes the function used to gather user input. for allowing custom input during unit tests only -// Arguments: -// - function - function to use to fetch input -void Popup::SetUserInputFunction(UserInputFunction function) noexcept -{ - _userInputFunction = function; -} - -// Routine Description: -// - gets a single char input from the user -// Arguments: -// - cookedReadData - cookedReadData for this popup operation -// - popupKey - on completion, will be true if key was a popup key -// - wch - on completion, the char read from the user -// Return Value: -// - relevant NTSTATUS -[[nodiscard]] NTSTATUS Popup::_getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept -{ - return _userInputFunction(cookedReadData, popupKey, modifiers, wch); -} - -// Routine Description: -// - gets a single char input from the user using the InputBuffer -// Arguments: -// - cookedReadData - cookedReadData for this popup operation -// - popupKey - on completion, will be true if key was a popup key -// - wch - on completion, the char read from the user -// Return Value: -// - relevant NTSTATUS -[[nodiscard]] NTSTATUS Popup::_getUserInputInternal(COOKED_READ_DATA& cookedReadData, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) noexcept -{ - const auto pInputBuffer = cookedReadData.GetInputBuffer(); - auto Status = GetChar(pInputBuffer, - &wch, - true, - nullptr, - &popupKey, - &modifiers); - if (FAILED_NTSTATUS(Status) && Status != CONSOLE_STATUS_WAIT) - { - cookedReadData.BytesRead() = 0; - } - return Status; -} diff --git a/src/host/popup.h b/src/host/popup.h deleted file mode 100644 index 73a91bafe27..00000000000 --- a/src/host/popup.h +++ /dev/null @@ -1,82 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- popup.h - -Abstract: -- This file contains the internal structures and definitions used by command line input and editing. - -Author: -- Therese Stowell (ThereseS) 15-Nov-1991 - -Revision History: -- Mike Griese (migrie) Jan 2018: - Refactored the history and alias functionality into their own files. -- Michael Niksa (miniksa) May 2018: - Separated out popups from the rest of command line functionality. ---*/ - -#pragma once - -#include "readDataCooked.hpp" -#include "screenInfo.hpp" -#include "readDataCooked.hpp" - -class CommandHistory; - -class Popup -{ -public: - static constexpr til::CoordType MINIMUM_COMMAND_PROMPT_SIZE = 5; - - using UserInputFunction = std::function; - - Popup(SCREEN_INFORMATION& screenInfo, const til::size proposedSize); - virtual ~Popup(); - [[nodiscard]] virtual NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept = 0; - - void Draw(); - - void End(); - - til::CoordType Width() const noexcept; - til::CoordType Height() const noexcept; - - til::point GetCursorPosition() const noexcept; - -protected: - // used in test code to alter how the popup fetches use input - void SetUserInputFunction(UserInputFunction function) noexcept; - -#ifdef UNIT_TESTING - friend class CopyFromCharPopupTests; - friend class CopyToCharPopupTests; - friend class CommandNumberPopupTests; - friend class CommandListPopupTests; -#endif - - [[nodiscard]] NTSTATUS _getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept; - void _DrawPrompt(const UINT id); - virtual void _DrawContent() = 0; - - til::inclusive_rect _region; // region popup occupies - SCREEN_INFORMATION& _screenInfo; - TextAttribute _attributes; // text attributes - -private: - til::size _CalculateSize(const SCREEN_INFORMATION& screenInfo, const til::size proposedSize); - til::point _CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const til::size size); - - void _DrawBorder(); - - [[nodiscard]] static NTSTATUS _getUserInputInternal(COOKED_READ_DATA& cookedReadData, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) noexcept; - - OutputCellRect _oldContents; // contains data under popup - til::size _oldScreenSize; - UserInputFunction _userInputFunction; -}; diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 75e57b55963..667a445f4b2 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -3,46 +3,42 @@ #include "precomp.h" #include "readDataCooked.hpp" -#include "dbcs.h" + +#include "alias.h" +#include "history.h" +#include "resource.h" #include "stream.h" -#include "misc.h" #include "_stream.h" -#include "inputBuffer.hpp" -#include "cmdline.h" -#include "../types/inc/GlyphWidth.hpp" -#include "../types/inc/convert.hpp" - #include "../interactivity/inc/ServiceLocator.hpp" -#define LINE_INPUT_BUFFER_SIZE (256 * sizeof(WCHAR)) - using Microsoft::Console::Interactivity::ServiceLocator; +// As per https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10Obvious +constexpr int integerLog10(uint32_t v) +{ + return (v >= 1000000000) ? 9 : + (v >= 100000000) ? 8 : + (v >= 10000000) ? 7 : + (v >= 1000000) ? 6 : + (v >= 100000) ? 5 : + (v >= 10000) ? 4 : + (v >= 1000) ? 3 : + (v >= 100) ? 2 : + (v >= 10) ? 1 : + 0; +} + // Routine Description: // - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'. // Arguments: // - pInputBuffer - Buffer that data will be read from. // - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately. // - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it -// - BufferSize - -// - BytesRead - -// - CurrentPosition - -// - BufPtr - -// - BackupLimit - // - UserBufferSize - The byte count of the buffer presented by the client // - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host. -// - OriginalCursorPosition - -// - NumberOfVisibleChars // - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application -// - Echo - -// - InsertMode - -// - Processed - -// - Line - -// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text. // - initialData - any text data that should be prepopulated into the buffer // - pClientProcess - Attached process handle object -// Return Value: -// - THROW: Throws E_INVALIDARG for invalid pointers. COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, SCREEN_INFORMATION& screenInfo, @@ -54,242 +50,68 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, _In_ ConsoleProcessHandle* const pClientProcess) : ReadData(pInputBuffer, pInputReadHandleData), _screenInfo{ screenInfo }, - _bytesRead{ 0 }, - _currentPosition{ 0 }, - _userBufferSize{ UserBufferSize }, - _userBuffer{ UserBuffer }, - _tempHandle{ nullptr }, + _userBuffer{ UserBuffer, UserBufferSize }, _exeName{ exeName }, - _pdwNumBytes{ nullptr }, - - _commandHistory{ CommandHistory::s_Find(pClientProcess) }, - _controlKeyState{ 0 }, + _processHandle{ pClientProcess }, + _history{ CommandHistory::s_Find(pClientProcess) }, _ctrlWakeupMask{ CtrlWakeupMask }, - _visibleCharCount{ 0 }, - _originalCursorPosition{ -1, -1 }, - _beforeDialogCursorPosition{ 0, 0 }, - - _echoInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_ECHO_INPUT) }, - _lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) }, - _processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) }, - _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() }, - _unicode{ false }, - _clientProcess{ pClientProcess } + _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() } { #ifndef UNIT_TESTING - THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, - GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - _tempHandle)); + // The screen buffer instance is basically a reference counted HANDLE given out to the user. + // We need to ensure that it stays alive for the duration of the read. + // Coincidentally this serves another important purpose: It checks whether we're allowed to read from + // the given buffer in the first place. If it's missing the FILE_SHARE_READ flag, we can't read from it. + THROW_IF_FAILED(_screenInfo.AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle)); #endif - // to emulate OS/2 KbdStringIn, we read into our own big buffer - // (256 bytes) until the user types enter. then return as many - // chars as will fit in the user's buffer. - _bufferSize = std::max(UserBufferSize, LINE_INPUT_BUFFER_SIZE); - _buffer = std::make_unique(_bufferSize); - _backupLimit = reinterpret_cast(_buffer.get()); - _bufPtr = reinterpret_cast(_buffer.get()); - - // Initialize the user's buffer to spaces. This is done so that - // moving in the buffer via cursor doesn't do strange things. - std::fill_n(_bufPtr, _bufferSize / sizeof(wchar_t), UNICODE_SPACE); - if (!initialData.empty()) { - memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size() * 2); - - _bytesRead += initialData.size() * 2; - - const auto cchInitialData = initialData.size(); - VisibleCharCount() = cchInitialData; - _bufPtr += cchInitialData; - _currentPosition = cchInitialData; - - OriginalCursorPosition() = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - OriginalCursorPosition().x -= (til::CoordType)_currentPosition; - - const auto sScreenBufferSizeX = screenInfo.GetBufferSize().Width(); - while (OriginalCursorPosition().x < 0) + _buffer.assign(initialData); + _bufferCursor = _buffer.size(); + _bufferDirty = !_buffer.empty(); + + // The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird. + // The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t, + // so when you press tab it can autocomplete the prompt based on the available file names. + // The weird part is that it's not us who then prints the autocompletion. It's cmd.exe which calls WriteConsoleW(). + // It then initiates another ReadConsole() where the `nInitialChars` is the amount of chars it wrote via WriteConsoleW(). + // + // In other words, `nInitialChars` is a "trust me bro, I just wrote that in the buffer" API. + // This unfortunately means that the API is inherently broken: ReadConsole() visualizes control + // characters like Ctrl+X as "^X" and WriteConsoleW() doesn't and so the column counts don't match. + // Solving these issues is technically possible, but it's also quite difficult to do so correctly. + // + // But unfortunately (or fortunately) the initial (from the 1990s up to 2023) looked something like that: + // cursor = cursor.GetPosition(); + // cursor.x -= initialData.size(); + // while (cursor.x < 0) + // { + // cursor.x += textBuffer.Width(); + // cursor.y -= 1; + // } + // + // In other words, it assumed that the number of code units in the initial data corresponds 1:1 to + // the column count. This meant that the API never supported tabs for instance (nor wide glyphs). + // The new implementation still doesn't support tabs, but it does fix support for wide glyphs. + // That seemed like a good trade-off. + + // NOTE: You can't just "measure" the length of the string in columns either, because previously written + // wide glyphs might have resulted in padding whitespace in the text buffer (see ROW::WasDoubleBytePadded). + // The alternative to the loop below is counting the number of padding glyphs while iterating backwards. Either approach is fine. + til::CoordType distance = 0; + for (size_t i = 0; i < initialData.size(); i = TextBuffer::GraphemeNext(initialData, i)) { - OriginalCursorPosition().x += sScreenBufferSizeX; - OriginalCursorPosition().y -= 1; + --distance; } - } - - // TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci -} - -// Routine Description: -// - Destructs a read data class. -// - Decrements count of readers waiting on the given handle. -COOKED_READ_DATA::~COOKED_READ_DATA() -{ - CommandLine::Instance().EndAllPopups(); -} - -std::span COOKED_READ_DATA::SpanWholeBuffer() -{ - return std::span{ _backupLimit, (_bufferSize / sizeof(wchar_t)) }; -} - -std::span COOKED_READ_DATA::SpanAtPointer() -{ - auto wholeSpan = SpanWholeBuffer(); - return wholeSpan.subspan(_bufPtr - _backupLimit); -} - -bool COOKED_READ_DATA::HasHistory() const noexcept -{ - return _commandHistory != nullptr; -} - -CommandHistory& COOKED_READ_DATA::History() noexcept -{ - return *_commandHistory; -} - -const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept -{ - return _visibleCharCount; -} - -size_t& COOKED_READ_DATA::VisibleCharCount() noexcept -{ - return _visibleCharCount; -} - -SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept -{ - return _screenInfo; -} - -til::point COOKED_READ_DATA::OriginalCursorPosition() const noexcept -{ - return _originalCursorPosition; -} - -til::point& COOKED_READ_DATA::OriginalCursorPosition() noexcept -{ - return _originalCursorPosition; -} - -til::point& COOKED_READ_DATA::BeforeDialogCursorPosition() noexcept -{ - return _beforeDialogCursorPosition; -} - -bool COOKED_READ_DATA::IsEchoInput() const noexcept -{ - return _echoInput; -} - -bool COOKED_READ_DATA::IsInsertMode() const noexcept -{ - return _insertMode; -} - -void COOKED_READ_DATA::SetInsertMode(const bool mode) noexcept -{ - _insertMode = mode; -} - -bool COOKED_READ_DATA::IsUnicode() const noexcept -{ - return _unicode; -} - -// Routine Description: -// - gets the size of the user buffer -// Return Value: -// - the size of the user buffer in bytes -size_t COOKED_READ_DATA::UserBufferSize() const noexcept -{ - return _userBufferSize; -} - -// Routine Description: -// - gets a pointer to the beginning of the prompt storage -// Return Value: -// - pointer to the first char in the internal prompt storage array -wchar_t* COOKED_READ_DATA::BufferStartPtr() noexcept -{ - return _backupLimit; -} - -// Routine Description: -// - gets a pointer to where the next char will be inserted into the prompt storage -// Return Value: -// - pointer to the current insertion point of the prompt storage array -wchar_t* COOKED_READ_DATA::BufferCurrentPtr() noexcept -{ - return _bufPtr; -} - -// Routine Description: -// - Set the location of the next char insert into the prompt storage to be at -// ptr. ptr must point into a valid portion of the internal prompt storage array -// Arguments: -// - ptr - the new char insertion location -void COOKED_READ_DATA::SetBufferCurrentPtr(wchar_t* ptr) noexcept -{ - _bufPtr = ptr; -} - -// Routine Description: -// - gets the number of bytes read so far into the prompt buffer -// Return Value: -// - the number of bytes read -const size_t& COOKED_READ_DATA::BytesRead() const noexcept -{ - return _bytesRead; -} - -// Routine Description: -// - gets the number of bytes read so far into the prompt buffer -// Return Value: -// - the number of bytes read -size_t& COOKED_READ_DATA::BytesRead() noexcept -{ - return _bytesRead; -} - -// Routine Description: -// - gets the index for the current insertion point of the prompt -// Return Value: -// - the index of the current insertion point -const size_t& COOKED_READ_DATA::InsertionPoint() const noexcept -{ - return _currentPosition; -} - -// Routine Description: -// - gets the index for the current insertion point of the prompt -// Return Value: -// - the index of the current insertion point -size_t& COOKED_READ_DATA::InsertionPoint() noexcept -{ - return _currentPosition; -} - -// Routine Description: -// - sets the number of bytes that will be reported when this read block completes its read -// Arguments: -// - count - the number of bytes to report -void COOKED_READ_DATA::SetReportedByteCount(const size_t count) noexcept -{ - FAIL_FAST_IF_NULL(_pdwNumBytes); - *_pdwNumBytes = count; -} -// Routine Description: -// - resets the prompt to be as if it was erased -void COOKED_READ_DATA::Erase() noexcept -{ - _bufPtr = _backupLimit; - _bytesRead = 0; - _currentPosition = 0; - _visibleCharCount = 0; + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto end = cursor.GetPosition(); + const auto beg = textBuffer.NavigateCursor(end, distance); + _distanceCursor = (end.y - beg.y) * textBuffer.GetSize().Width() + end.x - beg.x; + _distanceEnd = _distanceCursor; + } } // Routine Description: @@ -314,24 +136,15 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, _Out_ NTSTATUS* const pReplyStatus, _Out_ size_t* const pNumBytes, _Out_ DWORD* const pControlKeyState, - _Out_ void* const /*pOutputData*/) + _Out_ void* const /*pOutputData*/) noexcept +try { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // this routine should be called by a thread owning the same - // lock on the same console as we're reading from. - FAIL_FAST_IF(!gci.IsConsoleLocked()); - *pNumBytes = 0; *pControlKeyState = 0; - *pReplyStatus = STATUS_SUCCESS; - FAIL_FAST_IF(_pInputReadHandleData->IsInputPending()); - - // this routine should be called by a thread owning the same lock on the same console as we're reading from. - FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); - // if ctrl-c or ctrl-break was seen, terminate read. if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) { @@ -351,7 +164,6 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, // We must see if we were woken up because the handle is being closed. If // so, we decrement the read count. If it goes to zero, we wake up the // close thread. Otherwise, we wake up any other thread waiting for data. - if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) { *pReplyStatus = STATUS_ALERTED; @@ -359,71 +171,23 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, return true; } - // If we get to here, this routine was called either by the input thread - // or a write routine. Both of these callers grab the current console - // lock. - - // MSFT:13994975 This is REALLY weird. - // When we're doing cooked reading for popups, we come through this method - // twice. Once when we press F7 to bring up the popup, then again when we - // press enter to input the selected command. - // The first time, there is no popup, and we go to CookedRead. We pass into - // CookedRead `pNumBytes`, which is passed to us as the address of the - // stack variable dwNumBytes, in ConsoleWaitBlock::Notify. - // CookedRead sets this->_pdwNumBytes to that value, and starts the popup, - // which returns all the way up, and pops the ConsoleWaitBlock::Notify - // stack frame containing the address we're pointing at. - // Then on the second time through this function, we hit this if block, - // because there is a popup to get input from. - // However, pNumBytes is now the address of a different stack frame, and not - // necessarily the same as before (presumably not at all). The - // Callback would try and write the number of bytes read to the - // value in _pdwNumBytes, and then we'd return up to ConsoleWaitBlock::Notify, - // who's dwNumBytes had nothing in it. - // To fix this, when we hit this with a popup, we're going to make sure to - // refresh the value of _pdwNumBytes to the current address we want to put - // the out value into. - // It's still really weird, but limits the potential fallout of changing a - // piece of old spaghetti code. - if (_commandHistory) - { - if (CommandLine::Instance().HasPopup()) - { - // (see above comment, MSFT:13994975) - // Make sure that the popup writes the dwNumBytes to the right place - if (pNumBytes) - { - _pdwNumBytes = pNumBytes; - } - - auto& popup = CommandLine::Instance().GetPopup(); - *pReplyStatus = popup.Process(*this); - if (*pReplyStatus == CONSOLE_STATUS_READ_COMPLETE || - (*pReplyStatus != CONSOLE_STATUS_WAIT && *pReplyStatus != CONSOLE_STATUS_WAIT_NO_BLOCK)) - { - *pReplyStatus = S_OK; - gci.SetCookedReadData(nullptr); - return true; - } - return false; - } - } - - *pReplyStatus = Read(fIsUnicode, *pNumBytes, *pControlKeyState); - if (*pReplyStatus != CONSOLE_STATUS_WAIT) + if (Read(fIsUnicode, *pNumBytes, *pControlKeyState)) { gci.SetCookedReadData(nullptr); return true; } - else - { - return false; - } + + return false; } +NT_CATCH_RETURN() -bool COOKED_READ_DATA::AtEol() const noexcept +void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) noexcept { - return _bytesRead == (_currentPosition * 2); + // See the comment in WaitBlock.cpp for more information. + if (_userBuffer.data() == oldBuffer) + { + _userBuffer = { static_cast(newBuffer), _userBuffer.size() }; + } } // Routine Description: @@ -436,633 +200,1096 @@ bool COOKED_READ_DATA::AtEol() const noexcept // - numBytes - On in, the number of bytes available in the client // buffer. On out, the number of bytes consumed in the client buffer. // - controlKeyState - For some types of reads, this is the modifier key state with the last button press. -[[nodiscard]] HRESULT COOKED_READ_DATA::Read(const bool isUnicode, - size_t& numBytes, - ULONG& controlKeyState) noexcept +bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) { controlKeyState = 0; - auto Status = _readCharInputLoop(isUnicode, numBytes); + const auto done = _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 the read was completed (status != wait), free the cooked read - // data. also, close the temporary output handle that was opened to - // echo the characters read. - if (Status != CONSOLE_STATUS_WAIT) + if (done) { - Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); } - return Status; + return done; } -void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount) +// Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted. +// When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate. +// To fix this, this function is called before a resize and will clear the input line. Afterwards, RedrawAfterResize() will restore it. +void COOKED_READ_DATA::EraseBeforeResize() { - Alias::s_MatchAndCopyAliasLegacy(_backupLimit, - _bytesRead, - _backupLimit, - _bufferSize, - _bytesRead, - _exeName, - lineCount); -} + _popupsDone(); -// Routine Description: -// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and -// the final ENTER key press that will end the wait and finally return the data. -// Arguments: -// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.) -// - wch - The most recently pressed/retrieved character from the input buffer (keystroke) -// - keyState - Modifier keys/state information with the pressed key/character -// - status - The return code to pass to the client -// Return Value: -// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke. -bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig, - const DWORD keyState, - NTSTATUS& status) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - size_t NumSpaces = 0; - til::CoordType ScrollY = 0; - size_t NumToWrite; - auto wch = wchOrig; - bool fStartFromDelim; - - status = STATUS_SUCCESS; - if (_bytesRead >= (_bufferSize - (2 * sizeof(WCHAR))) && wch != UNICODE_CARRIAGERETURN && wch != UNICODE_BACKSPACE) + if (_distanceEnd) { - return false; + _unwindCursorPosition(_distanceCursor); + _erase(_distanceEnd); + _unwindCursorPosition(_distanceEnd); + _distanceCursor = 0; + _distanceEnd = 0; } +} - if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) +// The counter-part to EraseBeforeResize(). +void COOKED_READ_DATA::RedrawAfterResize() +{ + _markAsDirty(); + _flushBuffer(); +} + +void COOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept +{ + _insertMode = insertMode; +} + +bool COOKED_READ_DATA::IsEmpty() const noexcept +{ + return _buffer.empty() && _popups.empty(); +} + +bool COOKED_READ_DATA::PresentingPopup() const noexcept +{ + return !_popups.empty(); +} + +til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept +{ + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto beg = _offsetPosition(cursor.GetPosition(), -_distanceCursor); + const auto end = _offsetPosition(beg, _distanceEnd); + return { beg, end }; +} + +// _wordPrev and _wordNext implement the classic Windows word-wise cursor movement algorithm, as traditionally used by +// conhost, notepad, Visual Studio and other "old" applications. If you look closely you can see how they're the exact +// same "skip 1 char, skip x, skip not-x", but since the "x" between them is different (non-words for _wordPrev and +// words for _wordNext) it results in the inconsistent feeling that these have compared to more modern algorithms. +// TODO: GH#15787 +size_t COOKED_READ_DATA::_wordPrev(const std::wstring_view& chars, size_t position) +{ + if (position != 0) { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; - _controlKeyState = keyState; - return true; + --position; + while (position != 0 && chars[position] == L' ') + { + --position; + } + + const auto dc = DelimiterClass(chars[position]); + while (position != 0 && DelimiterClass(chars[position - 1]) == dc) + { + --position; + } } + return position; +} - if (wch == EXTKEY_ERASE_PREV_WORD) +size_t COOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t position) +{ + if (position < chars.size()) { - wch = UNICODE_BACKSPACE; + ++position; + const auto dc = DelimiterClass(chars[position - 1]); + while (position != chars.size() && dc == DelimiterClass(chars[position])) + { + ++position; + } + while (position != chars.size() && chars[position] == L' ') + { + ++position; + } } + 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; +} - if (AtEol()) +// Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents. +bool COOKED_READ_DATA::_readCharInputLoop() +{ + for (;;) { - // If at end of line, processing is relatively simple. Just store the character and write it to the screen. - if (wch == UNICODE_BACKSPACE2) + const auto hasPopup = !_popups.empty(); + auto charOrVkey = UNICODE_NULL; + auto commandLineEditingKeys = false; + auto popupKeys = false; + const auto pCommandLineEditingKeys = hasPopup ? nullptr : &commandLineEditingKeys; + const auto pPopupKeys = hasPopup ? &popupKeys : nullptr; + DWORD modifiers = 0; + + const auto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers); + if (status == CONSOLE_STATUS_WAIT) { - wch = UNICODE_BACKSPACE; + return false; } + THROW_IF_NTSTATUS_FAILED(status); - if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit) + if (hasPopup) + { + const auto wch = static_cast(popupKeys ? 0 : charOrVkey); + const auto vkey = static_cast(popupKeys ? charOrVkey : 0); + if (_popupHandleInput(wch, vkey, modifiers)) + { + return true; + } + } + else { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); + if (commandLineEditingKeys) + { + _handleVkey(charOrVkey, modifiers); + } + else if (_handleChar(charOrVkey, modifiers)) + { + return true; + } + } + } +} + +// Handles character input for _readCharInputLoop() when no popups exist. +bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) +{ + // All paths in this function modify the buffer. + + if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) + { + _flushBuffer(); + + // The old implementation (all the way since the 90s) overwrote the character at the current cursor position with the given wch. + // But simultaneously it incremented the buffer length, which would have only worked if it was written at the end of the buffer. + // Press tab past the "f" in the string "foo" and you'd get "f\to " (a trailing whitespace; the initial contents of the buffer back then). + // It's unclear whether the original intention was to write at the end of the buffer at all times or to implement an insert mode. + // I went with insert mode. + _buffer.insert(_bufferCursor, 1, wch); + _bufferCursor++; + + _controlKeyState = modifiers; + return true; + } - auto loop = true; - while (loop) + switch (wch) + { + case UNICODE_CARRIAGERETURN: + { + _buffer.append(_newlineSuffix()); + _bufferCursor = _buffer.size(); + _markAsDirty(); + return true; + } + case EXTKEY_ERASE_PREV_WORD: // Ctrl+Backspace + case UNICODE_BACKSPACE: + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT)) + { + size_t pos; + if (wch == EXTKEY_ERASE_PREV_WORD) { - loop = false; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - &NumSpaces, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY); - if (SUCCEEDED_NTSTATUS(status)) - { - _originalCursorPosition.y += ScrollY; - } - else - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } + pos = _wordPrev(_buffer, _bufferCursor); + } + else + { + pos = TextBuffer::GraphemePrev(_buffer, _bufferCursor); + } - _visibleCharCount += NumSpaces; - if (wch == UNICODE_BACKSPACE && _processedInput) - { - _bytesRead -= sizeof(WCHAR); - // clang-format off -#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine") - // clang-format on - *_bufPtr = (WCHAR)' '; - _bufPtr -= 1; - _currentPosition -= 1; - - // Repeat until it hits the word boundary - if (wchOrig == EXTKEY_ERASE_PREV_WORD && - _bufPtr != _backupLimit && - fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) - { - loop = true; - } - } - else + _buffer.erase(pos, _bufferCursor - pos); + _bufferCursor = pos; + _markAsDirty(); + + // Notify accessibility to read the backspaced character. + // See GH:12735, MSFT:31748387 + if (_screenInfo.HasAccessibilityEventing()) + { + if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow()) { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; + LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); } } + return false; } + // If processed mode is disabled, control characters like backspace are treated like any other character. + break; + default: + break; + } + + if (_insertMode) + { + _buffer.insert(_bufferCursor, 1, wch); } else { - auto CallWrite = true; - const auto sScreenBufferSizeX = _screenInfo.GetBufferSize().Width(); + // TODO GH#15875: If the input grapheme is >1 char, then this will replace >1 grapheme + // --> We should accumulate input text as much as possible and then call _processInput with wstring_view. + const auto nextGraphemeLength = TextBuffer::GraphemeNext(_buffer, _bufferCursor) - _bufferCursor; + _buffer.replace(_bufferCursor, nextGraphemeLength, 1, wch); + } - // processing in the middle of the line is more complex: + _bufferCursor++; + _markAsDirty(); + return false; +} - // calculate new cursor position - // store new char - // clear the current command line from the screen - // write the new command line to the screen - // update the cursor position +// Handles non-character input for _readCharInputLoop() when no popups exist. +void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) +{ + const auto ctrlPressed = WI_IsAnyFlagSet(modifiers, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); + const auto altPressed = WI_IsAnyFlagSet(modifiers, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); - if (wch == UNICODE_BACKSPACE && _processedInput) + switch (vkey) + { + case VK_ESCAPE: + if (!_buffer.empty()) { - // for backspace, use writechars to calculate the new cursor position. - // this call also sets the cursor to the right position for the - // second call to writechars. - - if (_bufPtr != _backupLimit) + _buffer.clear(); + _bufferCursor = 0; + _markAsDirty(); + } + break; + case VK_HOME: + if (_bufferCursor > 0) + { + if (ctrlPressed) { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); - - auto loop = true; - while (loop) - { - loop = false; - // we call writechar here so that cursor position gets updated - // correctly. we also call it later if we're not at eol so - // that the remainder of the string can be updated correctly. - - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - nullptr, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr); - if (FAILED_NTSTATUS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } - _bytesRead -= sizeof(WCHAR); - _bufPtr -= 1; - _currentPosition -= 1; - memmove(_bufPtr, - _bufPtr + 1, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - { - auto buf = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); - *buf = (WCHAR)' '; - } - NumSpaces = 0; - - // Repeat until it hits the word boundary - if (wchOrig == EXTKEY_ERASE_PREV_WORD && - _bufPtr != _backupLimit && - fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) - { - loop = true; - } - } + _buffer.erase(0, _bufferCursor); + } + _bufferCursor = 0; + _markAsDirty(); + } + break; + case VK_END: + if (_bufferCursor < _buffer.size()) + { + if (ctrlPressed) + { + _buffer.erase(_bufferCursor); + } + _bufferCursor = _buffer.size(); + _markAsDirty(); + } + break; + case VK_LEFT: + if (_bufferCursor != 0) + { + if (ctrlPressed) + { + _bufferCursor = _wordPrev(_buffer, _bufferCursor); } else { - CallWrite = false; + _bufferCursor = TextBuffer::GraphemePrev(_buffer, _bufferCursor); } + _markAsDirty(); } - else + break; + case VK_F1: + case VK_RIGHT: + if (_bufferCursor != _buffer.size()) { - // store the char - if (wch == UNICODE_CARRIAGERETURN) + if (ctrlPressed && vkey == VK_RIGHT) { - _bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); - *_bufPtr = wch; - _bufPtr += 1; - _bytesRead += sizeof(WCHAR); - _currentPosition += 1; + _bufferCursor = _wordNext(_buffer, _bufferCursor); } else { - auto fBisect = false; - - if (_echoInput) - { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.x, - _originalCursorPosition.x, - TRUE)) - { - fBisect = true; - } - } + _bufferCursor = TextBuffer::GraphemeNext(_buffer, _bufferCursor); + } + _markAsDirty(); + } + else if (_history) + { + // Traditionally pressing right at the end of an input line would paste characters from the previous command. + const auto cmd = _history->GetLastCommand(); + const auto bufferSize = _buffer.size(); + const auto cmdSize = cmd.size(); + size_t bufferBeg = 0; + size_t cmdBeg = 0; + + // We cannot just check if the cmd is longer than the _buffer, because we want to copy graphemes, + // not characters and there's no correlation between the number of graphemes and their byte length. + while (cmdBeg < cmdSize) + { + const auto cmdEnd = TextBuffer::GraphemeNext(cmd, cmdBeg); - if (_insertMode) + if (bufferBeg >= bufferSize) { - memmove(_bufPtr + 1, - _bufPtr, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - _bytesRead += sizeof(WCHAR); + _buffer.append(cmd, cmdBeg, cmdEnd - cmdBeg); + _bufferCursor = _buffer.size(); + _markAsDirty(); + break; } - *_bufPtr = wch; - _bufPtr += 1; - _currentPosition += 1; - // calculate new cursor position - if (_echoInput) - { - NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.x, - _backupLimit, - _currentPosition - 1); - if (NumSpaces > 0 && fBisect) - NumSpaces--; - } + bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg); + cmdBeg = cmdEnd; } } - - if (_echoInput && CallWrite) + break; + case VK_INSERT: + _insertMode = !_insertMode; + _screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode()); + _markAsDirty(); + break; + case VK_DELETE: + if (_bufferCursor < _buffer.size()) { - til::point CursorPosition; - - // save cursor position - CursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); - CursorPosition.x = (til::CoordType)(CursorPosition.x + NumSpaces); - - // clear the current command line from the screen - // clang-format off -#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast doesn't like this call.") - // clang-format on - DeleteCommandLine(*this, FALSE); - - // write the new command line to the screen - NumToWrite = _bytesRead; - - DWORD dwFlags = WC_INTERACTIVE; - if (wch == UNICODE_CARRIAGERETURN) + _buffer.erase(_bufferCursor, TextBuffer::GraphemeNext(_buffer, _bufferCursor) - _bufferCursor); + _markAsDirty(); + } + break; + case VK_UP: + case VK_F5: + if (_history && !_history->AtFirstCommand()) + { + _replaceBuffer(_history->Retrieve(CommandHistory::SearchDirection::Previous)); + } + break; + case VK_DOWN: + if (_history && !_history->AtLastCommand()) + { + _replaceBuffer(_history->Retrieve(CommandHistory::SearchDirection::Next)); + } + break; + case VK_PRIOR: + if (_history && !_history->AtFirstCommand()) + { + _replaceBuffer(_history->RetrieveNth(0)); + } + break; + case VK_NEXT: + if (_history && !_history->AtLastCommand()) + { + _replaceBuffer(_history->RetrieveNth(INT_MAX)); + } + break; + case VK_F2: + if (_history) + { + _popupPush(PopupKind::CopyToChar); + } + break; + case VK_F3: + if (_history) + { + const auto last = _history->GetLastCommand(); + if (last.size() > _bufferCursor) + { + const auto count = last.size() - _bufferCursor; + _buffer.replace(_bufferCursor, count, last, _bufferCursor, count); + _bufferCursor += count; + _markAsDirty(); + } + } + break; + case VK_F4: + // Historically the CopyFromChar popup was constrained to only work when a history exists, + // but I don't see why that should be. It doesn't depend on _history at all. + _popupPush(PopupKind::CopyFromChar); + break; + case VK_F6: + // Don't ask me why but F6 is an alias for ^Z. + _handleChar(0x1a, modifiers); + break; + case VK_F7: + if (!ctrlPressed && !altPressed) + { + if (_history && _history->GetNumberOfCommands()) { - dwFlags |= WC_KEEP_CURSOR_VISIBLE; + _popupPush(PopupKind::CommandList); } - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _backupLimit, - _backupLimit, - &NumToWrite, - &_visibleCharCount, - _originalCursorPosition.x, - dwFlags, - &ScrollY); - if (FAILED_NTSTATUS(status)) + } + else if (altPressed) + { + if (_history) { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - _bytesRead = 0; - return true; + _history->Empty(); + _history->Flags |= CommandHistory::CLE_ALLOCATED; } - - // update cursor position - if (wch != UNICODE_CARRIAGERETURN) + } + break; + case VK_F8: + if (_history) + { + CommandHistory::Index index = 0; + const auto prefix = std::wstring_view{ _buffer }.substr(0, _bufferCursor); + if (_history->FindMatchingCommand(prefix, _history->LastDisplayed, index, CommandHistory::MatchOptions::None)) { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.x, - _originalCursorPosition.x, - TRUE)) - { - if (CursorPosition.x == (sScreenBufferSizeX - 1)) - { - CursorPosition.x++; - } - } - - // adjust cursor position for WriteChars - _originalCursorPosition.y += ScrollY; - CursorPosition.y += ScrollY; - AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr); + _buffer.assign(_history->RetrieveNth(index)); + _bufferCursor = std::min(_bufferCursor, _buffer.size()); + _markAsDirty(); } } + break; + case VK_F9: + if (_history && _history->GetNumberOfCommands()) + { + _popupPush(PopupKind::CommandNumber); + } + break; + case VK_F10: + // Alt+F10 clears the aliases for specifically cmd.exe. + if (altPressed) + { + Alias::s_ClearCmdExeAliases(); + } + break; + default: + assert(false); // Unrecognized VK. Fix or don't call this function? + break; } +} - // in cooked mode, enter (carriage return) is converted to - // carriage return linefeed (0xda). carriage return is always - // stored at the end of the buffer. - if (wch == UNICODE_CARRIAGERETURN) +// Handles any tasks that need to be completed after the read input loop finishes, +// like handling doskey aliases and converting the input to non-UTF16. +void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) +{ + auto writer = _userBuffer; + std::wstring_view input{ _buffer }; + size_t lineCount = 1; + + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) { - if (_processedInput) + // 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)) { - if (_bytesRead < _bufferSize) + input.remove_suffix(suffix.size()); + + if (_history) { - *_bufPtr = UNICODE_LINEFEED; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - _bufPtr, - &NumToWrite, - nullptr, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr); - if (FAILED_NTSTATUS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - } - } - _bytesRead += sizeof(WCHAR); - _bufPtr++; - _currentPosition += 1; + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LOG_IF_FAILED(_history->Add(input, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); } - } - // reset the cursor back to 25% if necessary - if (_lineInput) - { - if (_insertMode != gci.GetInsertMode()) + + Tracing::s_TraceCookedRead(_processHandle, input); + + const auto alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount); + if (!alias.empty()) { - // Make cursor small. - LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0)); + _buffer = alias; } - status = STATUS_SUCCESS; - return true; + // NOTE: Even if there's no alias we should restore the trailing \r\n that we removed above. + 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)); + } } } - return false; + const auto inputSizeBefore = input.size(); + _pInputBuffer->Consume(isUnicode, input, writer); + + if (lineCount > 1) + { + // This is a continuation of the above identical if condition. + // We've truncated the `input` slice and now we need to restore it. + const auto inputSizeAfter = input.size(); + const auto amountConsumed = inputSizeBefore - inputSizeAfter; + input = std::wstring_view{ _buffer }; + input = input.substr(std::min(input.size(), amountConsumed)); + GetInputReadHandleData()->SaveMultilinePendingInput(input); + } + else if (!input.empty()) + { + GetInputReadHandleData()->SavePendingInput(input); + } + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; + + // If we previously called SetCursorDBMode() with true, + // this will ensure that the cursor returns to its normal look. + _screenInfo.SetCursorDBMode(false); + + numBytes = _userBuffer.size() - writer.size(); + controlKeyState = _controlKeyState; } -// Routine Description: -// - Writes string to current position in prompt line. can overwrite text to the right of the cursor. -// Arguments: -// - wstr - the string to write -// Return Value: -// - The number of chars written -size_t COOKED_READ_DATA::Write(const std::wstring_view wstr) +// 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(). +// +// By using _bufferDirty to avoid redrawing the buffer unless needed, we turn the amortized time complexity of _readCharInputLoop() +// from O(n^2) (n(n+1)/2 redraws) into O(n). Pasting text would quickly turn into "accidentally quadratic" meme material otherwise. +void COOKED_READ_DATA::_markAsDirty() { - auto end = wstr.end(); - const auto charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit); - if (wstr.size() > charsRemaining) + _bufferDirty = true; +} + +// Draws the contents of _buffer onto the screen. +void COOKED_READ_DATA::_flushBuffer() +{ + // _flushBuffer() is called often and is a good place to assert() that our _bufferCursor is still in bounds. + assert(_bufferCursor <= _buffer.size()); + _bufferCursor = std::min(_bufferCursor, _buffer.size()); + + if (!_bufferDirty) { - end = std::next(wstr.begin(), charsRemaining); + return; } - std::copy(wstr.begin(), end, _bufPtr); - const size_t charsInserted = end - wstr.begin(); - auto bytesInserted = charsInserted * sizeof(wchar_t); - _currentPosition += charsInserted; - _bytesRead += bytesInserted; - - if (IsEchoInput()) + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) { - size_t NumSpaces = 0; - til::CoordType ScrollY = 0; - - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(), - _backupLimit, - _bufPtr, - _bufPtr, - &bytesInserted, - &NumSpaces, - OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - OriginalCursorPosition().y += ScrollY; - VisibleCharCount() += NumSpaces; + _unwindCursorPosition(_distanceCursor); + + const std::wstring_view view{ _buffer }; + const auto distanceBeforeCursor = _writeChars(view.substr(0, _bufferCursor)); + const auto distanceAfterCursor = _writeChars(view.substr(_bufferCursor)); + const auto distanceEnd = distanceBeforeCursor + distanceAfterCursor; + const auto eraseDistance = std::max(0, _distanceEnd - distanceEnd); + + // If the contents of _buffer became shorter we'll have to erase the previously printed contents. + _erase(eraseDistance); + _unwindCursorPosition(distanceAfterCursor + eraseDistance); + + _distanceCursor = distanceBeforeCursor; + _distanceEnd = distanceEnd; } - _bufPtr += charsInserted; - return charsInserted; + _bufferDirty = false; } -// Routine Description: -// - saves data in the prompt buffer to the outgoing user buffer -// Arguments: -// - cch - the number of chars to write to the user buffer -// Return Value: -// - the number of bytes written to the user buffer -size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch) +// This is just a small helper to fill the next N cells starting at the current cursor position with whitespace. +// The implementation is inefficient for `count`s larger than 7, but such calls are uncommon to happen (namely only when resizing the window). +void COOKED_READ_DATA::_erase(const til::CoordType distance) +{ + if (distance > 0) + { + const std::wstring str(gsl::narrow_cast(distance), L' '); + std::ignore = _writeChars(str); + } +} + +// A helper to write text and calculate the number of cells we've written. +// _unwindCursorPosition then allows us to go that many cells back. Tracking cells instead of explicit +// buffer positions allows us to pay no further mind to whether the buffer scrolled up or not. +til::CoordType COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const { - size_t bytesToWrite = 0; - const auto hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite); - if (FAILED(hr)) + if (text.empty()) { return 0; } - memmove(_userBuffer, _backupLimit, bytesToWrite); + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto width = textBuffer.GetSize().Width(); + const auto initialCursorPos = cursor.GetPosition(); + til::CoordType scrollY = 0; + + WriteCharsLegacy(_screenInfo, text, true, &scrollY); + + const auto finalCursorPos = cursor.GetPosition(); + return (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; +} + +// Moves the given point by the given distance inside the text buffer, as if moving a cursor with the left/right arrow keys. +til::point COOKED_READ_DATA::_offsetPosition(til::point pos, til::CoordType distance) const +{ + const auto size = _screenInfo.GetTextBuffer().GetSize().Dimensions(); + const auto w = static_cast(size.width); + const auto h = static_cast(size.height); + const auto area = w * h; + + auto off = w * pos.y + pos.x; + off += distance; + off = off < 0 ? 0 : (off > area ? area : off); + + return { + gsl::narrow_cast(off % w), + gsl::narrow_cast(off / w), + }; +} + +// This moves the cursor `distance`-many cells back up in the buffer. +// It's intended to be used in combination with _writeChars. +void COOKED_READ_DATA::_unwindCursorPosition(til::CoordType distance) const +{ + if (distance <= 0) + { + // If all the code in this file works correctly, negative distances should not occur. + // If they do occur it would indicate a bug that would need to be fixed urgently. + assert(distance == 0); + return; + } + + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto pos = _offsetPosition(cursor.GetPosition(), -distance); + + std::ignore = _screenInfo.SetCursorPosition(pos, true); + _screenInfo.MakeCursorVisible(pos); +} + +// Just a simple helper to replace the entire buffer contents. +void COOKED_READ_DATA::_replaceBuffer(const std::wstring_view& str) +{ + _buffer.assign(str); + _bufferCursor = _buffer.size(); + _markAsDirty(); +} + +// If the viewport is large enough to fit a popup, this function prepares everything for +// showing the given type. It handles computing the size of the popup, its position, +// backs the affected area up and draws the border and initial contents. +void COOKED_READ_DATA::_popupPush(const PopupKind kind) +try +{ + auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto viewport = _screenInfo.GetViewport(); + const auto viewportOrigin = viewport.Origin(); + const auto viewportSize = viewport.Dimensions(); + + til::size proposedSize; + switch (kind) + { + case PopupKind::CopyToChar: + proposedSize = { 26, 1 }; + break; + case PopupKind::CopyFromChar: + proposedSize = { 28, 1 }; + break; + case PopupKind::CommandNumber: + proposedSize = { 22 + CommandNumberMaxInputLength, 1 }; + break; + case PopupKind::CommandList: + { + const auto& commands = _history->GetCommands(); + const auto commandCount = _history->GetNumberOfCommands(); + + size_t maxStringLength = 0; + for (const auto& c : commands) + { + maxStringLength = std::max(maxStringLength, c.size()); + } + + // Account for the "123: " prefix each line gets. + maxStringLength += integerLog10(commandCount); + maxStringLength += 3; + + // conhost used to draw the command list with a size of 40x10, but at some point it switched over to dynamically + // sizing it depending on the history count and width of the entries. Back then it was implemented with the + // assumption that the code unit count equals the column count, which I kept because I don't want the TextBuffer + // class to expose how wide characters are, any more than necessary. It makes implementing Unicode support + // much harder, because things like combining marks and work zones may cause TextBuffer to end up deciding + // a piece of text has a different size than what you thought it had when measuring it on its own. + proposedSize.width = gsl::narrow_cast(std::clamp(maxStringLength, 40, til::CoordTypeMax)); + proposedSize.height = std::clamp(commandCount, 10, 20); + break; + } + default: + assert(false); + return; + } + + // Subtract 2 because we need to draw the border around our content. We must return early if we're + // unable to do so, otherwise the remaining code fails because the size would be zero/negative. + const til::size contentSize{ + std::min(proposedSize.width, viewportSize.width - 2), + std::min(proposedSize.height, viewportSize.height - 2), + }; + if (!contentSize) + { + return; + } - if (!IsUnicode()) + const auto widthSizeT = gsl::narrow_cast(contentSize.width + 2); + const auto heightSizeT = gsl::narrow_cast(contentSize.height + 2); + const til::point contentOrigin{ + (viewportSize.width - contentSize.width) / 2 + viewportOrigin.x, + (viewportSize.height - contentSize.height) / 2 + viewportOrigin.y, + }; + const til::rect contentRect{ + contentOrigin, + contentSize, + }; + const auto backupRect = Viewport::FromExclusive({ + contentRect.left - 1, + contentRect.top - 1, + contentRect.right + 1, + contentRect.bottom + 1, + }); + + auto& popup = _popups.emplace_back(kind, contentRect, backupRect, std::vector{ widthSizeT * heightSizeT }); + + // Create a backup of the TextBuffer parts we're scribbling over. + // We need to flush the buffer to ensure we capture the latest contents. + // NOTE: This may theoretically modify popup.backupRect (practically unlikely). + _flushBuffer(); + THROW_IF_FAILED(ServiceLocator::LocateGlobals().api->ReadConsoleOutputWImpl(_screenInfo, popup.backup, backupRect, popup.backupRect)); + + // Draw the border around our content and fill it with whitespace to prepare it for future usage. { - try + const auto attributes = _screenInfo.GetPopupAttributes(); + + RowWriteState state{ + .columnBegin = contentRect.left - 1, + .columnLimit = contentRect.right + 1, + }; + + // top line ┌───┐ + std::wstring buffer; + buffer.assign(widthSizeT, L'─'); + buffer.front() = L'┌'; + buffer.back() = L'┐'; + state.text = buffer; + textBuffer.Write(contentRect.top - 1, attributes, state); + + // bottom line └───┘ + buffer.front() = L'└'; + buffer.back() = L'┘'; + state.text = buffer; + textBuffer.Write(contentRect.bottom, attributes, state); + + // middle lines │ │ + buffer.assign(widthSizeT, L' '); + buffer.front() = L'│'; + buffer.back() = L'│'; + for (til::CoordType y = contentRect.top; y < contentRect.bottom; ++y) { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto wstr = ConvertToW(gci.CP, { _userBuffer, cch }); - const auto copyAmount = std::min(wstr.size() * sizeof(wchar_t), _userBufferSize); - std::copy_n(reinterpret_cast(wstr.data()), copyAmount, _userBuffer); - return copyAmount; + state.text = buffer; + textBuffer.Write(y, attributes, state); } - CATCH_LOG(); } - return bytesToWrite; + + switch (kind) + { + case PopupKind::CopyToChar: + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF2); + break; + case PopupKind::CopyFromChar: + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF4); + break; + case PopupKind::CommandNumber: + popup.commandNumber.buffer.fill(' '); + popup.commandNumber.bufferSize = 0; + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF9); + break; + case PopupKind::CommandList: + popup.commandList.selected = _history->LastDisplayed; + popup.commandList.top = popup.commandList.selected - contentSize.height / 2; + _popupDrawCommandList(popup); + break; + default: + assert(false); + } + + // If this is the first popup to be shown, stop the cursor from appearing/blinking + if (_popups.size() == 1) + { + textBuffer.GetCursor().SetIsPopupShown(true); + } +} +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + // Using _popupsDone() is a convenient way to restore the buffer contents if anything in this call failed. + // This could technically dismiss an unrelated popup that was already in _popups, but reaching this point is unlikely anyways. + _popupsDone(); } -// Routine Description: -// - saves data in the prompt buffer as pending input -// Arguments: -// - index - the index of what wchar to start the saving -// - multiline - whether the pending input should be saved as multiline or not -void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline) +// Dismisses all current popups at once. Right now we don't need support for just dismissing the topmost popup. +// In fact, there's only a single situation right now where there can be >1 popup: +// Pressing F7 followed by F9 (CommandNumber on top of CommandList). +void COOKED_READ_DATA::_popupsDone() { - auto& inputReadHandleData = *GetInputReadHandleData(); - const std::wstring_view pending{ _backupLimit + index, - BytesRead() / sizeof(wchar_t) - index }; - if (multiline) + while (!_popups.empty()) { - inputReadHandleData.SaveMultilinePendingInput(pending); + auto& popup = _popups.back(); + + // Restore TextBuffer contents. They could be empty if _popupPush() + // threw an exception in the middle of construction. + if (!popup.backup.empty()) + { + [[maybe_unused]] Viewport unused; + LOG_IF_FAILED(ServiceLocator::LocateGlobals().api->WriteConsoleOutputWImpl(_screenInfo, popup.backup, popup.backupRect, unused)); + } + + _popups.pop_back(); } - else + + // Restore cursor blinking. + _screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(false); +} + +bool COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) +{ + if (_popups.empty()) + { + assert(false); // Don't call this function. + return false; + } + + auto& popup = _popups.back(); + switch (popup.kind) { - inputReadHandleData.SavePendingInput(pending); + case PopupKind::CopyToChar: + _popupHandleCopyToCharInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CopyFromChar: + _popupHandleCopyFromCharInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CommandNumber: + _popupHandleCommandNumberInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CommandList: + return _popupHandleCommandListInput(popup, wch, vkey, modifiers); + default: + return false; } } -// Routine Description: -// - saves data in the prompt buffer as pending input -// Arguments: -// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. -// - numBytes - On in, the number of bytes available in the client -// buffer. On out, the number of bytes consumed in the client buffer. -// Return Value: -// - Status code that indicates success, wait, etc. -[[nodiscard]] NTSTATUS COOKED_READ_DATA::_readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept +void COOKED_READ_DATA::_popupHandleCopyToCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) { - auto Status = STATUS_SUCCESS; - - while (_bytesRead < _bufferSize) + if (vkey) { - auto wch = UNICODE_NULL; - auto commandLineEditingKeys = false; - DWORD keyState = 0; - - // This call to GetChar may block. - Status = GetChar(_pInputBuffer, - &wch, - true, - &commandLineEditingKeys, - nullptr, - &keyState); - if (FAILED_NTSTATUS(Status)) + if (vkey == VK_ESCAPE) { - if (Status != CONSOLE_STATUS_WAIT) - { - _bytesRead = 0; - } - break; + _popupsDone(); } + } + else + { + // See PopupKind::CopyToChar for more information about this code. + const auto cmd = _history->GetLastCommand(); + const auto idx = cmd.find(wch, _bufferCursor); - // we should probably set these up in GetChars, but we set them - // up here because the debugger is multi-threaded and calls - // read before outputting the prompt. - - if (_originalCursorPosition.x == -1) + if (idx != decltype(cmd)::npos) { - _originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); + // When we enter this if condition it's guaranteed that _bufferCursor must be + // smaller than idx, which in turn implies that it's smaller than cmd.size(). + // As such, calculating length is safe and str.size() == length. + const auto count = idx - _bufferCursor; + _buffer.replace(_bufferCursor, count, cmd, _bufferCursor, count); + _bufferCursor += count; + _markAsDirty(); } - if (commandLineEditingKeys) + _popupsDone(); + } +} + +void COOKED_READ_DATA::_popupHandleCopyFromCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) +{ + if (vkey) + { + if (vkey == VK_ESCAPE) { - // TODO: this is super weird for command line popups only - _unicode = isUnicode; + _popupsDone(); + } + } + else + { + // See PopupKind::CopyFromChar for more information about this code. + const auto idx = _buffer.find(wch, _bufferCursor); + _buffer.erase(_bufferCursor, std::min(idx, _buffer.size()) - _bufferCursor); + _markAsDirty(); + _popupsDone(); + } +} - _pdwNumBytes = &numBytes; +void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) +{ + if (vkey) + { + if (vkey == VK_ESCAPE) + { + _popupsDone(); + } + } + else + { + if (wch == UNICODE_CARRIAGERETURN) + { + popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = L'\0'; + _replaceBuffer(_history->RetrieveNth(std::stoi(popup.commandNumber.buffer.data()))); + _popupsDone(); + return; + } - Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState); - if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT) + if (wch >= L'0' && wch <= L'9') + { + if (popup.commandNumber.bufferSize < CommandNumberMaxInputLength) { - break; + popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = wch; } - if (FAILED_NTSTATUS(Status)) + } + else if (wch == UNICODE_BACKSPACE) + { + if (popup.commandNumber.bufferSize > 0) { - if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK) - { - Status = CONSOLE_STATUS_WAIT; - } - else - { - _bytesRead = 0; - } - break; + popup.commandNumber.buffer[--popup.commandNumber.bufferSize] = L' '; } } else { - if (ProcessInput(wch, keyState, Status)) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; - break; - } + return; } + + RowWriteState state{ + .text = { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }, + .columnBegin = popup.contentRect.right - CommandNumberMaxInputLength, + .columnLimit = popup.contentRect.right, + }; + _screenInfo.GetTextBuffer().Write(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); } - return Status; } -// Routine Description: -// - handles any tasks that need to be completed after the read input loop finishes -// Arguments: -// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. -// - numBytes - On in, the number of bytes available in the client -// buffer. On out, the number of bytes consumed in the client buffer. -// - controlKeyState - For some types of reads, this is the modifier key state with the last button press. -// Return Value: -// - Status code that indicates success, out of memory, etc. -[[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept +bool COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers) { - std::span writer{ _userBuffer, _userBufferSize }; - std::wstring_view input{ _backupLimit, _bytesRead / sizeof(wchar_t) }; - DWORD LineCount = 1; + auto& cl = popup.commandList; + + if (wch == UNICODE_CARRIAGERETURN) + { + _buffer.assign(_history->RetrieveNth(cl.selected)); + _popupsDone(); + return _handleChar(UNICODE_CARRIAGERETURN, modifiers); + } - if (_echoInput) + switch (vkey) { - const auto idx = input.find(UNICODE_CARRIAGERETURN); - if (idx != decltype(input)::npos) + case VK_ESCAPE: + _popupsDone(); + return false; + case VK_F9: + _popupPush(PopupKind::CommandNumber); + return false; + case VK_DELETE: + _history->Remove(cl.selected); + if (_history->GetNumberOfCommands() <= 0) { - if (_commandHistory) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, idx }, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); - } + _popupsDone(); + return false; + } + break; + case VK_LEFT: + case VK_RIGHT: + _replaceBuffer(_history->RetrieveNth(cl.selected)); + _popupsDone(); + return false; + case VK_UP: + if (WI_IsFlagSet(modifiers, SHIFT_PRESSED)) + { + _history->Swap(cl.selected, cl.selected - 1); + } + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected--; + break; + case VK_DOWN: + if (WI_IsFlagSet(modifiers, SHIFT_PRESSED)) + { + _history->Swap(cl.selected, cl.selected + 1); + } + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected++; + break; + case VK_HOME: + cl.selected = 0; + break; + case VK_END: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected = INT_MAX; + break; + case VK_PRIOR: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected -= popup.contentRect.height(); + break; + case VK_NEXT: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected += popup.contentRect.height(); + break; + default: + return false; + } - Tracing::s_TraceCookedRead(_clientProcess, _backupLimit, base::saturated_cast(idx)); + _popupDrawCommandList(popup); + return false; +} - // Don't be fooled by ProcessAliases only taking one argument. It rewrites multiple - // class members on return, including `_bytesRead`, requiring us to reconstruct `input`. - ProcessAliases(LineCount); - input = { _backupLimit, _bytesRead / sizeof(wchar_t) }; +void COOKED_READ_DATA::_popupDrawPrompt(const Popup& popup, const UINT id) const +{ + const auto text = _LoadString(id); + RowWriteState state{ + .text = text, + .columnBegin = popup.contentRect.left, + .columnLimit = popup.contentRect.right, + }; + _screenInfo.GetTextBuffer().Write(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); +} - // The exact reasons for this are unclear to me (the one writing this comment), but this code used to - // split the contents of a multiline alias (for instance `doskey test=echo foo$Techo bar$Techo baz`) - // into multiple separate read outputs, ensuring that the client receives them line by line. - // - // This code first truncates the `input` to only contain the first line, so that Consume() below only - // writes that line into the user buffer. We'll later store the remainder in SaveMultilinePendingInput(). - 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)); - } - } - } +void COOKED_READ_DATA::_popupDrawCommandList(Popup& popup) const +{ + assert(popup.kind == PopupKind::CommandList); - const auto inputSizeBefore = input.size(); - GetInputBuffer()->Consume(isUnicode, input, writer); + auto& cl = popup.commandList; + const auto max = _history->GetNumberOfCommands(); + const auto width = popup.contentRect.narrow_width(); + const auto height = std::min(popup.contentRect.height(), _history->GetNumberOfCommands()); + const auto dirtyHeight = std::max(height, cl.dirtyHeight); - if (LineCount > 1) - { - // This is a continuation of the above identical if condition. - // We've truncated the `input` slice and now we need to restore it. - const auto inputSizeAfter = input.size(); - const auto amountConsumed = inputSizeBefore - inputSizeAfter; - input = { _backupLimit, _bytesRead / sizeof(wchar_t) }; - input = input.substr(std::min(input.size(), amountConsumed)); - GetInputReadHandleData()->SaveMultilinePendingInput(input); - } - else if (!input.empty()) { - GetInputReadHandleData()->SavePendingInput(input); + // The viewport movement of the popup is anchored around the current selection first and foremost. + cl.selected = std::clamp(cl.selected, 0, max - 1); + + // It then lazily follows it when the selection goes out of the viewport. + if (cl.selected < cl.top) + { + cl.top = cl.selected; + } + else if (cl.selected >= cl.top + height) + { + cl.top = cl.selected - height + 1; + } + + cl.top = std::clamp(cl.top, 0, max - height); } - numBytes = _userBufferSize - writer.size(); - controlKeyState = _controlKeyState; - return STATUS_SUCCESS; -} + std::wstring buffer; + buffer.reserve(width * 2 + 4); -void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) -{ - // See the comment in WaitBlock.cpp for more information. - if (_userBuffer == oldBuffer) + const auto& attrRegular = _screenInfo.GetPopupAttributes(); + auto attrInverted = attrRegular; + attrInverted.Invert(); + + RowWriteState state{ + .columnBegin = popup.contentRect.left, + .columnLimit = popup.contentRect.right, + }; + + for (til::CoordType off = 0; off < dirtyHeight; ++off) { - _userBuffer = static_cast(newBuffer); + const auto y = popup.contentRect.top + off; + const auto historyIndex = cl.top + off; + const auto str = _history->GetNth(historyIndex); + const auto& attr = historyIndex == cl.selected ? attrInverted : attrRegular; + + buffer.clear(); + if (!str.empty()) + { + buffer.append(std::to_wstring(historyIndex)); + buffer.append(L": "); + buffer.append(str); + } + buffer.append(width, L' '); + + state.text = buffer; + _screenInfo.GetTextBuffer().Write(y, attr, state); } + + cl.dirtyHeight = height; } diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index be2e8645bc1..23f99da94da 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -1,29 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- readDataCooked.hpp - -Abstract: -- This file defines the read data structure for reading the command line. -- Cooked reads specifically refer to when the console host acts as a command line on behalf - of another console application (e.g. aliases, command history, completion, line manipulation, etc.) -- The data struct will help store context across multiple calls or in the case of a wait condition. -- Wait conditions happen frequently for cooked reads because they're virtually always waiting for - the user to finish "manipulating" the edit line before hitting enter and submitting the final - result to the client application. -- A cooked read is also limited specifically to string/textual information. Only keyboard-type input applies. -- This can be triggered via ReadConsole A/W and ReadFile A/W calls. - -Author: -- Austin Diviness (AustDi) 1-Mar-2017 -- Michael Niksa (MiNiksa) 1-Mar-2017 - -Revision History: -- Pulled from original authoring by Therese Stowell (ThereseS, 1990) -- Separated from cmdline.h/cmdline.cpp (AustDi, 2017) ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -33,132 +9,141 @@ Revision History: class COOKED_READ_DATA final : public ReadData { public: - COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, - _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + COOKED_READ_DATA(_In_ InputBuffer* pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* pInputReadHandleData, SCREEN_INFORMATION& screenInfo, _In_ size_t UserBufferSize, _In_ char* UserBuffer, _In_ ULONG CtrlWakeupMask, - _In_ const std::wstring_view exeName, - _In_ const std::wstring_view initialData, - _In_ ConsoleProcessHandle* const pClientProcess); + _In_ std::wstring_view exeName, + _In_ std::wstring_view initialData, + _In_ ConsoleProcessHandle* pClientProcess); - ~COOKED_READ_DATA() override; - COOKED_READ_DATA(COOKED_READ_DATA&&) = default; + void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) noexcept override; - bool AtEol() const noexcept; + bool Notify(WaitTerminationReason TerminationReason, + bool fIsUnicode, + _Out_ NTSTATUS* pReplyStatus, + _Out_ size_t* pNumBytes, + _Out_ DWORD* pControlKeyState, + _Out_ void* pOutputData) noexcept override; - void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) override; + bool Read(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); - bool Notify(const WaitTerminationReason TerminationReason, - const bool fIsUnicode, - _Out_ NTSTATUS* const pReplyStatus, - _Out_ size_t* const pNumBytes, - _Out_ DWORD* const pControlKeyState, - _Out_ void* const pOutputData) override; + void EraseBeforeResize(); + void RedrawAfterResize(); - std::span SpanAtPointer(); - std::span SpanWholeBuffer(); - - size_t Write(const std::wstring_view wstr); - - void ProcessAliases(DWORD& lineCount); - - [[nodiscard]] HRESULT Read(const bool isUnicode, - size_t& numBytes, - ULONG& controlKeyState) noexcept; - - bool ProcessInput(const wchar_t wch, - const DWORD keyState, - NTSTATUS& status); - - CommandHistory& History() noexcept; - bool HasHistory() const noexcept; - - const size_t& VisibleCharCount() const noexcept; - size_t& VisibleCharCount() noexcept; - - SCREEN_INFORMATION& ScreenInfo() noexcept; - - til::point OriginalCursorPosition() const noexcept; - til::point& OriginalCursorPosition() noexcept; - - til::point& BeforeDialogCursorPosition() noexcept; - - bool IsEchoInput() const noexcept; - bool IsInsertMode() const noexcept; - void SetInsertMode(const bool mode) noexcept; - bool IsUnicode() const noexcept; - - size_t UserBufferSize() const noexcept; - - wchar_t* BufferStartPtr() noexcept; - wchar_t* BufferCurrentPtr() noexcept; - void SetBufferCurrentPtr(wchar_t* ptr) noexcept; - - const size_t& BytesRead() const noexcept; - size_t& BytesRead() noexcept; - - const size_t& InsertionPoint() const noexcept; - size_t& InsertionPoint() noexcept; - - void SetReportedByteCount(const size_t count) noexcept; - - void Erase() noexcept; - size_t SavePromptToUserBuffer(const size_t cch); - void SavePendingInput(const size_t cch, const bool multiline); - -#if UNIT_TESTING - friend class CommandLineTests; - friend class CopyToCharPopupTests; - friend class CommandNumberPopupTests; - friend class CommandListPopupTests; - friend class PopupTestHelper; -#endif + void SetInsertMode(bool insertMode) noexcept; + bool IsEmpty() const noexcept; + bool PresentingPopup() const noexcept; + til::point_span GetBoundaries() const noexcept; private: - size_t _bufferSize; // size in bytes - size_t _bytesRead; - - // insertion position into the buffer (where the conceptual prompt cursor is) - size_t _currentPosition; // char position, not byte position - - wchar_t* _bufPtr; // current position to insert chars at - - // should be const. the first char of the buffer - wchar_t* _backupLimit; - - size_t _userBufferSize; // doubled size in ansi case - char* _userBuffer; - - size_t* _pdwNumBytes; + static constexpr uint8_t CommandNumberMaxInputLength = 5; + + enum class PopupKind + { + // Copies text from the previous command between the current cursor position and the first instance + // of a given char (but not including it) into the current prompt line at the current cursor position. + // Basically, F3 and this prompt have identical behavior, but the prompt searches for a terminating character. + // + // Let's say your last command was: + // echo hello + // And you type the following with the cursor at "^": + // echo abcd efgh + // ^ + // Then this command, given the char "o" will turn it into + // echo hell efgh + CopyToChar, + // Erases text between the current cursor position and the first instance of a given char (but not including it). + // It's unknown to me why this is was historically called "copy from char" as it conhost never copied anything. + CopyFromChar, + // Let's you choose to replace the current prompt with one from the command history by index. + CommandNumber, + // Let's you choose to replace the current prompt with one from the command history via a + // visual select dialog. Among all the popups this one is the most widely used one by far. + CommandList, + }; + + struct Popup + { + PopupKind kind; + + // The inner rectangle of the popup, excluding the border that we draw. + // In absolute TextBuffer coordinates. + til::rect contentRect; + // The area we've backed up and need to restore when we dismiss the popup. + // It'll practically always be 1 larger than contentRect in all 4 directions. + Microsoft::Console::Types::Viewport backupRect; + // The backed up buffer contents. Uses CHAR_INFO for convenience. + std::vector backup; + + // Using a std::variant would be preferable in modern C++ but is practically equally annoying to use. + union + { + // Used by PopupKind::CommandNumber + struct + { + // Keep 1 char space for the trailing \0 char. + std::array buffer; + uint8_t bufferSize; + } commandNumber; + + // Used by PopupKind::CommandList + struct + { + // Command history index of the first row we draw in the popup. + CommandHistory::Index top; + // Command history index of the currently selected row. + CommandHistory::Index selected; + // Tracks the part of the popup that has previously been drawn and needs to be redrawn in the next paint. + // This becomes relevant when the length of the history changes while the popup is open (= when deleting entries). + til::CoordType dirtyHeight; + } commandList; + }; + }; + + 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 _handleVkey(uint16_t vkey, DWORD modifiers); + void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); + void _markAsDirty(); + void _flushBuffer(); + void _erase(til::CoordType distance); + til::CoordType _writeChars(const std::wstring_view& text) const; + til::point _offsetPosition(til::point pos, til::CoordType distance) const; + void _unwindCursorPosition(til::CoordType distance) const; + void _replaceBuffer(const std::wstring_view& str); + + void _popupPush(PopupKind kind); + void _popupsDone(); + 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 _popupDrawPrompt(const Popup& popup, UINT id) const; + void _popupDrawCommandList(Popup& popup) const; - std::unique_ptr _buffer; + SCREEN_INFORMATION& _screenInfo; + std::span _userBuffer; std::wstring _exeName; + ConsoleProcessHandle* _processHandle = nullptr; + CommandHistory* _history = nullptr; + ULONG _ctrlWakeupMask = 0; + ULONG _controlKeyState = 0; std::unique_ptr _tempHandle; - // TODO MSFT:11285829 make this something other than a deletable pointer - // non-ownership pointer - CommandHistory* _commandHistory; - - ULONG _controlKeyState; - ULONG _ctrlWakeupMask; - size_t _visibleCharCount; // TODO MSFT:11285829 is this cells or glyphs? ie. is a wide char counted as 1 or 2? - SCREEN_INFORMATION& _screenInfo; - - // Note that cookedReadData's _originalCursorPosition is the position before ANY text was entered on the edit line. - til::point _originalCursorPosition; - til::point _beforeDialogCursorPosition; // Currently only used for F9 (ProcessCommandNumberInput) since it's the only pop-up to move the cursor when it starts. - - const bool _echoInput; - const bool _lineInput; - const bool _processedInput; - bool _insertMode; - bool _unicode; - - ConsoleProcessHandle* const _clientProcess; - - [[nodiscard]] NTSTATUS _readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept; + std::wstring _buffer; + size_t _bufferCursor = 0; + til::CoordType _distanceCursor; + til::CoordType _distanceEnd; + bool _bufferDirty = false; + bool _insertMode = false; - [[nodiscard]] NTSTATUS _handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept; + std::vector _popups; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 048e819a34e..58ac0812d52 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -873,12 +873,7 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // 1.a In some modes, the screen buffer size needs to change on window size, // so do that first. - // _AdjustScreenBuffer might hide the commandline. If it does so, it'll - // return S_OK instead of S_FALSE. In that case, we'll need to re-show - // the commandline ourselves once the viewport size is updated. - // (See 1.b below) - const auto adjustBufferSizeResult = _AdjustScreenBuffer(prcClientNew); - LOG_IF_FAILED(adjustBufferSizeResult); + LOG_IF_FAILED(_AdjustScreenBuffer(prcClientNew)); // 2. Now calculate how large the new viewport should be til::size coordViewportSize; @@ -888,16 +883,6 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // The old/new comparison is to figure out which side the window was resized from. _AdjustViewportSize(prcClientNew, prcClientOld, &coordViewportSize); - // 1.b If we did actually change the buffer size, then we need to show the - // commandline again. We hid it during _AdjustScreenBuffer, but we - // couldn't turn it back on until the Viewport was updated to the new - // size. See MSFT:19976291 - if (SUCCEEDED(adjustBufferSizeResult) && adjustBufferSizeResult != S_FALSE) - { - auto& commandLine = CommandLine::Instance(); - commandLine.Show(); - } - // 4. Finally, update the scroll bars. UpdateScrollBars(); @@ -1016,23 +1001,7 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // Only attempt to modify the buffer if something changed. Expensive operation. if (coordBufferSizeOld != coordBufferSizeNew) { - auto& commandLine = CommandLine::Instance(); - - // TODO: Deleting and redrawing the command line during resizing can cause flickering. See: http://osgvsowi/658439 - // 1. Delete input string if necessary (see menu.c) - commandLine.Hide(FALSE); - - const auto savedCursorVisibility = _textBuffer->GetCursor().IsVisible(); - _textBuffer->GetCursor().SetIsVisible(false); - - // 2. Call the resize screen buffer method (expensive) to redimension the backing buffer (and reflow) LOG_IF_FAILED(ResizeScreenBuffer(coordBufferSizeNew, FALSE)); - - // MSFT:19976291 Don't re-show the commandline here. We need to wait for - // the viewport to also get resized before we can re-show the commandline. - // ProcessResizeWindow will call commandline.Show() for us. - _textBuffer->GetCursor().SetIsVisible(savedCursorVisibility); - // Return S_OK, to indicate we succeeded and actually did something. hr = S_OK; } @@ -1540,8 +1509,17 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions Selection::Instance().ClearSelection(); - // cancel any popups before resizing or they will not necessarily line up with new buffer positions - CommandLine::Instance().EndAllPopups(); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().EraseBeforeResize(); + } + const auto cookedReadRestore = wil::scope_exit([]() { + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().RedrawAfterResize(); + } + }); const auto fWrapText = gci.GetWrapText(); // GH#3493: Don't reflow the alt buffer. @@ -1937,10 +1915,7 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain) // too much work. if (newBufferSize != oldScreenBufferSize) { - auto& commandLine = CommandLine::Instance(); - commandLine.Hide(FALSE); LOG_IF_FAILED(siMain.ResizeScreenBuffer(newBufferSize, TRUE)); - commandLine.Show(); } // Not that the buffer is smaller, actually make sure to resize our @@ -2098,7 +2073,7 @@ bool SCREEN_INFORMATION::_IsInVTMode() const // // Return value: // - This screen buffer's attributes -TextAttribute SCREEN_INFORMATION::GetAttributes() const +const TextAttribute& SCREEN_INFORMATION::GetAttributes() const noexcept { return _textBuffer->GetCurrentAttributes(); } @@ -2109,7 +2084,7 @@ TextAttribute SCREEN_INFORMATION::GetAttributes() const // // Return value: // - This screen buffer's popup attributes -TextAttribute SCREEN_INFORMATION::GetPopupAttributes() const +const TextAttribute& SCREEN_INFORMATION::GetPopupAttributes() const noexcept { return _PopupAttributes; } @@ -2311,56 +2286,6 @@ void SCREEN_INFORMATION::SetTerminalConnection(_In_ VtEngine* const pTtyConnecti } } -// Routine Description: -// - This routine copies a rectangular region from the screen buffer. no clipping is done. -// Arguments: -// - viewport - rectangle in source buffer to copy -// Return Value: -// - output cell rectangle copy of screen buffer data -// Note: -// - will throw exception on error. -OutputCellRect SCREEN_INFORMATION::ReadRect(const Viewport viewport) const -{ - // If the viewport given doesn't fit inside this screen, it's not a valid argument. - THROW_HR_IF(E_INVALIDARG, !GetBufferSize().IsInBounds(viewport)); - - OutputCellRect result(viewport.Height(), viewport.Width()); - const OutputCell paddingCell{ std::wstring_view{ &UNICODE_SPACE, 1 }, {}, GetAttributes() }; - for (til::CoordType rowIndex = 0, height = viewport.Height(); rowIndex < height; ++rowIndex) - { - auto location = viewport.Origin(); - location.y += rowIndex; - - auto data = GetCellLineDataAt(location); - const auto span = result.GetRow(rowIndex); - auto it = span.begin(); - - // Copy row data while there still is data and we haven't run out of rect to store it into. - while (data && it < span.end()) - { - *it++ = *data++; - } - - // Pad out any remaining space. - while (it < span.end()) - { - *it++ = paddingCell; - } - - // if we're clipping a dbcs char then don't include it, add a space instead - if (span.begin()->DbcsAttr() == DbcsAttribute::Trailing) - { - *span.begin() = paddingCell; - } - if (span.rbegin()->DbcsAttr() == DbcsAttribute::Leading) - { - *span.rbegin() = paddingCell; - } - } - - return result; -} - // Routine Description: // - Writes cells to the output buffer at the cursor position. // Arguments: diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 513c73fac5e..09fca19809b 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -123,8 +123,6 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console static void s_InsertScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); static void s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); - OutputCellRect ReadRect(const Microsoft::Console::Types::Viewport location) const; - TextBufferCellIterator GetCellDataAt(const til::point at) const; TextBufferCellIterator GetCellLineDataAt(const til::point at) const; TextBufferCellIterator GetCellDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const; @@ -202,8 +200,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console SCREEN_INFORMATION& GetActiveBuffer(); const SCREEN_INFORMATION& GetActiveBuffer() const; - TextAttribute GetAttributes() const; - TextAttribute GetPopupAttributes() const; + const TextAttribute& GetAttributes() const noexcept; + const TextAttribute& GetPopupAttributes() const noexcept; void SetAttributes(const TextAttribute& attributes); void SetPopupAttributes(const TextAttribute& popupAttributes); diff --git a/src/host/scrolling.cpp b/src/host/scrolling.cpp index add5a205e80..2e42d620767 100644 --- a/src/host/scrolling.cpp +++ b/src/host/scrolling.cpp @@ -208,7 +208,7 @@ bool Scrolling::s_HandleKeyScrollingEvent(const INPUT_KEY_INFO* const pKeyInfo) const auto VirtualKeyCode = pKeyInfo->GetVirtualKey(); const auto fIsCtrlPressed = pKeyInfo->IsCtrlPressed(); - const auto fIsEditLineEmpty = CommandLine::IsEditLineEmpty(); + const auto fIsEditLineEmpty = !gci.HasPendingCookedRead() || gci.CookedReadData().IsEmpty(); // If escape, enter or ctrl-c, cancel scroll. if (VirtualKeyCode == VK_ESCAPE || diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp index 69d40b9ae79..f00bbb4b847 100644 --- a/src/host/selectionInput.cpp +++ b/src/host/selectionInput.cpp @@ -8,8 +8,6 @@ #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/convert.hpp" -#include - using namespace Microsoft::Console::Types; using Microsoft::Console::Interactivity::ServiceLocator; // Routine Description: @@ -950,49 +948,27 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe [[nodiscard]] bool Selection::s_GetInputLineBoundaries(_Out_opt_ til::point* const pcoordInputStart, _Out_opt_ til::point* const pcoordInputEnd) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize(); - - auto& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); - - const auto pendingCookedRead = gci.HasPendingCookedRead(); - const auto isVisible = CommandLine::Instance().IsVisible(); - - // if we have no read data, we have no input line. - if (!pendingCookedRead || gci.CookedReadData().VisibleCharCount() == 0 || !isVisible) - { - return false; - } - - const auto& cookedRead = gci.CookedReadData(); - const auto coordStart = cookedRead.OriginalCursorPosition(); - auto coordEnd = cookedRead.OriginalCursorPosition(); - if (coordEnd.x < 0 && coordEnd.y < 0) + if (gci.HasPendingCookedRead()) { - // if the original cursor position from the input line data is invalid, then the buffer cursor position is the final position - coordEnd = textBuffer.GetCursor().GetPosition(); - } - else - { - // otherwise, we need to add the number of characters in the input line to the original cursor position - bufferSize.MoveInBounds(gsl::narrow(cookedRead.VisibleCharCount()), coordEnd); - } - - // - 1 so the coordinate is on top of the last position of the text, not one past it. - bufferSize.MoveInBounds(-1, coordEnd); - - if (pcoordInputStart != nullptr) - { - pcoordInputStart->x = coordStart.x; - pcoordInputStart->y = coordStart.y; - } - - if (pcoordInputEnd != nullptr) - { - *pcoordInputEnd = coordEnd; + auto boundaries = gci.CookedReadData().GetBoundaries(); + if (boundaries.start < boundaries.end) + { + if (pcoordInputStart != nullptr) + { + *pcoordInputStart = boundaries.start; + } + if (pcoordInputEnd != nullptr) + { + // - 1 so the coordinate is on top of the last position of the text, not one past it. + gci.GetActiveOutputBuffer().GetBufferSize().MoveInBounds(-1, boundaries.end); + *pcoordInputEnd = boundaries.end; + } + return true; + } } - return true; + return false; } // Routine Description: diff --git a/src/host/server.h b/src/host/server.h index ebc2ac10091..3332cc6b383 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -16,20 +16,17 @@ Revision History: #pragma once +#include "conimeinfo.h" +#include "CursorBlinker.hpp" #include "IIoProvider.hpp" - +#include "readDataCooked.hpp" #include "settings.hpp" - -#include "conimeinfo.h" #include "VtIo.hpp" -#include "CursorBlinker.hpp" - +#include "../audio/midi/MidiAudio.hpp" +#include "../host/RenderData.hpp" #include "../server/ProcessList.h" #include "../server/WaitQueue.h" -#include "../host/RenderData.hpp" -#include "../audio/midi/MidiAudio.hpp" - #include // clang-format off @@ -91,8 +88,6 @@ class CONSOLE_INFORMATION : DWORD Flags = 0; - std::atomic PopupCount = 0; - // the following fields are used for ansi-unicode translation UINT CP = 0; UINT OutputCP = 0; @@ -121,6 +116,7 @@ class CONSOLE_INFORMATION : bool IsInVtIoMode() const; bool HasPendingCookedRead() const noexcept; + bool HasPendingPopup() const noexcept; const COOKED_READ_DATA& CookedReadData() const noexcept; COOKED_READ_DATA& CookedReadData() noexcept; void SetCookedReadData(COOKED_READ_DATA* readData) noexcept; @@ -167,8 +163,6 @@ class CONSOLE_INFORMATION : MidiAudio _midiAudio; }; -#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread) - #define CONSOLE_STATUS_WAIT 0xC0030001 #define CONSOLE_STATUS_READ_COMPLETE 0xC0030002 #define CONSOLE_STATUS_WAIT_NO_BLOCK 0xC0030003 diff --git a/src/host/sources.inc b/src/host/sources.inc index 173dae000cb..bf3b16bcae9 100644 --- a/src/host/sources.inc +++ b/src/host/sources.inc @@ -46,7 +46,6 @@ SOURCES = \ ..\scrolling.cpp \ ..\cmdline.cpp \ ..\CursorBlinker.cpp \ - ..\popup.cpp \ ..\alias.cpp \ ..\history.cpp \ ..\VtIo.cpp \ @@ -89,10 +88,6 @@ SOURCES = \ ..\conareainfo.cpp \ ..\conimeinfo.cpp \ ..\ConsoleArguments.cpp \ - ..\CommandNumberPopup.cpp \ - ..\CommandListPopup.cpp \ - ..\CopyFromCharPopup.cpp \ - ..\CopyToCharPopup.cpp \ ..\VtApiRoutines.cpp \ diff --git a/src/host/stream.cpp b/src/host/stream.cpp index e8dbe5aa2b0..11a7f6a4115 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -247,93 +247,6 @@ static bool IsCommandLineEditingKey(const KEY_EVENT_RECORD& event) } } -// Routine Description: -// - This routine returns the total number of screen spaces the characters up to the specified character take up. -til::CoordType RetrieveTotalNumberOfSpaces(const til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition) -{ - auto XPosition = sOriginalCursorPositionX; - til::CoordType NumSpaces = 0; - - for (size_t i = 0; i < ulCurrentPosition; i++) - { - const auto Char = pwchBuffer[i]; - - til::CoordType NumSpacesForChar; - if (Char == UNICODE_TAB) - { - NumSpacesForChar = NUMBER_OF_SPACES_IN_TAB(XPosition); - } - else if (IS_CONTROL_CHAR(Char)) - { - NumSpacesForChar = 2; - } - else if (IsGlyphFullWidth(Char)) - { - NumSpacesForChar = 2; - } - else - { - NumSpacesForChar = 1; - } - XPosition += NumSpacesForChar; - NumSpaces += NumSpacesForChar; - } - - return NumSpaces; -} - -// Routine Description: -// - This routine returns the number of screen spaces the specified character takes up. -til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition + 1) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition) -{ - auto Char = pwchBuffer[ulCurrentPosition]; - if (Char == UNICODE_TAB) - { - til::CoordType NumSpaces = 0; - auto XPosition = sOriginalCursorPositionX; - - for (size_t i = 0; i <= ulCurrentPosition; i++) - { - Char = pwchBuffer[i]; - if (Char == UNICODE_TAB) - { - NumSpaces = NUMBER_OF_SPACES_IN_TAB(XPosition); - } - else if (IS_CONTROL_CHAR(Char)) - { - NumSpaces = 2; - } - else if (IsGlyphFullWidth(Char)) - { - NumSpaces = 2; - } - else - { - NumSpaces = 1; - } - XPosition += NumSpaces; - } - - return NumSpaces; - } - else if (IS_CONTROL_CHAR(Char)) - { - return 2; - } - else if (IsGlyphFullWidth(Char)) - { - return 2; - } - else - { - return 1; - } -} - // Routine Description: // - if we have leftover input, copy as much fits into the user's // buffer and return. we may have multi line input, if a macro @@ -448,7 +361,7 @@ NT_CATCH_RETURN() gci.SetCookedReadData(cookedReadData.get()); bytesRead = buffer.size_bytes(); // This parameter on the way in is the size to read, on the way out, it will be updated to what is actually read. - if (CONSOLE_STATUS_WAIT == cookedReadData->Read(unicode, bytesRead, controlKeyState)) + if (!cookedReadData->Read(unicode, bytesRead, controlKeyState)) { // memory will be cleaned up by wait queue waiter.reset(cookedReadData.release()); diff --git a/src/host/stream.h b/src/host/stream.h index b4c37d82a45..5a03278fc99 100644 --- a/src/host/stream.h +++ b/src/host/stream.h @@ -20,8 +20,6 @@ Revision History: #include "../server/IWaitRoutine.h" #include "readData.hpp" -#define IS_CONTROL_CHAR(wch) ((wch) < L' ') - [[nodiscard]] NTSTATUS GetChar(_Inout_ InputBuffer* const pInputBuffer, _Out_ wchar_t* const pwchOut, const bool Wait, @@ -35,16 +33,4 @@ Revision History: INPUT_READ_HANDLE_DATA& readHandleState, const bool unicode); -// Routine Description: -// - This routine returns the total number of screen spaces the characters up to the specified character take up. -til::CoordType RetrieveTotalNumberOfSpaces(const til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition) const WCHAR* const pwchBuffer, - const size_t ulCurrentPosition); - -// Routine Description: -// - This routine returns the number of screen spaces the specified character takes up. -til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition + 1) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition); - VOID UnblockWriteConsole(const DWORD dwReason); diff --git a/src/host/tracing.cpp b/src/host/tracing.cpp index bf54edbed99..998410cf247 100644 --- a/src/host/tracing.cpp +++ b/src/host/tracing.cpp @@ -173,16 +173,17 @@ void Tracing::s_TraceInputRecord(const INPUT_RECORD& inputRecord) } } -void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength) +void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, const std::wstring_view& text) { if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::CookedRead)) { + const auto length = ::base::saturated_cast(text.size()); TraceLoggingWrite( g_hConhostV2EventTraceProvider, "CookedRead", TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), - TraceLoggingCountedWideString(pwchCookedBuffer, cchCookedBufferLength, "ReadBuffer"), - TraceLoggingULong(cchCookedBufferLength, "ReadBufferLength"), + TraceLoggingCountedWideString(text.data(), length, "ReadBuffer"), + TraceLoggingULong(length, "ReadBufferLength"), TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), TraceLoggingKeyword(TIL_KEYWORD_TRACE), TraceLoggingKeyword(TraceKeywords::CookedRead)); diff --git a/src/host/tracing.hpp b/src/host/tracing.hpp index 8be4bf21b82..03cf423f4d7 100644 --- a/src/host/tracing.hpp +++ b/src/host/tracing.hpp @@ -51,7 +51,7 @@ class Tracing static void s_TraceWindowMessage(const MSG& msg); static void s_TraceInputRecord(const INPUT_RECORD& inputRecord); - static void s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength); + static void s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, const std::wstring_view& text); static void s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_ bool bIsAttach); static void __stdcall TraceFailure(const wil::FailureInfo& failure) noexcept; diff --git a/src/host/ut_host/AliasTests.cpp b/src/host/ut_host/AliasTests.cpp index 6443dd22f63..cf499bea89e 100644 --- a/src/host/ut_host/AliasTests.cpp +++ b/src/host/ut_host/AliasTests.cpp @@ -111,167 +111,48 @@ class AliasTests // and match to our expected values. std::wstring alias(aliasName); std::wstring exe(exeName); + std::wstring original(originalString); std::wstring target; std::wstring expected; _RetrieveTargetExpectedPair(target, expected); - auto linesExpected = _ReplacePercentWithCRLF(expected); - - std::wstring original(originalString); + const auto linesExpected = _ReplacePercentWithCRLF(expected); Alias::s_TestAddAlias(exe, alias, target); - // Fill classic wchar_t[] buffer for interfacing with the MatchAndCopyAlias function - const auto bufferSize = 160ui16; - auto buffer = std::make_unique(bufferSize); - wcscpy_s(buffer.get(), bufferSize, original.data()); - - const auto cbBuffer = bufferSize * sizeof(wchar_t); - size_t bufferUsed = 0; - DWORD linesActual = 0; - // Run the match and copy function. - Alias::s_MatchAndCopyAliasLegacy(buffer.get(), - wcslen(buffer.get()) * sizeof(wchar_t), - buffer.get(), - cbBuffer, - bufferUsed, - exe, - linesActual); - - // Null terminate buffer for comparison - buffer[bufferUsed / sizeof(wchar_t)] = L'\0'; - - Log::Comment(String().Format(L"Expected: '%s'", expected.data())); - Log::Comment(String().Format(L"Actual : '%s'", buffer.get())); - - VERIFY_ARE_EQUAL(WEX::Common::String(expected.data()), WEX::Common::String(buffer.get())); + size_t linesActual = 0; + const auto actual = Alias::s_MatchAndCopyAlias(original, exe, linesActual); + VERIFY_ARE_EQUAL(expected, actual); VERIFY_ARE_EQUAL(linesExpected, linesActual); } - TEST_METHOD(TestMatchAndCopyTrailingCRLF) - { - const auto pwszSource = L"SourceWithoutCRLF\r\n"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 60; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtesttesttesttest"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - DWORD dwLines = 0; - - // Register the wrong alias name before we try. - std::wstring exe(L"exe.exe"); - std::wstring sourceWithoutCRLF(L"SourceWithoutCRLF"); - std::wstring target(L"someTarget"); - Alias::s_TestAddAlias(exe, sourceWithoutCRLF, target); - - const auto targetExpected = target + L"\r\n"; - const auto cbTargetExpected = targetExpected.size() * sizeof(wchar_t); // +2 for \r\n that will be added on replace. - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - // Terminate target buffer with \0 for comparison - rgwchTarget[cbTargetUsed] = L'\0'; - - VERIFY_ARE_EQUAL(cbTargetExpected, cbTargetUsed, L"Target bytes should be filled with target size."); - VERIFY_ARE_EQUAL(String(targetExpected.data()), String(rgwchTarget.get(), gsl::narrow(cbTargetUsed / sizeof(wchar_t))), L"Target string should be filled with target data."); - VERIFY_ARE_EQUAL(1u, dwLines, L"Line count should be 1."); - } - TEST_METHOD(TestMatchAndCopyInvalidExeName) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - const auto cbTarget = cchTarget * sizeof(wchar_t); - auto cbTargetUsed = cbTarget; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - + size_t dwLines = 1; std::wstring exeName; - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exeName, - dwLines); - - VERIFY_ARE_EQUAL(cbTarget, cbTargetUsed, L"Byte count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exeName, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyExeNotFound) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - std::wstring exeName(L"exe.exe"); - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exeName, // we didn't pre-set-up the exe name - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should have been written."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); + size_t dwLines = 1; + const std::wstring exeName(L"exe.exe"); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exeName, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyAliasNotFound) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; + size_t dwLines = 1; // Register the wrong alias name before we try. std::wstring exe(L"exe.exe"); @@ -279,71 +160,15 @@ class AliasTests std::wstring target(L"someTarget"); Alias::s_TestAddAlias(exe, badSource, target); - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should be used if nothing was found."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); - } - - TEST_METHOD(TestMatchAndCopyTargetTooSmall) - { - const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - - // Register the correct alias name before we try. - std::wstring exe(L"exe.exe"); - std::wstring source(pwszSource); - std::wstring target(L"someTarget"); - Alias::s_TestAddAlias(exe, source, target); - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - 1, // Make the target size too small - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"Byte count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exe, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyLeadingSpaces) { const auto pwszSource = L" Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; + size_t dwLines = 1; // Register the correct alias name before we try. std::wstring exe(L"exe.exe"); @@ -352,40 +177,9 @@ class AliasTests Alias::s_TestAddAlias(exe, source, target); // Leading spaces should bypass the alias. This should not match anything. - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should be used if nothing was found."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); - } - - TEST_METHOD(TrimTrailing) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"bar%=bar," // The character % will be turned into an \r\n - L"bar=bar" - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - // Substitute %s from metadata into \r\n (since metadata can't hold \r\n) - _ReplacePercentWithCRLF(target); - _ReplacePercentWithCRLF(expected); - - Alias::s_TrimTrailingCrLf(target); - - VERIFY_ARE_EQUAL(String(expected.data()), String(target.data())); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exe, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(Tokenize) diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp deleted file mode 100644 index de09dd2e241..00000000000 --- a/src/host/ut_host/CommandLineTests.cpp +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../cmdline.h" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -constexpr size_t PROMPT_SIZE = 512; - -class CommandLineTests -{ - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS(CommandLineTests); - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - void VerifyPromptText(COOKED_READ_DATA& cookedReadData, const std::wstring wstr) - { - const auto span = cookedReadData.SpanWholeBuffer(); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, wstr.size() * sizeof(wchar_t)); - VERIFY_ARE_EQUAL(wstr, (std::wstring_view{ span.data(), cookedReadData._bytesRead / sizeof(wchar_t) })); - } - - void InitCookedReadData(COOKED_READ_DATA& cookedReadData, - CommandHistory* pHistory, - wchar_t* pBuffer, - const size_t cchBuffer) - { - cookedReadData._commandHistory = pHistory; - cookedReadData._userBuffer = reinterpret_cast(pBuffer); - cookedReadData._userBufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._backupLimit = pBuffer; - cookedReadData._bufPtr = pBuffer; - cookedReadData._exeName = L"cmd.exe"; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - } - - void SetPrompt(COOKED_READ_DATA& cookedReadData, const std::wstring text) - { - std::copy(text.begin(), text.end(), cookedReadData._backupLimit); - cookedReadData._bytesRead = text.size() * sizeof(wchar_t); - cookedReadData._currentPosition = text.size(); - cookedReadData._bufPtr += text.size(); - cookedReadData._visibleCharCount = text.size(); - } - - void MoveCursor(COOKED_READ_DATA& cookedReadData, const size_t column) - { - cookedReadData._currentPosition = column; - cookedReadData._bufPtr = cookedReadData._backupLimit + column; - } - - TEST_METHOD(CanCycleCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - // should not have anything on the prompt - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); - - // go back one history item - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 3"); - - // try to go to the next history item, prompt shouldn't change - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 3"); - - // go back another - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 2"); - - // go forward - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 3"); - - // go back two - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 1"); - - // make sure we can't go back further - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 1"); - - // can still go forward - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 2"); - } - - TEST_METHOD(CanSetPromptToOldestHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._setPromptToOldestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 1"); - - // change prompt and go back to oldest - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - commandLine._setPromptToOldestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 1"); - } - - TEST_METHOD(CanSetPromptToNewestHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._setPromptToNewestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 3"); - - // change prompt and go back to newest - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - commandLine._setPromptToNewestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 3"); - } - - TEST_METHOD(CanDeletePromptAfterCursor) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - auto& commandLine = CommandLine::Instance(); - // set current cursor position somewhere in the middle of the prompt - MoveCursor(cookedReadData, 4); - commandLine.DeletePromptAfterCursor(cookedReadData); - VerifyPromptText(cookedReadData, L"test"); - } - - TEST_METHOD(CanDeletePromptBeforeCursor) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // set current cursor position somewhere in the middle of the prompt - MoveCursor(cookedReadData, 5); - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._deletePromptBeforeCursor(cookedReadData); - cookedReadData._currentPosition = cursorPos.x; - VerifyPromptText(cookedReadData, L"word blah"); - } - - TEST_METHOD(CanMoveCursorToEndOfPrompt) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // make sure the cursor is not at the start of the prompt - VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - // save current position for later checking - const auto expectedCursorPos = cookedReadData._currentPosition; - const auto expectedBufferPos = cookedReadData._bufPtr; - - MoveCursor(cookedReadData, 0); - - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._moveCursorToEndOfPrompt(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, gsl::narrow(expectedCursorPos)); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedCursorPos); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufferPos); - } - - TEST_METHOD(CanMoveCursorToStartOfPrompt) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // make sure the cursor is not at the start of the prompt - VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._moveCursorToStartOfPrompt(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, 0); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - } - - TEST_METHOD(CanMoveCursorLeftByWord) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - auto& commandLine = CommandLine::Instance(); - // cursor position at beginning of "blah" - til::CoordType expectedPos = 10; - auto cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // move again - expectedPos = 5; // before "word" - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // move again - expectedPos = 0; // before "test" - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // try to move again, nothing should happen - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - } - - TEST_METHOD(CanMoveCursorLeft) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - const std::wstring expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // move left from end of prompt text to the beginning of the prompt - auto& commandLine = CommandLine::Instance(); - for (auto it = expected.crbegin(); it != expected.crend(); ++it) - { - const auto cursorPos = commandLine._moveCursorLeft(cookedReadData); - VERIFY_ARE_EQUAL(*cookedReadData._bufPtr, *it); - } - // should now be at the start of the prompt - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - // try to move left a final time, nothing should change - const auto cursorPos = commandLine._moveCursorLeft(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, 0); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - } - - /* - // TODO MSFT:11285829 tcome back and turn these on once the system cursor isn't needed - TEST_METHOD(CanMoveCursorRightByWord) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // save current position for later checking - const auto endCursorPos = cookedReadData._currentPosition; - const auto endBufferPos = cookedReadData._bufPtr; - // NOTE: need to initialize the actually cursor and keep it up to date with the changes here. remove - once functions are fixed - // try to move right, nothing should happen - auto expectedPos = endCursorPos; - auto cursorPos = MoveCursorRightByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, endBufferPos); - - // move to beginning of prompt and walk to the right by word - } - - TEST_METHOD(CanMoveCursorRight) - { - } - - TEST_METHOD(CanDeleteFromRightOfCursor) - { - } - - */ - - TEST_METHOD(CanInsertCtrlZ) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto& commandLine = CommandLine::Instance(); - commandLine._insertCtrlZ(cookedReadData); - VerifyPromptText(cookedReadData, L"\x1a"); // ctrl-z - } - - TEST_METHOD(CanDeleteCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._deleteCommandHistory(cookedReadData); - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), 0); - } - - TEST_METHOD(CanFillPromptWithPreviousCommandFragment) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - SetPrompt(cookedReadData, L"short and stout"); - - auto& commandLine = CommandLine::Instance(); - commandLine._fillPromptWithPreviousCommandFragment(cookedReadData); - VerifyPromptText(cookedReadData, L"short and stoutapot"); - } - - TEST_METHOD(CanCycleMatchingCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"short and stout", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"inflammable", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"Indestructible", false)); - - SetPrompt(cookedReadData, L"I"); - - auto& commandLine = CommandLine::Instance(); - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"Indestructible"); - - // make sure we skip to the next "I" history item - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"I'm a little teapot"); - - // should cycle back to the start of the command history - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"Indestructible"); - } - - TEST_METHOD(CmdlineCtrlHomeFullwidthChars) - { - Log::Comment(L"Set up buffers, create cooked read data, get screen information."); - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - auto& consoleInfo = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& screenInfo = consoleInfo.GetActiveOutputBuffer(); - auto& cookedReadData = consoleInfo.CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - Log::Comment(L"Create Japanese text string and calculate the distance we expect the cursor to move."); - const std::wstring text(L"\x30ab\x30ac\x30ad\x30ae\x30af"); // katakana KA GA KI GI KU - const auto bufferSize = screenInfo.GetBufferSize(); - const auto cursorBefore = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - auto cursorAfterExpected = cursorBefore; - for (size_t i = 0; i < text.length() * 2; i++) - { - bufferSize.IncrementInBounds(cursorAfterExpected); - } - - Log::Comment(L"Write the text into the buffer using the cooked read structures as if it came off of someone's input."); - const auto written = cookedReadData.Write(text); - VERIFY_ARE_EQUAL(text.length(), written); - - Log::Comment(L"Retrieve the position of the cursor after insertion and check that it moved as far as we expected."); - const auto cursorAfter = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - VERIFY_ARE_EQUAL(cursorAfterExpected, cursorAfter); - - Log::Comment(L"Walk through the screen buffer data and ensure that the text we wrote filled the cells up as we expected (2 cells per fullwidth char)"); - { - auto cellIterator = screenInfo.GetCellDataAt(cursorBefore); - for (size_t i = 0; i < text.length() * 2; i++) - { - // Our original string was 5 wide characters which we expected to take 10 cells. - // Therefore each index of the original string will be used twice ( divide by 2 ). - const auto expectedTextValue = text.at(i / 2); - const String expectedText(&expectedTextValue, 1); - - const auto actualTextValue = cellIterator->Chars(); - const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); - - VERIFY_ARE_EQUAL(expectedText, actualText); - cellIterator++; - } - } - - Log::Comment(L"Now perform the command that is triggered with Ctrl+Home keys normally to erase the entire edit line."); - auto& commandLine = CommandLine::Instance(); - commandLine._deletePromptBeforeCursor(cookedReadData); - - Log::Comment(L"Check that the entire span of the buffer where we had the fullwidth text is now cleared out and full of blanks (nothing left behind)."); - { - auto cursorPos = cursorBefore; - auto cellIterator = screenInfo.GetCellDataAt(cursorPos); - - while (Utils::s_CompareCoords(cursorPos, cursorAfter) < 0) - { - const String expectedText(L"\x20"); // unicode space character - - const auto actualTextValue = cellIterator->Chars(); - const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); - - VERIFY_ARE_EQUAL(expectedText, actualText); - cellIterator++; - - bufferSize.IncrementInBounds(cursorPos); - } - } - } -}; diff --git a/src/host/ut_host/CommandListPopupTests.cpp b/src/host/ut_host/CommandListPopupTests.cpp deleted file mode 100644 index 342248702db..00000000000 --- a/src/host/ut_host/CommandListPopupTests.cpp +++ /dev/null @@ -1,538 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CommandListPopup.hpp" -#include "PopupTestHelper.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; -static constexpr size_t BUFFER_SIZE = 256; -static constexpr UINT s_NumberOfHistoryBuffers = 4; -static constexpr UINT s_HistoryBufferSize = 50; - -class CommandListPopupTests -{ - TEST_CLASS(CommandListPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.SetNumberOfHistoryBuffers(s_NumberOfHistoryBuffers); - gci.SetHistoryBufferSize(s_HistoryBufferSize); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - // resize command history storage to 50 items so that we don't cycle on accident - // when PopupTestHelper::InitLongHistory() is called. - CommandHistory::s_ResizeAll(50); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - void InitReadData(COOKED_READ_DATA& cookedReadData, - wchar_t* const pBuffer, - const size_t cchBuffer, - const size_t cursorPosition) - { - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufPtr = pBuffer + cursorPosition; - cookedReadData._backupLimit = pBuffer; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); - cookedReadData._currentPosition = cursorPosition; - cookedReadData.VisibleCharCount() = cursorPosition; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - modifiers = 0; - wch = VK_ESCAPE; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(UpMovesSelection) - { - // function to simulate user pressing up arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_UP; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto commandNumberBefore = popup._currentCommand; - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up one line - VERIFY_ARE_EQUAL(commandNumberBefore - 1, popup._currentCommand); - } - - TEST_METHOD(DownMovesSelection) - { - // function to simulate user pressing down arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_DOWN; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto commandNumberBefore = popup._currentCommand; - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved down one line - VERIFY_ARE_EQUAL(commandNumberBefore + 1, popup._currentCommand); - } - - TEST_METHOD(EndMovesSelectionToEnd) - { - // function to simulate user pressing end key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_END; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved to the bottom line - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands() - 1, popup._currentCommand); - } - - TEST_METHOD(HomeMovesSelectionToStart) - { - // function to simulate user pressing home key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_HOME; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved to the bottom line - VERIFY_ARE_EQUAL(0, popup._currentCommand); - } - - TEST_METHOD(PageUpMovesSelection) - { - // function to simulate user pressing page up key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_PRIOR; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitLongHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up a page - VERIFY_ARE_EQUAL(static_cast(m_pHistory->GetNumberOfCommands()) - popup.Height() - 1, popup._currentCommand); - } - - TEST_METHOD(PageDownMovesSelection) - { - // function to simulate user pressing page down key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_NEXT; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitLongHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up a page - VERIFY_ARE_EQUAL(popup.Height(), popup._currentCommand); - } - - TEST_METHOD(SideArrowsFillsPrompt) - { - // function to simulate user pressing right arrow key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - wch = VK_RIGHT; - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // prompt should have history item in prompt - const auto historyItem = m_pHistory->GetLastCommand(); - const std::wstring_view resultText{ buffer, historyItem.size() }; - VERIFY_ARE_EQUAL(historyItem, resultText); - } - - TEST_METHOD(CanLaunchCommandNumberPopup) - { - // function to simulate user pressing F9 - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - wch = VK_F9; - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - auto& commandLine = CommandLine::Instance(); - VERIFY_IS_FALSE(commandLine.HasPopup()); - // should spawn a CommandNumberPopup - auto scopeExit = wil::scope_exit([&]() { commandLine.EndAllPopups(); }); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT)); - VERIFY_IS_TRUE(commandLine.HasPopup()); - } - - TEST_METHOD(CanDeleteFromCommandHistory) - { - // function to simulate user pressing the delete key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_DELETE; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto startHistorySize = m_pHistory->GetNumberOfCommands(); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), startHistorySize - 1); - } - - TEST_METHOD(CanReorderHistoryUp) - { - // function to simulate user pressing shift + up arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_UP; - firstTime = false; - modifiers = SHIFT_PRESSED; - } - else - { - wch = VK_ESCAPE; - modifiers = 0; - } - popupKey = true; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); - VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); - } - - TEST_METHOD(CanReorderHistoryDown) - { - // function to simulate user pressing the up arrow, then shift + down arrow, then escape - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static unsigned int count = 0; - if (count == 0) - { - wch = VK_UP; - modifiers = 0; - } - else if (count == 1) - { - wch = VK_DOWN; - modifiers = SHIFT_PRESSED; - } - else - { - wch = VK_ESCAPE; - modifiers = 0; - } - popupKey = true; - ++count; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); - VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); - } -}; diff --git a/src/host/ut_host/CommandNumberPopupTests.cpp b/src/host/ut_host/CommandNumberPopupTests.cpp deleted file mode 100644 index 006c36207bd..00000000000 --- a/src/host/ut_host/CommandNumberPopupTests.cpp +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CommandNumberPopup.hpp" -#include "../CommandListPopup.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -static constexpr size_t BUFFER_SIZE = 256; - -class CommandNumberPopupTests -{ - TEST_CLASS(CommandNumberPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(CanDismissAllPopups) - { - Log::Comment(L"that that all popups are dismissed when CommandNumberPopup is dismissed"); - // CommandNumberPopup is the only popup that can act as a 2nd popup. make sure that it dismisses all - // popups when exiting - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // add popups to CommandLine - auto& commandLine = CommandLine::Instance(); - commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer(), *m_pHistory)); - commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer())); - auto& numberPopup = *commandLine._popups.front(); - numberPopup.SetUserInputFunction(fn); - - VERIFY_ARE_EQUAL(commandLine._popups.size(), 2u); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(numberPopup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_IS_FALSE(commandLine.HasPopup()); - } - - TEST_METHOD(EmptyInputCountsAsOldestHistory) - { - Log::Comment(L"hitting enter with no input should grab the oldest history item"); - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = UNICODE_CARRIAGERETURN; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the least recent history item - - const auto expected = m_pHistory->GetLastCommand(); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - - TEST_METHOD(CanSelectHistoryItem) - { - PopupTestHelper::InitHistory(*m_pHistory); - for (CommandHistory::Index historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) - { - Popup::UserInputFunction fn = [historyIndex](COOKED_READ_DATA& /*cookedReadData*/, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) { - static auto needReturn = false; - popupKey = false; - modifiers = 0; - if (!needReturn) - { - const auto str = std::to_string(historyIndex); - wch = str.at(0); - needReturn = true; - } - else - { - wch = UNICODE_CARRIAGERETURN; - needReturn = false; - } - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the correct nth history item - - const auto expected = m_pHistory->GetNth(gsl::narrow(historyIndex)); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - } - - TEST_METHOD(LargeNumberGrabsNewestHistoryItem) - { - Log::Comment(L"entering a number larger than the number of history items should grab the most recent history item"); - - // simulates user pressing 1, 2, 3, 4, 5, enter - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto num = 1; - popupKey = false; - modifiers = 0; - if (num <= 5) - { - const auto str = std::to_string(num); - wch = str.at(0); - ++num; - } - else - { - wch = UNICODE_CARRIAGERETURN; - } - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the most recent history item - - const auto expected = m_pHistory->GetLastCommand(); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - - TEST_METHOD(InputIsLimited) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - - // input can't delete past zero number input - popup._pop(); - VERIFY_ARE_EQUAL(popup._parse(), 0); - - // input can only be numbers - VERIFY_THROWS_SPECIFIC(popup._push(L'$'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - VERIFY_THROWS_SPECIFIC(popup._push(L'A'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - - // input can't be more than 5 numbers - popup._push(L'1'); - VERIFY_ARE_EQUAL(popup._parse(), 1); - popup._push(L'2'); - VERIFY_ARE_EQUAL(popup._parse(), 12); - popup._push(L'3'); - VERIFY_ARE_EQUAL(popup._parse(), 123); - popup._push(L'4'); - VERIFY_ARE_EQUAL(popup._parse(), 1234); - popup._push(L'5'); - VERIFY_ARE_EQUAL(popup._parse(), 12345); - // this shouldn't affect the parsed number - popup._push(L'6'); - VERIFY_ARE_EQUAL(popup._parse(), 12345); - // make sure we can delete input correctly - popup._pop(); - VERIFY_ARE_EQUAL(popup._parse(), 1234); - } -}; diff --git a/src/host/ut_host/CopyFromCharPopupTests.cpp b/src/host/ut_host/CopyFromCharPopupTests.cpp deleted file mode 100644 index ccc54172a18..00000000000 --- a/src/host/ut_host/CopyFromCharPopupTests.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CopyFromCharPopup.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -static constexpr size_t BUFFER_SIZE = 256; - -class CopyFromCharPopupTests -{ - TEST_CLASS(CopyFromCharPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(DeleteAllWhenCharNotFound) - { - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = L'x'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - // move cursor to beginning of prompt text - cookedReadData.InsertionPoint() = 0; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // all text to the right of the cursor should be gone - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), 0u); - } - - TEST_METHOD(CanDeletePartialLine) - { - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = L'f'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"By the rude bridge that arched the flood"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - // move cursor to index 12 - const size_t index = 12; - cookedReadData.SetBufferCurrentPtr(buffer + index); - cookedReadData.InsertionPoint() = index; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - std::wstring expectedText = L"By the rude flood"; - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), expectedText.size() * sizeof(wchar_t)); - std::wstring resultText(buffer, buffer + expectedText.size()); - VERIFY_ARE_EQUAL(resultText, expectedText); - } -}; diff --git a/src/host/ut_host/CopyToCharPopupTests.cpp b/src/host/ut_host/CopyToCharPopupTests.cpp deleted file mode 100644 index 320e0b7841a..00000000000 --- a/src/host/ut_host/CopyToCharPopupTests.cpp +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CopyToCharPopup.hpp" - -using Microsoft::Console::Interactivity::ServiceLocator; -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; - -static constexpr size_t BUFFER_SIZE = 256; - -class CopyToCharPopupTests -{ - TEST_CLASS(CopyToCharPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(NothingHappensWhenCharNotFound) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L'x'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); - } - - TEST_METHOD(CanCopyToEmptyPrompt) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - const std::wstring expectedText = L"here i"; - - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedText.size()); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedText.size() * sizeof(wchar_t)); - - // make sure that the text matches - const std::wstring resultText(buffer, buffer + expectedText.size()); - VERIFY_ARE_EQUAL(resultText, expectedText); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); - } - - TEST_METHOD(WontCopyTextBeforeCursor) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData with a string longer than the previous history - const std::wstring testString = L"Whose woods there are I think I know."; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - const wchar_t* const expectedBufPtr = cookedReadData._bufPtr; - const auto expectedBytesRead = cookedReadData._bytesRead; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // nothing should have changed - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufPtr); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedBytesRead); - const std::wstring resultText(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(resultText, testString); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[testString.size()], UNICODE_SPACE); - } - - TEST_METHOD(CanMergeLine) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData with a string longer than the previous history - const std::wstring testString = L"fear "; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - const std::wstring expectedText = L"fear is"; - const std::wstring resultText(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(resultText, testString); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); - } -}; diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj index 36afffca49c..f995dea0a44 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -15,12 +15,7 @@ - - - - - @@ -96,7 +91,6 @@ - diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters index d7045f877f0..4366c712fb9 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj.filters +++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters @@ -75,27 +75,12 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files Source Files - - Source Files - Source Files @@ -116,9 +101,6 @@ Header Files - - Header Files - diff --git a/src/host/ut_host/PopupTestHelper.hpp b/src/host/ut_host/PopupTestHelper.hpp deleted file mode 100644 index e427cd9624b..00000000000 --- a/src/host/ut_host/PopupTestHelper.hpp +++ /dev/null @@ -1,84 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- PopupTestHelper.hpp - -Abstract: -- helper functions for unit testing the various popups - -Author(s): -- Austin Diviness (AustDi) 06-Sep-2018 - ---*/ - -#pragma once - -#include "../history.h" -#include "../readDataCooked.hpp" - -class PopupTestHelper final -{ -public: - static void InitReadData(COOKED_READ_DATA& cookedReadData, - wchar_t* const pBuffer, - const size_t cchBuffer, - const size_t cursorPosition) noexcept - { - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufPtr = pBuffer + cursorPosition; - cookedReadData._backupLimit = pBuffer; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); - cookedReadData._currentPosition = cursorPosition; - cookedReadData.VisibleCharCount() = cursorPosition; - } - - static void InitHistory(CommandHistory& history) noexcept - { - history.Empty(); - history.Flags |= CommandHistory::CLE_ALLOCATED; - VERIFY_SUCCEEDED(history.Add(L"I'm a little teapot", false)); - VERIFY_SUCCEEDED(history.Add(L"hear me shout", false)); - VERIFY_SUCCEEDED(history.Add(L"here is my handle", false)); - VERIFY_SUCCEEDED(history.Add(L"here is my spout", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 4); - } - - static void InitLongHistory(CommandHistory& history) noexcept - { - history.Empty(); - history.Flags |= CommandHistory::CLE_ALLOCATED; - VERIFY_SUCCEEDED(history.Add(L"Because I could not stop for Death", false)); - VERIFY_SUCCEEDED(history.Add(L"He kindly stopped for me", false)); - VERIFY_SUCCEEDED(history.Add(L"The carriage held but just Ourselves", false)); - VERIFY_SUCCEEDED(history.Add(L"And Immortality", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We slowly drove - He knew no haste", false)); - VERIFY_SUCCEEDED(history.Add(L"And I had put away", false)); - VERIFY_SUCCEEDED(history.Add(L"My labor and my leisure too", false)); - VERIFY_SUCCEEDED(history.Add(L"For His Civility", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the School, where Children strove", false)); - VERIFY_SUCCEEDED(history.Add(L"At Recess - in the Ring", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the Fields of Gazing Grain", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the Setting Sun", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"Or rather - He passed us,", false)); - VERIFY_SUCCEEDED(history.Add(L"The Dews drew quivering and chill,", false)); - VERIFY_SUCCEEDED(history.Add(L"For only Gossamer, my Gown,", false)); - VERIFY_SUCCEEDED(history.Add(L"My Tippet - only Tulle", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We paused before a House that seemed", false)); - VERIFY_SUCCEEDED(history.Add(L"A Swelling of the Ground -", false)); - VERIFY_SUCCEEDED(history.Add(L"The Roof was scarcely visible -", false)); - VERIFY_SUCCEEDED(history.Add(L"The Cornice - in the Ground -", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"Since then - 'tis Centuries - and yet", false)); - VERIFY_SUCCEEDED(history.Add(L"Feels shorter than the Day", false)); - VERIFY_SUCCEEDED(history.Add(L"~ Emily Dickinson", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 28); - } -}; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 1dd058f057d..f1e901c91bd 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -2895,15 +2895,11 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}") - TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2}") END_TEST_METHOD_PROPERTIES(); bool writeSingly; VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false"); - DWORD writeCharsLegacyMode; - VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L""); - // Created for MSFT:19735050. // Kinda the same as above, but with WriteCharsLegacy instead. // The variable that really breaks this scenario @@ -2931,18 +2927,13 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() if (writeSingly) { - auto str = L"X"; - size_t seqCb = 2; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); - str = L"\x08"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); + WriteCharsLegacy(si, L"X", false, nullptr); + WriteCharsLegacy(si, L"X", false, nullptr); + WriteCharsLegacy(si, L"\x08", false, nullptr); } else { - const auto str = L"XX\x08"; - size_t seqCb = 6; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); + WriteCharsLegacy(si, L"XX\x08", false, nullptr); } TextAttribute expectedDefaults{}; @@ -7191,8 +7182,7 @@ void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt() Log::Comment(L"Now write several lines of content using WriteCharsLegacy"); const auto content = L"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; - auto numBytes = wcslen(content) * sizeof(wchar_t); - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, content, content, content, &numBytes, nullptr, 0, 0, nullptr)); + WriteCharsLegacy(si, content, false, nullptr); Log::Comment(L"Confirm that the cursor position has moved down 10 lines"); const auto newCursorPos = til::point{ initialCursorPos.x, initialCursorPos.y + 10 }; diff --git a/src/host/ut_host/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp index 99193049816..f33889992bb 100644 --- a/src/host/ut_host/SelectionTests.cpp +++ b/src/host/ut_host/SelectionTests.cpp @@ -10,7 +10,6 @@ #include "globals.h" #include "selection.hpp" -#include "cmdline.h" #include "../interactivity/inc/ServiceLocator.hpp" @@ -382,89 +381,6 @@ class SelectionInputTests return true; } - TEST_METHOD(TestGetInputLineBoundaries) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // 80x80 box - const til::CoordType sRowWidth = 80; - - til::inclusive_rect srectEdges; - srectEdges.left = srectEdges.top = 0; - srectEdges.right = srectEdges.bottom = sRowWidth - 1; - - // false when no cooked read data exists - VERIFY_IS_FALSE(gci.HasPendingCookedRead()); - - auto fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); - VERIFY_IS_FALSE(fResult); - - // prepare some read data - m_state->PrepareReadHandle(); - auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); }); - - m_state->PrepareCookedReadData(); - // set up to clean up read data later - auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); }); - - auto& readData = gci.CookedReadData(); - - // backup text info position over remainder of text execution duration - auto& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); - til::point coordOldTextInfoPos; - coordOldTextInfoPos.x = textBuffer.GetCursor().GetPosition().x; - coordOldTextInfoPos.y = textBuffer.GetCursor().GetPosition().y; - - // set various cursor positions - readData.OriginalCursorPosition().x = 15; - readData.OriginalCursorPosition().y = 3; - - readData.VisibleCharCount() = 200; - - textBuffer.GetCursor().SetXPosition(35); - textBuffer.GetCursor().SetYPosition(35); - - // try getting boundaries with no pointers. parameters should be fully optional. - fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); - VERIFY_IS_TRUE(fResult); - - // now let's get some actual data - til::point coordStart; - til::point coordEnd; - - fResult = Selection::s_GetInputLineBoundaries(&coordStart, &coordEnd); - VERIFY_IS_TRUE(fResult); - - // starting position/boundary should always be where the input line started - VERIFY_ARE_EQUAL(coordStart.x, readData.OriginalCursorPosition().x); - VERIFY_ARE_EQUAL(coordStart.y, readData.OriginalCursorPosition().y); - - // ending position can vary. it's in one of two spots - // 1. If the original cooked cursor was valid (which it was this first time), it's NumberOfVisibleChars ahead. - til::point coordFinalPos; - - const auto cCharsToAdjust = ((til::CoordType)readData.VisibleCharCount() - 1); // then -1 to be on the last piece of text, not past it - - coordFinalPos.x = (readData.OriginalCursorPosition().x + cCharsToAdjust) % sRowWidth; - coordFinalPos.y = readData.OriginalCursorPosition().y + ((readData.OriginalCursorPosition().x + cCharsToAdjust) / sRowWidth); - - VERIFY_ARE_EQUAL(coordEnd.x, coordFinalPos.x); - VERIFY_ARE_EQUAL(coordEnd.y, coordFinalPos.y); - - // 2. if the original cooked cursor is invalid, then it's the text info cursor position - readData.OriginalCursorPosition().x = -1; - readData.OriginalCursorPosition().y = -1; - - fResult = Selection::s_GetInputLineBoundaries(nullptr, &coordEnd); - VERIFY_IS_TRUE(fResult); - - VERIFY_ARE_EQUAL(coordEnd.x, textBuffer.GetCursor().GetPosition().x - 1); // -1 to be on the last piece of text, not past it - VERIFY_ARE_EQUAL(coordEnd.y, textBuffer.GetCursor().GetPosition().y); - - // restore text buffer info position - textBuffer.GetCursor().SetXPosition(coordOldTextInfoPos.x); - textBuffer.GetCursor().SetYPosition(coordOldTextInfoPos.y); - } - TEST_METHOD(TestWordByWordPrevious) { BEGIN_TEST_METHOD_PROPERTIES() diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 44781d38a72..d52dfc5ae69 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -1554,60 +1554,20 @@ void TextBufferTests::TestBackspaceStringsAPI() // should be the same. std::unique_ptr waiter; - size_t aCb = 2; - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); - - size_t seqCb = 6; Log::Comment(NoThrowString().Format( L"Using WriteCharsLegacy, write \\b \\b as a single string.")); - { - const auto str = L"\b \b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - - Log::Comment(NoThrowString().Format( - L"Using DoWriteConsole, write \\b \\b as a single string.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); - - VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, false, waiter)); - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - } + size_t aCb = 2; + size_t seqCb = 6; + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b \b", &seqCb, si, false, waiter)); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); seqCb = 2; - - Log::Comment(NoThrowString().Format( - L"Using DoWriteConsole, write \\b \\b as separate strings.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); - - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - - Log::Comment(NoThrowString().Format( - L"Using WriteCharsLegacy, write \\b \\b as separate strings.")); - { - const auto str = L"a"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L" "; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); } diff --git a/src/host/ut_host/sources b/src/host/ut_host/sources index 161939e3c73..9b47dded30d 100644 --- a/src/host/ut_host/sources +++ b/src/host/ut_host/sources @@ -37,11 +37,6 @@ SOURCES = \ ConptyOutputTests.cpp \ ViewportTests.cpp \ ConsoleArgumentsTests.cpp \ - CommandLineTests.cpp \ - CommandListPopupTests.cpp \ - CommandNumberPopupTests.cpp \ - CopyFromCharPopupTests.cpp \ - CopyToCharPopupTests.cpp \ ObjectTests.cpp \ DefaultResource.rc \ diff --git a/src/interactivity/win32/menu.cpp b/src/interactivity/win32/menu.cpp index 205a0135498..7b59887daaf 100644 --- a/src/interactivity/win32/menu.cpp +++ b/src/interactivity/win32/menu.cpp @@ -507,13 +507,7 @@ void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) if (coordBuffer.width != coordScreenBufferSize.width || coordBuffer.height != coordScreenBufferSize.height) { - const auto pCommandLine = &CommandLine::Instance(); - - pCommandLine->Hide(FALSE); - LOG_IF_FAILED(ScreenInfo.ResizeScreenBuffer(coordBuffer, TRUE)); - - pCommandLine->Show(); } // Finally, restrict window size to the maximum possible size for the given buffer now that it's processed. diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 6331a6590f7..5866400a730 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -397,7 +397,7 @@ void HandleKeyEvent(const HWND hWnd, } } // we need to check if there is an active popup because otherwise they won't be able to receive shift+key events - if (pSelection->s_IsValidKeyboardLineSelection(&inputKeyInfo) && IsInProcessedInputMode() && gci.PopupCount.load() == 0) + if (pSelection->s_IsValidKeyboardLineSelection(&inputKeyInfo) && IsInProcessedInputMode() && !gci.HasPendingPopup()) { if (!bKeyDown || pSelection->HandleKeyboardLineSelectionEvent(&inputKeyInfo)) { From c4436157c116880c320b5df278d4d886a930c0f3 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 28 Aug 2023 13:23:17 +0200 Subject: [PATCH 44/59] A small optimization of COOKED_READ_DATA::_erase (#15879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a small optimization that makes COOKED_READ_DATA erase short runs of text more quickly. It's not really necessary to do this as this code is not a hotpath, but I felt like it's neater this way. It requires no heap allocations even for long runs of text. ## Validation Steps Performed * Deleting text anywhere in a prompt erases it ✅ --- src/host/readDataCooked.cpp | 24 +++++++++++++++++++----- src/host/readDataCooked.hpp | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 667a445f4b2..83d2f21b24a 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -766,14 +766,28 @@ void COOKED_READ_DATA::_flushBuffer() } // This is just a small helper to fill the next N cells starting at the current cursor position with whitespace. -// The implementation is inefficient for `count`s larger than 7, but such calls are uncommon to happen (namely only when resizing the window). -void COOKED_READ_DATA::_erase(const til::CoordType distance) +void COOKED_READ_DATA::_erase(const til::CoordType distance) const { - if (distance > 0) + if (distance <= 0) { - const std::wstring str(gsl::narrow_cast(distance), L' '); - std::ignore = _writeChars(str); + return; } + + std::array whitespace; + auto remaining = gsl::narrow_cast(distance); + auto nextWriteSize = std::min(remaining, whitespace.size()); + + // If we only need to erase 1 character worth of whitespace, + // we don't need to initialize 256 bytes worth of a whitespace array. + // nextWriteSize can only ever shrink past this point if anything. + std::fill_n(whitespace.begin(), nextWriteSize, L' '); + + do + { + std::ignore = _writeChars({ whitespace.data(), nextWriteSize }); + remaining -= nextWriteSize; + nextWriteSize = std::min(remaining, whitespace.size()); + } while (remaining != 0); } // A helper to write text and calculate the number of cells we've written. diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 23f99da94da..49f6c06bc9b 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -113,7 +113,7 @@ class COOKED_READ_DATA final : public ReadData void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); void _markAsDirty(); void _flushBuffer(); - void _erase(til::CoordType distance); + void _erase(til::CoordType distance) const; til::CoordType _writeChars(const std::wstring_view& text) const; til::point _offsetPosition(til::point pos, til::CoordType distance) const; void _unwindCursorPosition(til::CoordType distance) const; From d54ce33afcd5e9875b53e4449ea3d2fd5c1ebc07 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 29 Aug 2023 18:47:26 +0200 Subject: [PATCH 45/59] Fix pattern coordinates to be viewport relative (#15892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pattern tree coordinates are viewport-relative. Closes #15891 ## Validation Steps Performed * Print some text so the viewport scrolls down * Print a URL * URL is underlined on hover ✅ --- src/cascadia/TerminalCore/Terminal.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index c4e6edc2685..74f48697c86 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1432,7 +1432,9 @@ PointTree Terminal::_getPatterns(til::CoordType beg, til::CoordType end) const do { auto range = ICU::BufferRangeFromMatch(&text, re.get()); - // PointTree uses half-open ranges. + // PointTree uses half-open ranges and viewport-relative coordinates. + range.start.y -= beg; + range.end.y -= beg; range.end.x++; intervals.push_back(PointTree::interval(range.start, range.end, 0)); } while (uregex_findNext(re.get(), &status)); From 636be7e514e243810e8944a61f5cf7044d316e05 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Wed, 30 Aug 2023 15:25:36 -0500 Subject: [PATCH 46/59] Canary: add the appLicensing capability (#15905) This should allow the package to be installed without AppXSvc consulting the store or the licensing service. It's free and open-source. It shouldn't need a license to run. --- src/cascadia/CascadiaPackage/Package-Can.appxmanifest | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index f2e697921a9..ba8a2214075 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -147,5 +147,6 @@ + From 5fb2518117ddf14e05f67eed47e6e4d940a3c025 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 31 Aug 2023 18:28:22 +0200 Subject: [PATCH 47/59] AtlasEngine: Fix support for font features (#15912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Font features require us to skip the fast path via `GetTextComplexity`. `IDWriteTextLayout` handles it the same way internally. Closes #15896 ## Validation Steps Performed * Use Cascadia Code * Set `features: { "ss19": 1 }` * "0" has a dash in it instead of a dot ✅ --- src/renderer/atlas/AtlasEngine.cpp | 52 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 874a4c38900..6d6ccb0251d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -648,6 +648,9 @@ void AtlasEngine::_flushBufferLine() const auto initialIndicesCount = row.glyphIndices.size(); + // GetTextComplexity() returns as many glyph indices as its textLength parameter (here: mappedLength). + // This block ensures that the buffer has sufficient capacity. It also initializes the glyphProps buffer because it and + // glyphIndices sort of form a "pair" in the _mapComplex() code and are always simultaneously resized there as well. if (mappedLength > _api.glyphIndices.size()) { auto size = _api.glyphIndices.size(); @@ -658,33 +661,40 @@ void AtlasEngine::_flushBufferLine() _api.glyphProps = Buffer{ size }; } - // We can reuse idx here, as it'll be reset to "idx = mappedEnd" in the outer loop anyways. - for (u32 complexityLength = 0; idx < mappedEnd; idx += complexityLength) + if (_api.s->font->fontFeatures.empty()) { - BOOL isTextSimple = FALSE; - THROW_IF_FAILED(_p.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data())); - - if (isTextSimple) + // We can reuse idx here, as it'll be reset to "idx = mappedEnd" in the outer loop anyways. + for (u32 complexityLength = 0; idx < mappedEnd; idx += complexityLength) { - const auto shift = gsl::narrow_cast(row.lineRendition != LineRendition::SingleWidth); - const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y; + BOOL isTextSimple = FALSE; + THROW_IF_FAILED(_p.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data())); - for (size_t i = 0; i < complexityLength; ++i) + if (isTextSimple) { - const size_t col1 = _api.bufferLineColumn[idx + i + 0]; - const size_t col2 = _api.bufferLineColumn[idx + i + 1]; - const auto glyphAdvance = (col2 - col1) * _p.s->font->cellSize.x; - const auto fg = colors[col1 << shift]; - row.glyphIndices.emplace_back(_api.glyphIndices[i]); - row.glyphAdvances.emplace_back(static_cast(glyphAdvance)); - row.glyphOffsets.emplace_back(); - row.colors.emplace_back(fg); + const auto shift = gsl::narrow_cast(row.lineRendition != LineRendition::SingleWidth); + const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y; + + for (size_t i = 0; i < complexityLength; ++i) + { + const size_t col1 = _api.bufferLineColumn[idx + i + 0]; + const size_t col2 = _api.bufferLineColumn[idx + i + 1]; + const auto glyphAdvance = (col2 - col1) * _p.s->font->cellSize.x; + const auto fg = colors[col1 << shift]; + row.glyphIndices.emplace_back(_api.glyphIndices[i]); + row.glyphAdvances.emplace_back(static_cast(glyphAdvance)); + row.glyphOffsets.emplace_back(); + row.colors.emplace_back(fg); + } + } + else + { + _mapComplex(mappedFontFace.get(), idx, complexityLength, row); } } - else - { - _mapComplex(mappedFontFace.get(), idx, complexityLength, row); - } + } + else + { + _mapComplex(mappedFontFace.get(), idx, mappedLength, row); } const auto indicesCount = row.glyphIndices.size(); From 60843faa9e41944c0cc64c29e19657a6b21439fd Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 5 Sep 2023 13:14:33 +0200 Subject: [PATCH 48/59] AtlasEngine: Fix invalidation when the cursor is invisible (#15904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PaintCursor()` is only called when the cursor is visible, but we need to invalidate the cursor area even if it isn't. Otherwise a transition from a visible to an invisible cursor wouldn't be rendered. I'm confident that this closes #15199 ## Validation Steps Performed * Set blink duration extremely high * Launch pwsh.exe * Press Enter a few times * Press Ctrl+L * There are never 2 cursors visible, not even briefly ✅ --- src/renderer/atlas/AtlasEngine.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 6d6ccb0251d..5ced24f7919 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -251,6 +251,16 @@ try { _flushBufferLine(); + // PaintCursor() is only called when the cursor is visible, but we need to invalidate the cursor area + // even if it isn't. Otherwise a transition from a visible to an invisible cursor wouldn't be rendered. + if (const auto r = _api.invalidatedCursorArea; r.non_empty()) + { + _p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, r.left * _p.s->font->cellSize.x); + _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, r.top * _p.s->font->cellSize.y); + _p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, r.right * _p.s->font->cellSize.x); + _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, r.bottom * _p.s->font->cellSize.y); + } + _api.invalidatedCursorArea = invalidatedAreaNone; _api.invalidatedRows = invalidatedRowsNone; _api.scrollOffset = 0; @@ -423,15 +433,6 @@ try } } - // Clear the previous cursor - if (const auto r = _api.invalidatedCursorArea; r.non_empty()) - { - _p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, r.left * _p.s->font->cellSize.x); - _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, r.top * _p.s->font->cellSize.y); - _p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, r.right * _p.s->font->cellSize.x); - _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, r.bottom * _p.s->font->cellSize.y); - } - if (options.isOn) { const auto cursorWidth = 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar)); From c5e0908b98573c9fccccb0dd8e4189c5a0769a5e Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 5 Sep 2023 11:42:05 -0500 Subject: [PATCH 49/59] Update cmd.exe FAQ (#15918) https://github.com/microsoft/terminal/issues/15870#issuecomment-1701435579 is an important note I think deserves to be committed. --- .github/actions/spelling/expect/web.txt | 1 + doc/Niksa.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/actions/spelling/expect/web.txt b/.github/actions/spelling/expect/web.txt index 52c1cfd1f0f..8f5b59ac730 100644 --- a/.github/actions/spelling/expect/web.txt +++ b/.github/actions/spelling/expect/web.txt @@ -4,3 +4,4 @@ appshellintegration mdtauk gfycat Guake +xkcd diff --git a/doc/Niksa.md b/doc/Niksa.md index 40ffb1a0ec7..5efcdda0abd 100644 --- a/doc/Niksa.md +++ b/doc/Niksa.md @@ -27,6 +27,8 @@ I would highly recommend that Gulp convert to using PowerShell scripts and that Original Source: https://github.com/microsoft/terminal/issues/217#issuecomment-404240443 +_Addendum_: cmd.exe is the literal embodiment of [xkcd#1172]([url](https://xkcd.com/1172/)). Every change, no matter how small, will break _someone_. + ## Why is typing-to-screen performance better than every other app? I really do not mind when someone comes by and decides to tell us that we're doing a good job at something. We hear so many complaints every day that a post like this is a breath of fresh air. Thanks for your thanks! From 0cbde94e4b2e530eac00acc7de1808fe2fd10273 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 5 Sep 2023 16:23:09 -0500 Subject: [PATCH 50/59] Show number of search results & positions of hits in scrollbar (#14045) This is a resurrection of #8588. That PR became painfully stale after the `ControlCore` split. Original description: > ## Summary of the Pull Request > This is a PoC for: > * Search status in SearchBox (aka number of matches + index of the current match) > * Live search (aka search upon typing) > ## Detailed Description of the Pull Request / Additional comments > * Introduced this optionally (global setting to enable it) > * The approach is following: > * Every time the filter changes, enumerate all matches > * Upon navigation just take the relevant match and select it > I cleaned it up a bit, and added support for also displaying the positions of the matches in the scrollbar (if `showMarksOnScrollbar` is also turned on). It's also been made SUBSTANTIALLY easier after #15858 was merged. Similar to before, searching while there's piles of output running isn't _perfect_. But it's pretty awful currently, so that's not the end of the world. Gifs below. * closes #8631 (which is a bullet point in #3920) * closes #6319 Co-authored-by: Don-Vito --------- Co-authored-by: khvitaly --- src/buffer/out/search.cpp | 25 ++- src/buffer/out/search.h | 7 +- src/cascadia/TerminalControl/ControlCore.cpp | 40 ++++- src/cascadia/TerminalControl/ControlCore.h | 5 + src/cascadia/TerminalControl/ControlCore.idl | 4 + src/cascadia/TerminalControl/EventArgs.h | 2 + src/cascadia/TerminalControl/EventArgs.idl | 2 + .../Resources/en-US/Resources.resw | 14 +- .../TerminalControl/SearchBoxControl.cpp | 158 ++++++++++++++++++ .../TerminalControl/SearchBoxControl.h | 16 ++ .../TerminalControl/SearchBoxControl.idl | 4 + .../TerminalControl/SearchBoxControl.xaml | 13 +- src/cascadia/TerminalControl/TermControl.cpp | 87 ++++++++-- src/cascadia/TerminalControl/TermControl.h | 5 +- src/cascadia/TerminalControl/TermControl.xaml | 1 + src/cascadia/TerminalCore/Terminal.hpp | 3 + src/cascadia/TerminalCore/TerminalApi.cpp | 2 +- .../UnitTests_TerminalCore/ScrollTest.cpp | 15 +- src/inc/til/winrt.h | 4 +- 19 files changed, 383 insertions(+), 24 deletions(-) diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 0186110f6cf..eef5fac17cb 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,6 +8,14 @@ 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(); @@ -71,8 +79,10 @@ void Search::MovePastPoint(const til::point anchor) noexcept void Search::FindNext() noexcept { - const auto count = gsl::narrow_cast(_results.size()); - _index = (_index + _step + count) % count; + if (const auto count{ gsl::narrow_cast(_results.size()) }) + { + _index = (_index + _step + count) % count; + } } const til::point_span* Search::GetCurrent() const noexcept @@ -87,6 +97,7 @@ const til::point_span* Search::GetCurrent() const noexcept // Routine Description: // - Takes the found word and selects it in the screen buffer + bool Search::SelectCurrent() const { if (const auto s = GetCurrent()) @@ -102,3 +113,13 @@ bool Search::SelectCurrent() const return false; } + +const std::vector& Search::Results() const noexcept +{ + return _results; +} + +size_t Search::CurrentMatch() const noexcept +{ + return _index; +} diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index a337552d59a..2c6bc80a724 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -25,6 +25,7 @@ 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(); @@ -34,10 +35,14 @@ class Search final const til::point_span* GetCurrent() const noexcept; bool SelectCurrent() const; + const std::vector& Results() const noexcept; + size_t CurrentMatch() const noexcept; + bool CurrentDirection() const noexcept; + private: // _renderData is a pointer so that Search() is constexpr default constructable. Microsoft::Console::Render::IRenderData* _renderData = nullptr; - std::wstring_view _needle; + std::wstring _needle; bool _reverse = false; bool _caseInsensitive = false; uint64_t _lastMutationId = 0; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c487a81960f..7530909aaab 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -39,6 +39,9 @@ constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); +// The delay before performing the search after change of search criteria +constexpr const auto SearchAfterChangeDelay = std::chrono::milliseconds(200); + namespace winrt::Microsoft::Terminal::Control::implementation { static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c) @@ -1346,6 +1349,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation nullptr; } + til::color ControlCore::ForegroundColor() const + { + return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultForeground); + } + til::color ControlCore::BackgroundColor() const { return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultBackground); @@ -1552,6 +1560,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto foundMatch = _searcher.SelectCurrent(); + auto foundResults = winrt::make_self(foundMatch); if (foundMatch) { // this is used for search, @@ -1560,15 +1569,44 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetBlockSelection(false); _renderer->TriggerSelection(); _UpdateSelectionMarkersHandlers(*this, winrt::make(true)); + + foundResults->TotalMatches(gsl::narrow(_searcher.Results().size())); + foundResults->CurrentMatch(gsl::narrow(_searcher.CurrentMatch())); + + _terminal->AlwaysNotifyOnBufferRotation(true); } // Raise a FoundMatch event, which the control will use to notify // narrator if there was any results in the buffer - _FoundMatchHandlers(*this, winrt::make(foundMatch)); + _FoundMatchHandlers(*this, *foundResults); + } + + Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() + { + auto lock = _terminal->LockForWriting(); + if (_searcher.ResetIfStale(*GetRenderData())) + { + auto results = std::vector(); + + auto lastRow = til::CoordTypeMin; + for (const auto& match : _searcher.Results()) + { + const auto row{ match.start.y }; + if (row != lastRow) + { + results.push_back(row); + lastRow = row; + } + } + _cachedSearchResultRows = winrt::single_threaded_vector(std::move(results)); + } + + return _cachedSearchResultRows; } void ControlCore::ClearSearch() { + _terminal->AlwaysNotifyOnBufferRotation(false); _searcher = {}; } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 37058ebfbd3..c21730f4789 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -106,6 +106,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::hstring FontFaceName() const noexcept; uint16_t FontWeight() const noexcept; + til::color ForegroundColor() const; til::color BackgroundColor() const; void SendInput(const winrt::hstring& wstr); @@ -208,6 +209,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void ClearSearch(); + Windows::Foundation::Collections::IVector SearchResultRows(); + void LeftClickOnTerminal(const til::point terminalPosition, const int numberOfClicks, const bool altEnabled, @@ -338,6 +341,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point _contextMenuBufferPosition{ 0, 0 }; + Windows::Foundation::Collections::IVector _cachedSearchResultRows{ nullptr }; + void _setupDispatcherAndCallbacks(); bool _setFontSizeUnderLock(float fontSize); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 1b9f67d8d9c..01223fb1a14 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -127,8 +127,12 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.Point CursorPosition { get; }; void ResumeRendering(); void BlinkAttributeTick(); + void Search(String text, Boolean goForward, Boolean caseSensitive); void ClearSearch(); + IVector SearchResultRows { get; }; + + Microsoft.Terminal.Core.Color ForegroundColor { get; }; Microsoft.Terminal.Core.Color BackgroundColor { get; }; SelectionData SelectionInfo { get; }; diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 9d3f3e2a3f6..12632871fdb 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -176,6 +176,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation } WINRT_PROPERTY(bool, FoundMatch); + WINRT_PROPERTY(int32_t, TotalMatches); + WINRT_PROPERTY(int32_t, CurrentMatch); }; struct ShowWindowArgs : public ShowWindowArgsT diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 75e1a20211c..3caea5a0f38 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -82,6 +82,8 @@ namespace Microsoft.Terminal.Control runtimeclass FoundResultsArgs { Boolean FoundMatch { get; }; + Int32 TotalMatches { get; }; + Int32 CurrentMatch { get; }; } runtimeclass ShowWindowArgs diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index c94ab15d368..a604fc912e4 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -178,6 +178,18 @@ Ctrl+Click to follow link + + No results + Will be presented near the search box when Find operation returned no results. + + + Searching... + Will be presented near the search box when Find operation is running. + + + {0}/{1} + Will be displayed to indicate what result the user has selected, of how many total results. {0} will be replaced with the index of the current result. {1} will be replaced with the total number of results. + Invalid URI Whenever we encounter an invalid URI or URL we show this string as a warning. @@ -276,4 +288,4 @@ Please either install the missing font or choose another one. Select output The tooltip for a button for selecting all of a command's output - + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index a841f11a7d8..bd5aeae3433 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "SearchBoxControl.h" #include "SearchBoxControl.g.cpp" +#include using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -18,12 +19,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation this->CharacterReceived({ this, &SearchBoxControl::_CharacterHandler }); this->KeyDown({ this, &SearchBoxControl::_KeyDownHandler }); + this->RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + // Once the control is visible again we trigger SearchChanged event. + // We do this since we probably have a value from the previous search, + // and in such case logically the search changes from "nothing" to this value. + // A good example for SearchChanged event consumer is Terminal Control. + // Once the Search Box is open we want the Terminal Control + // to immediately perform the search with the value appearing in the box. + if (Visibility() == Visibility::Visible) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + }); _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); _focusableElements.insert(CaseSensitivityButton()); _focusableElements.insert(GoForwardButton()); _focusableElements.insert(GoBackwardButton()); + + StatusBox().Width(_GetStatusMaxWidth()); } // Method Description: @@ -62,6 +77,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Enter) { + // If the buttons are disabled, then don't allow enter to search either. + if (!GoForwardButton().IsEnabled() || !GoBackwardButton().IsEnabled()) + { + return; + } + const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) { @@ -209,4 +230,141 @@ namespace winrt::Microsoft::Terminal::Control::implementation { e.Handled(true); } + + // Method Description: + // - Handler for changing the text. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: event data + // Return Value: + // - + void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Handler for clicking the case sensitivity toggle. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: not used + // Return Value: + // - + void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Formats a status message representing the search state: + // * "Searching" - if totalMatches is negative + // * "No results" - if totalMatches is 0 + // * "?/n" - if totalMatches=n matches and we didn't start the iteration over matches + // (usually we will get this after buffer update) + // * "m/n" - if we are currently at match m out of n. + // * "m/max+" - if n > max results to show + // * "?/max+" - if m > max results to show + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - status message + winrt::hstring SearchBoxControl::_FormatStatus(int32_t totalMatches, int32_t currentMatch) + { + if (totalMatches < 0) + { + return RS_(L"TermControl_Searching"); + } + + if (totalMatches == 0) + { + return RS_(L"TermControl_NoMatch"); + } + + std::wstring currentString; + std::wstring totalString; + + if (currentMatch < 0 || currentMatch > (MaximumTotalResultsToShowInStatus - 1)) + { + currentString = CurrentIndexTooHighStatus; + } + else + { + currentString = fmt::format(L"{}", currentMatch + 1); + } + + if (totalMatches > MaximumTotalResultsToShowInStatus) + { + totalString = TotalResultsTooHighStatus; + } + else + { + totalString = fmt::format(L"{}", totalMatches); + } + + return winrt::hstring{ fmt::format(RS_(L"TermControl_NumResults").c_str(), currentString, totalString) }; + } + + // Method Description: + // - Helper method to measure the width of the text block given the text and the font size + // Arguments: + // - text: the text to measure + // - fontSize: the size of the font to measure + // Return Value: + // - the size in pixels + double SearchBoxControl::_TextWidth(winrt::hstring text, double fontSize) + { + winrt::Windows::UI::Xaml::Controls::TextBlock t; + t.FontSize(fontSize); + t.Text(text); + t.Measure({ FLT_MAX, FLT_MAX }); + return t.ActualWidth(); + } + + // Method Description: + // - This method tries to predict the maximal size of the status box + // by measuring different possible statuses + // Return Value: + // - the size in pixels + double SearchBoxControl::_GetStatusMaxWidth() + { + const auto fontSize = StatusBox().FontSize(); + const auto maxLength = std::max({ _TextWidth(_FormatStatus(-1, -1), fontSize), + _TextWidth(_FormatStatus(0, -1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus), fontSize) }); + + return maxLength; + } + + // Method Description: + // - Formats and sets the status message in the status box. + // Increases the size of the box if required. + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - + void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch) + { + const auto status = _FormatStatus(totalMatches, currentMatch); + StatusBox().Text(status); + } + + // Method Description: + // - Enables / disables results navigation buttons + // Arguments: + // - enable: if true, the buttons should be enabled + // Return Value: + // - + void SearchBoxControl::NavigationEnabled(bool enabled) + { + GoBackwardButton().IsEnabled(enabled); + GoForwardButton().IsEnabled(enabled); + } + bool SearchBoxControl::NavigationEnabled() + { + return GoBackwardButton().IsEnabled() || GoForwardButton().IsEnabled(); + } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 4a680edcb68..5ab9ee291bc 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -21,6 +21,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct SearchBoxControl : SearchBoxControlT { + static constexpr int32_t MaximumTotalResultsToShowInStatus = 999; + static constexpr std::wstring_view TotalResultsTooHighStatus = L"999+"; + static constexpr std::wstring_view CurrentIndexTooHighStatus = L"?"; + static constexpr std::wstring_view StatusDelimiter = L"/"; + SearchBoxControl(); void TextBoxKeyDown(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); @@ -28,17 +33,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); + void SetStatus(int32_t totalMatches, int32_t currentMatch); + bool NavigationEnabled(); + void NavigationEnabled(bool enabled); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); void GoForwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); void CloseClick(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + WINRT_CALLBACK(Search, SearchHandler); + WINRT_CALLBACK(SearchChanged, SearchHandler); TYPED_EVENT(Closed, Control::SearchBoxControl, Windows::UI::Xaml::RoutedEventArgs); private: std::unordered_set _focusableElements; + static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch); + static double _TextWidth(winrt::hstring text, double fontSize); + double _GetStatusMaxWidth(); + bool _GoForward(); bool _CaseSensitive(); void _KeyDownHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index a6bd0a486d7..de909157376 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -11,8 +11,12 @@ namespace Microsoft.Terminal.Control void SetFocusOnTextbox(); void PopulateTextbox(String text); Boolean ContainsFocus(); + void SetStatus(Int32 totalMatches, Int32 currentMatch); + + Boolean NavigationEnabled; event SearchHandler Search; + event SearchHandler SearchChanged; event Windows.Foundation.TypedEventHandler Closed; } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index c0716e39d44..4956d255662 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -161,7 +161,15 @@ HorizontalAlignment="Left" VerticalAlignment="Center" IsSpellCheckEnabled="False" - KeyDown="TextBoxKeyDown" /> + KeyDown="TextBoxKeyDown" + TextChanged="TextBoxTextChanged" /> + + + BackgroundSizing="OuterBorderEdge" + Click="CaseSensitivityButtonClicked"> diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 88a8db66b00..fedd24f8e14 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -302,6 +302,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation 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) + { + Windows::UI::Xaml::Controls::Canvas::SetLeft(r, 16.0f * .66f); + } + }; + for (const auto m : marks) { Windows::UI::Xaml::Shapes::Rectangle r; @@ -312,14 +327,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation // 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)); - r.Fill(brush); - r.Width(16.0f / 3.0f); // pip width - 1/3rd of the scrollbar width. - r.Height(2); - const auto markRow = m.Start.Y; - const auto fractionalHeight = markRow / totalBufferRows; - const auto relativePos = fractionalHeight * fullHeight; - ScrollBarCanvas().Children().Append(r); - Windows::UI::Xaml::Controls::Canvas::SetTop(r, relativePos); + drawPip(m.Start.Y, false, brush); + } + + if (_searchBox) + { + const auto searchMatches{ _core.SearchResultRows() }; + if (searchMatches && + searchMatches.Size() > 0 && + _searchBox->Visibility() == Visibility::Visible) + { + const til::color fgColor{ _core.ForegroundColor() }; + Media::SolidColorBrush searchMarkBrush{}; + searchMarkBrush.Color(fgColor); + for (const auto m : searchMatches) + { + drawPip(m, true, searchMarkBrush); + } + } } } } @@ -388,8 +413,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // Method Description: - // - Search text in text buffer. This is triggered if the user click - // search button or press enter. + // - Search text in text buffer. This is triggered if the user clicks the + // search button, presses enter, or changes the search criteria. // Arguments: // - text: the text to search // - goForward: boolean that represents if the current search direction is forward @@ -403,6 +428,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.Search(text, goForward, caseSensitive); } + // Method Description: + // - The handler for the "search criteria changed" event. Clears selection and initiates a new search. + // Arguments: + // - text: the text to search + // - goForward: indicates whether the search should be performed forward (if set to true) or backward + // - caseSensitive: boolean that represents if the current search is case sensitive + // Return Value: + // - + void TermControl::_SearchChanged(const winrt::hstring& text, + const bool goForward, + const bool caseSensitive) + { + if (_searchBox && _searchBox->Visibility() == Visibility::Visible) + { + _core.Search(text, goForward, caseSensitive); + } + } + // Method Description: // - The handler for the close button or pressing "Esc" when focusing on the // search dialog. @@ -3420,8 +3463,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - args: contains information about the results that were or were not found. // Return Value: // - - void TermControl::_coreFoundMatch(const IInspectable& /*sender*/, const Control::FoundResultsArgs& args) + winrt::fire_and_forget TermControl::_coreFoundMatch(const IInspectable& /*sender*/, Control::FoundResultsArgs args) { + co_await wil::resume_foreground(Dispatcher()); if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) }) { automationPeer.RaiseNotificationEvent( @@ -3430,6 +3474,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation args.FoundMatch() ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); } + + // Manually send a scrollbar update, now, on the UI thread. We're + // already UI-driven, so that's okay. We're not really changing the + // scrollbar, but we do want to update the position of any marks. The + // Core might send a scrollbar updated event too, but if the first + // search hit is in the visible viewport, then the pips won't display + // until the user first scrolls. + auto scrollBar = ScrollBar(); + ScrollBarUpdate update{ + .newValue = scrollBar.Value(), + .newMaximum = scrollBar.Maximum(), + .newMinimum = scrollBar.Minimum(), + .newViewportSize = scrollBar.ViewportSize(), + }; + _throttledUpdateScrollbar(update); + + if (_searchBox) + { + _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch()); + _searchBox->NavigationEnabled(true); + } } void TermControl::OwningHwnd(uint64_t owner) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 895f46128e4..f2f76971ade 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -274,6 +274,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _UpdateSettingsFromUIThread(); void _UpdateAppearanceFromUIThread(Control::IControlAppearance newAppearance); + void _ApplyUISettings(); winrt::fire_and_forget UpdateAppearance(Control::IControlAppearance newAppearance); void _SetBackgroundImage(const IControlAppearance& newAppearance); @@ -343,6 +344,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; void _Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); + + void _SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); // TSFInputControl Handlers @@ -357,7 +360,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); - void _coreFoundMatch(const IInspectable& sender, const Control::FoundResultsArgs& args); + winrt::fire_and_forget _coreFoundMatch(const IInspectable& sender, Control::FoundResultsArgs args); til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index eff29b6b694..9f1c9dc83a5 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -1302,6 +1302,7 @@ x:Load="False" Closed="_CloseSearchBoxControl" Search="_Search" + SearchChanged="_SearchChanged" Visibility="Collapsed" /> diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index ac5a18ec91f..9c83710c3b5 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -16,6 +16,7 @@ #include "../../cascadia/terminalcore/ITerminalInput.hpp" #include +#include inline constexpr size_t TaskbarMinProgress{ 10 }; @@ -118,6 +119,8 @@ class Microsoft::Terminal::Core::Terminal final : const til::point& end, const bool fromUi); + til::property AlwaysNotifyOnBufferRotation; + std::wstring_view CurrentCommand() const; #pragma region ITerminalApi diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 4c31cbc4bbc..6815876b270 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -472,7 +472,7 @@ void Terminal::NotifyBufferRotation(const int delta) const auto oldScrollOffset = _scrollOffset; _PreserveUserScrollOffset(delta); - if (_scrollOffset != oldScrollOffset || hasScrollMarks) + if (_scrollOffset != oldScrollOffset || hasScrollMarks || AlwaysNotifyOnBufferRotation()) { _NotifyScrollEvent(); } diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index 7a30714f42e..12223e94a63 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -154,6 +154,13 @@ void ScrollTest::TestNotifyScrolling() // SHRT_MAX // - Have a selection + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:notifyOnCircling", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + INIT_TEST_PROPERTY(bool, notifyOnCircling, L"Controls whether we should always request scroll notifications"); + + _term->AlwaysNotifyOnBufferRotation(notifyOnCircling); + Log::Comment(L"Watch out - this test takes a while to run, and won't " L"output anything unless in encounters an error. This is expected."); @@ -180,10 +187,12 @@ void ScrollTest::TestNotifyScrolling() // causes the first scroll event auto scrolled = currentRow >= TerminalViewHeight - 1; - // When we circle the buffer, the scroll bar's position does not - // change. + // When we circle the buffer, the scroll bar's position does not change. + // However, as of GH#14045, we will send a notification IF the control + // requested on (by setting AlwaysNotifyOnBufferRotation) auto circledBuffer = currentRow >= totalBufferSize - 1; - auto expectScrollBarNotification = scrolled && !circledBuffer; + auto expectScrollBarNotification = (scrolled && !circledBuffer) || // If we scrolled, but didn't circle the buffer OR + (circledBuffer && notifyOnCircling); // we circled AND we asked for notifications. if (expectScrollBarNotification) { diff --git a/src/inc/til/winrt.h b/src/inc/til/winrt.h index ed815d17671..e5faece3185 100644 --- a/src/inc/til/winrt.h +++ b/src/inc/til/winrt.h @@ -13,7 +13,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" property& operator=(const property& other) = default; - T operator()() const + T operator()() const noexcept { return _value; } @@ -23,11 +23,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } explicit operator bool() const noexcept { +#ifdef WINRT_Windows_Foundation_H if constexpr (std::is_same_v) { return !_value.empty(); } else +#endif { return _value; } From b53ddd1b47d08dcff27e6e1e18700dfce81ebbf7 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 5 Sep 2023 23:24:00 +0200 Subject: [PATCH 51/59] AtlasEngine: Fix overly aggressive invalidation (#15929) When marking newly scrolled in rows as invalidated we used: ``` if (offset < 0) ... else ... ``` But it should've been: ``` if (offset < 0) ... else if (offset > 0) ... ``` Because now it always set the start of the invalidated rows range to 0. Additionally, this includes a commented debug helper which I've used to figure out an unrelated bug. During that search I found this bug. --- src/renderer/atlas/AtlasEngine.cpp | 1 + src/renderer/atlas/BackendD3D.cpp | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 5ced24f7919..478705b542e 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -87,6 +87,7 @@ try _api.invalidatedRows.start = std::min(_api.invalidatedRows.start, _p.s->viewportCellCount.y); _api.invalidatedRows.end = clamp(_api.invalidatedRows.end, _api.invalidatedRows.start, _p.s->viewportCellCount.y); } + if (_api.scrollOffset) { const auto limit = gsl::narrow_cast(_p.s->viewportCellCount.y & 0x7fff); const auto offset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit)); diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 677a92396d4..5598ce8d7c2 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -1206,6 +1206,29 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI .glyphIndices = &glyphEntry.glyphIndex, }; + // To debug issues with this function it may be helpful to know which file + // a given font face corresponds to. This code works for most cases. +#if 0 + wchar_t filePath[MAX_PATH]{}; + { + UINT32 fileCount = 1; + wil::com_ptr file; + if (SUCCEEDED(fontFaceEntry.fontFace->GetFiles(&fileCount, file.addressof()))) + { + wil::com_ptr loader; + THROW_IF_FAILED(file->GetLoader(loader.addressof())); + + if (const auto localLoader = loader.try_query()) + { + void const* fontFileReferenceKey; + UINT32 fontFileReferenceKeySize; + THROW_IF_FAILED(file->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize)); + THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &filePath[0], MAX_PATH)); + } + } + } +#endif + // It took me a while to figure out how to rasterize glyphs manually with DirectWrite without depending on Direct2D. // The benefits are a reduction in memory usage, an increase in performance under certain circumstances and most // importantly, the ability to debug the renderer more easily, because many graphics debuggers don't support Direct2D. From 6cff135f376482b3c4fb83089063e6ff0687a651 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 5 Sep 2023 16:25:10 -0500 Subject: [PATCH 52/59] Don't explode if HKCU\Console is write-protected (#15916) I manually changed the permissions on `HKCU\Console` to deny "Create subkey" to myself. Then confirmed that it explodes before this change, and not after this change. Closes #15458 --- src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp b/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp index 7ab0a0b15dc..754ed7cbd82 100644 --- a/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp +++ b/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp @@ -99,7 +99,9 @@ bool DefaultTerminal::HasCurrent() void DefaultTerminal::Current(const Model::DefaultTerminal& term) { - THROW_IF_FAILED(DelegationConfig::s_SetDefaultByPackage(winrt::get_self(term)->_pkg)); + // Just log if we fail to write the defterm configuration. It's not worth + // exploding over if the regkey is write-protected or something. + LOG_IF_FAILED(DelegationConfig::s_SetDefaultByPackage(winrt::get_self(term)->_pkg)); TraceLoggingWrite(g_hSettingsModelProvider, "DefaultTerminalChanged", From 2f41d23d6daaf3da8cd3770bf4f295a424fe47b5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 6 Sep 2023 08:51:06 -0500 Subject: [PATCH 53/59] Update close button visibility based on BOTH settings and readonly mode (#15914) `TerminalTab::_RecalculateAndApplyReadOnly` didn't know about whether a tab should be closable or not, based on the theme settings. Similarly (though, unreported), the theme update in `TerminalPage::_updateAllTabCloseButtons` didn't really know about readonly mode. This fixes both these issues by moving responsibility for the tab close button visibility into `TabBase` itself. Closes #15902 --- src/cascadia/TerminalApp/TabBase.cpp | 63 ++++++++++++++++++++-- src/cascadia/TerminalApp/TabBase.h | 9 +++- src/cascadia/TerminalApp/TabBase.idl | 2 +- src/cascadia/TerminalApp/TabManagement.cpp | 4 +- src/cascadia/TerminalApp/TerminalPage.cpp | 33 +++--------- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/TerminalApp/TerminalTab.cpp | 6 +-- 7 files changed, 80 insertions(+), 39 deletions(-) diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp index 2d7293ccd6d..5077067762a 100644 --- a/src/cascadia/TerminalApp/TabBase.cpp +++ b/src/cascadia/TerminalApp/TabBase.cpp @@ -25,10 +25,6 @@ namespace winrt namespace winrt::TerminalApp::implementation { - WUX::FocusState TabBase::FocusState() const noexcept - { - return _focusState; - } // Method Description: // - Prepares this tab for being removed from the UI hierarchy @@ -590,4 +586,63 @@ namespace winrt::TerminalApp::implementation VisualStateManager::GoToState(item, L"Normal", true); } } + + TabCloseButtonVisibility TabBase::CloseButtonVisibility() + { + return _closeButtonVisibility; + } + + // Method Description: + // - set our internal state to track if we were requested to have a visible + // tab close button or not. + // - This is called every time the active tab changes. That way, the changes + // in focused tab can be reflected for the "ActiveOnly" state. + void TabBase::CloseButtonVisibility(TabCloseButtonVisibility visibility) + { + _closeButtonVisibility = visibility; + _updateIsClosable(); + } + + // Method Description: + // - Update our close button's visibility, to reflect both the ReadOnly + // state of the tab content, and also if if we were told to have a visible + // close button at all. + // - the tab being read-only takes precedence. That will always suppress + // the close button. + // - Otherwise we'll use the state set in CloseButtonVisibility to control + // the tab's visibility. + void TabBase::_updateIsClosable() + { + bool isClosable = true; + + if (ReadOnly()) + { + isClosable = false; + } + else + { + switch (_closeButtonVisibility) + { + case TabCloseButtonVisibility::Never: + isClosable = false; + break; + case TabCloseButtonVisibility::Hover: + isClosable = true; + break; + case TabCloseButtonVisibility::ActiveOnly: + isClosable = _focused(); + break; + default: + isClosable = true; + break; + } + } + TabViewItem().IsClosable(isClosable); + } + + bool TabBase::_focused() const noexcept + { + return _focusState != FocusState::Unfocused; + } + } diff --git a/src/cascadia/TerminalApp/TabBase.h b/src/cascadia/TerminalApp/TabBase.h index 24ac6d2da6f..108b30a502c 100644 --- a/src/cascadia/TerminalApp/TabBase.h +++ b/src/cascadia/TerminalApp/TabBase.h @@ -16,7 +16,6 @@ namespace winrt::TerminalApp::implementation { public: virtual void Focus(winrt::Windows::UI::Xaml::FocusState focusState) = 0; - winrt::Windows::UI::Xaml::FocusState FocusState() const noexcept; virtual void Shutdown(); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); @@ -30,6 +29,9 @@ namespace winrt::TerminalApp::implementation const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& unfocused, const til::color& tabRowColor); + Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility CloseButtonVisibility(); + void CloseButtonVisibility(Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility visible); + WINRT_CALLBACK(RequestFocusActiveControl, winrt::delegate); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); @@ -60,6 +62,8 @@ namespace winrt::TerminalApp::implementation winrt::Microsoft::Terminal::Settings::Model::ThemeColor _unfocusedThemeColor{ nullptr }; til::color _tabRowColor; + Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility _closeButtonVisibility{ Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always }; + virtual void _CreateContextMenu(); virtual winrt::hstring _CreateToolTipTitle(); @@ -78,6 +82,9 @@ namespace winrt::TerminalApp::implementation void _RefreshVisualState(); virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() = 0; + bool _focused() const noexcept; + void _updateIsClosable(); + friend class ::TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/TerminalApp/TabBase.idl b/src/cascadia/TerminalApp/TabBase.idl index a8406a5c51e..20ee8ffa9d0 100644 --- a/src/cascadia/TerminalApp/TabBase.idl +++ b/src/cascadia/TerminalApp/TabBase.idl @@ -9,10 +9,10 @@ namespace TerminalApp String Title { get; }; String Icon { get; }; Boolean ReadOnly { get; }; + Microsoft.Terminal.Settings.Model.TabCloseButtonVisibility CloseButtonVisibility { get; set; }; Microsoft.UI.Xaml.Controls.TabViewItem TabViewItem { get; }; Windows.UI.Xaml.FrameworkElement Content { get; }; - Windows.UI.Xaml.FocusState FocusState { get; }; UInt32 TabViewIndex; UInt32 TabViewNumTabs; diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 0d8bcbee26f..02bd8940746 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -936,7 +936,7 @@ namespace winrt::TerminalApp::implementation { tab.Focus(FocusState::Programmatic); _UpdateMRUTab(tab); - _updateAllTabCloseButtons(tab); + _updateAllTabCloseButtons(); } tab.TabViewItem().StartBringIntoView(); @@ -1114,7 +1114,7 @@ namespace winrt::TerminalApp::implementation { tab.Focus(FocusState::Programmatic); _UpdateMRUTab(tab); - _updateAllTabCloseButtons(tab); + _updateAllTabCloseButtons(); } } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 024ae18c10a..b96111994d9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -3283,44 +3283,23 @@ namespace winrt::TerminalApp::implementation // Begin Theme handling _updateThemeColors(); - _updateAllTabCloseButtons(_GetFocusedTab()); + _updateAllTabCloseButtons(); } - void TerminalPage::_updateAllTabCloseButtons(const winrt::TerminalApp::TabBase& focusedTab) + void TerminalPage::_updateAllTabCloseButtons() { // Update the state of the CloseButtonOverlayMode property of // our TabView, to match the tab.showCloseButton property in the theme. // // Also update every tab's individual IsClosable to match the same property. const auto theme = _settings.GlobalSettings().CurrentTheme(); - const auto visibility = theme && theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always; + const auto visibility = (theme && theme.Tab()) ? + theme.Tab().ShowCloseButton() : + Settings::Model::TabCloseButtonVisibility::Always; for (const auto& tab : _tabs) { - switch (visibility) - { - case Settings::Model::TabCloseButtonVisibility::Never: - tab.TabViewItem().IsClosable(false); - break; - case Settings::Model::TabCloseButtonVisibility::Hover: - tab.TabViewItem().IsClosable(true); - break; - case Settings::Model::TabCloseButtonVisibility::ActiveOnly: - { - if (focusedTab && focusedTab == tab) - { - tab.TabViewItem().IsClosable(true); - } - else - { - tab.TabViewItem().IsClosable(false); - } - break; - } - default: - tab.TabViewItem().IsClosable(true); - break; - } + tab.CloseButtonVisibility(visibility); } switch (visibility) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index fd63f7cc674..cb82efeaa8a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -511,7 +511,7 @@ namespace winrt::TerminalApp::implementation static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); void _updateThemeColors(); - void _updateAllTabCloseButtons(const winrt::TerminalApp::TabBase& focusedTab); + void _updateAllTabCloseButtons(); void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); winrt::fire_and_forget _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 1b881029b56..864ff6aad4e 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -223,7 +223,7 @@ namespace winrt::TerminalApp::implementation _focusState = focusState; - if (_focusState != FocusState::Unfocused) + if (_focused()) { auto lastFocusedControl = GetActiveTerminalControl(); if (lastFocusedControl) @@ -961,7 +961,7 @@ namespace winrt::TerminalApp::implementation co_await wil::resume_foreground(dispatcher); if (const auto tab{ weakThis.get() }) { - if (tab->_focusState != FocusState::Unfocused) + if (tab->_focused()) { if (const auto termControl{ sender.try_as() }) { @@ -1693,7 +1693,7 @@ namespace winrt::TerminalApp::implementation } ReadOnly(_rootPane->ContainsReadOnly()); - TabViewItem().IsClosable(!ReadOnly()); + _updateIsClosable(); } std::shared_ptr TerminalTab::GetActivePane() const From 3830c62a814e0d55d8899243d0fd48891cc2d93f Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 6 Sep 2023 15:19:03 -0500 Subject: [PATCH 54/59] Update roadmap for 2023 (#15931) Closes #15661 --- README.md | 2 +- doc/roadmap-2022.md | 6 ++++ doc/roadmap-2023.md | 76 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 doc/roadmap-2023.md diff --git a/README.md b/README.md index f0e06e1d145..b174aeaa5a9 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ repository. ## Windows Terminal Roadmap -The plan for the Windows Terminal [is described here](/doc/roadmap-2022.md) and +The plan for the Windows Terminal [is described here](/doc/roadmap-2023.md) and will be updated as the project proceeds. ## Project Build Status diff --git a/doc/roadmap-2022.md b/doc/roadmap-2022.md index eab75a0e9b2..5c4d5bf7e70 100644 --- a/doc/roadmap-2022.md +++ b/doc/roadmap-2022.md @@ -1,5 +1,9 @@ # Terminal 2022 Roadmap +> **NOTE** +> +> This document has been superseded by the [Terminal 2023 Roadmap]. Please refer to that document for the updated roadmap. + ## Overview This document outlines the roadmap of features we're planning for the Windows Terminal during 2022. This serves as a successor to the [Terminal v2 Roadmap], to reflect changes to our planning going forward. @@ -126,3 +130,5 @@ Incoming issues/asks/etc. are triaged several times a week, labeled appropriatel [Windows Terminal Preview 1.12 Release]: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-12-release/ [Windows Terminal Preview 1.13 Release]: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-13-release/ [Windows Terminal Preview 1.14 Release]: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-14-release/ + +[Terminal 2023 Roadmap]: https://github.com/microsoft/terminal/tree/main/doc/roadmap-2023.md diff --git a/doc/roadmap-2023.md b/doc/roadmap-2023.md new file mode 100644 index 00000000000..c27faa340e0 --- /dev/null +++ b/doc/roadmap-2023.md @@ -0,0 +1,76 @@ +# Terminal 2023 Roadmap + +## Overview + +This document outlines the roadmap of features we're planning for the Windows Terminal during 2023. This serves as a successor to the [2022 Roadmap], to reflect changes to our planning going forward. + +## Release cadence + +We've settled on a roughly quarterly release cycle - about once every three months. In May we released [Terminal 1.18]. We're targeting 1.19 for sometime in late September, and 1.20 likely in early January 2024. (These timelines are rough estimates, not strict rules. For example, 1.18's release was pushed back slightly to better align with Build 2023.) + +New features will go into [Windows Terminal Preview](https://aka.ms/terminal-preview) first. Typically, one release after they've been in Preview, those features will move into [Windows Terminal](https://aka.ms/terminal) ("Terminal Stable"). In the case of some more risky or experimental features, we might hold them to only Preview builds for an extended period[^1]. + + +| Quarter | Date | Release Version | Preview Release Blog Post | +| --------|------------|---------------- | ------------------------- | +| CY23 Q1 | 2023-01-24 | [Terminal 1.17] | [Windows Terminal Preview 1.17 Release] | +| CY23 Q2 | 2023-05-23 | [Terminal 1.18] | [Windows Terminal Preview 1.18 Release] | +| CY23 Q3 | | [Terminal 1.19] | [Windows Terminal Preview 1.19 Release] | +| CY23 Q4 | | [Terminal 1.20] | [Windows Terminal Preview 1.20 Release] | + +Within a single milestone, we typically reserve the last month as "bake time", to polish off bugfixes and get the release ready to ship. In this last month, we'll likely slow down our ingestion of community PRs just to stabilize what's already in `main`. For example, a given release might look like: + +```mermaid +gantt + title Proposed Terminal Releases 1.14-1.18 + dateFormat YYYY-MM-DD + axisFormat %d %b + section Terminal 1.18 + Lock down & bake :l18, 2023-05-09 , 2w + Release 1.18 :milestone, 2023-05-23, 0 + 1.18 becomes Stable :milestone, after l19, 0 + section Terminal 1.19 + Features :f19, after l18, 10w + Bugfix :b19, after f19 , 4w + Lock down & bake :l19, after b19 , 2w + Release 1.19 :milestone, after l19, 0 +``` + +_informative, not normative_ + +## Up next in the Terminal + +### Terminal 1.19 + +* Canary builds. Nightly builds of the Terminal from `main`. More unstable, but quicker access to experimental features. +* Terminal AI. While this will only be shipping in Canary builds to begin with, the v0 implementation will be available roughly at the same time as 1.19. +* The Suggestions UI. This is the starting point for shell completions [#3121], tasks [#1595], and probably Terminal AI at some point too. +* Unicode input for `cmd.exe` (and any other console app using "cooked reads"). See [#15567] +* Miscellaneous performance improvements. Conhost should be a _lot_ faster now. +* Broadcast input mode, for sending text to multiple panes at once. + +## Team member "north stars" + +For a more fluid take on what each of the team's personal goals are, head on over to [Core team North Stars]. This has a list of more long-term goals that each team member is working towards, but not things that are necessarily committed work. + + +[^1]: A conclusive list of these features can be found at https://github.com/microsoft/terminal/blob/main/src/features.xml. Note that this is a raw XML doc used to light up specific parts of the codebase, and not something authored for human consumption. + +[2022 Roadmap]: https://github.com/microsoft/terminal/tree/main/doc/roadmap-2022.md + +[Terminal 1.17]: https://github.com/microsoft/terminal/releases/tag/v1.17.1023 +[Terminal 1.18]: https://github.com/microsoft/terminal/releases/tag/v1.18.1462.0 +[Windows Terminal Preview 1.17 Release]: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-17-release/ +[Windows Terminal Preview 1.18 Release]: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-18-release/ + +[@DHowett]: https://github.com/DHowett +[@zadjii-msft]: https://github.com/zadjii-msft +[@lhecker]: https://github.com/lhecker +[@carlos-zamora]: https://github.com/carlos-zamora +[@PankajBhojwani]: https://github.com/PankajBhojwani + +[Core team North Stars]: https://github.com/microsoft/terminal/wiki/Core-team-North-Stars + +[#1595]: https://github.com/microsoft/terminal/issues/1595 +[#3121]: https://github.com/microsoft/terminal/issues/3121 +[#15567]: https://github.com/microsoft/terminal/issues/15567 From 2f7f759af460a0b753ad82a45b2b8205e8404c23 Mon Sep 17 00:00:00 2001 From: Jaswir Date: Thu, 7 Sep 2023 23:19:16 +0200 Subject: [PATCH 55/59] Enable unfocused acrylic (#15923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Closes #7158 Enabling Acrylic as both an appearance setting (with all the plumbing), allowing it to be set differently in both focused and unfocused terminals. EnableUnfocusedAcrylic Global Setting that controls if unfocused acrylic is possible so that people can disable that behavior. ## References and Relevant Issues #7158 , references: #15913 , #11092 ## Detailed Description of the Pull Request / Additional comments ### Allowing Acrylic to be set differently in both focused and unfocused terminals: #### A ![A](https://github.com/microsoft/terminal/assets/15957528/c43965f2-f458-46ae-af1c-a2107dac981a) #### B ![B](https://github.com/microsoft/terminal/assets/15957528/e84ef1a8-8f4c-467a-99c2-9427061b3e3e) #### C ![C](https://github.com/microsoft/terminal/assets/15957528/63fd35ba-a55a-4c1a-8b1c-5b571aa902ed) #### D ![D](https://github.com/microsoft/terminal/assets/15957528/05108166-1c6e-406e-aec0-80023fc3f57c) ``` json "profiles": { "list": [ { "commandline": "pwsh.exe", "name": "A", "unfocusedAppearance": { "useAcrylic": true, }, "useAcrylic": true, }, { "commandline": "pwsh.exe", "name": "B", "unfocusedAppearance": { "useAcrylic": false, }, "useAcrylic": true, }, { "commandline": "pwsh.exe", "name": "C", "unfocusedAppearance": { "useAcrylic": true, }, "useAcrylic": false, }, { "commandline": "pwsh.exe", "name": "D", "unfocusedAppearance": { }, "useAcrylic": false, }, ] } ``` - **A**: AcrylicBlur always on - **B**: Acrylic when focused, not acrylic when unfocused - **C**: Why the hell not. Not Acrylic when focused, Acrylic when unfocused. - **D:** Possible today by not using Acrylic. ### EnableUnfocusedACrylic global setting that controls if unfocused acrylic is possible So that people can disable that behavior: ![256926990-3c42d99a-67de-4145-bf41-ce3995035136](https://github.com/microsoft/terminal/assets/15957528/eef62c14-d2bd-4737-b86e-dcb3588eb8f7) ### Alternate approaches I considered: Using `_InitializeBackgroundBrush` call instead of `_changeBackgroundColor(bg) in ``TermControl::_UpdateAppearanceFromUIThread`. Comments in this function mentioned: ``` *.cs' // In the future, this might need to be changed to a // _InitializeBackgroundBrush call instead, because we may need to // switch from a solid color brush to an acrylic one. ``` I considered using this to tackle to problem, but don't see the benefit. The only time we need to update the brush is when the user changes the `EnableUnfocusedAcrylic ` setting which is already covered by `fire_and_forget TermControl::UpdateControlSettings` ### Supporting different Opacity in Focused and Unfocused Appearance??? This PR is split up in two parts #7158 covers allowing Acrylic to be set differently in both focused and unfocused terminals. And EnableUnfocusedAcrylic Global Setting that controls if unfocused acrylic is possible so that people can disable that behavior. #11092 will be about enabling opacity as both an appearance setting, allowing it to be set differently in both focused and unfocused terminals. ### Skipping the XAML for now: “I actually think we may want to skip the XAML on this one for now. We've been having some discussions about compatibility settings, global settings, stuff like this, and it might be _more- confusing to have you do something here. We can always add it in post when we decide where to put it.” -- Mike Griese ## Validation Steps Performed #### When Scrolling Mouse , opacity changes appropriately, on opacity 100 there are no gray lines or artefacts ![edgecase_scrollwheel](https://github.com/microsoft/terminal/assets/15957528/29a1b11e-05b8-4626-abd2-4f084ae94a8d) ![image](https://github.com/microsoft/terminal/assets/15957528/c05ea435-8867-4804-bcdc-2074df08cec1) #### When Adjusting Opacity through command palette, opacity changes appropriately, on opacity 100 there are no gray lines or artefacts ![edgecase_adjustopacity](https://github.com/microsoft/terminal/assets/15957528/a59b4d6d-f12e-48da-96bb-3eb333ac4637) ![image](https://github.com/microsoft/terminal/assets/15957528/c05ea435-8867-4804-bcdc-2074df08cec1) #### When opening command palette state goes to unfocused, the acrylic and color change appropriately ![edge_case_command_palette](https://github.com/microsoft/terminal/assets/15957528/ec0cd8b5-676e-4235-8231-a10a5689c0b8) ![image](https://github.com/microsoft/terminal/assets/15957528/4300df70-f64b-4001-8731-b3b69471ea78) #### Stumbled upon a new bug when performing validation steps #15913 ![264637964-494d4417-6a35-450a-89f7-52085ef9b546](https://github.com/microsoft/terminal/assets/15957528/fee59c4a-400b-4e40-912b-ea8c638fc979) ## PR Checklist - [x] Closes #7158 - [X] Tests added/passed - [X] Documentation updated - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx - [x] Schema updated (if necessary) --------- Co-authored-by: Mike Griese --- doc/cascadia/profiles.schema.json | 9 +++++++++ src/cascadia/TerminalControl/ControlCore.cpp | 19 ++++++++++++++++++- .../TerminalControl/IControlAppearance.idl | 1 + .../TerminalControl/IControlSettings.idl | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 10 +++++++++- .../TerminalSettingsEditor/ProfileViewModel.h | 2 +- .../GlobalAppSettings.idl | 1 + .../IAppearanceConfig.idl | 1 + .../TerminalSettingsModel/MTSMSettings.h | 5 +++-- .../TerminalSettingsModel/Profile.idl | 1 - .../TerminalSettings.cpp | 3 ++- .../TerminalSettingsModel/TerminalSettings.h | 1 + src/cascadia/inc/ControlProperties.h | 3 ++- 13 files changed, 49 insertions(+), 9 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 50a8a6a854f..d43a9e4e7d8 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -269,6 +269,10 @@ "experimental.pixelShaderPath": { "description": "Use to set a path to a pixel shader to use with the Terminal when unfocused. Overrides `experimental.retroTerminalEffect`. This is an experimental feature, and its continued existence is not guaranteed.", "type": "string" + }, + "useAcrylic":{ + "description": "When set to true, the window will have an acrylic material background when unfocused. When set to false, the window will have a plain, untextured background when unfocused.", + "type": "boolean" } }, "type": "object" @@ -2173,6 +2177,11 @@ "description": "When set to true, tabs are always displayed. When set to false and \"showTabsInTitlebar\" is set to false, tabs only appear after opening a new tab.", "type": "boolean" }, + "compatibility.enableUnfocusedAcrylic":{ + "default": true, + "description": "When set to true, unfocused windows can have acrylic instead of opaque.", + "type": "boolean" + }, "centerOnLaunch": { "default": false, "description": "When set to `true`, the terminal window will auto-center itself on the display it opens on. The terminal will use the \"initialPosition\" to determine which display to open on.", diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 7530909aaab..52dc7d4087f 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -868,7 +868,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderEngine->SetSelectionBackground(til::color{ newAppearance->SelectionBackground() }); _renderEngine->SetRetroTerminalEffect(newAppearance->RetroTerminalEffect()); _renderEngine->SetPixelShaderPath(newAppearance->PixelShaderPath()); - _renderer->TriggerRedrawAll(); + + // No need to update Acrylic if UnfocusedAcrylic is disabled + if (_settings->EnableUnfocusedAcrylic()) + { + // Manually turn off acrylic if they turn off transparency. + _runtimeUseAcrylic = Opacity() < 1.0 && newAppearance->UseAcrylic(); + + // Update the renderer as well. It might need to fall back from + // cleartype -> grayscale if the BG is transparent / acrylic. + _renderEngine->EnableTransparentBackground(_isBackgroundTransparent()); + _renderer->NotifyPaintFrame(); + + auto eventArgs = winrt::make_self(Opacity()); + + _TransparencyChangedHandlers(*this, *eventArgs); + } + + _renderer->TriggerRedrawAll(true, true); } } diff --git a/src/cascadia/TerminalControl/IControlAppearance.idl b/src/cascadia/TerminalControl/IControlAppearance.idl index 96c3db75a11..124eb641ba1 100644 --- a/src/cascadia/TerminalControl/IControlAppearance.idl +++ b/src/cascadia/TerminalControl/IControlAppearance.idl @@ -13,6 +13,7 @@ namespace Microsoft.Terminal.Control Windows.UI.Xaml.VerticalAlignment BackgroundImageVerticalAlignment { get; }; // IntenseIsBold and IntenseIsBright are in Core Appearance Double Opacity { get; }; + Boolean UseAcrylic { get; }; // Experimental settings Boolean RetroTerminalEffect { get; }; diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index f03981f64cf..a64dd4f7987 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -31,7 +31,7 @@ namespace Microsoft.Terminal.Control String ProfileName; String ProfileSource; - Boolean UseAcrylic { get; }; + Boolean EnableUnfocusedAcrylic; ScrollbarState ScrollState { get; }; Boolean UseAtlasEngine { get; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index fedd24f8e14..d26adcaead6 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -733,7 +733,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (acrylic == nullptr) { acrylic = Media::AcrylicBrush{}; - acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + + if (_core.Settings().EnableUnfocusedAcrylic()) + { + acrylic.BackgroundSource(Media::AcrylicBackgroundSource::Backdrop); + } + else + { + acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + } } // see GH#1082: Initialize background color so we don't get a diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 0946d9146ad..e752c8cffca 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -87,7 +87,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_profile, TabTitle); OBSERVABLE_PROJECTED_SETTING(_profile, TabColor); OBSERVABLE_PROJECTED_SETTING(_profile, SuppressApplicationTitle); - OBSERVABLE_PROJECTED_SETTING(_profile, UseAcrylic); OBSERVABLE_PROJECTED_SETTING(_profile, ScrollState); OBSERVABLE_PROJECTED_SETTING(_profile, Padding); OBSERVABLE_PROJECTED_SETTING(_profile, Commandline); @@ -98,6 +97,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_profile.DefaultAppearance(), SelectionBackground); OBSERVABLE_PROJECTED_SETTING(_profile.DefaultAppearance(), CursorColor); OBSERVABLE_PROJECTED_SETTING(_profile.DefaultAppearance(), Opacity); + OBSERVABLE_PROJECTED_SETTING(_profile.DefaultAppearance(), UseAcrylic); OBSERVABLE_PROJECTED_SETTING(_profile, HistorySize); OBSERVABLE_PROJECTED_SETTING(_profile, SnapOnInput); OBSERVABLE_PROJECTED_SETTING(_profile, AltGrAliasing); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 02b3788b1ed..a1847408fdf 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -99,6 +99,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(IVector, NewTabMenu); INHERITABLE_SETTING(Boolean, EnableColorSelection); INHERITABLE_SETTING(Boolean, EnableShellCompletionMenu); + INHERITABLE_SETTING(Boolean, EnableUnfocusedAcrylic); INHERITABLE_SETTING(Boolean, IsolatedMode); INHERITABLE_SETTING(Boolean, AllowHeadless); INHERITABLE_SETTING(String, SearchWebDefaultQueryUrl); diff --git a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl index 6352de64b08..5161a6716bb 100644 --- a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl +++ b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl @@ -54,5 +54,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_APPEARANCE_SETTING(IntenseStyle, IntenseTextStyle); INHERITABLE_APPEARANCE_SETTING(Microsoft.Terminal.Core.AdjustTextMode, AdjustIndistinguishableColors); INHERITABLE_APPEARANCE_SETTING(Double, Opacity); + INHERITABLE_APPEARANCE_SETTING(Boolean, UseAcrylic); }; } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 6276c3d1354..6c7b08649f9 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -64,6 +64,7 @@ Author(s): X(bool, TrimPaste, "trimPaste", true) \ X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \ X(bool, EnableShellCompletionMenu, "experimental.enableShellCompletionMenu", false) \ + X(bool, EnableUnfocusedAcrylic, "compatibility.enableUnfocusedAcrylic", true) \ X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) \ X(bool, AllowHeadless, "compatibility.allowHeadless", false) \ X(bool, IsolatedMode, "compatibility.isolatedMode", false) \ @@ -79,7 +80,6 @@ Author(s): X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \ X(bool, SnapOnInput, "snapOnInput", true) \ X(bool, AltGrAliasing, "altGrAliasing", true) \ - X(bool, UseAcrylic, "useAcrylic", false) \ X(hstring, Commandline, "commandline", L"%SystemRoot%\\System32\\cmd.exe") \ X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible) \ X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ @@ -128,7 +128,8 @@ Author(s): X(ConvergedAlignment, BackgroundImageAlignment, "backgroundImageAlignment", ConvergedAlignment::Horizontal_Center | ConvergedAlignment::Vertical_Center) \ X(hstring, BackgroundImagePath, "backgroundImage") \ X(Model::IntenseStyle, IntenseTextStyle, "intenseTextStyle", Model::IntenseStyle::Bright) \ - X(Core::AdjustTextMode, AdjustIndistinguishableColors, "adjustIndistinguishableColors", Core::AdjustTextMode::Never) + X(Core::AdjustTextMode, AdjustIndistinguishableColors, "adjustIndistinguishableColors", Core::AdjustTextMode::Never) \ + X(bool, UseAcrylic, "useAcrylic", false) // Intentionally omitted Appearance settings: // * ForegroundKey, BackgroundKey, SelectionBackgroundKey, CursorColorKey: all optional colors diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 3cfaf96aff9..86bce9beeb4 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -63,7 +63,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(String, TabTitle); INHERITABLE_PROFILE_SETTING(Windows.Foundation.IReference, TabColor); INHERITABLE_PROFILE_SETTING(Boolean, SuppressApplicationTitle); - INHERITABLE_PROFILE_SETTING(Boolean, UseAcrylic); INHERITABLE_PROFILE_SETTING(Microsoft.Terminal.Control.ScrollbarState, ScrollState); INHERITABLE_PROFILE_SETTING(String, Padding); INHERITABLE_PROFILE_SETTING(String, Commandline); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index c594d42fb3b..35881c8c715 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -257,6 +257,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _AdjustIndistinguishableColors = appearance.AdjustIndistinguishableColors(); _Opacity = appearance.Opacity(); + _UseAcrylic = appearance.UseAcrylic(); } // Method Description: @@ -276,7 +277,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Fill in the remaining properties from the profile _ProfileName = profile.Name(); _ProfileSource = profile.Source(); - _UseAcrylic = profile.UseAcrylic(); const auto fontInfo = profile.FontInfo(); _FontFace = fontInfo.FontFace(); @@ -356,6 +356,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _ForceVTInput = globalSettings.ForceVTInput(); _TrimBlockSelection = globalSettings.TrimBlockSelection(); _DetectURLs = globalSettings.DetectURLs(); + _EnableUnfocusedAcrylic = globalSettings.EnableUnfocusedAcrylic(); } // Method Description: diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index fe74c26d9d6..daa895518e8 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -118,6 +118,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, hstring, ProfileName); INHERITABLE_SETTING(Model::TerminalSettings, hstring, ProfileSource); + INHERITABLE_SETTING(Model::TerminalSettings, bool, EnableUnfocusedAcrylic, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAcrylic, false); INHERITABLE_SETTING(Model::TerminalSettings, double, Opacity, UseAcrylic() ? 0.5 : 1.0); INHERITABLE_SETTING(Model::TerminalSettings, hstring, Padding, DEFAULT_PADDING); diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 1d87cc57165..c18e70e7cb2 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -20,6 +20,7 @@ #define CONTROL_APPEARANCE_SETTINGS(X) \ X(til::color, SelectionBackground, DEFAULT_FOREGROUND) \ X(double, Opacity, 1.0) \ + X(bool, UseAcrylic, false) \ X(winrt::hstring, BackgroundImage) \ X(double, BackgroundImageOpacity, 1.0) \ X(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill) \ @@ -55,7 +56,7 @@ #define CONTROL_SETTINGS(X) \ X(winrt::hstring, ProfileName) \ X(winrt::hstring, ProfileSource) \ - X(bool, UseAcrylic, false) \ + X(bool, EnableUnfocusedAcrylic, false) \ X(winrt::hstring, Padding, DEFAULT_PADDING) \ X(winrt::hstring, FontFace, L"Consolas") \ X(float, FontSize, DEFAULT_FONT_SIZE) \ From 5d300b20ed3c057872399c2766b6423c023d66e6 Mon Sep 17 00:00:00 2001 From: inisarg Date: Fri, 8 Sep 2023 03:00:24 +0530 Subject: [PATCH 56/59] Allows negative values in launch parameters (#15941) Added a style that allows negative values in the launch position parameters. ## PR Checklist - [x] Closes #15832 --- src/cascadia/TerminalSettingsEditor/Launch.xaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index a76d78cacec..71d0827e2b3 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -30,6 +30,14 @@ + @@ -242,7 +250,7 @@ Grid.Column="0" Width="118" IsEnabled="{x:Bind local:Converters.InvertBoolean(ViewModel.UseDefaultLaunchPosition), Mode=OneWay}" - Style="{StaticResource LaunchSizeNumberBoxStyle}" + Style="{StaticResource LaunchPositionNumberBoxStyle}" Value="{x:Bind ViewModel.InitialPosX, Mode=TwoWay}" /> Date: Sat, 9 Sep 2023 00:08:05 +0530 Subject: [PATCH 57/59] Add support for underline style and color in VT (#15795) Underline color sequence _SGR 58_ (unlike *SGR 38*, *SGR 48*) only works with sub parameters, eg. `\e[58:5:m` or `\e[58:2::::m` will work, but something like `\e[58;5;m` won't work. This is a requirement for the implementation to avoid problems with VT clients that don't support sub parameters. ## Detailed Description - Added `underlineColor` to `TextAttribute`, and `UnderlineStyle` into `CharacterAttributes`. - Added two new entries in `GraphicOptions` namely, `UnderlineColor` (58) and `UnderlineColorDefault` (59). - _SGR 58_ renders a sequence with sub parameters in the VT renderer. - _SGR 4:x_ renders a sequence with sub parameters in the VT renderer, except for single, double, and no-underline, which still use backward-compatible _SGR 4_, _SGR 21_ and _SGR 24_. - `XtermEngine` will send `\e[4m` without any styling information. This means all underline style (except NoUnderline) will be rendered as single underline. ## Reference issues - #7228 ### PR Checklist - [x] update DECRARA, DECCARA to respect underline color and style. - [x] update DECRQSS to send underline color and style in the query response. - [x] update DECRQPSR/DECRSPS/DECCIR - [x] Tests added --- src/buffer/out/OutputCellIterator.cpp | 2 +- src/buffer/out/TextAttribute.cpp | 57 ++++-- src/buffer/out/TextAttribute.hpp | 45 ++++- src/cascadia/TerminalCore/Terminal.cpp | 2 +- src/host/ut_host/OutputCellIteratorTests.cpp | 2 +- src/host/ut_host/ScreenBufferTests.cpp | 129 ++++++------ src/host/ut_host/VtRendererTests.cpp | 191 ++++++++++++++++-- src/inc/conattrs.hpp | 13 +- .../UiaTextRangeTests.cpp | 18 +- src/renderer/base/renderer.cpp | 19 +- src/renderer/vt/VtSequences.cpp | 72 ++++++- src/renderer/vt/Xterm256Engine.cpp | 36 ++-- src/renderer/vt/XtermEngine.cpp | 2 +- src/renderer/vt/paint.cpp | 29 ++- src/renderer/vt/state.cpp | 2 +- src/renderer/vt/vtrenderer.hpp | 6 +- src/terminal/adapter/DispatchTypes.hpp | 4 +- src/terminal/adapter/adaptDispatch.cpp | 50 +++-- src/terminal/adapter/adaptDispatch.hpp | 2 + .../adapter/adaptDispatchGraphics.cpp | 93 ++++++--- .../adapter/ut_adapter/adapterTest.cpp | 133 ++++++++++-- src/types/UiaTextRangeBase.cpp | 31 +-- src/types/sgrStack.cpp | 60 +++++- 23 files changed, 758 insertions(+), 240 deletions(-) diff --git a/src/buffer/out/OutputCellIterator.cpp b/src/buffer/out/OutputCellIterator.cpp index 6e332ee6d15..a623350f031 100644 --- a/src/buffer/out/OutputCellIterator.cpp +++ b/src/buffer/out/OutputCellIterator.cpp @@ -11,7 +11,7 @@ #include "../../types/inc/GlyphWidth.hpp" #include "../../inc/conattrs.hpp" -static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; +static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR, INVALID_COLOR }; // Routine Description: // - This is a fill-mode iterator for one particular wchar. It will repeat forever if fillLimit is 0. diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 55e3b15a5ea..d5b0e5e4a5a 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -7,7 +7,7 @@ // Keeping TextColor compact helps us keeping TextAttribute compact, // which in turn ensures that our buffer memory usage is low. -static_assert(sizeof(TextAttribute) == 12); +static_assert(sizeof(TextAttribute) == 16); static_assert(alignof(TextAttribute) == 2); // Ensure that we can memcpy() and memmove() the struct for performance. static_assert(std::is_trivially_copyable_v); @@ -156,6 +156,25 @@ uint16_t TextAttribute::GetHyperlinkId() const noexcept return _hyperlinkId; } +TextColor TextAttribute::GetUnderlineColor() const noexcept +{ + return _underlineColor; +} + +// Method description: +// - Retrieves the underline style of the text. +// - If the attribute is not the **current** attribute of the text buffer, +// (eg. reading an attribute from another part of the text buffer, which +// was modified using DECRARA), this might return an invalid style. In this +// case, treat the style as singly underlined. +// Return value: +// - The underline style. +UnderlineStyle TextAttribute::GetUnderlineStyle() const noexcept +{ + const auto styleAttr = WI_EnumValue(_attrs & CharacterAttributes::UnderlineStyle); + return static_cast(styleAttr >> UNDERLINE_STYLE_SHIFT); +} + void TextAttribute::SetForeground(const TextColor foreground) noexcept { _foreground = foreground; @@ -166,6 +185,13 @@ void TextAttribute::SetBackground(const TextColor background) noexcept _background = background; } +void TextAttribute::SetUnderlineColor(const TextColor color) noexcept +{ + // Index16 colors are not supported for underline colors. + assert(!color.IsIndex16()); + _underlineColor = color; +} + void TextAttribute::SetForeground(const COLORREF rgbForeground) noexcept { _foreground = TextColor(rgbForeground); @@ -277,14 +303,12 @@ bool TextAttribute::IsCrossedOut() const noexcept return WI_IsFlagSet(_attrs, CharacterAttributes::CrossedOut); } +// Method description: +// - Returns true if the text is underlined with any underline style. bool TextAttribute::IsUnderlined() const noexcept { - return WI_IsFlagSet(_attrs, CharacterAttributes::Underlined); -} - -bool TextAttribute::IsDoublyUnderlined() const noexcept -{ - return WI_IsFlagSet(_attrs, CharacterAttributes::DoublyUnderlined); + const auto style = GetUnderlineStyle(); + return (style != UnderlineStyle::NoUnderline); } bool TextAttribute::IsOverlined() const noexcept @@ -332,14 +356,14 @@ void TextAttribute::SetCrossedOut(bool isCrossedOut) noexcept WI_UpdateFlag(_attrs, CharacterAttributes::CrossedOut, isCrossedOut); } -void TextAttribute::SetUnderlined(bool isUnderlined) noexcept -{ - WI_UpdateFlag(_attrs, CharacterAttributes::Underlined, isUnderlined); -} - -void TextAttribute::SetDoublyUnderlined(bool isDoublyUnderlined) noexcept +// Method description: +// - Sets underline style to singly, doubly, or one of the extended styles. +// Arguments: +// - style - underline style to set. +void TextAttribute::SetUnderlineStyle(const UnderlineStyle style) noexcept { - WI_UpdateFlag(_attrs, CharacterAttributes::DoublyUnderlined, isDoublyUnderlined); + const auto shiftedStyle = WI_EnumValue(style) << UNDERLINE_STYLE_SHIFT; + _attrs = (_attrs & ~CharacterAttributes::UnderlineStyle) | static_cast(shiftedStyle); } void TextAttribute::SetOverlined(bool isOverlined) noexcept @@ -374,6 +398,11 @@ void TextAttribute::SetDefaultBackground() noexcept _background = TextColor(); } +void TextAttribute::SetDefaultUnderlineColor() noexcept +{ + _underlineColor = TextColor{}; +} + // Method description: // - Resets only the rendition character attributes, which includes everything // except the Protected attribute. diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 2a61e8902de..4fe8c9e637e 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -27,6 +27,17 @@ Revision History: #include "WexTestClass.h" #endif +enum class UnderlineStyle +{ + NoUnderline = 0U, + SinglyUnderlined = 1U, + DoublyUnderlined = 2U, + CurlyUnderlined = 3U, + DottedUnderlined = 4U, + DashedUnderlined = 5U, + Max = DashedUnderlined +}; + class TextAttribute final { public: @@ -34,7 +45,8 @@ class TextAttribute final _attrs{ CharacterAttributes::Normal }, _foreground{}, _background{}, - _hyperlinkId{ 0 } + _hyperlinkId{ 0 }, + _underlineColor{} { } @@ -42,16 +54,28 @@ class TextAttribute final _attrs{ gsl::narrow_cast(wLegacyAttr & USED_META_ATTRS) }, _foreground{ gsl::at(s_legacyForegroundColorMap, wLegacyAttr & FG_ATTRS) }, _background{ gsl::at(s_legacyBackgroundColorMap, (wLegacyAttr & BG_ATTRS) >> 4) }, - _hyperlinkId{ 0 } + _hyperlinkId{ 0 }, + _underlineColor{} { } constexpr TextAttribute(const COLORREF rgbForeground, - const COLORREF rgbBackground) noexcept : + const COLORREF rgbBackground, + const COLORREF rgbUnderline = INVALID_COLOR) noexcept : _attrs{ CharacterAttributes::Normal }, _foreground{ rgbForeground }, _background{ rgbBackground }, - _hyperlinkId{ 0 } + _hyperlinkId{ 0 }, + _underlineColor{ rgbUnderline } + { + } + + constexpr TextAttribute(const CharacterAttributes attrs, const TextColor foreground, const TextColor background, const uint16_t hyperlinkId, const TextColor underlineColor) noexcept : + _attrs{ attrs }, + _foreground{ foreground }, + _background{ background }, + _hyperlinkId{ hyperlinkId }, + _underlineColor{ underlineColor } { } @@ -87,7 +111,6 @@ class TextAttribute final bool IsInvisible() const noexcept; bool IsCrossedOut() const noexcept; bool IsUnderlined() const noexcept; - bool IsDoublyUnderlined() const noexcept; bool IsOverlined() const noexcept; bool IsReverseVideo() const noexcept; bool IsProtected() const noexcept; @@ -98,8 +121,7 @@ class TextAttribute final void SetBlinking(bool isBlinking) noexcept; void SetInvisible(bool isInvisible) noexcept; void SetCrossedOut(bool isCrossedOut) noexcept; - void SetUnderlined(bool isUnderlined) noexcept; - void SetDoublyUnderlined(bool isDoublyUnderlined) noexcept; + void SetUnderlineStyle(const UnderlineStyle underlineStyle) noexcept; void SetOverlined(bool isOverlined) noexcept; void SetReverseVideo(bool isReversed) noexcept; void SetProtected(bool isProtected) noexcept; @@ -118,8 +140,11 @@ class TextAttribute final TextColor GetForeground() const noexcept; TextColor GetBackground() const noexcept; uint16_t GetHyperlinkId() const noexcept; + TextColor GetUnderlineColor() const noexcept; + UnderlineStyle GetUnderlineStyle() const noexcept; void SetForeground(const TextColor foreground) noexcept; void SetBackground(const TextColor background) noexcept; + void SetUnderlineColor(const TextColor color) noexcept; void SetForeground(const COLORREF rgbForeground) noexcept; void SetBackground(const COLORREF rgbBackground) noexcept; void SetIndexedForeground(const BYTE fgIndex) noexcept; @@ -131,6 +156,7 @@ class TextAttribute final void SetDefaultForeground() noexcept; void SetDefaultBackground() noexcept; + void SetDefaultUnderlineColor() noexcept; void SetDefaultRenditionAttributes() noexcept; bool BackgroundIsDefault() const noexcept; @@ -147,8 +173,8 @@ class TextAttribute final // global ^ local == false: the foreground attribute is the visible foreground, so we care about the backgrounds being identical const auto checkForeground = (inverted != IsReverseVideo()); return !IsAnyGridLineEnabled() && // grid lines have a visual representation - // crossed out, doubly and singly underlined have a visual representation - WI_AreAllFlagsClear(_attrs, CharacterAttributes::CrossedOut | CharacterAttributes::DoublyUnderlined | CharacterAttributes::Underlined) && + // styled underline and crossed out have a visual representation + !IsUnderlined() && WI_IsFlagClear(_attrs, CharacterAttributes::CrossedOut) && // hyperlinks have a visual representation !IsHyperlink() && // all other attributes do not have a visual representation @@ -175,6 +201,7 @@ class TextAttribute final uint16_t _hyperlinkId; // sizeof: 2, alignof: 2 TextColor _foreground; // sizeof: 4, alignof: 1 TextColor _background; // sizeof: 4, alignof: 1 + TextColor _underlineColor; // sizeof: 4, alignof: 1 #ifdef UNIT_TESTING friend class TextBufferTests; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 74f48697c86..40156064b3f 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -288,7 +288,7 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept // 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 oldBufferAttributes = _mainBuffer->GetCurrentAttributes(); + const auto& oldBufferAttributes = _mainBuffer->GetCurrentAttributes(); newTextBuffer = std::make_unique(bufferSize, TextAttribute{}, 0, // temporarily set size to 0 so it won't render. diff --git a/src/host/ut_host/OutputCellIteratorTests.cpp b/src/host/ut_host/OutputCellIteratorTests.cpp index 8fc2dfa0dd7..e724ae07a84 100644 --- a/src/host/ut_host/OutputCellIteratorTests.cpp +++ b/src/host/ut_host/OutputCellIteratorTests.cpp @@ -11,7 +11,7 @@ using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; -static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; +static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR, INVALID_COLOR }; class OutputCellIteratorTests { diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index f1e901c91bd..334d770d0b8 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -1421,9 +1421,9 @@ void ScreenBufferTests::VtResizePreservingAttributes() WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); // Set the attributes to something not supported by the legacy console. - auto testAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto testAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(188, 20, 24) }; testAttr.SetCrossedOut(true); - testAttr.SetDoublyUnderlined(true); + testAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); testAttr.SetItalic(true); si.GetTextBuffer().SetCurrentAttributes(testAttr); @@ -1600,10 +1600,10 @@ void ScreenBufferTests::VtNewlinePastViewport() cursor.SetPosition({ 0, initialViewport.BottomInclusive() }); // Set the attributes that will be used to initialize new rows. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -1675,10 +1675,10 @@ void ScreenBufferTests::VtNewlinePastEndOfBuffer() cursor.SetPosition({ 0, initialViewport.BottomInclusive() }); // Set the attributes that will be used to initialize new rows. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3797,10 +3797,10 @@ void ScreenBufferTests::ScrollOperations() } // Set the attributes that will be used to fill the revealed area. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3888,10 +3888,10 @@ void ScreenBufferTests::InsertReplaceMode() _FillLine(targetRow, initialChars, initialAttr); // Set the attributes that will be used for the new content. - auto newAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto newAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; newAttr.SetCrossedOut(true); newAttr.SetReverseVideo(true); - newAttr.SetUnderlined(true); + newAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(newAttr); Log::Comment(L"Write additional content into a line of text with IRM mode enabled."); @@ -4002,10 +4002,10 @@ void ScreenBufferTests::InsertChars() _FillLine({ viewportStart, insertLine }, textChars, textAttr); // Set the attributes that will be used to fill the revealed area. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -4202,10 +4202,10 @@ void ScreenBufferTests::DeleteChars() _FillLine({ viewportStart, deleteLine }, textChars, textAttr); // Set the attributes that will be used to fill the revealed area. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -4385,10 +4385,10 @@ void ScreenBufferTests::HorizontalScrollOperations() _FillLines(0, 25, bufferChars, bufferAttr); // Set the attributes that will be used to fill the revealed area. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -4648,10 +4648,10 @@ void ScreenBufferTests::EraseTests() } // Set the attributes that will be used to fill the erased area. - auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12), RGB(18, 29, 55) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderlined(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -6043,54 +6043,57 @@ void ScreenBufferTests::TestExtendedTextAttributes() auto& stateMachine = si.GetStateMachine(); auto& cursor = tbi.GetCursor(); - auto expectedAttrs{ CharacterAttributes::Normal }; + TextAttribute expectedAttrs{}; std::wstring vtSeq = L""; // Collect up a VT sequence to set the state given the method properties if (intense) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Intense); + expectedAttrs.SetIntense(true); vtSeq += L"\x1b[1m"; } if (faint) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Faint); + expectedAttrs.SetFaint(true); vtSeq += L"\x1b[2m"; } if (italics) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Italics); + expectedAttrs.SetItalic(true); vtSeq += L"\x1b[3m"; } + + // underlined and doublyUnderlined are mutually exclusive if (underlined) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Underlined); + expectedAttrs.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); vtSeq += L"\x1b[4m"; } - if (doublyUnderlined) + else if (doublyUnderlined) { - WI_SetFlag(expectedAttrs, CharacterAttributes::DoublyUnderlined); + expectedAttrs.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); vtSeq += L"\x1b[21m"; } + if (blink) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Blinking); + expectedAttrs.SetBlinking(true); vtSeq += L"\x1b[5m"; } if (invisible) { - WI_SetFlag(expectedAttrs, CharacterAttributes::Invisible); + expectedAttrs.SetInvisible(true); vtSeq += L"\x1b[8m"; } if (crossedOut) { - WI_SetFlag(expectedAttrs, CharacterAttributes::CrossedOut); + expectedAttrs.SetCrossedOut(true); vtSeq += L"\x1b[9m"; } // Helper lambda to write a VT sequence, then an "X", then check that the // attributes of the "X" match what we think they should be. - auto validate = [&](const CharacterAttributes expectedAttrs, + auto validate = [&](const CharacterAttributes expectedCharAttrs, const std::wstring& vtSequence) { auto cursorPos = cursor.GetPosition(); @@ -6115,52 +6118,53 @@ void ScreenBufferTests::TestExtendedTextAttributes() stateMachine.ProcessString(L"X"); auto iter = tbi.GetCellDataAt(cursorPos); - auto currentAttrs = iter->TextAttr().GetCharacterAttributes(); - VERIFY_ARE_EQUAL(expectedAttrs, currentAttrs); + auto currentCharAttrs = iter->TextAttr().GetCharacterAttributes(); + VERIFY_ARE_EQUAL(expectedCharAttrs, currentCharAttrs); }; // Check setting all the states collected above - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); // One-by-one, turn off each of these states with VT, then check that the // state matched. if (intense || faint) { // The intense and faint attributes share the same reset sequence. - WI_ClearAllFlags(expectedAttrs, CharacterAttributes::Intense | CharacterAttributes::Faint); + expectedAttrs.SetIntense(false); + expectedAttrs.SetFaint(false); vtSeq = L"\x1b[22m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } if (italics) { - WI_ClearFlag(expectedAttrs, CharacterAttributes::Italics); + expectedAttrs.SetItalic(false); vtSeq = L"\x1b[23m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } if (underlined || doublyUnderlined) { // The two underlined attributes share the same reset sequence. - WI_ClearAllFlags(expectedAttrs, CharacterAttributes::Underlined | CharacterAttributes::DoublyUnderlined); + expectedAttrs.SetUnderlineStyle(UnderlineStyle::NoUnderline); vtSeq = L"\x1b[24m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } if (blink) { - WI_ClearFlag(expectedAttrs, CharacterAttributes::Blinking); + expectedAttrs.SetBlinking(false); vtSeq = L"\x1b[25m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } if (invisible) { - WI_ClearFlag(expectedAttrs, CharacterAttributes::Invisible); + expectedAttrs.SetInvisible(false); vtSeq = L"\x1b[28m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } if (crossedOut) { - WI_ClearFlag(expectedAttrs, CharacterAttributes::CrossedOut); + expectedAttrs.SetCrossedOut(false); vtSeq = L"\x1b[29m"; - validate(expectedAttrs, vtSeq); + validate(expectedAttrs.GetCharacterAttributes(), vtSeq); } stateMachine.ProcessString(L"\x1b[0m"); @@ -6237,16 +6241,19 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors() expectedAttr.SetItalic(true); vtSeq += L"\x1b[3m"; } + + // The two underlined attributes are mutually exclusive. if (underlined) { - expectedAttr.SetUnderlined(true); + expectedAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); vtSeq += L"\x1b[4m"; } - if (doublyUnderlined) + else if (doublyUnderlined) { - expectedAttr.SetDoublyUnderlined(true); + expectedAttr.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); vtSeq += L"\x1b[21m"; } + if (blink) { expectedAttr.SetBlinking(true); @@ -6360,8 +6367,7 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors() if (underlined || doublyUnderlined) { // The two underlined attributes share the same reset sequence. - expectedAttr.SetUnderlined(false); - expectedAttr.SetDoublyUnderlined(false); + expectedAttr.SetUnderlineStyle(UnderlineStyle::NoUnderline); vtSeq = L"\x1b[24m"; validate(expectedAttr, vtSeq); } @@ -6993,7 +6999,7 @@ void ScreenBufferTests::ScreenAlignmentPattern() // Set the initial attributes. auto initialAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; initialAttr.SetReverseVideo(true); - initialAttr.SetUnderlined(true); + initialAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); si.SetAttributes(initialAttr); // Set some margins. @@ -8048,14 +8054,15 @@ void ScreenBufferTests::RectangularAreaOperations() si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 20 }), true); const auto viewport = si.GetViewport(); - // Fill the entire buffer with Zs. Blue on Green and Underlined. + // Fill the entire buffer with Zs. Blue on Green and Red Curly Underlined. const auto bufferChar = L'Z'; - auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; - bufferAttr.SetUnderlined(true); + auto bufferAttr = TextAttribute{ RGB(0, 0, 255), RGB(0, 255, 0), RGB(255, 0, 0) }; + bufferAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + bufferAttr.SetIntense(true); _FillLines(0, bufferHeight, bufferChar, bufferAttr); // Set the active attributes to Red on Blue and Intense; - auto activeAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + auto activeAttr = TextAttribute{ RGB(255, 0, 0), RGB(0, 0, 255), RGB(255, 0, 0) }; activeAttr.SetIntense(true); si.SetAttributes(activeAttr); @@ -8101,17 +8108,19 @@ void ScreenBufferTests::RectangularAreaOperations() Log::Comment(L"DECCARA: update the attributes in a rectangle but leave the text unchanged"); expectedAttr = bufferAttr; expectedAttr.SetReverseVideo(true); + expectedAttr.SetUnderlineStyle(UnderlineStyle::DottedUnderlined); + expectedAttr.SetUnderlineColor(RGB(55, 23, 28)); expectedChar = bufferChar; // The final parameter specifies the reverse video attribute that will be set. - stateMachine.ProcessString(L"\033[3;27;6;54;7$r"); + stateMachine.ProcessString(L"\033[3;27;6;54;7;4:4;58:2::55:23:28$r"); break; case DECRARA: Log::Comment(L"DECRARA: reverse the attributes in a rectangle but leave the text unchanged"); expectedAttr = bufferAttr; - expectedAttr.SetUnderlined(false); + expectedAttr.SetIntense(false); expectedChar = bufferChar; - // The final parameter specifies the underline attribute that will be reversed. - stateMachine.ProcessString(L"\033[3;27;6;54;4$t"); + // The final parameter specifies the intense attribute that will be reversed. + stateMachine.ProcessString(L"\033[3;27;6;54;1$t"); break; case DECCRA: Log::Comment(L"DECCRA: copy a rectangle from the lower part of the viewport to the top"); @@ -8153,7 +8162,7 @@ void ScreenBufferTests::CopyDoubleWidthRectangularArea() const auto bufferChar = L'Z'; const auto bufferHeight = si.GetBufferSize().Height(); auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; - bufferAttr.SetUnderlined(true); + bufferAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); _FillLines(0, bufferHeight, bufferChar, bufferAttr); // Fill the first three lines with Cs. Green on Red and Intense. @@ -8333,7 +8342,7 @@ void ScreenBufferTests::EraseColorMode() auto activeAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; activeAttr.SetCrossedOut(true); activeAttr.SetReverseVideo(true); - activeAttr.SetUnderlined(true); + activeAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); si.SetAttributes(activeAttr); // By default, the meta attributes are expected to be cleared when erasing. diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index 1bc2fd75a90..279012e71fe 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -6,6 +6,8 @@ #include "../../inc/consoletaeftemplates.hpp" #include "../../types/inc/Viewport.hpp" +#include "../../terminal/adapter/DispatchTypes.hpp" +#include "../host/RenderData.hpp" #include "../../renderer/vt/Xterm256Engine.hpp" #include "../../renderer/vt/XtermEngine.hpp" #include "../Settings.hpp" @@ -66,9 +68,11 @@ class Microsoft::Console::Render::VtRendererTest TEST_METHOD(Xterm256TestInvalidate); TEST_METHOD(Xterm256TestColors); + TEST_METHOD(Xterm256TestITUColors); TEST_METHOD(Xterm256TestCursor); TEST_METHOD(Xterm256TestExtendedAttributes); TEST_METHOD(Xterm256TestAttributesAcrossReset); + TEST_METHOD(Xterm256TestDoublyUnderlinedResetBeforeSettingStyle); TEST_METHOD(XtermTestInvalidate); TEST_METHOD(XtermTestColors); @@ -415,7 +419,7 @@ void VtRendererTest::Xterm256TestColors() L"Test changing the text attributes")); Log::Comment(NoThrowString().Format( - L"Begin by setting some test values - FG,BG = (1,2,3), (4,5,6) to start" + L"Begin by setting some test values - FG,BG = (1,2,3), (4,5,6) to start. " L"These values were picked for ease of formatting raw COLORREF values.")); qExpectedInput.push_back("\x1b[38;2;1;2;3m"); qExpectedInput.push_back("\x1b[48;2;5;6;7m"); @@ -578,6 +582,115 @@ void VtRendererTest::Xterm256TestColors() }); } +void VtRendererTest::Xterm256TestITUColors() +{ + auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + auto engine = std::make_unique(std::move(hFile), SetUpViewport()); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + RenderSettings renderSettings; + RenderData renderData; + + VerifyFirstPaint(*engine); + + auto view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test changing the text attributes")); + + // Internally _lastTextAttributes starts with a fg and bg set to INVALID_COLOR(s), + // and initializing textAttributes with the default colors will output "\e[39m\e[49m" + // in the beginning. + auto textAttributes = TextAttribute{}; + qExpectedInput.push_back("\x1b[39m"); + qExpectedInput.push_back("\x1b[49m"); + + Log::Comment(NoThrowString().Format( + L"Begin by setting some test values - UL = (1,2,3) to start. " + L"This value is picked for ease of formatting raw COLORREF values.")); + qExpectedInput.push_back("\x1b[58:2::1:2:3m"); + textAttributes.SetUnderlineColor(RGB(1, 2, 3)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + + TestPaint(*engine, [&]() { + Log::Comment(NoThrowString().Format( + L"----Change the color----")); + qExpectedInput.push_back("\x1b[58:2::7:8:9m"); + textAttributes.SetUnderlineColor(RGB(7, 8, 9)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + }); + + TestPaint(*engine, [&]() { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint")); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + + TestPaint(*engine, [&]() { + Log::Comment(NoThrowString().Format( + L"----Change the UL color to a 256-color index----")); + textAttributes.SetUnderlineColor(TextColor{ TextColor::DARK_RED, true }); + qExpectedInput.push_back("\x1b[58:5:1m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + + // to test the sequence for the default underline color, temporarily modify fg and bg to be something else. + textAttributes.SetForeground(RGB(9, 10, 11)); + qExpectedInput.push_back("\x1b[38;2;9;10;11m"); + textAttributes.SetBackground(RGB(5, 6, 7)); + qExpectedInput.push_back("\x1b[48;2;5;6;7m"); + + Log::Comment(NoThrowString().Format( + L"----Change only the UL color to the 'Default'----")); + textAttributes.SetDefaultUnderlineColor(); + qExpectedInput.push_back("\x1b[59m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + + Log::Comment(NoThrowString().Format( + L"----Back to defaults (all colors)----")); + textAttributes = {}; + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, + renderSettings, + &renderData, + false, + false)); + }); + + TestPaint(*engine, [&]() { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint")); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, + renderSettings, + &renderData, + false, + false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); +} + void VtRendererTest::Xterm256TestCursor() { auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); @@ -714,23 +827,21 @@ void VtRendererTest::Xterm256TestExtendedAttributes() onSequences.push_back("\x1b[2m"); offSequences.push_back("\x1b[22m"); } + + // underlined and doublyUnderlined are mutually exclusive if (underlined) { - desiredAttrs.SetUnderlined(true); - onSequences.push_back("\x1b[4m"); + desiredAttrs.SetUnderlineStyle(UnderlineStyle::DashedUnderlined); + onSequences.push_back("\x1b[4:5m"); offSequences.push_back("\x1b[24m"); } - if (doublyUnderlined) + else if (doublyUnderlined) { - desiredAttrs.SetDoublyUnderlined(true); + desiredAttrs.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); onSequences.push_back("\x1b[21m"); - // The two underlines share the same off sequence, so we - // only add it here if that hasn't already been done. - if (!underlined) - { - offSequences.push_back("\x1b[24m"); - } + offSequences.push_back("\x1b[24m"); } + if (italics) { desiredAttrs.SetItalic(true); @@ -803,7 +914,15 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() VERIFY_SUCCEEDED(TestData::TryGetValue(L"renditionAttribute", renditionAttribute)); std::stringstream renditionSequence; - renditionSequence << "\x1b[" << renditionAttribute << "m"; + // test underline with curly underlined + if (renditionAttribute == 4) + { + renditionSequence << "\x1b[4:3m"; + } + else + { + renditionSequence << "\x1b[" << renditionAttribute << "m"; + } auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); auto engine = std::make_unique(std::move(hFile), SetUpViewport()); @@ -835,11 +954,11 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); - textAttributes.SetUnderlined(true); + textAttributes.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); break; case GraphicsOptions::DoublyUnderlined: Log::Comment(L"----Set Doubly Underlined Attribute----"); - textAttributes.SetDoublyUnderlined(true); + textAttributes.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); break; case GraphicsOptions::Overline: Log::Comment(L"----Set Overline Attribute----"); @@ -890,6 +1009,48 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() VerifyExpectedInputsDrained(); } +void VtRendererTest::Xterm256TestDoublyUnderlinedResetBeforeSettingStyle() +{ + auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + auto engine = std::make_unique(std::move(hFile), SetUpViewport()); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + VerifyFirstPaint(*engine); + + auto attrs = TextAttribute{}; + + Log::Comment(NoThrowString().Format( + L"----Testing Doubly underlined is properly reset before applying the new underline style----")); + + Log::Comment(NoThrowString().Format( + L"----Set Doubly Underlined----")); + TestPaint(*engine, [&]() { + attrs.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + qExpectedInput.push_back("\x1b[21m"); + VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(attrs)); + }); + + Log::Comment(NoThrowString().Format( + L"----Set Underline To Any Other Style----")); + TestPaint(*engine, [&]() { + attrs.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + qExpectedInput.push_back("\x1b[24m"); + qExpectedInput.push_back("\x1b[4:3m"); + VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(attrs)); + }); + + Log::Comment(NoThrowString().Format( + L"----Remove The Underline----")); + TestPaint(*engine, [&]() { + attrs.SetUnderlineStyle(UnderlineStyle::NoUnderline); + qExpectedInput.push_back("\x1b[24m"); + VERIFY_SUCCEEDED(engine->_UpdateExtendedAttrs(attrs)); + }); + + VerifyExpectedInputsDrained(); +} + void VtRendererTest::XtermTestInvalidate() { auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); @@ -1345,7 +1506,7 @@ void VtRendererTest::XtermTestAttributesAcrossReset() break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); - textAttributes.SetUnderlined(true); + textAttributes.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); break; case GraphicsOptions::Negative: Log::Comment(L"----Set Negative Attribute----"); diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp index f70697c33b0..a61407e5b14 100644 --- a/src/inc/conattrs.hpp +++ b/src/inc/conattrs.hpp @@ -17,11 +17,12 @@ enum class CharacterAttributes : uint16_t Blinking = 0x04, Invisible = 0x08, CrossedOut = 0x10, - Underlined = 0x20, - DoublyUnderlined = 0x40, - Faint = 0x80, - Unused1 = 0x100, - Unused2 = 0x200, + Faint = 0x20, + + // 7th, 8th, 9th bit reserved for underline styles + UnderlineStyle = 0x1C0, + + Unused1 = 0x200, TopGridline = COMMON_LVB_GRID_HORIZONTAL, // 0x400 LeftGridline = COMMON_LVB_GRID_LVERTICAL, // 0x800 RightGridline = COMMON_LVB_GRID_RVERTICAL, // 0x1000 @@ -34,6 +35,8 @@ enum class CharacterAttributes : uint16_t }; DEFINE_ENUM_FLAG_OPERATORS(CharacterAttributes); +constexpr uint8_t UNDERLINE_STYLE_SHIFT = 6; + enum class CursorType : unsigned int { Legacy = 0x0, // uses the cursor's height value to range from underscore-like to full box diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 34b4e20caef..8096b878c84 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -1648,26 +1648,20 @@ class UiaTextRangeTests Log::Comment(L"Test Underline"); // Single underline - attr.SetUnderlined(true); + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); updateBuffer(attr); VARIANT result; VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); VERIFY_ARE_EQUAL(TextDecorationLineStyle_Single, result.lVal); - // Double underline (double supersedes single) - attr.SetDoublyUnderlined(true); - updateBuffer(attr); - VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); - VERIFY_ARE_EQUAL(TextDecorationLineStyle_Double, result.lVal); - - // Double underline (double on its own) - attr.SetUnderlined(false); + // Double underline (new style supersedes the old one) + attr.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); updateBuffer(attr); VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); VERIFY_ARE_EQUAL(TextDecorationLineStyle_Double, result.lVal); // No underline - attr.SetDoublyUnderlined(false); + attr.SetUnderlineStyle(UnderlineStyle::NoUnderline); updateBuffer(attr); VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); VERIFY_ARE_EQUAL(TextDecorationLineStyle_None, result.lVal); @@ -1695,9 +1689,9 @@ class UiaTextRangeTests THROW_IF_FAILED(utr->ExpandToEnclosingUnit(TextUnit_Line)); // set first cell as underlined, but second cell as not underlined - attr.SetUnderlined(true); + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); _pTextBuffer->Write({ attr }, { 0, 0 }); - attr.SetUnderlined(false); + attr.SetUnderlineStyle(UnderlineStyle::NoUnderline); _pTextBuffer->Write({ attr }, { 1, 0 }); VERIFY_SUCCEEDED(utr->GetAttributeValue(UIA_UnderlineStyleAttributeId, &result)); diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index c23bd85bbfa..917c5ba05f9 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -950,14 +950,21 @@ GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcep lines.set(GridLines::Strikethrough); } - if (textAttribute.IsUnderlined()) - { - lines.set(GridLines::Underline); - } - - if (textAttribute.IsDoublyUnderlined()) + const auto underlineStyle = textAttribute.GetUnderlineStyle(); + switch (underlineStyle) { + case UnderlineStyle::NoUnderline: + break; + case UnderlineStyle::DoublyUnderlined: lines.set(GridLines::DoubleUnderline); + break; + case UnderlineStyle::SinglyUnderlined: + case UnderlineStyle::CurlyUnderlined: + case UnderlineStyle::DottedUnderlined: + case UnderlineStyle::DashedUnderlined: + default: + lines.set(GridLines::Underline); + break; } if (textAttribute.IsHyperlink()) diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp index f22d76234ff..1b728aae500 100644 --- a/src/renderer/vt/VtSequences.cpp +++ b/src/renderer/vt/VtSequences.cpp @@ -241,6 +241,19 @@ using namespace Microsoft::Console::Render; return _WriteFormatted(FMT_COMPILE("\x1b[{}8;5;{}m"), fIsForeground ? '3' : '4', index); } +// Method Description: +// - Formats and writes a sequence to change the current underline color to an +// indexed color from the 256-color table. +// - Uses sub parameters. +// Arguments: +// - index: color table index to emit as a VT sequence +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionUnderline256Color(const BYTE index) noexcept +{ + return _WriteFormatted(FMT_COMPILE("\x1b[58:5:{}m"), index); +} + // Method Description: // - Formats and writes a sequence to change the current text attributes to an // RGB color. @@ -258,6 +271,22 @@ using namespace Microsoft::Console::Render; return _WriteFormatted(FMT_COMPILE("\x1b[{}8;2;{};{};{}m"), fIsForeground ? '3' : '4', r, g, b); } +// Method Description: +// - Formats and writes a sequence to change the current underline color to an +// RGB color. +// - Uses sub parameters. +// Arguments: +// - color: The color to emit a VT sequence for. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionUnderlineRGBColor(const COLORREF color) noexcept +{ + const auto r = GetRValue(color); + const auto g = GetGValue(color); + const auto b = GetBValue(color); + return _WriteFormatted(FMT_COMPILE("\x1b[58:2::{}:{}:{}m"), r, g, b); +} + // Method Description: // - Formats and writes a sequence to change the current text attributes to the // default foreground or background. Does not affect the intensity of text. @@ -270,6 +299,16 @@ using namespace Microsoft::Console::Render; return _Write(fIsForeground ? ("\x1b[39m") : ("\x1b[49m")); } +// Method Description: +// - Formats and writes a sequence to change the current underline color to the +// default color. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] HRESULT VtEngine::_SetGraphicsRenditionUnderlineDefaultColor() noexcept +{ + return _Write("\x1b[59m"); +} + // Method Description: // - Formats and writes a sequence to change the terminal's window size. // Arguments: @@ -333,25 +372,42 @@ using namespace Microsoft::Console::Render; } // Method Description: -// - Formats and writes a sequence to change the underline of the following text. +// - Formats and writes a sequence to change the extended underline styling of the following text. +// - Uses backward compatible SGR 4 (without sub parameter) and SGR 21 for single and doubly underline. // Arguments: -// - isUnderlined: If true, we'll underline the text. Otherwise we'll remove the underline. +// - style: underline style to use. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_SetUnderlined(const bool isUnderlined) noexcept +[[nodiscard]] HRESULT VtEngine::_SetUnderlineExtended(const UnderlineStyle style) noexcept { - return _Write(isUnderlined ? "\x1b[4m" : "\x1b[24m"); + switch (style) + { + case UnderlineStyle::NoUnderline: + return _SetUnderlined(false); + case UnderlineStyle::SinglyUnderlined: + return _SetUnderlined(true); + case UnderlineStyle::DoublyUnderlined: + return _Write("\x1b[21m"); + case UnderlineStyle::CurlyUnderlined: + return _Write("\x1b[4:3m"); + case UnderlineStyle::DottedUnderlined: + return _Write("\x1b[4:4m"); + case UnderlineStyle::DashedUnderlined: + return _Write("\x1b[4:5m"); + default: + return _SetUnderlined(true); // treat unknown style as singly underlined + } } // Method Description: -// - Formats and writes a sequence to change the double underline of the following text. +// - Formats and writes a sequence to change the underline of the following text. // Arguments: -// - isUnderlined: If true, we'll doubly underline the text. Otherwise we'll remove the underline. +// - isUnderlined: If true, we'll underline the text. Otherwise we'll remove the underline. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_SetDoublyUnderlined(const bool isUnderlined) noexcept +[[nodiscard]] HRESULT VtEngine::_SetUnderlined(const bool isUnderlined) noexcept { - return _Write(isUnderlined ? "\x1b[21m" : "\x1b[24m"); + return _Write(isUnderlined ? "\x1b[4m" : "\x1b[24m"); } // Method Description: diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index dc506322ccf..7527db3dee7 100644 --- a/src/renderer/vt/Xterm256Engine.cpp +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -85,28 +85,22 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, _lastTextAttributes.SetFaint(true); } - // Turning off the underline styles must be handled at the same time, - // since there is only one sequence that resets both of them. - const auto singleTurnedOff = !textAttributes.IsUnderlined() && _lastTextAttributes.IsUnderlined(); - const auto doubleTurnedOff = !textAttributes.IsDoublyUnderlined() && _lastTextAttributes.IsDoublyUnderlined(); - if (singleTurnedOff || doubleTurnedOff) - { - RETURN_IF_FAILED(_SetUnderlined(false)); - _lastTextAttributes.SetUnderlined(false); - _lastTextAttributes.SetDoublyUnderlined(false); - } - - // Once we've handled the cases where they need to be turned off, - // we can then check if either should be turned back on again. - if (textAttributes.IsUnderlined() && !_lastTextAttributes.IsUnderlined()) - { - RETURN_IF_FAILED(_SetUnderlined(true)); - _lastTextAttributes.SetUnderlined(true); - } - if (textAttributes.IsDoublyUnderlined() && !_lastTextAttributes.IsDoublyUnderlined()) + // We check the singly, doubly underlined and extended styling together, + // since only one of them can be active at a time. + const auto ulStyle = textAttributes.GetUnderlineStyle(); + const auto lastUlStyle = _lastTextAttributes.GetUnderlineStyle(); + if (ulStyle != lastUlStyle) { - RETURN_IF_FAILED(_SetDoublyUnderlined(true)); - _lastTextAttributes.SetDoublyUnderlined(true); + // Reset doubly underlined if it was previously set. Avoids an edge case + // where a pty client tracks doubly underlined and singly underlined separately, + // and setting single underlined would leave the text doubly underlined + // because it was never turned-off. + if (lastUlStyle == UnderlineStyle::DoublyUnderlined && ulStyle != UnderlineStyle::NoUnderline) + { + RETURN_IF_FAILED(_SetUnderlined(false)); + } + RETURN_IF_FAILED(_SetUnderlineExtended(ulStyle)); + _lastTextAttributes.SetUnderlineStyle(ulStyle); } if (textAttributes.IsOverlined() != _lastTextAttributes.IsOverlined()) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 413779b0479..c1a0d838852 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -162,7 +162,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, if (textAttributes.IsUnderlined() != _lastTextAttributes.IsUnderlined()) { RETURN_IF_FAILED(_SetUnderlined(textAttributes.IsUnderlined())); - _lastTextAttributes.SetUnderlined(textAttributes.IsUnderlined()); + _lastTextAttributes.SetUnderlineStyle(textAttributes.GetUnderlineStyle()); } return S_OK; diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index dda813b92fe..d261b1c1624 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -257,11 +257,13 @@ using namespace Microsoft::Console::Types; { const auto fg = textAttributes.GetForeground(); const auto bg = textAttributes.GetBackground(); + const auto ul = textAttributes.GetUnderlineColor(); auto lastFg = _lastTextAttributes.GetForeground(); auto lastBg = _lastTextAttributes.GetBackground(); + auto lastUl = _lastTextAttributes.GetUnderlineColor(); - // If both the FG and BG should be the defaults, emit a SGR reset. - if (fg.IsDefault() && bg.IsDefault() && !(lastFg.IsDefault() && lastBg.IsDefault())) + // If the FG, BG and UL should be the defaults, emit an SGR reset. + if (fg.IsDefault() && bg.IsDefault() && ul.IsDefault() && !(lastFg.IsDefault() && lastBg.IsDefault() && lastUl.IsDefault())) { // SGR Reset will clear all attributes (except hyperlink ID) - which means // we cannot reset _lastTextAttributes by simply doing @@ -270,9 +272,11 @@ using namespace Microsoft::Console::Types; RETURN_IF_FAILED(_SetGraphicsDefault()); _lastTextAttributes.SetDefaultBackground(); _lastTextAttributes.SetDefaultForeground(); + _lastTextAttributes.SetDefaultUnderlineColor(); _lastTextAttributes.SetDefaultRenditionAttributes(); lastFg = {}; lastBg = {}; + lastUl = {}; } if (fg != lastFg) @@ -317,6 +321,27 @@ using namespace Microsoft::Console::Types; _lastTextAttributes.SetBackground(bg); } + if (ul != lastUl) + { + if (ul.IsDefault()) + { + RETURN_IF_FAILED(_SetGraphicsRenditionUnderlineDefaultColor()); + } + else if (ul.IsIndex16()) // underline can't be 16 color + { + /* do nothing */ + } + else if (ul.IsIndex256()) + { + RETURN_IF_FAILED(_SetGraphicsRenditionUnderline256Color(ul.GetIndex())); + } + else if (ul.IsRgb()) + { + RETURN_IF_FAILED(_SetGraphicsRenditionUnderlineRGBColor(ul.GetRGB())); + } + _lastTextAttributes.SetUnderlineColor(ul); + } + return S_OK; } diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 29a8657cd5a..d5db02afba9 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -32,7 +32,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _usingLineRenditions(false), _stopUsingLineRenditions(false), _usingSoftFont(false), - _lastTextAttributes(INVALID_COLOR, INVALID_COLOR), + _lastTextAttributes(INVALID_COLOR, INVALID_COLOR, INVALID_COLOR), _lastViewport(initialViewport), _pool(til::pmr::get_default_resource()), _invalidMap(initialViewport.Dimensions(), false, &_pool), diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 3ba1e702af6..a9f2c76cf93 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -182,6 +182,10 @@ namespace Microsoft::Console::Render const bool fIsForeground) noexcept; [[nodiscard]] HRESULT _SetGraphicsRenditionDefaultColor(const bool fIsForeground) noexcept; + [[nodiscard]] HRESULT _SetGraphicsRenditionUnderline256Color(const BYTE index) noexcept; + [[nodiscard]] HRESULT _SetGraphicsRenditionUnderlineRGBColor(const COLORREF color) noexcept; + [[nodiscard]] HRESULT _SetGraphicsRenditionUnderlineDefaultColor() noexcept; + [[nodiscard]] HRESULT _SetGraphicsDefault() noexcept; [[nodiscard]] HRESULT _ResizeWindow(const til::CoordType sWidth, const til::CoordType sHeight) noexcept; @@ -189,7 +193,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _SetIntense(const bool isIntense) noexcept; [[nodiscard]] HRESULT _SetFaint(const bool isFaint) noexcept; [[nodiscard]] HRESULT _SetUnderlined(const bool isUnderlined) noexcept; - [[nodiscard]] HRESULT _SetDoublyUnderlined(const bool isUnderlined) noexcept; + [[nodiscard]] HRESULT _SetUnderlineExtended(const UnderlineStyle style) noexcept; [[nodiscard]] HRESULT _SetOverlined(const bool isOverlined) noexcept; [[nodiscard]] HRESULT _SetItalic(const bool isItalic) noexcept; [[nodiscard]] HRESULT _SetBlinking(const bool isBlinking) noexcept; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index feeb0413979..612f82c093f 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -398,7 +398,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes // as well as the Faint/Blink options. RGBColorOrFaint = 2, // 2 is also Faint, decreased intensity (ISO 6429). Italics = 3, - Underline = 4, + Underline = 4, // same for extended underline styles `SGR 4:x`. BlinkOrXterm256Index = 5, // 5 is also Blink. RapidBlink = 6, Negative = 7, @@ -434,6 +434,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes BackgroundDefault = 49, Overline = 53, NoOverline = 55, + UnderlineColor = 58, + UnderlineColorDefault = 59, BrightForegroundBlack = 90, BrightForegroundRed = 91, BrightForegroundGreen = 92, diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index d49a5b816b7..85582542980 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -77,7 +77,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) auto& cursor = textBuffer.GetCursor(); auto cursorPosition = cursor.GetPosition(); const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); - const auto attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = textBuffer.GetCurrentAttributes(); const auto viewport = _api.GetViewport(); const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); @@ -502,7 +502,7 @@ bool AdaptDispatch::CursorSaveState() // First retrieve some information about the buffer const auto viewport = _api.GetViewport(); const auto& textBuffer = _api.GetTextBuffer(); - const auto attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = textBuffer.GetCurrentAttributes(); // The cursor is given to us by the API as relative to the whole buffer. // But in VT speak, the cursor row should be relative to the current viewport top. @@ -1012,6 +1012,10 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec { attr.SetBackground(*changeOps.background); } + if (changeOps.underlineColor) + { + attr.SetUnderlineColor(*changeOps.underlineColor); + } rowBuffer.ReplaceAttributes(col, col + 1, attr); } } @@ -1121,7 +1125,7 @@ bool AdaptDispatch::ChangeAttributesRectangularArea(const VTInt top, const VTInt // provides us with an OR mask and an AND mask which can then be applied to // each cell to set and reset the appropriate attribute bits. auto allAttrsOff = TextAttribute{}; - auto allAttrsOn = TextAttribute{ 0, 0 }; + auto allAttrsOn = TextAttribute{ 0, 0, 0 }; allAttrsOn.SetCharacterAttributes(CharacterAttributes::All); _ApplyGraphicsOptions(attrs, allAttrsOff); _ApplyGraphicsOptions(attrs, allAttrsOn); @@ -1144,6 +1148,10 @@ bool AdaptDispatch::ChangeAttributesRectangularArea(const VTInt top, const VTInt changeOps.foreground = foregroundChanged ? std::optional{ foreground } : std::nullopt; changeOps.background = backgroundChanged ? std::optional{ background } : std::nullopt; + const auto underlineColor = allAttrsOff.GetUnderlineColor(); + const auto underlineColorChanged = !underlineColor.IsDefault() || allAttrsOn.GetUnderlineColor().IsDefault(); + changeOps.underlineColor = underlineColorChanged ? std::optional{ underlineColor } : std::nullopt; + _ChangeRectOrStreamAttributes({ left, top, right, bottom }, changeOps); return true; @@ -1152,6 +1160,8 @@ bool AdaptDispatch::ChangeAttributesRectangularArea(const VTInt top, const VTInt // Routine Description: // - DECRARA - Reverses the attributes in a rectangular area. The affected range // is dependent on the change extent setting defined by DECSACE. +// Note: Reversing the underline style has some unexpected consequences. +// See https://github.com/microsoft/terminal/pull/15795#issuecomment-1702559350. // Arguments: // - top - The first row of the area. // - left - The first column of the area. @@ -1168,6 +1178,7 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn // then combine them with XOR, because if we're reversing the same attribute // twice, we'd expect the two instances to cancel each other out. auto reverseMask = CharacterAttributes::Normal; + if (!attrs.empty()) { for (size_t i = 0; i < attrs.size();) @@ -1178,7 +1189,9 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn // the empty check above. if (attrs.at(i).value_or(0) == 0) { - reverseMask ^= CharacterAttributes::Rendition; + // With param 0, we only reverse the SinglyUnderlined bit. + const auto singlyUnderlinedAttr = static_cast(WI_EnumValue(UnderlineStyle::SinglyUnderlined) << UNDERLINE_STYLE_SHIFT); + reverseMask ^= (CharacterAttributes::Rendition & ~CharacterAttributes::UnderlineStyle) | singlyUnderlinedAttr; i++; } else @@ -4165,7 +4178,8 @@ void AdaptDispatch::_ReportSGRSetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r0"sv); - const auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto ulStyle = attr.GetUnderlineStyle(); // For each boolean attribute that is set, we add the appropriate // parameter value to the response string. const auto addAttribute = [&](const auto& parameter, const auto enabled) { @@ -4177,12 +4191,15 @@ void AdaptDispatch::_ReportSGRSetting() const addAttribute(L";1"sv, attr.IsIntense()); addAttribute(L";2"sv, attr.IsFaint()); addAttribute(L";3"sv, attr.IsItalic()); - addAttribute(L";4"sv, attr.IsUnderlined()); + addAttribute(L";4"sv, ulStyle == UnderlineStyle::SinglyUnderlined); + addAttribute(L";4:3"sv, ulStyle == UnderlineStyle::CurlyUnderlined); + addAttribute(L";4:4"sv, ulStyle == UnderlineStyle::DottedUnderlined); + addAttribute(L";4:5"sv, ulStyle == UnderlineStyle::DashedUnderlined); addAttribute(L";5"sv, attr.IsBlinking()); addAttribute(L";7"sv, attr.IsReverseVideo()); addAttribute(L";8"sv, attr.IsInvisible()); addAttribute(L";9"sv, attr.IsCrossedOut()); - addAttribute(L";21"sv, attr.IsDoublyUnderlined()); + addAttribute(L";21"sv, ulStyle == UnderlineStyle::DoublyUnderlined); addAttribute(L";53"sv, attr.IsOverlined()); // We also need to add the appropriate color encoding parameters for @@ -4209,6 +4226,7 @@ void AdaptDispatch::_ReportSGRSetting() const }; addColor(30, attr.GetForeground()); addColor(40, attr.GetBackground()); + addColor(50, attr.GetUnderlineColor()); // The 'm' indicates this is an SGR response, and ST ends the sequence. response.append(L"m\033\\"sv); @@ -4277,7 +4295,7 @@ void AdaptDispatch::_ReportDECSCASetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); response.append(attr.IsProtected() ? L"1"sv : L"0"sv); // The '"q' indicates this is an DECSCA response, and ST ends the sequence. @@ -4299,7 +4317,6 @@ void AdaptDispatch::_ReportDECSACESetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto attr = _api.GetTextBuffer().GetCurrentAttributes(); response.append(_modes.test(Mode::RectangularChangeExtent) ? L"2"sv : L"1"sv); // The '*x' indicates this is an DECSACE response, and ST ends the sequence. @@ -4399,7 +4416,7 @@ void AdaptDispatch::_ReportCursorInformation() const auto viewport = _api.GetViewport(); const auto& textBuffer = _api.GetTextBuffer(); const auto& cursor = textBuffer.GetCursor(); - const auto attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = textBuffer.GetCurrentAttributes(); // First pull the cursor position relative to the entire buffer out of the console. til::point cursorPosition{ cursor.GetPosition() }; @@ -4422,7 +4439,16 @@ void AdaptDispatch::_ReportCursorInformation() const auto pageNumber = 1; // Only some of the rendition attributes are reported. - auto renditionAttributes = L'@'; + // Bit Attribute + // 1 bold + // 2 underlined + // 3 blink + // 4 reverse video + // 5 invisible + // 6 extension indicator + // 7 Always 1 (on) + // 8 Always 0 (off) + auto renditionAttributes = L'@'; // (0100 0000) renditionAttributes += (attributes.IsIntense() ? 1 : 0); renditionAttributes += (attributes.IsUnderlined() ? 2 : 0); renditionAttributes += (attributes.IsBlinking() ? 4 : 0); @@ -4543,7 +4569,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() { auto attr = textBuffer.GetCurrentAttributes(); attr.SetIntense(state.value & 1); - attr.SetUnderlined(state.value & 2); + attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline); attr.SetBlinking(state.value & 4); attr.SetReverseVideo(state.value & 8); attr.SetInvisible(state.value & 16); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 7417f6f82fd..4b145f1b8ca 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -207,6 +207,7 @@ namespace Microsoft::Console::VirtualTerminal CharacterAttributes xorAttrMask = CharacterAttributes::Normal; std::optional foreground; std::optional background; + std::optional underlineColor; }; void _WriteToBuffer(const std::wstring_view string); @@ -296,6 +297,7 @@ namespace Microsoft::Console::VirtualTerminal SgrStack _sgrStack; + void _SetUnderlineStyleHelper(const VTParameter option, TextAttribute& attr) noexcept; size_t _SetRgbColorsHelper(const VTParameters options, TextAttribute& attr, const bool isForeground) noexcept; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index a6567abd09e..ac1b0180eff 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -12,6 +12,23 @@ using namespace Microsoft::Console::VirtualTerminal; using namespace Microsoft::Console::VirtualTerminal::DispatchTypes; +// Routine Description: +// - Helper to parse the underline style option. +// Arguments: +// - options - An option that will be used to interpret the underline style. +// - attr - The attribute that will be updated with the parsed underline style. +// Return Value: +// - +void AdaptDispatch::_SetUnderlineStyleHelper(const VTParameter option, TextAttribute& attr) noexcept +{ + const auto style = option.value_or(0); + // Only apply the style if it's one of the valid underline styles (0-5). + if ((style >= 0) && (style <= WI_EnumValue(UnderlineStyle::Max))) + { + attr.SetUnderlineStyle(gsl::narrow_cast(style)); + } +} + // Routine Description: // - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG) // These options are followed by either a 2 (RGB) or 5 (xterm index) @@ -67,14 +84,14 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options, } // Routine Description: -// - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG) +// - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG) or 58 (UL) // - These options are followed by either a 2 (RGB) or 5 (xterm index): // - RGB sequences then take 4 MORE options to designate the ColorSpaceID, R, G, B parts // of the color. // - Xterm index will use the option that follows to use a color from the // preset 256 color xterm color table. // Arguments: -// - colorItem - One of FG(38) and BG(48), indicating which color we're setting. +// - colorItem - One of the FG(38), BG(48), UL(58), indicating which color we're setting. // - options - An array of options that will be used to generate the RGB color // - attr - The attribute that will be updated with the parsed color. // Return Value: @@ -83,24 +100,36 @@ void AdaptDispatch::_SetRgbColorsHelperFromSubParams(const VTParameter colorItem const VTSubParameters options, TextAttribute& attr) noexcept { - // This should be called for applying FG and BG colors only. - assert(colorItem == GraphicsOptions::ForegroundExtended || - colorItem == GraphicsOptions::BackgroundExtended); + const auto applyColor = [&](const TextColor& color) { + switch (colorItem) + { + case ForegroundExtended: + attr.SetForeground(color); + break; + case BackgroundExtended: + attr.SetBackground(color); + break; + case UnderlineColor: + attr.SetUnderlineColor(color); + break; + default: + break; + }; + }; - const bool isForeground = (colorItem == GraphicsOptions::ForegroundExtended); const DispatchTypes::GraphicsOptions typeOpt = options.at(0); - - if (typeOpt == DispatchTypes::GraphicsOptions::RGBColorOrFaint) + switch (typeOpt) + { + case DispatchTypes::GraphicsOptions::RGBColorOrFaint: { // sub params are in the order: // :2:::: - // We treat a color as invalid, if it has a color space ID, as some - // applications that support non-standard ODA color sequence may send - // the red value in its place. + // We treat a color as invalid if it has a non-empty color space ID, as + // some applications that support non-standard ODA color sequence might + // send the red value in its place. const bool hasColorSpaceId = options.at(1).has_value(); - // Skip color-space-id. const size_t red = options.at(2).value_or(0); const size_t green = options.at(3).value_or(0); const size_t blue = options.at(4).value_or(0); @@ -109,11 +138,11 @@ void AdaptDispatch::_SetRgbColorsHelperFromSubParams(const VTParameter colorItem // This is to match XTerm's and VTE's behavior. if (!hasColorSpaceId && red <= 255 && green <= 255 && blue <= 255) { - const auto rgbColor = RGB(red, green, blue); - attr.SetColor(rgbColor, isForeground); + applyColor(TextColor{ RGB(red, green, blue) }); } + break; } - else if (typeOpt == DispatchTypes::GraphicsOptions::BlinkOrXterm256Index) + case DispatchTypes::GraphicsOptions::BlinkOrXterm256Index: { // sub params are in the order: // :5: @@ -125,16 +154,13 @@ void AdaptDispatch::_SetRgbColorsHelperFromSubParams(const VTParameter colorItem if (tableIndex <= 255) { const auto adjustedIndex = gsl::narrow_cast(tableIndex); - if (isForeground) - { - attr.SetIndexedForeground256(adjustedIndex); - } - else - { - attr.SetIndexedBackground256(adjustedIndex); - } + applyColor(TextColor{ adjustedIndex, true }); } + break; } + default: + break; + }; } // Routine Description: @@ -164,6 +190,7 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options, case Off: attr.SetDefaultForeground(); attr.SetDefaultBackground(); + attr.SetDefaultUnderlineColor(); attr.SetDefaultRenditionAttributes(); return 1; case ForegroundDefault: @@ -172,6 +199,9 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options, case BackgroundDefault: attr.SetDefaultBackground(); return 1; + case UnderlineColorDefault: + attr.SetDefaultUnderlineColor(); + return 1; case Intense: attr.SetIntense(true); return 1; @@ -213,15 +243,14 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options, case Positive: attr.SetReverseVideo(false); return 1; - case Underline: - attr.SetUnderlined(true); + case Underline: // SGR 4 (without extended styling) + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); return 1; case DoublyUnderlined: - attr.SetDoublyUnderlined(true); + attr.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); return 1; case NoUnderline: - attr.SetUnderlined(false); - attr.SetDoublyUnderlined(false); + attr.SetUnderlineStyle(UnderlineStyle::NoUnderline); return 1; case Overline: attr.SetOverlined(true); @@ -351,8 +380,12 @@ void AdaptDispatch::_ApplyGraphicsOptionWithSubParams(const VTParameter option, // we should just skip over them. switch (option) { + case Underline: + _SetUnderlineStyleHelper(subParams.at(0), attr); + break; case ForegroundExtended: case BackgroundExtended: + case UnderlineColor: _SetRgbColorsHelperFromSubParams(option, subParams, attr); break; default: @@ -437,7 +470,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) // - True. bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) { - const auto currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); _sgrStack.Push(currentAttributes, options); return true; } @@ -451,7 +484,7 @@ bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::PopGraphicsRendition() { - const auto currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); _api.SetTextAttributes(_sgrStack.Pop(currentAttributes)); return true; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 2b29d78bf49..3c2e1ba4d76 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -852,13 +852,13 @@ class AdapterTest Log::Comment(L"Testing graphics 'Underline'"); startingAttribute = TextAttribute{ 0 }; _testGetSet->_expectedAttribute = TextAttribute{ 0 }; - _testGetSet->_expectedAttribute.SetUnderlined(true); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); break; case DispatchTypes::GraphicsOptions::DoublyUnderlined: Log::Comment(L"Testing graphics 'Doubly Underlined'"); startingAttribute = TextAttribute{ 0 }; _testGetSet->_expectedAttribute = TextAttribute{ 0 }; - _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); break; case DispatchTypes::GraphicsOptions::Overline: Log::Comment(L"Testing graphics 'Overline'"); @@ -892,8 +892,7 @@ class AdapterTest case DispatchTypes::GraphicsOptions::NoUnderline: Log::Comment(L"Testing graphics 'No Underline'"); startingAttribute = TextAttribute{ 0 }; - startingAttribute.SetUnderlined(true); - startingAttribute.SetDoublyUnderlined(true); + startingAttribute.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); _testGetSet->_expectedAttribute = TextAttribute{ 0 }; break; case DispatchTypes::GraphicsOptions::NoOverline: @@ -1136,7 +1135,7 @@ class AdapterTest TEST_METHOD(GraphicsSingleWithSubParamTests) { BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{38, 48}") // corresponds to options in DispatchTypes::GraphicsOptions + TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{4, 38, 48, 58}") // corresponds to options in DispatchTypes::GraphicsOptions END_TEST_METHOD_PROPERTIES() Log::Comment(L"Starting test..."); @@ -1157,6 +1156,13 @@ class AdapterTest TextAttribute startingAttribute; switch (graphicsOption) { + case DispatchTypes::GraphicsOptions::Underline: + Log::Comment(L"Testing graphics 'Underline'"); + _testGetSet->MakeSubParamsAndRanges({ { 3 } }, subParams, subParamRanges); + startingAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + break; case DispatchTypes::GraphicsOptions::ForegroundExtended: Log::Comment(L"Testing graphics 'ForegroundExtended'"); _testGetSet->MakeSubParamsAndRanges({ { DispatchTypes::GraphicsOptions::BlinkOrXterm256Index, TextColor::DARK_RED } }, subParams, subParamRanges); @@ -1171,6 +1177,13 @@ class AdapterTest _testGetSet->_expectedAttribute = TextAttribute{ 0 }; _testGetSet->_expectedAttribute.SetIndexedBackground256(TextColor::BRIGHT_WHITE); break; + case DispatchTypes::GraphicsOptions::UnderlineColor: + Log::Comment(L"Testing graphics 'UnderlineColor'"); + _testGetSet->MakeSubParamsAndRanges({ { DispatchTypes::GraphicsOptions::BlinkOrXterm256Index, TextColor::DARK_RED } }, subParams, subParamRanges); + startingAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetUnderlineColor({ TextColor::DARK_RED, true }); + break; default: VERIFY_FAIL(L"Test not implemented yet!"); break; @@ -1186,6 +1199,8 @@ class AdapterTest _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED VTParameter rgOptions[16]; + std::vector subParams; + std::vector> subParamRanges; VTParameter rgStackOptions[16]; size_t cOptions = 1; @@ -1289,7 +1304,7 @@ class AdapterTest _testGetSet->_expectedAttribute.SetIndexedForeground(TextColor::DARK_GREEN); _testGetSet->_expectedAttribute.SetIndexedBackground(TextColor::DARK_GREEN); _testGetSet->_expectedAttribute.SetIntense(true); - _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); cOptions = 1; @@ -1298,14 +1313,14 @@ class AdapterTest _testGetSet->_expectedAttribute.SetIndexedForeground(TextColor::DARK_RED); _testGetSet->_expectedAttribute.SetIndexedBackground(TextColor::DARK_GREEN); _testGetSet->_expectedAttribute.SetIntense(true); - _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); rgOptions[0] = DispatchTypes::GraphicsOptions::NotIntenseOrFaint; _testGetSet->_expectedAttribute = {}; _testGetSet->_expectedAttribute.SetIndexedForeground(TextColor::DARK_RED); _testGetSet->_expectedAttribute.SetIndexedBackground(TextColor::DARK_GREEN); - _testGetSet->_expectedAttribute.SetDoublyUnderlined(true); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); // And then restore... @@ -1315,6 +1330,76 @@ class AdapterTest _testGetSet->_expectedAttribute.SetIndexedBackground(TextColor::DARK_BLUE); _testGetSet->_expectedAttribute.SetIntense(true); VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 5: Save 'no singly underline' state, set singly underlined, and pop. " + L"Singly underlined is off after the pop."); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::NoUnderline; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::NoUnderline); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // save 'no underlined' state + cOptions = 1; + rgStackOptions[0] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::Underline; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + // set underlined + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::Underline; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // restore, expect no underline + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::NoUnderline); + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 6: Save 'no singly underlined' state, set doubly underlined, and pop. " + L"Doubly underlined is retained after the pop."); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::NoUnderline; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::NoUnderline); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // save no underline state + cOptions = 1; + rgStackOptions[0] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::Underline; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + // set doubly underlined + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::DoublyUnderlined; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // restore, expect doubly underlined + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); + + Log::Comment(L"Test 7: Save 'curly underlined' state, set doubly underlined, and pop. " + L"Curly underlined is restored after the pop."); + + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::Underline; + _testGetSet->MakeSubParamsAndRanges({ { 3 } }, subParams, subParamRanges); + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ std::span{ rgOptions, cOptions }, subParams, subParamRanges })); + + // save curly underlined state + cOptions = 1; + rgStackOptions[0] = (size_t)DispatchTypes::SgrSaveRestoreStackOptions::Underline; + VERIFY_IS_TRUE(_pDispatch->PushGraphicsRendition({ rgStackOptions, cOptions })); + + // set doubly underlined + cOptions = 1; + rgOptions[0] = DispatchTypes::GraphicsOptions::DoublyUnderlined; + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); + + // restore, expect curly underlined + _testGetSet->_expectedAttribute.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + VERIFY_IS_TRUE(_pDispatch->PopGraphicsRendition()); } TEST_METHOD(GraphicsPersistBrightnessTests) @@ -1713,12 +1798,20 @@ class AdapterTest _testGetSet->PrepData(); attribute = {}; attribute.SetIntense(true); - attribute.SetUnderlined(true); + attribute.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); attribute.SetReverseVideo(true); _testGetSet->_textBuffer->SetCurrentAttributes(attribute); requestSetting(L"m"); _testGetSet->ValidateInputEvent(L"\033P1$r0;1;4;7m\033\\"); + Log::Comment(L"Requesting SGR attributes (extended underline style)."); + _testGetSet->PrepData(); + attribute = {}; + attribute.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + _testGetSet->_textBuffer->SetCurrentAttributes(attribute); + requestSetting(L"m"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;4:3m\033\\"); + Log::Comment(L"Requesting SGR attributes (faint, blinking, invisible)."); _testGetSet->PrepData(); attribute = {}; @@ -1741,7 +1834,7 @@ class AdapterTest Log::Comment(L"Requesting SGR attributes (doubly underlined, overlined)."); _testGetSet->PrepData(); attribute = {}; - attribute.SetDoublyUnderlined(true); + attribute.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); attribute.SetOverlined(true); _testGetSet->_textBuffer->SetCurrentAttributes(attribute); requestSetting(L"m"); @@ -1770,18 +1863,20 @@ class AdapterTest attribute = {}; attribute.SetIndexedForeground256(123); attribute.SetIndexedBackground256(45); + attribute.SetUnderlineColor(TextColor{ 128, true }); _testGetSet->_textBuffer->SetCurrentAttributes(attribute); requestSetting(L"m"); - _testGetSet->ValidateInputEvent(L"\033P1$r0;38:5:123;48:5:45m\033\\"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;38:5:123;48:5:45;58:5:128m\033\\"); Log::Comment(L"Requesting SGR attributes (ITU RGB colors)."); _testGetSet->PrepData(); attribute = {}; attribute.SetForeground(RGB(12, 34, 56)); attribute.SetBackground(RGB(65, 43, 21)); + attribute.SetUnderlineColor(RGB(128, 222, 45)); _testGetSet->_textBuffer->SetCurrentAttributes(attribute); requestSetting(L"m"); - _testGetSet->ValidateInputEvent(L"\033P1$r0;38:2::12:34:56;48:2::65:43:21m\033\\"); + _testGetSet->ValidateInputEvent(L"\033P1$r0;38:2::12:34:56;48:2::65:43:21;58:2::128:222:45m\033\\"); Log::Comment(L"Requesting DECSCA attributes (unprotected)."); _testGetSet->PrepData(); @@ -1956,7 +2051,7 @@ class AdapterTest }); verifyChecksumReport(L"FECF"); outputTextWithAttributes(L"A"sv, [](auto& attr) { - attr.SetUnderlined(true); + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); }); verifyChecksumReport(L"FF3F"); outputTextWithAttributes(L"A"sv, [](auto& attr) { @@ -1973,7 +2068,7 @@ class AdapterTest verifyChecksumReport(L"FF47"); outputTextWithAttributes(L"A"sv, [](auto& attr) { attr.SetIntense(true); - attr.SetUnderlined(true); + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); attr.SetReverseVideo(true); }); verifyChecksumReport(L"FE9F"); @@ -2083,7 +2178,7 @@ class AdapterTest _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;A;@;@;0;2;@;BBBB\033\\"); Log::Comment(L"Underlined rendition"); - attributes.SetUnderlined(true); + attributes.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); textBuffer.SetCurrentAttributes(attributes); _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;C;@;@;0;2;@;BBBB\033\\"); @@ -2168,15 +2263,19 @@ class AdapterTest VERIFY_ARE_EQUAL(expectedPosition, textBuffer.GetCursor().GetPosition()); Log::Comment(L"Restore rendition attributes"); + + // In the following sequence, U (0101 0101) represents the bold, blinking, invisible attributes to be active. stateMachine.ProcessString(L"\033P1$t1;1;1;U;@;@;0;2;@;BBBB\033\\"); attributes = {}; - attributes.SetIntense(true); + attributes.SetIntense(true); // bold attributes.SetBlinking(true); attributes.SetInvisible(true); VERIFY_ARE_EQUAL(attributes, textBuffer.GetCurrentAttributes()); + + // In the following sequence, J (0100 1010) represents the underline, reverse video attributes to be active. stateMachine.ProcessString(L"\033P1$t1;1;1;J;A;@;0;2;@;BBBB\033\\"); attributes = {}; - attributes.SetUnderlined(true); + attributes.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); attributes.SetReverseVideo(true); attributes.SetProtected(true); VERIFY_ARE_EQUAL(attributes, textBuffer.GetCurrentAttributes()); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 321b86756fb..1f56c04b535 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -410,11 +410,11 @@ std::optional UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, V switch (val.lVal) { case TextDecorationLineStyle_None: - return !attr.IsUnderlined() && !attr.IsDoublyUnderlined(); + return !attr.IsUnderlined(); case TextDecorationLineStyle_Double: - return attr.IsDoublyUnderlined(); - case TextDecorationLineStyle_Single: - return attr.IsUnderlined(); + return attr.GetUnderlineStyle() == UnderlineStyle::DoublyUnderlined; + case TextDecorationLineStyle_Single: // singly underlined and extended styles are treated the same + return attr.IsUnderlined() && attr.GetUnderlineStyle() != UnderlineStyle::DoublyUnderlined; default: return std::nullopt; } @@ -694,19 +694,24 @@ bool UiaTextRangeBase::_initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT case UIA_UnderlineStyleAttributeId: { pRetVal->vt = VT_I4; - if (attr.IsDoublyUnderlined()) - { - pRetVal->lVal = TextDecorationLineStyle_Double; - } - else if (attr.IsUnderlined()) + const auto style = attr.GetUnderlineStyle(); + switch (style) { + case UnderlineStyle::SinglyUnderlined: pRetVal->lVal = TextDecorationLineStyle_Single; - } - else - { + return true; + case UnderlineStyle::DoublyUnderlined: + pRetVal->lVal = TextDecorationLineStyle_Double; + return true; + case UnderlineStyle::NoUnderline: pRetVal->lVal = TextDecorationLineStyle_None; + return true; + default: + // TODO: Handle other underline styles once they're supported in the graphic renderer. + // For now, extended styles are treated (and rendered) as single underline. + pRetVal->lVal = TextDecorationLineStyle_Single; + return true; } - return true; } default: // This attribute is not supported. diff --git a/src/types/sgrStack.cpp b/src/types/sgrStack.cpp index a342dc59a5f..77e1c62c596 100644 --- a/src/types/sgrStack.cpp +++ b/src/types/sgrStack.cpp @@ -117,6 +117,9 @@ namespace Microsoft::Console::VirtualTerminal // Ps = 3 1 -> Background color. // // (some closing braces for people with editors that get thrown off without them: }}) + // + // Additionally, we support extended underline styles to be pushed/popped + // using Parameter 4, except doubly underlined, which uses Parameter 21. // Intense = 1, if (validParts.test(SgrSaveRestoreStackOptions::Intense)) @@ -136,10 +139,55 @@ namespace Microsoft::Console::VirtualTerminal result.SetItalic(savedAttribute.IsItalic()); } - // Underline = 4, - if (validParts.test(SgrSaveRestoreStackOptions::Underline)) + // Underline = 4, DoublyUnderlined = 21, + const bool isUnderlinedValid = validParts.test(SgrSaveRestoreStackOptions::Underline); + const bool isDoublyUnderlinedValid = validParts.test(SgrSaveRestoreStackOptions::DoublyUnderlined); + if (isUnderlinedValid && isDoublyUnderlinedValid) + { + // all the styles are valid, we can simply apply the saved style. + result.SetUnderlineStyle(savedAttribute.GetUnderlineStyle()); + } + else if (isUnderlinedValid) + { + const auto savedUl = savedAttribute.GetUnderlineStyle(); + const auto isUnderlinedOn = savedUl != UnderlineStyle::NoUnderline && + savedUl != UnderlineStyle::DoublyUnderlined; + if (isUnderlinedOn) + { + result.SetUnderlineStyle(savedUl); + } + else + { + // Turn off singly and extended styles, but if the current style is + // doubly underlined, no need to overwrite it. This mimics having + // two flags each for singly and doubly underlined, where flag for + // doubly underlined would be left 'on' even if we had turned off + // the singly underlined. + if (result.GetUnderlineStyle() != UnderlineStyle::DoublyUnderlined) + { + result.SetUnderlineStyle(UnderlineStyle::NoUnderline); + } + } + } + else if (isDoublyUnderlinedValid) { - result.SetUnderlined(savedAttribute.IsUnderlined()); + const auto isDoublyUnderlinedOn = savedAttribute.GetUnderlineStyle() == UnderlineStyle::DoublyUnderlined; + if (isDoublyUnderlinedOn) + { + result.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined); + } + else + { + // Turn off doubly underlined, but if the current style is + // singly underlined (or extended style), no need to overwrite it. + // This mimics having two flags each for singly and doubly + // underlined, where flag for singly underlined would be left 'on' + // even if we had turned off the doubly underlined. + if (result.GetUnderlineStyle() == UnderlineStyle::DoublyUnderlined) + { + result.SetUnderlineStyle(UnderlineStyle::NoUnderline); + } + } } // Blink = 5, @@ -166,12 +214,6 @@ namespace Microsoft::Console::VirtualTerminal result.SetCrossedOut(savedAttribute.IsCrossedOut()); } - // DoublyUnderlined = 21, - if (validParts.test(SgrSaveRestoreStackOptions::DoublyUnderlined)) - { - result.SetDoublyUnderlined(savedAttribute.IsDoublyUnderlined()); - } - // SaveForegroundColor = 30, if (validParts.test(SgrSaveRestoreStackOptions::SaveForegroundColor)) { From 43535b0d1c8e486fb1c32dd907bc879f36cec61d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 8 Sep 2023 22:28:38 +0200 Subject: [PATCH 58/59] Fix identity of canary appxmanifest (#15945) This commit fixes the identity of our new canary packages. Additionally, it slightly reorders one block so that the file is almost entirely in the same layout as the preview appxmanifest, allowing for a better direct comparison (with git diff, etc.). --- .../CascadiaPackage/Package-Can.appxmanifest | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index ba8a2214075..d72c96e1405 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -7,17 +7,17 @@ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" + xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" - xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> @@ -72,17 +72,17 @@ + + + com.microsoft.windows.terminal.settings + + - - - com.microsoft.windows.terminal.settings - - + + From 4ddfc3eaf387b770530da8bb985bac9613063327 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Sat, 9 Sep 2023 00:43:32 +0200 Subject: [PATCH 59/59] COOKED_READ_DATA: Fix scrolling under ConPTY (#15930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes 3 bugs: * `COOKED_READ_DATA` failed to initialize its `_distanceCursor` and `_distanceEnd` members. I took this as an opportunity to make them `ptrdiff_t`, to reduce the likelihood of overflows in the future. * `COOKED_READ_DATA::_writeChars` added `scrollY` to the written distance, even though `WriteCharsLegacy` writes a negative value into that out parameter. This was fixed by changing `WriteCharsLegacy` to write positive values and by adding a debug assertion. * `StreamScrollRegion` calls `IncrementCircularBuffer` which causes a synchronous (!) ConPTY flush to the output pipe (side note: this is the primary reason why newlines are so slow in ConPTY). Since cooked reads are supposed to behave like a pager and not write into the scrollback, we temporarily mark the buffer as inactive which prevents `TextBuffer` from snitching about it to VtEngine. Even after this change, there's still some weird behavior left: * You cannot move your cursor back beyond (0,0), because this isn't a real pager-like implementation. That might be a neat future extension. * Writing a lot of text and pressing Ctrl+C doesn't properly place the cursor and scroll the buffer, unless the cursor is at the end. That might also be worth investigating in the future (minor issue). * When the viewport is full, backspacing more than 1 line of text (using Ctrl+Backspace) doesn't erase all of the affected lines, because `COOKED_READ_DATA::_erase` uses the same `WriteCharsLegacy` function to write whitespace to erase that text. It's only gone after typing one more character. I've written the code to mostly fix this, but decided against it as I considered the problem to be too niche to warrant extra code. Closes #15899 ## Validation Steps Performed * Generate some text to paste in PowerShell: ```pwsh "" + (0..512 | % { "word" + $_.ToString().PadLeft(4, "0") }) ``` * Launch cmd.exe and paste that text * No flickering ✅ * No writing into the scrollback ✅ * No weird behavior when backspacing ✅ --- .../ConptyRoundtripTests.cpp | 2 +- src/host/_stream.cpp | 45 +++++++++++++++++-- src/host/output.cpp | 32 ------------- src/host/output.h | 2 - src/host/readDataCooked.cpp | 16 ++++--- src/host/readDataCooked.hpp | 12 ++--- 6 files changed, 57 insertions(+), 52 deletions(-) diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 3c5136b1910..99c2bd02ec9 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -3401,7 +3401,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() } else if (writingMethod == PrintWithWriteCharsLegacy) { - WriteCharsLegacy(si, str, true, nullptr); + WriteCharsLegacy(si, str, false, nullptr); } }; diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 5ff5c68f5db..d9858c7e003 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -70,13 +70,50 @@ static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point if (coordCursor.y >= bufferSize.height) { - // At the end of the buffer. Scroll contents of screen buffer so new position is visible. - StreamScrollRegion(screenInfo); + const auto vtIo = ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo(); + const auto renderer = ServiceLocator::LocateGlobals().pRender; + const auto needsConPTYWorkaround = interactive && vtIo->IsUsingVt(); + auto& buffer = screenInfo.GetTextBuffer(); + const auto isActiveBuffer = buffer.IsActiveBuffer(); + + // ConPTY translates scrolling into newlines. We don't want that during cooked reads (= "cmd.exe prompts") + // because the entire prompt is supposed to fit into the VT viewport, with no scrollback. If we didn't do that, + // any prompt larger than the viewport will cause >1 lines to be added to scrollback, even if typing backspaces. + // You can test this by removing this branch, launch Windows Terminal, and fill the entire viewport with text in cmd.exe. + if (needsConPTYWorkaround) + { + buffer.SetAsActiveBuffer(false); + buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes()); + buffer.SetAsActiveBuffer(isActiveBuffer); + + if (isActiveBuffer && renderer) + { + renderer->TriggerRedrawAll(); + } + } + else + { + buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes()); + + if (isActiveBuffer) + { + if (const auto notifier = ServiceLocator::LocateAccessibilityNotifier()) + { + notifier->NotifyConsoleUpdateScrollEvent(0, -1); + } + if (renderer) + { + static constexpr til::point delta{ 0, -1 }; + renderer->TriggerScroll(&delta); + } + } + } - if (nullptr != psScrollY) + if (psScrollY) { - *psScrollY += bufferSize.height - coordCursor.y - 1; + *psScrollY += 1; } + coordCursor.y = bufferSize.height - 1; } diff --git a/src/host/output.cpp b/src/host/output.cpp index beae8a8ef98..bd168fce9cd 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -293,38 +293,6 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source textBuffer.TriggerRedraw(fill); } -// Routine Description: -// - This routine is a special-purpose scroll for use by AdjustCursorPosition. -// Arguments: -// - screenInfo - reference to screen buffer info. -// Return Value: -// - true if we succeeded in scrolling the buffer, otherwise false (if we're out of memory) -void StreamScrollRegion(SCREEN_INFORMATION& screenInfo) -{ - // Rotate the circular buffer around and wipe out the previous final line. - auto& buffer = screenInfo.GetTextBuffer(); - buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes()); - - // Trigger a graphical update if we're active. - if (screenInfo.IsActiveScreenBuffer()) - { - til::point coordDelta; - coordDelta.y = -1; - - auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); - if (pNotifier) - { - // Notify accessibility that a scroll has occurred. - pNotifier->NotifyConsoleUpdateScrollEvent(coordDelta.x, coordDelta.y); - } - - if (ServiceLocator::LocateGlobals().pRender != nullptr) - { - ServiceLocator::LocateGlobals().pRender->TriggerScroll(&coordDelta); - } - } -} - // Routine Description: // - This routine copies ScrollRectangle to DestinationOrigin then fills in ScrollRectangle with Fill. // - The scroll region is copied to a third buffer, the scroll region is filled, then the original contents of the scroll region are copied to the destination. diff --git a/src/host/output.h b/src/host/output.h index fff6c001c25..ffbe75b3ad6 100644 --- a/src/host/output.h +++ b/src/host/output.h @@ -47,7 +47,5 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo, VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pProcessData); -void StreamScrollRegion(SCREEN_INFORMATION& screenInfo); - // For handling process handle state, not the window state itself. void CloseConsoleProcessState(); diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 83d2f21b24a..a5859d6766f 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -752,7 +752,7 @@ void COOKED_READ_DATA::_flushBuffer() const auto distanceBeforeCursor = _writeChars(view.substr(0, _bufferCursor)); const auto distanceAfterCursor = _writeChars(view.substr(_bufferCursor)); const auto distanceEnd = distanceBeforeCursor + distanceAfterCursor; - const auto eraseDistance = std::max(0, _distanceEnd - distanceEnd); + const auto eraseDistance = std::max(0, _distanceEnd - distanceEnd); // If the contents of _buffer became shorter we'll have to erase the previously printed contents. _erase(eraseDistance); @@ -766,7 +766,7 @@ void COOKED_READ_DATA::_flushBuffer() } // This is just a small helper to fill the next N cells starting at the current cursor position with whitespace. -void COOKED_READ_DATA::_erase(const til::CoordType distance) const +void COOKED_READ_DATA::_erase(ptrdiff_t distance) const { if (distance <= 0) { @@ -793,7 +793,7 @@ void COOKED_READ_DATA::_erase(const til::CoordType distance) const // A helper to write text and calculate the number of cells we've written. // _unwindCursorPosition then allows us to go that many cells back. Tracking cells instead of explicit // buffer positions allows us to pay no further mind to whether the buffer scrolled up or not. -til::CoordType COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const +ptrdiff_t COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const { if (text.empty()) { @@ -802,18 +802,20 @@ til::CoordType COOKED_READ_DATA::_writeChars(const std::wstring_view& text) cons const auto& textBuffer = _screenInfo.GetTextBuffer(); const auto& cursor = textBuffer.GetCursor(); - const auto width = textBuffer.GetSize().Width(); + const auto width = static_cast(textBuffer.GetSize().Width()); const auto initialCursorPos = cursor.GetPosition(); til::CoordType scrollY = 0; WriteCharsLegacy(_screenInfo, text, true, &scrollY); const auto finalCursorPos = cursor.GetPosition(); - return (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; + const auto distance = (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; + assert(distance >= 0); + return distance; } // Moves the given point by the given distance inside the text buffer, as if moving a cursor with the left/right arrow keys. -til::point COOKED_READ_DATA::_offsetPosition(til::point pos, til::CoordType distance) const +til::point COOKED_READ_DATA::_offsetPosition(til::point pos, ptrdiff_t distance) const { const auto size = _screenInfo.GetTextBuffer().GetSize().Dimensions(); const auto w = static_cast(size.width); @@ -832,7 +834,7 @@ til::point COOKED_READ_DATA::_offsetPosition(til::point pos, til::CoordType dist // This moves the cursor `distance`-many cells back up in the buffer. // It's intended to be used in combination with _writeChars. -void COOKED_READ_DATA::_unwindCursorPosition(til::CoordType distance) const +void COOKED_READ_DATA::_unwindCursorPosition(ptrdiff_t distance) const { if (distance <= 0) { diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 49f6c06bc9b..8f1e5849e81 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -113,10 +113,10 @@ class COOKED_READ_DATA final : public ReadData void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); void _markAsDirty(); void _flushBuffer(); - void _erase(til::CoordType distance) const; - til::CoordType _writeChars(const std::wstring_view& text) const; - til::point _offsetPosition(til::point pos, til::CoordType distance) const; - void _unwindCursorPosition(til::CoordType distance) const; + void _erase(ptrdiff_t distance) const; + ptrdiff_t _writeChars(const std::wstring_view& text) const; + til::point _offsetPosition(til::point pos, ptrdiff_t distance) const; + void _unwindCursorPosition(ptrdiff_t distance) const; void _replaceBuffer(const std::wstring_view& str); void _popupPush(PopupKind kind); @@ -140,8 +140,8 @@ class COOKED_READ_DATA final : public ReadData std::wstring _buffer; size_t _bufferCursor = 0; - til::CoordType _distanceCursor; - til::CoordType _distanceEnd; + ptrdiff_t _distanceCursor = 0; + ptrdiff_t _distanceEnd = 0; bool _bufferDirty = false; bool _insertMode = false;

@o}Od<$c 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 SrMs#2K>V$N? zpoFAuM;kILi)$k@4W-oS+*)xf#n8p#&z-aV)%Tp|Jm>p)zu)&g&p*%2vtBMLAT1C8 z04i>-&OQLJ$7HvZ|LC%Abh}GJPc2)yES6lA!>Qkp%!-GL-Qi z0OAn9Yp52>5n8CRHV6i zq7-M9G?WpLv0EBD0HAQe&DrTf;^@LS{>DYJrsDEWC$<_=n0)vEN$*}bQFygGaHOW33yj(0u6uqkshuaSNj|~ zG0)3_s=#o~3HV3`c^0` zni2nr_7UYp#ZLzx1kr8m+05z0G?crZoNG&#b3@L8poN8V8C z&SVQgW#RmXfYK6&B!if-Wf*s61wIG)onj(r(XJS=F~aur;fv8+O4Uc5(orQ}iFNIr zRV#INloYM=LaRn_yNIxcm8%<_z}LvLn`dr`Veyg0bq8zSmcX!GlFsO0BOJf!^)B$A z$L+_)m?rG{H0-dM;(#|fVisr8s+*wlzMfnWm?cfsD_O}X+>Dhr)>iWLmo&$dO-0i^ zAH@PS(#HFt2$m4CIx)a!H_$JsF=onp%2Xq*_l5L+zr1mbExu`sWt#;(txO-5i$|6F zFg)x#d?Q@SeU<_?=7&|11^zj78=Z+*+BRF9>^tqg`C*?TmAojl;Yru+%%21$SqRBg zs}^Wy$xShLmB8tirW*FV9lPs-p4^x%P6}9jnNm1bwjGyXCB3A23zAYT>1cmhVa@5W zF%az(T&@MDwfR&PCD&9}>*+MNd;1=5YQeIr=dMm-6Di?=t!5~g2dmdKDJ^x$dEI(Q zzVPwNtYMG%(BzILUn@O4cUwENJS@A%zcH<-xndJ%#1(nE6;zsi!#BBJUoeGZX)*Z6 z?rV>4k<*6O^ogN|!-Iy!bctg8<)e;N|Hy0?$@*5v->IMOGPS#Emjy_?nJ-4nUaM0P zfgrkxu!|*Nn42_TWYyZmL`Px@#vCJVxUI|#-0xbc(plJgv-q2j0^7lORjiz*v`vGH z>te?9B0N+r@Hn|^dpwTtb2W|6habv*v5z$NZ*UYDp0%y~8JGJ8M9mJ3G0LZG6l z*tCKPQvA5@@t{6P!F+FX3%vf2KQR)!D)%Lb?sB`c6rqZ3(lOCj+T&i+Wq8P`ij)++{-654 zs~0bLsOEyda3jg$3tJE(&6&*Lx&9(kMl=+T7DA+vkil^zSpzH(7M7-nlcp9H7tGC( zRwt2`=B9`fNCX09PLKXqASxy#JT&S516CB#AF_bv?hofG1IANNO-K YmIRN9BQqE6wPYs14dvy`IYY?&2L%PmGXMYp literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-400.png b/res/terminal/images-Can/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..93140af5acff2147f2746bc18cc34e305f346391 GIT binary patch literal 9151 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-black.png b/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..543f5bc8178184f95c63d79d09810ad07ef60fed GIT binary patch literal 3117 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-white.png b/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a77e7292eb0451840330d14d700027798becf702 GIT binary patch literal 3073 zcmeHJ`!`$Z8s5`t-RjZ{qi(Hxhp2?2?nTultzJb!s!<_D-4c-|su#{MMp0B!L9|-B zBqzuRBI%}OD<`0;?*7rWo`|S7G-@D&$|FE-9 z1p2C}=&Aq!pyuy)_!IzaLT)T21yI?Qa3UB41)S$GPXPESUv)iv3z+YX@H=%304U}F zaP1!eunJ1orT~Bl1%Me0065}m0thDo@XwN%)Lx)#}W-*yw86a`65odYO5460ldlO=6iBYgGYO%(s#{{rEra(6T`yoMfUihU$ePPxzbS| z4m~ECzu(OZmu8zqGrayvfmP$q*iz*?#Xc+hNnipk!`KV6s}!diiUKsAC8i6{+Mcyq1|Y5vtTf(E_^x-OWK9xwil(eD=) z{>?)pTKn@d($2gWIsKKKgCK%xyOH?zV`+gJ{I%kegHWGwpET-`6IOZ}?x8aIyPa;wfZz;)f_R@)v{a;j zZZ&D&#%VC9)$})Yo;r>wk6t~v$BxL2LKr(wX9YQw#C^bXh zdYSDa;`rn>FO3(!4*%JTOrMPnpYMHT6933dRn<*@C$9H`X@auL>?0GYCAn7Kmt2M+ z4tAp5pCcYq4NLQ6KR>RKELp$0#9f7}_elQpBrCSMu1#>hRQR$(OU(>fZWmu0HmRij zn=;}>yH=bzvfMbhp%g>>0o|XHF5HdxSyM*%wrkmIvIBKMn*_?OJgLCi{Jcw8jw<{@ zH8j|&gjfQz5r_OOH0iZ8B}C7V~o@)vWhW53Ja}fOpwKeRxzLsa#N%h z$jy+#0;?G_WO0F2j5#t0-+N6xu4psGr9X#dR?pC zHXrHJo2zefe7Ydl&iim$@_JDkZBAjYOpjGwZ}fo8xqTom436y=zx*r@l~Y>ALUDE$ zxf41Fvpjurj(O{D*Mp*o$4)6jvwMm|7hB$*rCc;W!m)gxzg?7YzBVk;Z@v>|!v4Zw zFMWP!2Y$vZ-w+;`8&K#*AS~;If7*!;oGvK2c^0nmY!q4ihR_KS#Rb+5O(ys+w}0bg zYct3De6y@%mV{i(9;KQNTi11*-26opR#vyo?QH`re;aq%qiVt73~nN0Se4>v|KY^i zyNvmTk(9a5Ep=)~Kb9J0^)1ZviX&2I6^A_Hr#x3G@;t!yt;6k`qI`zBpCAZfXVoaq zxJhd3q)2)vTJ$9{a1YYGQf`1TR$v+1S+Hujls4w|DsLQeZQfPKZ9D2Nw40)wv6lXtv8ZJc@|=cfeDvuwWX!xpz*~xZ+BFpJm=%EP0qPy zoXv++L|_stRWfBz>jG_{VS-}d``ek#c04Ig?FujXiu=ti!xlNRr;xY2E2nGq>zBj4 z7mF@-X5lWc`B^Kl&gZMUeF`XQPW^GVuA?G~tg0csu~o5zE4ky6D|?Bma9UDDSq7KC z*3!-ubkSrb7fU8ba8ijYwCA55rHu=OPWR=9j{bn@_tGPzj0-A9 z|1j+^FhVv6RtT}<0&$azi*_S^TMpIP#zLNUjxXf8>7^j0f1qL5uB&P-R0mB+$V?sA zRTLjO;wX}MyRciiet8EY~H%(a}hu#^?>%IWz8+=e0VnXPw z(ks>Sw1|UkG0Y{Gt2>uMX{Bpab$%p)_>1AEO#C)h&3v~&VA7p`b)|g$lH^fx+)QZd z?_{w}XX&H0rm6v1?se;GI_Ai|tBm68ckhdI1yUL{t)F|$;^`yM_IR(lig|qlfv^^~t zdF-(%PIA8)&$F3}sEzB6_~;qmz7U|X>F+%!_wszLggVwgTXJ9br0|%WC*jhvFouSYx`q-KOF~lWcqoc_G#{m1&;9m2* x3pDKB*x__SR5B?X9|@31r2WzHv4n{5#K`>#cwFhctL{c){yu?+YrQb({{n=jxNZOd literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-100.png b/res/terminal/images-Can/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..15abb73cd3ac47c90cc81952979442e6dc4a253b GIT binary patch literal 1331 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv7~+mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fHRz`*!2z$e5NsNf73oIZVe z`t<3ksi|pT02Bk#zxS*C-=p~FfQo~IgNuucy}dnqf1Aqh11dlU z2pv=f>Y6!oCQ#$Vi4!MGn9$$fUs6(1TwGjKR1_8#78)8F5)$I$FMd= z;ogozK-`UyO$;rvl(Gh41(5~$4Y&$!<*|TRqdGch}u3dqFfsY?Qe)#a= zg9i_G?AT#zYunh^c<y;D%JCk2DL#CD*4$GHMIWtEsE!rVlxO>~SGh4+DAkv5WuruWqR^LJ$}OqFbIQeD zZ&ph1dsHii`N&D?pBmOU-xo-c>83S~m6H1gc_8 z@^*J&=wOxg0CG4BJR*yM)C~}3JicJ(TA(0%iKnkC`*RL4Zhd2AzLF-Op68w}jv*GO z-(L70EbJ(9;A6x_rLv`}O^v5h-+wE+9-70wt3mMV24MkHv82fA@GA+98^7zV_^Fr z%BJfF5>me(oafvgJW1ZN>W#&f>(l*~KQzoP<2*J)ZHsMT(Cy&mQ*X`*@{zDE7F+Y+ zc|y&!Vlc64!Poc&t3`>_0DUo-Kb;ZIoV(=0i69FCU(2fBkj#cKLf_dAmBU2RmJ-v;O&c`g;ET zs~=AE&p*7MX-7?*>(M_k++u8f3&mRNu0K-s?wzK~@B3xF8P^ZXO>^zeX^R0b*$|wcR#Ki=l*&+EUaps!mtCBkSdglhUz9%kosASw5re0zpUXO@geCy1 C9yi4R literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..a50b0a4ecee70578c7f36a8340496a8c236c482f GIT binary patch literal 667 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!0wlNePU-?uEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4}~E}a3I%9!ct;uzv_{O#4fUWWrDj(uFd zR>8vJWxz{?AErA(qAnbD)Zpq0-rd3RO5p`>Ny$swEny2~WxAUyc=_U*fZ{Jg|GtMsBdf>)^*wD<2RoA%KcdSU7u-l|Rg;DX_OS5$! zOg_2pK6JfjR*7B24gI%_+b(@Rc&Om$2G8}s8~$4y_pdTvFXSj|-cZbTwyL}-$8~aV z5bvj_Ka*__B&$VljNIcki#IMp>8#lLHu`3rEmh3rYwmHiFOWW`Gd^gpz zUQ!o#dhNQtc%JE!E}={1OU|zpeR}SGef7HxhJwT5@>ePs_|5eh@|j_<_nf}; z3twir@VcA8cu*~IjVMV;EJ?LWE=mPb3`Pb<#<~U;x<*DJhK5!q=2oVbx(23J1_qA1 zvYAmdBuF?hQAxvX!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4}dum0JEamTas2JoyYjEne=-3gWuBjo*B0f=UmxYIDj2C-h zp%XLF8VY{`Dmx`^Zms-!4}b?J58p?1&dlwEjduR)Jb1! zX>PuS&24il$*_;!hw6ft{%>U6D!Qfo@G{GUod*)9|4fg5$S5A#QzErm&70xP&ae*$ zp0V3IS={2E_xS3f?~7ZVFaO&xcVD-d*ylZwUm`gw9Ay2ql4DC3X_+fd`?29#bYGEn z-3_^&8uoD>*W~Lq-q>^cL9x^vqjRRbeiDy6)O?K&K9I5H58D#)xJf8hY-jppD{Bdr zrgtB?R?Drw{b+*vwUswJ)wB`Jv|saDBFsX&Us$iT>0*T6#8$SB0n(8|Qz%G6TVz|_jX zz;RbLGm3`X{FKbJO57R@o?qS%)F276Aviy+q&%@Gm7%=6TrV>(yEr+qAXP8FD1G)j R8!4b722WQ%mvv4FO#lFB_@V#+ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-125.png b/res/terminal/images-Can/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..dafd4c9258b04bf7f32853c34835c3be625471b1 GIT binary patch literal 1585 zcmZuxcTkf_6#sHULK8)L5Md(9h?G!L_y{E-KtdBCN+{AHAaHO%ga}at5u^#;9S}m1 zP~>n9&WNCw!yrg;1i?rzkz#LHIYH1{=Ki{w+u66T?EAg9`}WPA_wsbvu7Fm6AZRJ zG6aI)3JB6ntorjX34(rLdbu;4fWy$xkVGPBZ*Q-yt&NF^`PTxBEiElkQBf=w3&@d? zkwE5hx&HqC&d$yb4i0v9b|exBhG8O+7{#>we#da5d+$cKfxW#w*aEAir6q9aDKiGf zO)zg800B%8jmP7GXcZL|<>loZ4ks-wjmc#C`ue)NySur$xw^X2>2w;6=HlW4aydCU zQK?i%M@I^ULMD?zY7iWpDmFG2BuY(9U0hsTT3X7=$_fYwSYBR!^ypDWMux4eZGV6N z!oq@7DxI5~OHWVl>FJrDpP!wbot~Zs7XfDzi^VfDGgDJj6B83QHa6fcU0q$1lau4) z<6~oEd_LdW+FB$MJ$(2O)aSv22PGvX1Oj1XWMp)7G$JCRva)h;a1d0cxw*NnuCA=C zOehpKH8lwYf~u;joSYmy9#1CQGdy75L-0{g>mwdShC9K>4PRehf9cYtf`S4Z4o7yd zlhhvoWoc||Y-nf*^tLH1EUc-i$mzWG+IsO?R89XoEmH_ z_oZtu{(u}Pes*?tUS8h)YTcC%%=#?@Uy4#NO__f}<6M-g#eP&SOZ9*r61+vgVM0-y z+Us@g3>buot94!^(kcgQkz0sOI&Nu}ledN1Nj4=k8 z7NBx14^2EMH`$IwjAwO*DzfrI%9%8tD?Q2 zg$xHXGy&V6;_c}JDX6IIR6!Je>wdg%jgvXe<7IA*QgrRtwSr9r71Ix=hVCB2wTbxv zfe3SRp+b4Gx-_83C(`_qAxOz{ix8(%3%5d}Qs^E|sAqB-ir8&~MXrBE{a!?j~vUHTqQC<x&f(z9zYw^`AAEj-XPv5O9 zMSWqdvxYT@>sW|U`O&<}YQUb55Oizs;jb;Ldv$|QJ8rCT6csc~FUquJl|0od`&nOn zBZR&v$?!AKKI>Ia{EJ##&aP`t^n3q7J)f$ajUMK?J(lg;>8sIZck#8fQ!Eg= z4!GMou%cHapTrM zttnE1#oPp{eP#1~d{{(Mz4u&byNO45zXVONtG@Tki{R&mE(k_W2z&7~XvBRfPRx{u z(3D6xJSh?|$O3C&X@Vu1SXeO3&0#AdjI}bs;$SS6cP`lIKY;jzh?pqO{{R4f3t*Bl xtpxzuTN!*3qEb>rlOiGT*O|ur7@Hg(niy%Cki_D@C82>6M0fI}wo*c}{sFNNrE>rP literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..de7ae9e6e877e25c83f54ca9fabbe02f316805c8 GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^<{-?$0wkF(Nu32!Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4V8%2%~UC1_s8fo-U3d5r^Mi+wU!%D0BQ{ zdEF1@DQz5_%P!qG==jHQXSt^2t)q@58bKO;Q=@LxFJQfR(pQ8<(0%!qSg9Riu`IEn zZ!R2N8}K42NTj94N+Z4M*~z)n=QP{Qe>2rhCk~Kcg7Lje+ne1PC zK7dW{%iW4s_ur>qZz!(l5?)*P*HdF<@QUF0t3FJ{k&3g}n2WW$wleUX@yyn{u}=SD z@mhxbqgR^@Ydhvmo)#;h>vlf$>yf8bi=M5jdYe>O+cj-<*0rOPwA_Bb$=bMv&pBcH zgRG4{9&gm9n;d)}v2WL@Pje?)Y5V=;{Br5IykCP1zvW|z2F4(k6JPhq{oZy_uxd%m zv@Hu;K6zyXyE|zvy?Z_`;az)?lccPK^iz)57mI&wT^Q%7|Ha4MNu76A=AZk#j|`5Q zE#}?+eP%unL+ZWYB&LQ;u0KX=@;_9|rgbOI&(oQZEGy8y#gOknv0Cr(t7`oGzjH3~ z+bnRIciQ-uM)Dl?C;!-%-|~o$EuElrPD{1Sgcy WqV(DCY@~pS7(8A5T-G@yGywp>CN)g} literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a07b08d631355805db6d3df807954de245c59a35 GIT binary patch literal 777 zcmeAS@N?(olHy`uVBq!ia0vp^<{-?$0wkF(Nu32!Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4V8%2%~UC1_s9Ko-U3d5r^Mi-R~`(D0BSd zcTx84TT268T+wTinA_^7>zgrckzbxtN3+IOi_T`fb-Eq0-tCw4bouo39zBqMu!NEQ zp!|Z1Q?4y@Q&Bm4+;QhI75{fV-z}y*|E%-V4(kWp;po$*k>90!piw^&h%m zw{rP6kG{@RPv3`rd7jbv=34X&?njFbGek98oZgvyZudRkmEX3c-TILj@klW8Q`^Rf zqrXIBd169t|J*-6R#xwz7U$_b^=f-A?Oal<+PmCyh1fLfYpHphZBN}oyO?-RFBFtN z^LlHop3n!i7|V!DYijv=e=aado&R~uvts+);(2yIZp44R_0&f9M}%W-+5A|`vNd|U z^V2M+Ws0nRG|T5*^|AHqjP|5#KGws&h1p%^!-VAyvz*^u<~(3_b3t$H{kwh%l8+~S z3%#BJ56|{WDvHy3) zIQM;Q>fKd--Fr^%g5GkKNR|iO+BRlq)_<<{HFVHgAaQw#B%94H%?&IEjP6_RwiH*h z<=MdgNAaZHXNH^&e#IW|dM;=d6qQa@_wEdwWb?e`{F)2(mJ_(6lAA8G?ceaK?9}Yr zSI->t)x7$D3zhSyj(9cFS|H7 au^?41zbJk7I~ysWA_h-aKbLh*2~7Y-5j{Tu literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-150.png b/res/terminal/images-Can/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..c40a92a47ad58254812cfc3b702252ea8448a51d GIT binary patch literal 1732 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..5a07009093316d176bd635ee60b9ddde58dc45ce GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|0wg_SPb32=mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5SklRjhKRTX}jt@Xhz+YE_RFOq}Xd zzSz5+D^PKzKl{wmlIO>*PoL5FXm|hgnLT?x+kLKkR?E(&Bi4O1Y2%;tFM&>VU;Vzj zI`C#1t=Q|5|FmJNoRDRdaP@((qBNnCnhaZJdM@dkTCif}9u-xal%q2wmDjge3KvFj zCU@#&ubJo@duHqP_-pB&m$t2AsWVgx4e1QEtO+zT?3`e+EoS4HrBYp|cg4Lgb@5r~ zGS$OHd6x^5)K!l;SAMLGUanXe^w?p$zcbU^>x;aT?a!)K9Z{VXn{K`A?y6&l@7=WI z|G9M9m&X?CKE$4VxqrrSQLz%mzp7HZok}`cL~K;DY*bG@d8s3R{_^p4>wjuoZxC9q zVc^HT;i&hv$`i5-X%&yH(@qHeOC2CK<7@PF@Y&b9Xy~0=aaO38J6P8vlpX<+duuir~m?-aH8+?FU ztfo5cP~P6t3m&qRO?ciQXx5<9|I;}<)G_2pV%+Aa>!GnCPOQGGW36Wi6dn<5tu>2D ze{J*7GXHd;&S#S#nfTQ|oj1B{tobuh$>1C>Tf{?VUx(()5k|Y?^^ZzyQ+cm-N%;Pq zXmI3M3jbmD&pF%crk~&nOem@)t`Q|Ei6yC4$wjF^iowXh$XM6FLf6PB#L&>n#KOwZ zMAyL7%D^C0Cw>cxhTQy=%(P0}8dM6Oa{x6+f@}!RPb(=;EJ|f4FE7{2%*!rLPAo{( X%P&fw{mw=TsEEPS)z4*}Q$iB}yWc!w literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-200.png b/res/terminal/images-Can/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..58dd848bd262f78302126dcb950d04774891aaee GIT binary patch literal 2207 zcmZ`)c{tQtAOFoD#!wg8*UX@7OJk*xqrNWyx-?J-|hK+&gXm1dCoaW4tAD;{8Ib?00`o7=I21` z@^kQUf!e~l>kw$T$tJcY08oD$&LQ%Ev4RiooGk!cRR)0AcmVhgrefCsAPfZn>jVJ6 zj3!Gc%+2!p{OlgT6!$DRDZHoNna70kGGF z;B+C4|B*9i&VVkcjf{*83=A+B%vjm+y>2D2=HQVs=fA}u_MlC3uNw&_Ih}}uP9#VH zGJ-SP+S=OK*jQUzTUlA*@pv2#XK86^VPS#AV$IFX&CJY9O-)TqOn$L|6j@nW_4V}x z0-?CLcx7ef&6_tfGc!aYv8=3&&1NqzFHcNNT)cSE(9kdoCJe0+9?3=}k{h zPfbm^ySr1V)XB-ov9YnBpdd6F9U2-sGBQG;P=z8XO#SaB!%os33ms;sOmDJi*g=T1&e&fU9r zeO=IH@h59jH18*C)Fi2wmzNh675RIhzdioBZ*FdG;OS^-X+6wR|Na;`St>tRDEq!q z@$Xi|b{pbz2cj}YLPnGuJX0KF`6LU&yaO6gk#kCbaiz{@5ucXCy7V%JDUhh zl*+ZHN(l)@s&7Fp!HsX$ALRN@~Za6afWy3C6_ z*y27&wgm)@>2vfLrb8!LhP2G2C!h9YbXGS)czb0pSS6~V3L=Lx5e_V@`=GwA`=+5 zaqt3q;>}H*TzWUgzqva(OYjEIe!0f_!ZXahy4jbP`}|#AE>t&H4SgXs1X+i}Ne9RK z7c0~zS_dgnz4tyQ21=LcK%Ijy!ejrSlo|wKbzMu&ScUmd6xqxWd=V$E^Jl z4f-72rms6QP)Cn>+?;v$J2^WC4;^xJuX7bkJ>qi4sS=_edOjlJod67N0gs>1QcbnL z9Lpwc{7oUKsj4AJ4afPQr96ZgRKZC*r=6K>QPFJYVuxKBNrM)|+th0Rvltt(8zM;G zSBIUQ6W$Ki^cPm*O4cY(WX`9I5(7riUVl2Xlcfj&0loF#lGX7(^7X_VSZ2Z^&)M^A zc#rmjrHl-h`6#5d=E^-C>4~bkN92bJC)_wMTw0N0YBkkEmJJ`=G(*A)VeXO2i5bFy zE{QYkxF62<6g%hvZR+SoGBj1@QTwO7(RN(f?t*1=Y z#fGv|>BK1tEC5AZpeU`h6H3a%UM#P#m*YLL$&qdGr0&TFgtnK$8Uqn&-8^_Rv&3_n zM0SYkrFosj5^OgqFzK_?*zR_h5?ARdIEtD=>YYxAj?-;Ya+`ImA>k4Cdb)HM#z#g% z8VlXSZQkxKnjJQLC9B*b*jm+WuF~o$DL&u+dgsqxPvQrwjMFEw*VHc`sc1isWIw^m zdU=7sqXvhM!oV|XW(CCIGq*f4JIc_Zwo73d#X z*H)d~sv7Rh`J)pw0s_hMCjmZ&Vy;tM*Z5A3A|0OwfojFIiW_-|-a`I8?(gm+;18^HnOCEoV z-WNB0J^JoH5R&}5yCXem7Q=d`JEhM1wWfvTx+3_DCOArEdoItWmQr0ByFU3 zzWy%y@qOol4q9u+mgOf9)3Kzl6IC|1@<%W0V0fEtZ+zrO1FWd!3n!Tvx~HQPH9n$A zX9o>NbN2g~aWT8KPlsX-8tdh^o-KCTpjUH!ZtE`$!q6&idi`Toj)azi5B-Lu)g?#! z?Zu+#XM??ZZ_)tio0ZyY% z|E_^TYn(pqbm|mF3ynFYr-9POpio+SM%4cS1P1x~`$hbJ0Jh@ha}Xf;lff~_k4hs_ kNC5aRYWiOe2=yTblQe@UV3oBOs5n zz$3DlfkAjZ2s0)>>pKh-WH0gbb!C6fA;xVWY5IQiDWIlno-U3d8Ta1K^7jx)lxUYf z%3&&;GL_FWMBJO>jE8lEh-hnRhuCWYO^FT<36B>G4XiUNUj$6wCCM>KG^D*t%!|K~ zQ`PapPUm(dfnx53H{#pduHLW{AMn)AES$ZY4F*9DSgei4;^I}RMo zx%zd3Am7XnCYM)xRy43lNu1cciTT^@D|LysRk8OOtJ9AjYW9q6`E^)7W~$Ph>Qr_8 z>$Z0$hVd7umj8Xh`g>mWWutSs`o9%q)@;&0+qgD9T zHqH0^n>S1eSngY?Baqm?efsA!H|E`bldHM^$Mtv5uVkG5UR4pT#1Sjs`gT|Au7|tV z2j!RyHd1-tffu&;dGT(Du+xkIjb<^^plr|g<>T&AS(j>UV16;D{A8=`C2 z7IcZ3F$vrFpWC}>zjV{^ z^ZXtlHhcdG?^{1z%{dv~eVBFlPs#-;d%qqD!&7^o&wjaNcF4;1d%2>!G+)jZ__e5N zVfZAs*0M;>@@I?M6ZQK$COufV`kwD|&Hz7FhWwey5*gc$sl8-awev~-p-Eq2H%HEU zyVglCSHQ_x)3(NsThlh?dHErR)OlwAXMZ)h=(jp_{xjwGkqK)<-Cr_$#jKsWbc6Z2 zBD@)4NY>D-yGda-KihvOIGC1SP5KHPfcE)X%tX z`+UDVEM)-`SmM`D>{-Tl9xzW`49aY(C9V-ADTyViR>?)FK#IZ0z{ptFz(Uu^D8$gv z%EZFT$W+(B)XKo1_Db+$6b-rgDVb@NxHa%}zK#cKkObKfoS#-wo>-L1P+nfHmzkGc coSayYs+V7sKKq@G6i^X^r>mdKI;Vst03^=1UjP6A literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c591574d12a6538326cb4ab4efee06f31eccf GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL0wizG`LQ2Jv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i!>V3oBOs5n zz$3DlfkAjZ2s0)>>pKh-WH0gbb!C6fA;xVWY5IQiDWIkUo-U3d8Ta1K^7RM_lxUMb zsuA>itC?;TyJJLxQ-ToZ)|C#13titu?aWZV=C)FdS@f!trqhf?t}`zv>8zOG;i0r~ z>ceG={W(R6^Y;RqB&Z79!xjWY7$9~M)Y*l=2?{~}hHRaFF@JqBMLeYQu&@aO7 z+FP@$_CA`^`pqqT&zbJe6SSs>%?$pb*2~(pYL1#q@w0(u?EZr3VjXMP9u|8oTm7R|eM!O8zg+PX4uyV;KRnUo>7Hww z>K{&++AP2qYANwhU~03_xg~!#?PxXDR&)68CbT$T;dQbIiT!^_ z@wHX!={>*hFE(3hf4nfR!eZUby$n8|I{F=?tr>ne%HMDJwPgJ-eWS^>8`ib#*YVzd zFgfVr(d4?-t{EvC?H5}u>|3%q@AHHW&&zj(Jqx;2JMZW%{o;#<7QO7&%~(!+T*R}A(%V);Ae(u>t*I;7bhncr0V4trO$q6BL!5%;OXk;vd$@?2>=vp Bq0ayS literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.scale-400.png b/res/terminal/images-Can/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..b060cedd537c08c29b7cd57febd60e17dea0b9cd GIT binary patch literal 4821 zcmcI|XIN8Duy>FyAW8|LH<22uA`lRi-g}cyAP9seE!2pDG!fyi5?bgeigY1Dq=qOE zL<9^PihzJfjTl-eH-3CSyx;CU&)GB2?)lA}-I>{)Ig@E)Wz2D2;5+~T;4n2Yu%pqY ze~pES_H69<=}99D0XHpg0szgqY{x!~w7wYB#Lf}`h`#~=+)D)jj%Y*oegOc{$^gJ` zZva3G1px4cmvq`{(_S$7nj0I?DD67b??TcDYq*JPBmf}o^{>$hKkN*m4KhcWS{gF1 zvH;nX#f3N78~}hzWu^u<9T49)OOfF`{zV^pX4OoeqWotrw>A5kLpaoaCf#%E0aTe%_E z=PBONFfjo!@g7+1JQey!Jxpu16g?ER4;k96D>AblgIM>3jm4tl=2qjn=r1zd{6D#G zR3W0vfB*h1^{6=-kSwa#IZw}bv>eNqUxzHss!Z$!k@k+(8%4=oYJl?ih$l28((jS* zxh58_%z}(UcKoyiAl z?P{(1%?t}}+%ceW{q$>>3Z8#&l%-%$AA$=pw0-D5lHHvPbW8pb4D4Ua9KRZ3B6!?r z`b6|ei_B6bHpBj(j8T;?VLHzJy%2MGRJ z-#7Cvl0qCq>tqWG4e{Yk=lv*{y0$W<=;`gx;h`5O%luaJ#t*=*sUu{A zUMpvZqN2#;rvs6QE(``#Q&VH(82v=LJs}L6lv^`v(j+D5IpaBHE7*#N zp2NmH@a!o1n@P>e%CfnA`&EK(RmNaa<`(r%`!aJkOuRmIBrqf-nnw%8+WGS39X*Xc zM)~u^-}-sq=!fr&SSJ?aG&SQA_O}eZyu1h!KsFI~T%z4aQpShO8ZZ1ld%z>fzUT7@ z#UCzKfB3u-Q(9VDd@@ni8-x~a{F3G-F`V=W=;ryO@Lnkz^te*ShKKR(+qW|E^7=A7 zcdurukgf32vetI+ObyPV&=GU(LY$_K%3{CJsf{Bq4LsfApRMTIWm;UWKL zeM3_Z=UF?QKU+NfGup6PPAWS?!ik6R+O=!XC1Q#yP&9c?jh=5l;Pm~~{1n(f28}3H z%SZ|Gkuv5A&X&7@~15v3Y_lX$V*c`ANlJW zw*_G-7caX79;b}7Nr!xl^4Yh@=35M99DLIA*H{bt+Z&a1Z zs{bK;dQ$lmK84pP%7nI)rO^y_i?LZWw|5V*`&3#wP{SmQhE5T zje`2QsJ3|6n%R)A;A<4>?Ak+a8tTK|CMkV0GtNSxmRN!xpc${_d%h_~sLpH%hMLtH zmV@*`7Yn?SUHO{F+wCvuQa4(k6N!Ur`0*cwCwkxQT3jK$S|u0L+}+%=ojv0QV|a_l z8!)3FeJ3X%P{GCfyRKTX)e-vRu2?a+ho(|(2S~4WiLyl2VSgH%M(kEeNDy2mA|N23 zcrW5IV#1G|kMN}7sZcP)yUFPJqhT&2lRs|`HzrzCEM1qaIm%FD}p|4bLvF7&3*%P1)k2hIDR*<*y(!oqbN z3?C(Y7|iCAR8q3GPN;iz-g4qE3YFk1Cg)qFg9pteQ$rja98fDIOKS=BPzxuwWQh}w zhMdjAb`2G`7QZ0?`L4mG-I4X;;^ID#esc0Dpg8P31t=#c_cyVnd2tmFqPd4Ei+iVO zNptD5)#d)5i*JXg{@C1C%34KZy1E1>YAy9IbMu+`Md%kb;eN ztlqGod$A7R;K;U%Y!*@R4O9UE=$(4)JpT3TL;Dw270t~NPgbw37dw-cS&5ootQPD0 znYQBM2=ZA(3!Hn@4EJtQQEkPmS3&m>OB}-Wcu?>uLSuELp>)(C_5I5x`#t$Nf#B}k?2hVczk7(B z?dI>&@#L(m^OHnkaaI9T+7k~_4I5XENMxWJj2H4eQy_g)rAlkSpxJM|URVJWv%_96 zOe)TJm&r?&QLeyk^u?x-vRRH;Cp0mLv5;odh{e=5h|N@Xz`Or$x!B+3h=l#UMM;u< z$}ZEh;C2arulX1I`|Gj$Fl2N7_bdg+C}nyb-AsR$MW;d>{JZF$b3t zz?c?i#F2MJJx@sTC|cZkE3WTz&}9;E#2OO?D0VRh>%LI&ePxR8TuD_5h({C&Ff!i!>4 zoAf4*X6g-*zqYoFV6fWL+uCSn?D3o#*6I~J^x}E7A$+mj_dWL@FT zj*e+H<^{hiBY(%`3PWo7WH)f@)ppBm+s&URf%vhQcIa56*5~nS2Ue&xsKdt5kYD0H z=Y-|_S+o)=k#tM^XZo6iB-5hpNM4Q9c_W zMq)0X9;_N8=L(n1s;jACOury}OG7B;Y9L$sfJX8$q93>32D-$y9MBoQUqv}Mgb*zT z*#Ot$ueu+7*IJp2JMLVFGqy-_>8auB3Y$vEsScv(Wc2rc*ZDV&I*JK<@@|<5d zATPzY_nBcARW@JQgg&(y8H-xz;~2$!=jYv;Rh!tMO*gScnlB`MmrO2xr_rX#JOcC= z8(i#Yex2@f)>R6)H#0PDGEz;q;LQFTZu2*QDv^l@}IED%pyr*+>DyCm}~lj1d{zuZ!F$-OxL2B zo%Mo4^x{#@%RGvF%Mq1*Lu-M+hFp4l$AVgRHjyH^Js<90?1iDLS-cv(37 z%gL1a2T6#0{JY7bP3ON#`YjTJZ)ym@BGhbKbzL3nKlK|Mr=x2$?*7d1Cj5hSy<2}??%bziEOwp+7L-05@o`87d3ZL#eV810UAkrH0XtnQ7n40EbgFaQzBs^@ zN~=i;r#X#_A_eRSC$pF@Miv3CvsQ0uChm&e#?w-freLCCTYGzvr>VGN*-wqp!y-zs z#-zu_z_ZumrW41I31z5RVqKkwv{r9p(d}fU1BP=RsaD)HaT9cD9Q0gixc9*c|6`;M zf}^KljsNSxF>c?k#TL`+ohu6z$2@^Rs<`=2-4!nF?tcRlGNsg1-Mg_-k@p=@d|sit z0;@)?obv=r!-$(vr4~NPad z2&dbKW&s>vx;b@8-_V~`64D&5V?=(V18Av^Cvju0FA=HG9}Shb9=b6)L9oBmEU}Wz z%zALjz9SA{mDI=uf5P?0bpQbE8zIJ<)`#z2ND#SRfC~#!2^R?6IW)n zN2khg$<4Q7l?G0SUj{fj{^;WXUs(kUz)I@3ZY@3Axd**n7W`v%y;Ws3?DzQ8KvTi9@_>7?@sB5&`NpLh7O#ms6z zkFONSX6Hm#I2{>B6#&sYy^rGGey_y=wJXh8F1S^9P=>~Ebif#x-o6{J@;P~YCpu(5 z^8y@kc_iY@>}=8Lm^fsW7+%4wbWA0pkT2Hx>@Ks*ex;Wle$E|1<+VEwHw5mTzM|h0 z7I>w*Hdv-Tk?I9U)jQ)2ZZvG`J97?T!>TJ5?t-Mpj3RqtM@=ZCCH-hF<+ z=C!v#n|LC%7%D^ommBdhjCy^$_bHTPJ6R*@hAR-uB5r{kTRdD-r*4?^gl`V&8K zY<;}oyBcZ#*FSfgxL1BK9)C>kyiz|u{&4kdQ~vnJi-Mhm-0S7P8rLZ z*t7_A@Z`EzuD>d?L64B`srjm>8xZ@EzDz}X|Ef9#LNES@b{s6?z6vE>veM|^*-D>9 z#GN_9lUs!o7jva<_sZ9fUY|YPG-{a{H*pK60Keu z?#HXxu$G`Fvbi9rO^;yym>rnA-0M1!HUS!Is@7^R2 zW6!iQzkfoNrcak4rR9ey=k~i*G#{^F++0HrEnV0I0x7;rS-}luuk(y{y2sq7P||^I zw4CL{z;Q|nV08{;BWLCZ|D6u*_xgF3^CUHVnUG%Y-7|k;qU!PwAD!Rrm6xZW{48lR zRy6-2krZuuh8)_jg(`4#Rq=c?N$Ccs30?IB%VMOh=is zMxJMEq$~ecszU@Qn0M{(3UqK9{D;x_1##U08`^fikTFy2JvY#y;rq*pC&tS0Y3Z;| znFpo#Sm^qHSI5;l^u+Nto{b)9f$$|nh^kGWCOsKtd;Q^0FtZ# zU@Z~=9IF68o5UJCg_I5UV*R|_0idlAijY-GB=3+!05FdD5_|M=1~15(AhHk210+(^ zQn50Y7Ik7|f9HMNU4y6#;@XJZ?>%(KzN4H@kbZzSjG|2v+5SUX_WOUQub} zm^T}DL}0x+6>UT=m&3*SQ`k69ffg0HqAb0se{iZPdy38?USpOj)l(SJ>Fa8kK!_ni zS-qF}A2?8ojMeiQp&_$2dmk2PqeMO@pbWS5)o+FZCt-6?-feBeEA7;x| zYt+IzIO&>m8N#9~H0!F($H{vY(WywPZN6Dk-_w;HEmFRiN?OhO*&)n2t=~-5MVRhW zF0~UI|FkboLwhG|MpyFn23=4z>XyBtGkeCF{~oN#b92wJwSKoMl~)EaoON9C4wyV? z5Il^j(fnN8xZ(>N&h7al5(XJr$+)e__}%VM;M<+@QcZf*4uP6k-2RG|foeEEYWOiS z;349s<`kA=WARXZ$EP!@8LN=zy!5IOQTK;*!G)X0oyhlY#}@AoKy+X+Ghy_8I13D- z*3Lg2Sv#1=VVhnB8^>>^4$L6W+RhCgM=aeE*!10%0H3x%o`%c&arok-98Il;E}`4q28MEc3HbkS7a8xrf2hN$)|-U3)$ z@r`Rsn|M>&9kDvtk2-e8=wol2m@!#rJAsd0HZ`5nYUWf?Pyx$2ou(mtH>I>fvx~g4 zp*0IFB)iZ$qFf!xM%j8fE1{8JO?6bQwd0a=Qg(t2b`>`qBn=Q%n!5gb=-8vCfoDz? z!5NTa8*%`*Lzq7#9YD&nu9emAu^%ydb%2FA#Z%VeEw=@IZWbd>`+a(XvNqliR%IGs zeaTA+3%>+-R@V)wO+%XNZBWz3ejsM9+|5qHI^WgqMyc4}V(VG(nEBoA7>Ry5tklJk zQ^gP2*LzWVB)CIBxI-ZmyQJb|$E`Oa*pulDI&`smqV)9x3GQCsYl52(^EdXl3x|;& z>x$Z!wsHL~4GZ@4#_0!ITI~H*yh_Li$RlR%xdBJX#*Li`1vI<#@}7Qf%uKsN%69(Q zvXg1L!-xy3ahp&1{_yq2-TPdYNYe|N`JU#wWtsd^@A>hT7LRJya9P~7dwx)WHXlk_ zx_>u%V|esj*t^znh;iLVC|`N`ddVG>cuee1&K*;noJv zk84cs1;sY@n2Z?`$tSx8)l>Dh+g|h+jP++Z0IByIkt@yNuabRS0ND|>|Y&8 z{cdfjb|nQq=T|GdIHV_z*S!^W7ZvqqUte}70ZtXbH`w-eOg+RPLFz* zHMT43o(lYCYuPow7FUd8ZX3=T7$%l5rA%V1+7@-;jHHC7UX3Q=(|C)2ZnfkmjXy|* z3?BNw_L&{=Ne=$p3wmb+ocdUUpFWmmoeNDi)ve-vuB>X0J@3^X0?o1U#97xfBghC=|s7*y#LwY4%4l8hy2h)Cl#mZ3U|P8+3_ zp?#*++EdGfhPERuX@X*=NlhCBQM(LdZ%umMKkuFQ=ALu!cen4}``5klRe-mSmWdVs z06M-to`EWC`lO$MRaT?x=&vdOlimH@0f1Yqy%VLOiVsBl1o{KOMQZ@a{1yPVRH@AO z06;?kz$_jBFl7K>l+5WkgH;tk1V3+20H`TEgi=MHC;Nn_0D#TcpH$5>uOm*CgrxfV zdqJi@Gt@@dtQIvysrFv=^>hzTA6a-8nIBBt-=kcL=xAB9IN5%e&33;YK>`z!;x8VI z1RsnE)K3qMJbR7S3@!7OdifUkw#cNpAqn^JdHSTX_b0~vuHnME>VzRa$DAUzmk7lo z@uIk&F^L^WQHYnC9nmXdXJ$X0++Tqvr$IdbT5k89cbnXE^SPvtL~4WIV3e@x9nsBe zbqdJ?CX1hKfqGcDOaG46u%-u&u~7etg`K~K)oEz?G=VN zcDghcPxgb}-X+53gqWhQM&iHhV(Fp^np zZbV(rZ2Xvx*ZkA40*cS~yeRsJ90E_5W5MuLc?KAsEa!va3Gy8<{2Mt|10EyK(17FR ze2swebmn|=A=HT?@8k~KCyuG^Adi8p`s2rbw(L*BC!DVd9Z7o;AmwEZTVHZW6YNok zT$=mkfbARHI_`8{_G_g{$?qOT4RY$w!@UcTI!Kiyt_%l<`%!W3_sZ0~nV?>IC*y)GdGTB+{H z<(yOF6?ng|FZ0kN+bWFw-q}zdn?Bt7aa(ww;Z!I?V%P7^!PXL~enRV;6pN1Q%DMR% zs%T5UJl_R6)$+mEP&x77oX0|$agtT3b+TWHGG+cG++VlJuA*r2OB>zxQD)*s;2*9v ze&$32#o?i0?L~5!AXI#^A=kBzw|c=N=d6KS@Ab){e*HtuF9|y@lz5R#;B5Vb-ER~U z>7Tf^&epbP%?qAXk?gy7Ev{{^U%xT5wURt(y@^a4cI!=m&MVo2N6ZLjl=)iPDfYwI z3}O0xDm2vcC;k~^*R@=tM7J3$K&n>^2z7>xVyu9Xu=5u>%Ln3Q>!!G=rQ@P~%Y`=M z?6B@xi@n$*}8npT$~WDWxx|096KGrEvInI>_3_$Jz^K0(p0>PoSJZEZanLz z?z3J#_BLqRY%Kzga8%*eW+l2!PXu%_dzO!tv=@qiSV8=N%YroB>r% zl}rv*9j*Ohvx`9g$)##(+Ke~;55@S0@zP)kT1b<-OFn|J|d)4R;D>W87ouFc?DYaBY1p`+oZf-+8|J*K>^- zw#drv#2dUM3*F!M+>^$ecwMQETtV%;%rEd*rM}Bw8TXP5RAcY4reGF-DSZl7<#OMY zxmhMx^{TB>y~0vsqEd;NXev<}=7JqCf8$)R=s7lD!# z6H8+JF9GfRZvrGIt5roX{{u2!C9Z>)P literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000000000000000000000000000000000..4613842aae7488588f0521eff89b9cddad3f205c GIT binary patch literal 554 zcmV+_0@eMAP)2>u z-+(Y~}U&3=E8do-U3d z7QII&ZR|SaAmZ9@6C*1!a&!i}CzBNH{$w%fjy2lb0b|{DmZk)LL ztk&+lX`wncPg~3H&zae{T{&~5^_s2U12ff5hBo$#e`6QTUiCZNm^Opat z`4`~$b+=!jNQ|C%kD-ZF@AG#DqO_BHe*N8d?e*8gT-(DX_}b^3Pv4}o_-fYQh)K&d z9R)T-=t%IeSv>D}JVT>vho;Z+;Iz#**C#f-E%RP}IdN-Lq19Xq8NMr7rVKnASeiEZ v7pZy*xh)pl_rBOh&hXfV+i!o&sx_W}UpIB#|C`RB@MrLJ^>bP0l+XkK?c%3e literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..df9dfc917f25f47ed077e5206baae8878eb09bdf GIT binary patch literal 485 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L9(7-n!%-EIpw+JZ6UgGKN%Kn@~j9cIG@a?&CfI^!+T^vI+&M%#~pN}z7;7I-b zOqWFhy)n(f8G9StlqRlN|2091|3RRNR_if!u0?`@1%)b$cbr!I?w+{(W=eXQ*=9G9 z>4(p6(r8*=a{SoBSdH&J^JM~`PkD7fp`g+EozTLm+p_l7?r*%{(OCBB6KBSnfQ40( zEf(!ss`KuA%KPy3LR8+eR>uuX&NL*inUVBz)1t#Yt|DJ2)ETw0RnH1pQONG)vx1dH zeqQ*qIXjDg~GCBD`$3O1D&s0;u=wsl30>zm0Xkxq!^40jEr>+EOd>G zLJSS9Ow6rJEp!b`tqcsFpH0|~q9HdwB{QuOiw1LuhJ+g{9sxB-f@}!RPb(=;EJ|f4 jFE7{2%*!rLPAo{(%P&fw{mw=TsEEPS)z4*}Q$iB}jWezq literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..3640ec3fa32ceadea3e7a3df4e3180d8465e6708 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2>u z-+(Y~}U&3=E8do-U3d z7QII&ZR|SaAmZ9@6C*1!a&!i}CzBNH{$w%fjy2lb0b|{DmZk)LL ztk&+lX`wncPg~3H&zae{T{&~5^_s2U12ff5hBo$#e`6QTUiCZNm^Opat z`4`~$b+=!jNQ|C%kD-ZF@AG#DqO_BHe*N8d?e*8gT-(DX_}b^3Pv4}o_-fYQh)K&d z9R)T-=t%IeSv>D}JVT>vho;Z+;Iz#**C#f-E%RP}IdN-Lq19Xq8NMr7rVKnASeiEZ v7pZy*xh)pl_rBOh&hXfV+i!o&sx_W}UpIB#|C`RB@MrLJ^>bP0l+XkK?c%3e literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..df9dfc917f25f47ed077e5206baae8878eb09bdf GIT binary patch literal 485 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L9(7-n!%-EIpw+JZ6UgGKN%Kn@~j9cIG@a?&CfI^!+T^vI+&M%#~pN}z7;7I-b zOqWFhy)n(f8G9StlqRlN|2091|3RRNR_if!u0?`@1%)b$cbr!I?w+{(W=eXQ*=9G9 z>4(p6(r8*=a{SoBSdH&J^JM~`PkD7fp`g+EozTLm+p_l7?r*%{(OCBB6KBSnfQ40( zEf(!ss`KuA%KPy3LR8+eR>uuX&NL*inUVBz)1t#Yt|DJ2)ETw0RnH1pQONG)vx1dH zeqQ*qIXjDg~GCBD`$3O1D&s0;u=wsl30>zm0Xkxq!^40jEr>+EOd>G zLJSS9Ow6rJEp!b`tqcsFpH0|~q9HdwB{QuOiw1LuhJ+g{9sxB-f@}!RPb(=;EJ|f4 jFE7{2%*!rLPAo{(%P&fw{mw=TsEEPS)z4*}Q$iB}jWezq literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20.png new file mode 100644 index 0000000000000000000000000000000000000000..517a25e0c9061fa0045e014ca02adf86581add71 GIT binary patch literal 669 zcmV;O0%HA%P)uoJ|nx8*BJlyT~ z`!}a(Z5E5gJH|Azzk$|z6+o0qrS3U_*4iuR_xqQC50jkMn$c*qET!~%+}ALS2!Lp} z+m`{9Quv$wgWNbE_XfbwS|g>zFbp!84B>DXDJ5EKZ-_sh_9>x(`izv6uDdu z(=hnt4Tc+7mPMgZU~g}acsx!%pC=ZJT@Va% zDWyCUbnOZu5JFI^)ffy004y#pqLiA+r3*26v9+}|lF1~sT8;hveOj#+bCEDnEAX;t ztqFxfgbgipBWViPdLofveSMwrcucigrPu2*9}QEEJiGA&0utUKEbC&YNwyQ%npPcaiv%}r{A9?rb>^4g))kK8t>X6g#zwYoYu+?K(I>0jF!M!-oUwtKN zg}Ab8^16NM)dl{(0CXd1s^Q5?4J)%O5opd>_Yd&{uo%@wUGMN300000NkvXXu0mjf DS>`F- literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..517a25e0c9061fa0045e014ca02adf86581add71 GIT binary patch literal 669 zcmV;O0%HA%P)uoJ|nx8*BJlyT~ z`!}a(Z5E5gJH|Azzk$|z6+o0qrS3U_*4iuR_xqQC50jkMn$c*qET!~%+}ALS2!Lp} z+m`{9Quv$wgWNbE_XfbwS|g>zFbp!84B>DXDJ5EKZ-_sh_9>x(`izv6uDdu z(=hnt4Tc+7mPMgZU~g}acsx!%pC=ZJT@Va% zDWyCUbnOZu5JFI^)ffy004y#pqLiA+r3*26v9+}|lF1~sT8;hveOj#+bCEDnEAX;t ztqFxfgbgipBWViPdLofveSMwrcucigrPu2*9}QEEJiGA&0utUKEbC&YNwyQ%npPcaiv%}r{A9?rb>^4g))kK8t>X6g#zwYoYu+?K(I>0jF!M!-oUwtKN zg}Ab8^16NM)dl{(0CXd1s^Q5?4J)%O5opd>_Yd&{uo%@wUGMN300000NkvXXu0mjf DS>`F- literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea768ed4a0ad24b660e0fe9f9d39e76546c4738 GIT binary patch literal 509 zcmVI>X^Go>4DMCKCZlBoZ1726_~J-*mfO0Wu6j{eC~bP?9>GjsO8_Hk7?QpMpFcGH8Q=br=O6M6gPsCffgkUt00000NkvXXu0mjffbrIO literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc5a22917e6c33584f56598357c3e601586bbb5 GIT binary patch literal 546 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc0wmQNuC@UwmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1OBOz`&*s!i>HlH;w}Z*-JcqUD=;=h;i#%9=<(y4p8WWr;B5V#`(3A_IfcpinyI$ z^V*D2w9_$!h54YLfM5-~u1GXzhUyJYyVj*^m-gx}T$!7bvxX(oSFxj)MeIvN%O9Ob z@5JtDwiy@iDA+$!;i}fE7lJoZOBvX`!(YD8a_=|1uEIR)+}9IyMTZktYQ0+-FXwsjRsQl<+mAn0nzynlQg7RzfZeG{r$cHt1nQXiUX9K^ z{J5=DIDF2Ayvv@3Y!PXFeAYF5i!I!PuAQuS*vozIw`JLlq-r1Lj>@!&dIrw?9Va~= z{0rZcdu>KivqOL7yiEt}W{bvs(EIb7|39O%&DxkX3!2^l{jOT#8c~vxSdwa$T$Bo= z7>o>zjCBnxbd8Kc3=OSJ%&klN&PEETh{4m<&t;ucLK6VcF~MB` literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea768ed4a0ad24b660e0fe9f9d39e76546c4738 GIT binary patch literal 509 zcmVI>X^Go>4DMCKCZlBoZ1726_~J-*mfO0Wu6j{eC~bP?9>GjsO8_Hk7?QpMpFcGH8Q=br=O6M6gPsCffgkUt00000NkvXXu0mjffbrIO literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc5a22917e6c33584f56598357c3e601586bbb5 GIT binary patch literal 546 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc0wmQNuC@UwmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1OBOz`&*s!i>HlH;w}Z*-JcqUD=;=h;i#%9=<(y4p8WWr;B5V#`(3A_IfcpinyI$ z^V*D2w9_$!h54YLfM5-~u1GXzhUyJYyVj*^m-gx}T$!7bvxX(oSFxj)MeIvN%O9Ob z@5JtDwiy@iDA+$!;i}fE7lJoZOBvX`!(YD8a_=|1uEIR)+}9IyMTZktYQ0+-FXwsjRsQl<+mAn0nzynlQg7RzfZeG{r$cHt1nQXiUX9K^ z{J5=DIDF2Ayvv@3Y!PXFeAYF5i!I!PuAQuS*vozIw`JLlq-r1Lj>@!&dIrw?9Va~= z{0rZcdu>KivqOL7yiEt}W{bvs(EIb7|39O%&DxkX3!2^l{jOT#8c~vxSdwa$T$Bo= z7>o>zjCBnxbd8Kc3=OSJ%&klN&PEETh{4m<&t;ucLK6VcF~MB` literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000000000000000000000000000000000..c9003ee24150f658e8478c9f63bfced4ff3cd7cd GIT binary patch literal 834 zcmV-I1HJr-P)4Q_K~zYI?Ul_-R8bhle|PQ~CPRl387nEsuT5akB4X6U(x^m& zHuWD=AmYkJVS)CDt?aPF$z(EAY+Y-;9SVis#pCg9 z0el21iUA$RnFiVpBhXqOs@!p$GT>F3=SBFxnWD-ANj=&2P4u>&KlfJ$_YHMo`koZ*|N(X>a+*{b|IQV?7zMJAKMG)(|5R?FsFFB?0A7hEYtS63HWYciP( z6B83Cr4T}J_sSlRZ|?%T04%P%1ckwwN|W^sN#wN2+J>b1s>#NV;&ep-paY2JdXUX# zNhA`iuC6jRHincEAq0kD5DKcC;`g6+n4H;Te$gSln&sZp73{3$*=vgrQ@g}Rb6*yK zlOn&tb8~YflgZpv`O5*2nzxBPsRH1|+YKJvC}(nJkMj}1x0Nj4w`2i6ptaudnj?fD z7!2l=`)3;9+NBdbe`7H^y~~Q_a{Y1{U%vhTpy&Q6rc?U`c%!|&y;(}x;x)=|N+|{( zZnfQPvhR3nuG9$AIDjo;`@e4Q_K~zYI?Ul_-R8bhle|PQ~CPRl387nEsuT5akB4X6U(x^m& zHuWD=AmYkJVS)CDt?aPF$z(EAY+Y-;9SVis#pCg9 z0el21iUA$RnFiVpBhXqOs@!p$GT>F3=SBFxnWD-ANj=&2P4u>&KlfJ$_YHMo`koZ*|N(X>a+*{b|IQV?7zMJAKMG)(|5R?FsFFB?0A7hEYtS63HWYciP( z6B83Cr4T}J_sSlRZ|?%T04%P%1ckwwN|W^sN#wN2+J>b1s>#NV;&ep-paY2JdXUX# zNhA`iuC6jRHincEAq0kD5DKcC;`g6+n4H;Te$gSln&sZp73{3$*=vgrQ@g}Rb6*yK zlOn&tb8~YflgZpv`O5*2nzxBPsRH1|+YKJvC}(nJkMj}1x0Nj4w`2i6ptaudnj?fD z7!2l=`)3;9+NBdbe`7H^y~~Q_a{Y1{U%vhTpy&Q6rc?U`c%!|&y;(}x;x)=|N+|{( zZnfQPvhR3nuG9$AIDjo;`@e0lVgzvom!Xe@LwG8n`^aC2rMDXFPTV>S8{ z{2we5lNuNqM5OLuu{09D1_lFyMXRF8bEezi>T%DxJ=4A4@qM26^Ld~40siM%i0CKT zq*klZXf(1bld7szuh(tsM5Md|`0iXgmHmHxmSdK>$Fj)j~F# zb!L)ln`xSq%jF0F0iZ-8LAtL0)daXdxR-DoNBjNWIRO#T@pzNS0+9kH<8d%}ADInog&* zTCGTyW$JV~R`1Q41_A*9fFwz%R4QN?=Hcckih_2#4N(+Ptya{b&E`$- zji6jEqtR%52>+GIWCFk650+)o>-7+gM&WX~psFf{!{K8A!!Qs8;VB#d1VKQz+l9yD zL7`B<;c&oiw?jN0M=Tbz`d)}g0r2*ec$MXH3DYzYi9|4;&*65vp=la6n+?|MHCUEK hDwP5NNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1LNlkv$2787-=2-UbS?mw5WRvOnh#a%U~jDU1@HE{-7R^FwA|)7-TY8;t&mT$}8#!2Q20$DnJ1+T~>9$=_2? z{eE|6^UmeJtKV6EFMqX6<))YV6?OIpBKwmMG9Lc9;Pb>eMYArtTo)JGJ|(L6#+9Se z?-$y5v-1ZwwCuYU%@v{8(b=(My_}?rY^Ky#uKoi&yEm@tm{I6_RrvP*RTpoVOsFpY z$Z)1=`Kv0M1y`9v^K-hHO`hD|cBK07yj8Q)oZR<{oxPoYv2M4nmE^XqOgS4}bW`q2 zTz<2`qtV%IMz3Gh%$@Q@4L16FjLxyn=P{IF%bHSma^Vpc`85+4Pc}Zsc*4Nd|8u8Z zv1hCLI*VnebVJuIob#)G{sHlMZyA|tGxqBNgH5%>HKHUXu_Vl&C^85jt*3dEsk$jwj5OsmAA!3?6|*!sm`Kn;>08-nxGO3D+9 mQW?t2%k?tzvWt@w3sUv+i_&MmvylQSV(@hJb6Mw<&;$Ug3)vX} literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..ce51243c6282340f88620091a98ee3bd6e11de7e GIT binary patch literal 595 zcmV-Z0<8UsP)0lVgzvom!Xe@LwG8n`^aC2rMDXFPTV>S8{ z{2we5lNuNqM5OLuu{09D1_lFyMXRF8bEezi>T%DxJ=4A4@qM26^Ld~40siM%i0CKT zq*klZXf(1bld7szuh(tsM5Md|`0iXgmHmHxmSdK>$Fj)j~F# zb!L)ln`xSq%jF0F0iZ-8LAtL0)daXdxR-DoNBjNWIRO#T@pzNS0+9kH<8d%}ADInog&* zTCGTyW$JV~R`1Q41_A*9fFwz%R4QN?=Hcckih_2#4N(+Ptya{b&E`$- zji6jEqtR%52>+GIWCFk650+)o>-7+gM&WX~psFf{!{K8A!!Qs8;VB#d1VKQz+l9yD zL7`B<;c&oiw?jN0M=Tbz`d)}g0r2*ec$MXH3DYzYi9|4;&*65vp=la6n+?|MHCUEK hDwP5NNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1LNlkv$2787-=2-UbS?mw5WRvOnh#a%U~jDU1@HE{-7R^FwA|)7-TY8;t&mT$}8#!2Q20$DnJ1+T~>9$=_2? z{eE|6^UmeJtKV6EFMqX6<))YV6?OIpBKwmMG9Lc9;Pb>eMYArtTo)JGJ|(L6#+9Se z?-$y5v-1ZwwCuYU%@v{8(b=(My_}?rY^Ky#uKoi&yEm@tm{I6_RrvP*RTpoVOsFpY z$Z)1=`Kv0M1y`9v^K-hHO`hD|cBK07yj8Q)oZR<{oxPoYv2M4nmE^XqOgS4}bW`q2 zTz<2`qtV%IMz3Gh%$@Q@4L16FjLxyn=P{IF%bHSma^Vpc`85+4Pc}Zsc*4Nd|8u8Z zv1hCLI*VnebVJuIob#)G{sHlMZyA|tGxqBNgH5%>HKHUXu_Vl&C^85jt*3dEsk$jwj5OsmAA!3?6|*!sm`Kn;>08-nxGO3D+9 mQW?t2%k?tzvWt@w3sUv+i_&MmvylQSV(@hJb6Mw<&;$Ug3)vX} literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8208e4a35c38de8a1b2609b34fe021c8a3a62f GIT binary patch literal 9204 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8208e4a35c38de8a1b2609b34fe021c8a3a62f GIT binary patch literal 9204 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..1ecd842894dd55013f6a5c06b8c9798fe3081efa GIT binary patch literal 4062 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 A4*&oF literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b34489eaae91b2af87a86757b2f52c600596b2 GIT binary patch literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX0wgC|rfC8xmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1O92q^v-g@k7caFQ6cMiKnkC`*RL4Zhdo=M!r%hS{Hr*PTriIcUTK6}}vnoF@HU$Uybldh7^ncIoc z-`*Jh%u}EL{^3u%`S;e_)z;beJBQ?-le%#$fnAp;DEw8|mSyq%@oP3EB==X_CKo>S z*wG=piHrHpt)z;Nx0&TFPX{t{U!C^3XKnc5n8Mg+uNO_OwZ4CvsJq+!(iX;m;}){}zZ;#_oA9`g*X~3?|7;OSkAELt z{$2dk!{Xob_s_V!Hcvjs%v0sWnj=I#aND_AvZrIGp!Q0hVSk>PXjebf@}!RPb(=;EJ|f4FE7{2%*!rL bPAo{(%P&fw{mw=TsEEPS)z4*}Q$iB}_+bPI literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..f7094e3374bc9e8b31667b321b2847e02409b164 GIT binary patch literal 770 zcmV+d1O5DoP)JFvx|1?*8T@>1qZi+LuV(WE{!;dTA{6AZ&1^#Pvv`CxoF#1XuQP_WJx~xz4#@+ zJivFpD2j*#fM0;8RVkaz2E*ZSZE9-jPrJd+&dz>bUS9r)#bUqP)at;A7dQl_+YMMK z6c`y9X;%TjvMg?HZXA1n+2e$|lFDT)P$Q!y@7cyOE~%IG4*&tybNYhebq^ z$)xR`kB^Uc4Q~0$v@DBkHf!6bX_|YFlZc3AS+cymY@6-c+FI9UE7IgrH$#)jWLMD- z@bmMtXqsk2Q>m2WAg8CNVw$GZ>vg%mzZb(W$;qupEu62Ew0hgQ4S6c zP*s(5I!!zt|MXhf+uNg7t5GZ#nVFekV`GDhiwgiEkqEl3GchsI=zHG}plKSnx3?&Y z($ONFot*Fbp!845d7V%heij!OQ4|G_$AhY>#9}eLUN6C5 zkfEU=o}QkFMxzW54+G>s^?Tdp)Pa*Pvc3a<0r;As!%ER>KmY&$07*qoM6N<$g3R`3 A4*&oF literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b34489eaae91b2af87a86757b2f52c600596b2 GIT binary patch literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX0wgC|rfC8xmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1O92q^v-g@k7caFQ6cMiKnkC`*RL4Zhdo=M!r%hS{Hr*PTriIcUTK6}}vnoF@HU$Uybldh7^ncIoc z-`*Jh%u}EL{^3u%`S;e_)z;beJBQ?-le%#$fnAp;DEw8|mSyq%@oP3EB==X_CKo>S z*wG=piHrHpt)z;Nx0&TFPX{t{U!C^3XKnc5n8Mg+uNO_OwZ4CvsJq+!(iX;m;}){}zZ;#_oA9`g*X~3?|7;OSkAELt z{$2dk!{Xob_s_V!Hcvjs%v0sWnj=I#aND_AvZrIGp!Q0hVSk>PXjebf@}!RPb(=;EJ|f4FE7{2%*!rL bPAo{(%P&fw{mw=TsEEPS)z4*}Q$iB}_+bPI literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea6ca6dcba7c3f291a04f174ecf5c4b33b37c71 GIT binary patch literal 1039 zcmV+q1n~QbP)gfu1PNr3hDgwqO2KZDCB_wN*9DR8B1qyTIh1CJ$^J<) z^PBNdXPC+CZa3L@vR@c>_V?br@Av)Qd%qcYg#SB4Hk*wB{f^`4jU2Pttkq1wTrMXY z5(=S!-1PMH-vAQG02hQ14FhYM)(k%ni}Nq;0xtk)z=ftFpDsMk=F<8PAv2i_j^osI zaBZ8Lo1wK@emgrm5A_KMNTpH%qu=T1+G*N60kxd{_ij_2mYAdWnt#EzZIn{<_V!}i zws*Or>WMv$&Hvxq0r#?Pn~{+bhK7d7=krWWO<~(Mnx-LyIMq^kJK)d0qoaet!9kQ# z^!4>IK0c1)I4Gq8J8{zN{kDJrccEA;GBY#d5ysU0~P)daYw<;#o15!$smX?^E zoyD>&PsHfxXh=jeb#5dkR04Q#+)61~US1}dOrn(XnsRb-62mZRmf|F0LM3nwx~?Oo zWMyTAxw$!SGTPhQnV6UeO+_nWLOtMj&@_!`G|JZ2mN$-tLgCcL(S`Ic_~$;6NYK^Q z#m2@4)oPXX^>yaw=ZQojfxs?KI*#%vxn)@-5(#>GdQeIsrDSn&k$gUnVHg0M>o$o+ zRY*iVLLhXau6^1!AeH%hK!9nQ3=a=`WZ62Ja&0Kkn* zncMk&0IsF>j*2)J7{kNELlzbm*xlVFola9I6p&IPga{nPe%!iK;YOy+<+nR|v9}G| zhPkB@SHF4iT3*Rj`RL1SD*F!Ce=LWdmw|?mlI`tnlF6iZCb*wswMGaab*I96Z^Vdo zXv8`+(zz0~%0Qg|!3?CFKW(s_s{n9d zI(&6&kKg`0V7F}Zb-Dz=%jesWx*-48VORig*PodX0$tYw+g>kwfOjrFPN{11{%802 z?x!-(J}LQP{0XjI?q;WC^HMxQv`ypETW5Lo`S#HBVqjn(W}0Td5JCsAtomQymSu72 z&ArzyzH)G-uG8_bf9zp$ZpPcJ$M{6RGcA%k2ywl|7?038{s9@am=7cd7is_i002ov JPDHLkV1mRt@_zsT literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea6ca6dcba7c3f291a04f174ecf5c4b33b37c71 GIT binary patch literal 1039 zcmV+q1n~QbP)gfu1PNr3hDgwqO2KZDCB_wN*9DR8B1qyTIh1CJ$^J<) z^PBNdXPC+CZa3L@vR@c>_V?br@Av)Qd%qcYg#SB4Hk*wB{f^`4jU2Pttkq1wTrMXY z5(=S!-1PMH-vAQG02hQ14FhYM)(k%ni}Nq;0xtk)z=ftFpDsMk=F<8PAv2i_j^osI zaBZ8Lo1wK@emgrm5A_KMNTpH%qu=T1+G*N60kxd{_ij_2mYAdWnt#EzZIn{<_V!}i zws*Or>WMv$&Hvxq0r#?Pn~{+bhK7d7=krWWO<~(Mnx-LyIMq^kJK)d0qoaet!9kQ# z^!4>IK0c1)I4Gq8J8{zN{kDJrccEA;GBY#d5ysU0~P)daYw<;#o15!$smX?^E zoyD>&PsHfxXh=jeb#5dkR04Q#+)61~US1}dOrn(XnsRb-62mZRmf|F0LM3nwx~?Oo zWMyTAxw$!SGTPhQnV6UeO+_nWLOtMj&@_!`G|JZ2mN$-tLgCcL(S`Ic_~$;6NYK^Q z#m2@4)oPXX^>yaw=ZQojfxs?KI*#%vxn)@-5(#>GdQeIsrDSn&k$gUnVHg0M>o$o+ zRY*iVLLhXau6^1!AeH%hK!9nQ3=a=`WZ62Ja&0Kkn* zncMk&0IsF>j*2)J7{kNELlzbm*xlVFola9I6p&IPga{nPe%!iK;YOy+<+nR|v9}G| zhPkB@SHF4iT3*Rj`RL1SD*F!Ce=LWdmw|?mlI`tnlF6iZCb*wswMGaab*I96Z^Vdo zXv8`+(zz0~%0Qg|!3?CFKW(s_s{n9d zI(&6&kKg`0V7F}Zb-Dz=%jesWx*-48VORig*PodX0$tYw+g>kwfOjrFPN{11{%802 z?x!-(J}LQP{0XjI?q;WC^HMxQv`ypETW5Lo`S#HBVqjn(W}0Td5JCsAtomQymSu72 z&ArzyzH)G-uG8_bf9zp$ZpPcJ$M{6RGcA%k2ywl|7?038{s9@am=7cd7is_i002ov JPDHLkV1mRt@_zsT literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..7363adf4cf4dfa495fae568322fb38a8676bbe03 GIT binary patch literal 681 zcmV;a0#^NrP)uwsEx%D;sTQEV)fD6(QBsbdz3 zFJNioJ9y)qSXjzJK7)miK$3blA{&K;am~uqJ2=A`hdO7j<x_f|c{r4jI zljM${D3wZmR&s6aj$};$@B_fp17?2$@FG0O{2fAgpg90QkxHd_N#Srfgu~&usOdGe z3X&HA{4G8F2~Gl3t5sC1)%y^93}>ZMApinkxm>tzs`Oy;Hlzug1#`5c?gMt1i6{Cvw09>?&Y ze!owxRx6u6pU>ZPdh*N@d^8sXfz4*~=H@2>?rDA!z-qP1%o~lyMUB$wG<`lFv)L?{ z%O#`HD1AO3bGaN>s}&1{0=-@@y-BknX0u5_5Uw&ONw?e0PN&0UGRaD%LW9A; z!C=5ZAi!3u#c(*xdcDr^c+CBNe;$A&d%YgJ-R{Na2Y_O+NT<`ucDqf3!N6E7M!Vh4 z>2%6)I82+(#z-V0yLU2%@pv4GMB?g1sZ=V6qKITN2>=L%LfCG%&}cLOfKsW1TCFC# zqaeA%4gQ55j|ZYC!teJ30E|W>`u#pcQG_H(n9XKbEEdq|bZ9ghhkf#@AXx+O@>K{) zlAzb?p;D;;0MqFdCX)&K{T`AeL9JF}yWIi+%w{u`O64nnUtjV3*1w@Q)foB~Q7{<& P00000NkvXXu0mjf!l*Al literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b4aa9036f3e5f7142ac309e4ad300112d6e3d6c2 GIT binary patch literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKptm- zM`SUOx&gwB#~18e3lwB8@$_|Nf6gJst#1`|=?u_R#!yce#}JR>Z>R3{Vs;d1J+JnU zHHXD@!h%g5`3VcxD~Kg!PSj93X}o0P74`rR0dB$B8LEt`tqIDCjV5A3tg|&2WW?p{TJKHlB8h3;_SN>;acp0!Squ++BqaiSUXUC+jD1EuB zX(}x-lNK$^e(;33$6)=eU$Uhlmpu2sY~FGyjN|fU9tP`1z8^-jPRia;oOM#ipKW6{ z`v$j~x=h8qHw;OA>CF}|g)cm8<~-^X_CHg)HR8$DHS@RLbPB$wkQnUZUCI3+uwsEx%D;sTQEV)fD6(QBsbdz3 zFJNioJ9y)qSXjzJK7)miK$3blA{&K;am~uqJ2=A`hdO7j<x_f|c{r4jI zljM${D3wZmR&s6aj$};$@B_fp17?2$@FG0O{2fAgpg90QkxHd_N#Srfgu~&usOdGe z3X&HA{4G8F2~Gl3t5sC1)%y^93}>ZMApinkxm>tzs`Oy;Hlzug1#`5c?gMt1i6{Cvw09>?&Y ze!owxRx6u6pU>ZPdh*N@d^8sXfz4*~=H@2>?rDA!z-qP1%o~lyMUB$wG<`lFv)L?{ z%O#`HD1AO3bGaN>s}&1{0=-@@y-BknX0u5_5Uw&ONw?e0PN&0UGRaD%LW9A; z!C=5ZAi!3u#c(*xdcDr^c+CBNe;$A&d%YgJ-R{Na2Y_O+NT<`ucDqf3!N6E7M!Vh4 z>2%6)I82+(#z-V0yLU2%@pv4GMB?g1sZ=V6qKITN2>=L%LfCG%&}cLOfKsW1TCFC# zqaeA%4gQ55j|ZYC!teJ30E|W>`u#pcQG_H(n9XKbEEdq|bZ9ghhkf#@AXx+O@>K{) zlAzb?p;D;;0MqFdCX)&K{T`AeL9JF}yWIi+%w{u`O64nnUtjV3*1w@Q)foB~Q7{<& P00000NkvXXu0mjf!l*Al literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b4aa9036f3e5f7142ac309e4ad300112d6e3d6c2 GIT binary patch literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKptm- zM`SUOx&gwB#~18e3lwB8@$_|Nf6gJst#1`|=?u_R#!yce#}JR>Z>R3{Vs;d1J+JnU zHHXD@!h%g5`3VcxD~Kg!PSj93X}o0P74`rR0dB$B8LEt`tqIDCjV5A3tg|&2WW?p{TJKHlB8h3;_SN>;acp0!Squ++BqaiSUXUC+jD1EuB zX(}x-lNK$^e(;33$6)=eU$Uhlmpu2sY~FGyjN|fU9tP`1z8^-jPRia;oOM#ipKW6{ z`v$j~x=h8qHw;OA>CF}|g)cm8<~-^X_CHg)HR8$DHS@RLbPB$wkQnUZUCI3+FV0rVQp2mR(|YgS zJ9ADC>pgq*j;r`D2){3L_?_SR&1ZhUbAB@e``Xw4Ok#X|{2Aa3;B4d^jYj`YCX+`u z6Zq!Mn}5b)v9gW)wAM>XsS90QUB3a?z-8d@W^Us9{v=SnN#6(%-pEe~foB&hr_hD zw^LG5g75n%rO5OP8gFMlodj4pWoe^QirU&*8X6iXD=VX=rNuyP6m&h1&CF+IDAS>| zv=pTjzVA~|P(W*I>z+bom8`e7m!Y8{ld=5#d|F#u357y?0wptzLI|XkbaZqOi9`%k zZf-8k&CRPg9i;GvCyzOQu@RX%X1|X%xvMhuUM59q$*ELX{=OLvuKs!;gtPHIe zpuWDIii!%x#>Q}67vJ~k?(Sx4YRUxIX(&6>kbY{<^9YB-R904!N~Q2TkFl{aB9RC) zGc!1jW3st6Q$e7aHX+2iogV_;WuE6zS64?>RTZA+0e3h1;NT#0b930Xjg*q|kjE=0 zmN@cY4taTbfx)v&B8bQ1B$LTi)&mdd>c=lU@+I(@$xwQa)YQ}%7~l6vBog%X^%0B3 z%w`98{i$0#`B)N2084CBhk=v8@vKvpNF?w)56|<6$KzbRdey*4dGC8J*U8?HSBC!K z`E%2}{O$~iI~t|n)Z5ei^2Z$jy00zq!Q}-2CVowFall<0qQI0a4K+DAi4cOy%1ZkC z`psRmrLy-qyXCzd~ zR92k8#yBxCK~GN)3kwUTQKrE&ubCPcU*^nnh2-W4avXt_(DRkchfPO`Ovdq7TFv>_ zia7U449^dIuGNhZFpbf19Fv)}*6SSr$BJyCzu()ew{9!G`zg*_oiP?&#nAO-0E!AF zubwRAqbrMnacp2gYrX2vLkNLoS>~7q9p|-bc=jMKouA>wcczg-aJ)K~rc;M_^YkIc ze@^o8=ZicPvH>_*$9q18I#&4L$mV5aQsDplFq{3zvV~ky!iMOZ^M_Y(U0n Sk4Db`0000FV0rVQp2mR(|YgS zJ9ADC>pgq*j;r`D2){3L_?_SR&1ZhUbAB@e``Xw4Ok#X|{2Aa3;B4d^jYj`YCX+`u z6Zq!Mn}5b)v9gW)wAM>XsS90QUB3a?z-8d@W^Us9{v=SnN#6(%-pEe~foB&hr_hD zw^LG5g75n%rO5OP8gFMlodj4pWoe^QirU&*8X6iXD=VX=rNuyP6m&h1&CF+IDAS>| zv=pTjzVA~|P(W*I>z+bom8`e7m!Y8{ld=5#d|F#u357y?0wptzLI|XkbaZqOi9`%k zZf-8k&CRPg9i;GvCyzOQu@RX%X1|X%xvMhuUM59q$*ELX{=OLvuKs!;gtPHIe zpuWDIii!%x#>Q}67vJ~k?(Sx4YRUxIX(&6>kbY{<^9YB-R904!N~Q2TkFl{aB9RC) zGc!1jW3st6Q$e7aHX+2iogV_;WuE6zS64?>RTZA+0e3h1;NT#0b930Xjg*q|kjE=0 zmN@cY4taTbfx)v&B8bQ1B$LTi)&mdd>c=lU@+I(@$xwQa)YQ}%7~l6vBog%X^%0B3 z%w`98{i$0#`B)N2084CBhk=v8@vKvpNF?w)56|<6$KzbRdey*4dGC8J*U8?HSBC!K z`E%2}{O$~iI~t|n)Z5ei^2Z$jy00zq!Q}-2CVowFall<0qQI0a4K+DAi4cOy%1ZkC z`psRmrLy-qyXCzd~ zR92k8#yBxCK~GN)3kwUTQKrE&ubCPcU*^nnh2-W4avXt_(DRkchfPO`Ovdq7TFv>_ zia7U449^dIuGNhZFpbf19Fv)}*6SSr$BJyCzu()ew{9!G`zg*_oiP?&#nAO-0E!AF zubwRAqbrMnacp2gYrX2vLkNLoS>~7q9p|-bc=jMKouA>wcczg-aJ)K~rc;M_^YkIc ze@^o8=ZicPvH>_*$9q18I#&4L$mV5aQsDplFq{3zvV~ky!iMOZ^M_Y(U0n Sk4Db`0000?>2K~z|U?bk6#8*voJ@oy4|xyylA1VJ3MNhAn@IaE*)2eT>I z!Kq{m6rJ4M1Q927RB;Ik4h3E6q7^bp7Keaf21zg=h-fedAxbco*GbE1QqO77eD)azD!R~PsYsVkw~QW;NYNkY-}uPqWl-| zqYC&5xXe5tl}eG%=gpJ?kk98yrBbHrfp1o`m@>sHA z(@fQ8vstdMuc@!EHxCjK)1Y$X-CbExP%@b$m&=vY0u%wQtgPVicZqyTyh{*2lu4tNOK()8G%f-b-Wk8ig z6$KSTIh{^PBodVYiHL;5VNn#tfKpYpWbf3oA4Z?gM`L57VeRJThU4Sow;FU~{-(`j zGSb=EX*eA~=H})~aylY^xw1kShr=PEP^e^>qM$+;RaGS%4wn>D5R}PeMA!9-Uu&=3HTNQ91#4wjad482u_HniLAOixe0 z7;>NU^K&jQFWK4Ip{}kDfSQ^bY&IKSua~{OJ-l8oGcz+ZH#a{IdTsjp`dC<40O088 z2!PqySx!z)7#|-;*LAM0u82mXoSvTIa{c)evrSD+1%OX)g}J-Cqp7Kh*49=4L%Ki<5fK;g6>z>4>K~H@zE{}rk^dmS0l9V! U>QdEUKmY&$07*qoM6N<$g2{SzrT_o{ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d842bbded59bfb39fe63efef96c32a3fc306c2 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8JTOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>{XE)7O>#Ifodx0Z)WcxFQ1s;|fm~#}JR>Z>QONNGD1h zs~7M6vHXIB<5CVMA=eE{o!j(YK2exiveG#)!&u~EUf1Lu7Nu`e+RL;pc5Deac(6&q zYK38ULPmg~*TMjfrQ*UZA9kqBd8X&~KB(Zs&uQPQ-|xPE_xrx{<#QBYDaX&XC>KZ( zNSxAk>Pv~n)Vt?yO+S8X{uX`N+m~GTnQU^qQRl2)cl`OUmnC%z&F$2CxWC>#n3Qi} zwMkm|vf}>+vpVbM6_dCA_wRKwsdiXt6SsQB6swlqfqaj&ye3WJs$#Yc_50Mc>Cp3E z;Z8emJ$Txwe&y=QRwL1mOis>L+6#-eubO&xf@a;cMPXHUe{w8rC=0o^=J(pj8GD4@ zb>}7)Nfq7y*Dxv0KujZ$`Lkb;qx@BM&r=xz#*>yzJ+-d(nf8SAmiM8%K6MJLui!EH z*vy$A+AKbG{>0*A5h~}Zu73Mw>^U#~0Q;gE=A<`ouKz!Gd zO1D{+es2~;-Fl)c!1nCLL)+grDyNkD7%~#(Jn%KB^Rr~B=_uVM@ql5n7}p!76rMwS zmb?v1Kd3i3_3^*|E0*aotYFc8XlESx4j7H9C9V-ADTyViR>?)FK#IZ0z{ptFz(Uu^ zD8$gv%EZFT&`8(7)XKoX>`zu7iiX_$l+3hB+#1{$UNQn|kObKfoS#-wo>-L1P+nfH gmzkGcoSayYs+V7sKKq@G6i^X^r>mdKI;Vst0H<*r3IG5A literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..62f84ba9aab999a0dd93d7f6f501366879c97a10 GIT binary patch literal 842 zcmV-Q1GW5#P)?>2K~z|U?bk6#8*voJ@oy4|xyylA1VJ3MNhAn@IaE*)2eT>I z!Kq{m6rJ4M1Q927RB;Ik4h3E6q7^bp7Keaf21zg=h-fedAxbco*GbE1QqO77eD)azD!R~PsYsVkw~QW;NYNkY-}uPqWl-| zqYC&5xXe5tl}eG%=gpJ?kk98yrBbHrfp1o`m@>sHA z(@fQ8vstdMuc@!EHxCjK)1Y$X-CbExP%@b$m&=vY0u%wQtgPVicZqyTyh{*2lu4tNOK()8G%f-b-Wk8ig z6$KSTIh{^PBodVYiHL;5VNn#tfKpYpWbf3oA4Z?gM`L57VeRJThU4Sow;FU~{-(`j zGSb=EX*eA~=H})~aylY^xw1kShr=PEP^e^>qM$+;RaGS%4wn>D5R}PeMA!9-Uu&=3HTNQ91#4wjad482u_HniLAOixe0 z7;>NU^K&jQFWK4Ip{}kDfSQ^bY&IKSua~{OJ-l8oGcz+ZH#a{IdTsjp`dC<40O088 z2!PqySx!z)7#|-;*LAM0u82mXoSvTIa{c)evrSD+1%OX)g}J-Cqp7Kh*49=4L%Ki<5fK;g6>z>4>K~H@zE{}rk^dmS0l9V! U>QdEUKmY&$07*qoM6N<$g2{SzrT_o{ literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d842bbded59bfb39fe63efef96c32a3fc306c2 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8JTOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>{XE)7O>#Ifodx0Z)WcxFQ1s;|fm~#}JR>Z>QONNGD1h zs~7M6vHXIB<5CVMA=eE{o!j(YK2exiveG#)!&u~EUf1Lu7Nu`e+RL;pc5Deac(6&q zYK38ULPmg~*TMjfrQ*UZA9kqBd8X&~KB(Zs&uQPQ-|xPE_xrx{<#QBYDaX&XC>KZ( zNSxAk>Pv~n)Vt?yO+S8X{uX`N+m~GTnQU^qQRl2)cl`OUmnC%z&F$2CxWC>#n3Qi} zwMkm|vf}>+vpVbM6_dCA_wRKwsdiXt6SsQB6swlqfqaj&ye3WJs$#Yc_50Mc>Cp3E z;Z8emJ$Txwe&y=QRwL1mOis>L+6#-eubO&xf@a;cMPXHUe{w8rC=0o^=J(pj8GD4@ zb>}7)Nfq7y*Dxv0KujZ$`Lkb;qx@BM&r=xz#*>yzJ+-d(nf8SAmiM8%K6MJLui!EH z*vy$A+AKbG{>0*A5h~}Zu73Mw>^U#~0Q;gE=A<`ouKz!Gd zO1D{+es2~;-Fl)c!1nCLL)+grDyNkD7%~#(Jn%KB^Rr~B=_uVM@ql5n7}p!76rMwS zmb?v1Kd3i3_3^*|E0*aotYFc8XlESx4j7H9C9V-ADTyViR>?)FK#IZ0z{ptFz(Uu^ zD8$gv%EZFT&`8(7)XKoX>`zu7iiX_$l+3hB+#1{$UNQn|kObKfoS#-wo>-L1P+nfH gmzkGcoSayYs+V7sKKq@G6i^X^r>mdKI;Vst0H<*r3IG5A literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40.png new file mode 100644 index 0000000000000000000000000000000000000000..a18d3f74d5553f3808621c6c7f740161dae2a41f GIT binary patch literal 1346 zcmV-I1-<%-P)=`H1F=y_d{#*a_##OGO@J7aMy(H&h6bVrF$lcWoFztIjHnG_QqF8@n8hMz@y(c=hVla=M3v5YL}HdGap+ z4S2UyxO=2JrPNOQMFY(H2;(Usgp+RC{RAmRx}6`EwQ$~BmPI%mUY*HgTx|Un>G;$1RTg@GD%-wU&V56K1k2A*nXOtn&|B8B$-T-NF)dZ z0@T;nbK=AarlzJaO>FDSnlgT&*5s5@NbLI@`blQOfsC!s0A{>Bt!&M|oDWcIR z6B83iDam9qOioU^;Or{T{z^}`0~g|K-8;?;`$By2ZT2>O;geqdl*L@iqVH4;fOtZA`YSj|bX`X&#o4oG85|sR;Jj`!UXVZi zI?HR@{VcD^_SZCs-Bw)xH^YZLEBNYs-08db)mpy$Il4 zm~3sX;?g%!Pe9+gb?a(@HbCS@#s)36pVih#y4q%44oySQ{nBz~<}9RytsAO%a*e?= z8>;|lT5D3hOwii2jP>;f5BYtxZ>(ZNgGuw_h2IbaKx=DjO?7p3Ngt1L_3`Vmwe8Jw zzm+?#48`yIn~c1){v!mhRDvc~mW2d9tF+%GEva1n2k@$hn^t6nPyhe`07*qoM6N<$ Eg6_(MD*ylh literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..a18d3f74d5553f3808621c6c7f740161dae2a41f GIT binary patch literal 1346 zcmV-I1-<%-P)=`H1F=y_d{#*a_##OGO@J7aMy(H&h6bVrF$lcWoFztIjHnG_QqF8@n8hMz@y(c=hVla=M3v5YL}HdGap+ z4S2UyxO=2JrPNOQMFY(H2;(Usgp+RC{RAmRx}6`EwQ$~BmPI%mUY*HgTx|Un>G;$1RTg@GD%-wU&V56K1k2A*nXOtn&|B8B$-T-NF)dZ z0@T;nbK=AarlzJaO>FDSnlgT&*5s5@NbLI@`blQOfsC!s0A{>Bt!&M|oDWcIR z6B83iDam9qOioU^;Or{T{z^}`0~g|K-8;?;`$By2ZT2>O;geqdl*L@iqVH4;fOtZA`YSj|bX`X&#o4oG85|sR;Jj`!UXVZi zI?HR@{VcD^_SZCs-Bw)xH^YZLEBNYs-08db)mpy$Il4 zm~3sX;?g%!Pe9+gb?a(@HbCS@#s)36pVih#y4q%44oySQ{nBz~<}9RytsAO%a*e?= z8>;|lT5D3hOwii2jP>;f5BYtxZ>(ZNgGuw_h2IbaKx=DjO?7p3Ngt1L_3`Vmwe8Jw zzm+?#48`yIn~c1){v!mhRDvc~mW2d9tF+%GEva1n2k@$hn^t6nPyhe`07*qoM6N<$ Eg6_(MD*ylh literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..93ef2fac9c51b2ffcccd13e58f196fcf9ba8710e GIT binary patch literal 867 zcmV-p1DyPcP)_n7YETn zL=Z=X9y;ElAUe4f1wo2qr`j$CqUa*%YDEW^Zi3)qD;9)2Hyjr%No#Xn{~YvzEa82V zANe5fmjK;#(@p)5{6VtJ26J_F#g&znM$4@%FE8`@`r1uPnPgG~uni#4?g! z1Cx`JFbt!W9($Z#;kvHV@Ane`{eC}BPfuM>j(-(LpsK0_U>)s5Is*6i_bCWMM<5-6 zBzbsvXhZh&^e~^#x6)S|NOlIYE|7Fa3WC7R&COQ$z6O$HDwVP!0dRMBx7ohNfTn43 zVq(Gu?Cx6C&4%-MJj`aZO~=&**21Z(%0i*g z5?oDSEnF}dY$=W;Jx=p^eSICNRI2Kns;W3YKgZbESVJ@Z_4O5xkB3SXV6j+qypH33aAafzhGAfSejXPW7l_B>4K1rva=9GN&dv~xMgah& zQVAOy8_;zfv$L~S-QwaR5{U$!o}Q}uYi?L$u^2L$3}Ug^U*iB^XJ-eRrs3%52mnA5 zg+c*duNN~jGgjU7^fcz?<`4)}f2KEm%YSoogG?raNF)LP=BRR$V+E z$N2a-DwT>2_|^=V*XxC%C{^ctJ|ARRhA4_SI5>c=>$tqUgrX=|T3SLPkw7+^{oGO( tNG9?7|4eT)px;Otz|VFgy6L9l^9$DivAHGCtBwEw002ovPDHLkV1ftuf3pAp literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb56d4b7eda07039527f07d1bf74dce654738a0 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU0wmSG7d!(}Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4?NMQuI$e_#JCN3B8SwjD*xj2cW2sn_7_Wd&&X9gd@r0Oaf(Tm{Muj2HA#WG z_mnf;oP2`w-aI}vlTY#qgH~64@R}X|tEL2(x4$cdg z;-&ZS@I^&!*y(<>scG7?T)oxn7G%G8cJ8W3$@IH1pEl3&nzLkH>Wih*mo&%rZ&|-c z&f;-P)~g1V$w}22>HGW*z3iyHY2!qQr(Vug;x*TDy_M6?p*!q*dBx54ywj3k?aXGb+h(z<;=-@ zBtPXmr_dKhv9l>R84@O&zT11iDT@294C8he%QU_Q*?)mq&EH#-BiF@ONd`TURPd6% zuCwvz`{#`&Jq6!)3O^St=ZU_*|Hx5xJH|9auG7C0)H#7Ms#@Y2QIe8al4_M)lnSI6 zj0}v7bqy?Zjf_GJ4XsQptPG8G4NR>J49xyy^`U6U%}>cptHiCrec>e|paw~h4Z-BuF?hQAxvX_n7YETn zL=Z=X9y;ElAUe4f1wo2qr`j$CqUa*%YDEW^Zi3)qD;9)2Hyjr%No#Xn{~YvzEa82V zANe5fmjK;#(@p)5{6VtJ26J_F#g&znM$4@%FE8`@`r1uPnPgG~uni#4?g! z1Cx`JFbt!W9($Z#;kvHV@Ane`{eC}BPfuM>j(-(LpsK0_U>)s5Is*6i_bCWMM<5-6 zBzbsvXhZh&^e~^#x6)S|NOlIYE|7Fa3WC7R&COQ$z6O$HDwVP!0dRMBx7ohNfTn43 zVq(Gu?Cx6C&4%-MJj`aZO~=&**21Z(%0i*g z5?oDSEnF}dY$=W;Jx=p^eSICNRI2Kns;W3YKgZbESVJ@Z_4O5xkB3SXV6j+qypH33aAafzhGAfSejXPW7l_B>4K1rva=9GN&dv~xMgah& zQVAOy8_;zfv$L~S-QwaR5{U$!o}Q}uYi?L$u^2L$3}Ug^U*iB^XJ-eRrs3%52mnA5 zg+c*duNN~jGgjU7^fcz?<`4)}f2KEm%YSoogG?raNF)LP=BRR$V+E z$N2a-DwT>2_|^=V*XxC%C{^ctJ|ARRhA4_SI5>c=>$tqUgrX=|T3SLPkw7+^{oGO( tNG9?7|4eT)px;Otz|VFgy6L9l^9$DivAHGCtBwEw002ovPDHLkV1ftuf3pAp literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb56d4b7eda07039527f07d1bf74dce654738a0 GIT binary patch literal 741 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c646e57cfb0a88d4b377440f0a6a69d6015906f1 GIT binary patch literal 792 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`q0KcVYP7-hXC4kjGiz z5n0T@z;^_M8K-LVNdpDhOFVsD*`ITWaU1Y$voYgkU|@Xa>Eaj?aro`@{ob=3C63#7 zUOKqyMSzooDu?KDg)1u)V!i${HU)Hg7mECE6_btOdZe<{dDo4VSAq&Iuf4?8HF;Ub zgpU1_E*)JsO@O)UuT{pQ-)^6ri`7Lx*_^g5|86(`arODTrz=}xKS!O5XZ*v?@It-u z)6a0;PA0oGJDnbL{@T-$xoO)EwL@{;FBxaO|F0I!UXk+C=u?7Z!T;|M)-f_}yP=dI z@^FWB)D~q=tAOl-(l6ZJ@t95BXv@mO7$fx7dP$U#+LY$7#reC8oSk+$b9da~d))Cl zHt$I1yqRoIl>DydN;Sx3oo)1(t#w(uy5+o}s_)dyW5(*g4*ffoAEs3P%lYS-yiL}B zH@!MDZRN)=hl;9p{yQnybn_ekFVClId(2B1IPC;d+3Yu*ujZ3@YRSM|vuDmrt;_dS zm$mD~JxFZ{m3r}1Gqh(Ti%sH&DYv)mu;}jMXjS>Dv8r;Rre`nLYmM}@t&>a7Fl-Jn z3@Z34{;ffw`A@QVM)I8SH6PhmJmmh+_w;n-)pzHETD)xLzjd*AYI@tDf%Du8t-mVb z-(8P>iTLUSG;6EoO8-SOT+BC`-0ssjrgFLFoY$6vx7WDr&-?5Yp*}Bny#RaKizlk~ zKmC*gW=r}w$bbCQA1v2@`Sr>@U$h%Gy_K26abc!v{bOTbB2X=HjVMV;EJ?LWE=mPb z3`Pb<#<~U;x<*DJhK5!q7FLEPx(23J1_q%z@mo+d&tmyoe-$JTDsgV^BMG-ce4K|w%q9_(AQkF;^ zG_J3&X*QdQ_%21G(a6io%UsVbiXkb`+1Z&0|8<#6CSF}#6*1oLNVbs4WJ((oMG=A^ zlvg`RnZ3QeQ4oZ*r+_kRCrJUaG9+XL$nux?Re+S2(CKtMIyzE1rz9B(kn$2*t(J#} zhf3y@Bx3=Rj7FpE>FN1VqNb)ssf@B@b(DB`c)dU1Pu3m|@ooSK?K zdwV<9*4A)vaFCeqa=9=uF_BbLavN=JZ7pbc00;(y*x%pB=jSJUJ|Ft}`tbbxj6fiO zPTK8ulH}&*CIK)I2+(S^a%5zL zBsn`fO8|U)e5A+YVN+9+Y)!3{SY2Hmc6WC%IXQ_~EC!#?m-EVh=i}o8p->2ZzaJWn z24=Gv01ys`vADR1_4Rf1_V%K=xfuZP{{9ZP+YJDio}R|w;9yccNdW-R>-E^$+R8ih zch1kx;cz&h)9J9hybO!Q0wDgew6L&%>gs9)gTc%T=G7Bf;&eJ`u~--mhxzjIk{JJL zp^J+P*4Ni_e0-cF^J^icsI9F{ta>U|s}=L}^O%{L!PwXsfaDhH>gvMW++5NeEr7o$ z(m#Uk?rs>3#7?XKr|Xnof@ek`3Ld8GzLY} l7oG?JPpLzwsHpt!`~&*JRf*~>{eA!d002ovPDHLkV1mx~w^RTC literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c646e57cfb0a88d4b377440f0a6a69d6015906f1 GIT binary patch literal 792 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..d83ab159dbe2665bff41ebdeb58e7661c0c6e275 GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw0wgDV75oXLSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXICJ|Af_a5cNd2L?fqx=19_YU z9+AZi3_>L!%y=(Nz5*!7UgGKN%Kn@~jN3rM=|yu4P*c07i(^Q{;kPplGlT;rj@OGj z2eM4P*5w$()f61|a@-%XeaD#h83oQn^2S-}il|-|zcg|9$UtYwwU_PNI*l9WX5@ z5Mx_CH&;hn#%#~uhrgxeyZ*;~KlY8c_OWYIP){~b(v1}n)r*r|lLfk7PMvQt`=7w_ zjXmc#ADt;6_&B2B>(%Y6R`chqbo#yM>O<~5GtZy-(Gl&<9Q^MfnK#p=KF2Mv_<`Qjs0kU(1lX?}TUmU_IK}w&}Wk z_~HxAYwC+$u8donu9|%LfuHR4Jxk1ZJ$Ehe^YyTr&&ad9`ZveJ8n|D~xsJ8l;H-dKBGX7^5S>Af+!8(G|LF#23%%3<|m_->TUe1@rTS@+a4 zqN-~zv@D7AytHDCOs_v@`>~5olFQ$^pH%(NS=?hdr_!S>^6b7j!ms?|m_PqB>%3&N z?sQdV@8K%N%>R?5*uO2$ZIpPV#LI9eX_Dt7%g2gB|M`A;T~Z956n{Y3jl=kW`6QQ& zMV+PHj`Fqpf^>>Ezuew(^_*T9o9WZaH`d?3-dI)3v$8S!LD9|pNs|sNY@K>H0GLWu zOI#yLQW8s2t&)pUffR$0fswJUfrYM-QHY_Tm5GIwk%_K>sg;4j?Zpw{C>nC}Q!>*k zacfxh@4;)J21$?&!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NC6cwc)I$ztaD0e F0sxBkkjVf5 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..d742b734893faedac5728062c45d74778ae668a0 GIT binary patch literal 1234 zcmV;@1TFiCP)#?W-CWYi7V{!c%I3Abm_P~Tv=HO zQH(w0?CcD!t*!9;{gSm;R!FoXGcz+3Me)T-f3%)XC`{&-bW*}(?noysOy-7kO2TAf zX;Bn8KR=&|TqPl8VKT9_APAhBo0B?MNk~hWB;9UzLh0q@Whrx>Uc8a&U0)MPUHc zXf#sgDH&=1Tk`w;7#kalJEpU-A8pAMWJt@9*R2=qNH)qtT$Qt`36m za9ewQeI3Wg$4}zvbh_9F`0;jQe0)6aZ)UgKE!obi)#@kl^YZf8-``J??CtG+wtjVW zHAA5gNwT@Q`Puc6k&&3|#1+20yku={ZN_Dzg%=hU=yW<+Qc^+y+}_@f2-|EnE-Wn2 zU@#B>`}+Ex2oDYp(&ci|<#O@h;2?Ejk~}{@k1T);_UxnZ&CLx-(qu9b0B2`sBf=h! zha@>UIY|ICo6RKu6MjiDn*2n)rBEoay1I(?_IB*->|knY3TCr8(X>|)3Pf;cX#9N?hX!z1I5L~P%4!O27?F$0tf^G2!%qiLj5IB!bcC^IRx+}`r*=F w0DSkRDP@j-0Q@cpg7>Wxk&%&+k&%hw8+GfrA&^ydTmS$707*qoM6N<$f~6%iCjbBd literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..d83ab159dbe2665bff41ebdeb58e7661c0c6e275 GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw0wgDV75oXLSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXICJ|Af_a5cNd2L?fqx=19_YU z9+AZi3_>L!%y=(Nz5*!7UgGKN%Kn@~jN3rM=|yu4P*c07i(^Q{;kPplGlT;rj@OGj z2eM4P*5w$()f61|a@-%XeaD#h83oQn^2S-}il|-|zcg|9$UtYwwU_PNI*l9WX5@ z5Mx_CH&;hn#%#~uhrgxeyZ*;~KlY8c_OWYIP){~b(v1}n)r*r|lLfk7PMvQt`=7w_ zjXmc#ADt;6_&B2B>(%Y6R`chqbo#yM>O<~5GtZy-(Gl&<9Q^MfnK#p=KF2Mv_<`Qjs0kU(1lX?}TUmU_IK}w&}Wk z_~HxAYwC+$u8donu9|%LfuHR4Jxk1ZJ$Ehe^YyTr&&ad9`ZveJ8n|D~xsJ8l;H-dKBGX7^5S>Af+!8(G|LF#23%%3<|m_->TUe1@rTS@+a4 zqN-~zv@D7AytHDCOs_v@`>~5olFQ$^pH%(NS=?hdr_!S>^6b7j!ms?|m_PqB>%3&N z?sQdV@8K%N%>R?5*uO2$ZIpPV#LI9eX_Dt7%g2gB|M`A;T~Z956n{Y3jl=kW`6QQ& zMV+PHj`Fqpf^>>Ezuew(^_*T9o9WZaH`d?3-dI)3v$8S!LD9|pNs|sNY@K>H0GLWu zOI#yLQW8s2t&)pUffR$0fswJUfrYM-QHY_Tm5GIwk%_K>sg;4j?Zpw{C>nC}Q!>*k zacfxh@4;)J21$?&!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NC6cwc)I$ztaD0e F0sxBkkjVf5 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8b45704fca650b7c19fbac4a4a11fbef1cfdfe GIT binary patch literal 2141 zcmV-j2%`6iP)$MH>1GhF_qXw1MN!mgawG=^H zQ@fR#h{qzKNUd7HsZ=FWE0>zc6;f65p+%umLK4ZuphzJ~LX;qk5-I}57%&etHmYb; zTPE=XZw)1JA-2KTJCA;FXEHN8`!Ke5jk14fG&A>}d+t5|Ip^FvcLp-ZAcG7t$RL9Z zzMv2Q?d|QWfbRkFbQN}VbOffW!;Xs=FOE%Dhpy|I5aL2(V`H1UPPDhT{|4C2|H+sx zIyyRLLd|UGGl1VVHa5OwFO+~kFhj=AaR^|$yHEm3Q$Lopp~PJvrDGIx2^j*?J%DuM z3r~QuW5*5v48w3#xbKrKyW4i@&;_8(=0N5u8FnbC8pvEB!wn@J1DQ)?M1_FYeKJhm zOJvw#g@Bi!uIpBvy=s!v}qI7)zuh=K}AIcJ9qBn%9SfflH}ZCq2-%G`?^RW$rly9f zsw%545C~9LS4S`yMAvn6U5^S8ue^~=Zkg$J$q*@d3F^9zrfDo+zMRU+N-GRZ+1%V* zwr$%+Sy`D?Znm3kqh^W>i-357*RNl97;eAL$;qLvu8y*@GBizdB+OJ9H66np1jK6G z-Q7(?LxaO``(b)Qd3kvhQK>X)I%G((1SLsAmSxVIIYUE31Dd8qd4aC$1OfrJZrw^S z7_?;UdeY(U$rx@C;1(7_Scjd2J$v?82HFpkgxcC#D=~T*KUc``Bp?<`CJBn7(AL&Q zV`HNg?`|S;a&oAxt+jH(-Q3d!tA&uLFSq_4V~ETed9j6q#-^ z90bG?w6E^WP%uf@w{KrmIM}DanwlDmghZZ@YBJo2yC2|Q^P8S<_Uu^C@Co+KR=&vI2@G_O)@TAxZpV>OviwkCSyulVy4ms3l^|;?OG&B zqOY$HP16VlgN{zmojb>oBS%maC9dx2lubvMiG*8X?mdCR!a_o!5I&y|P1ErCeDwA8 z0Z>{x`H`u$wUq-04&d|oX603RI=bA5yCr6S^17~5P*6Z96e2r28%@&ykYyR4&qr@> zFGE8^T)K3L)2C0PD2nyE?1i~PM$9cS`&rX8hw*l=5JI3R3Yw;I?AS57ySq^o1zDCY z2}@DB`;$b5hTvbKltbkQC(eac(G!{UH_p)6Fgt@De@fq4rO5MiV6X{d*|il5ekLK%F1#WZ6~6+xtYtCFC)vcr||$k zS$ccU84n;3Pk1hc&FiwVGJHOt^`z~GrfD=cH`CqSZ7q&s?GerkIQIu)LD!*sAi|}O z$GI^Q-R8!K&h0zy2R2zgHtsB+h!~zaTqyM15;LD}XJ;qZu3d8&ZJtk@IN?Zm$?g)z z!rLcDS^Rh(5B{Q`ho2arWL*y*_Dopy?Oo#(uj!%ccmH;@`*<)y@tPi1JUPHbd|2yTaim%K;rSLJi?!-oV<8DO76wlVmkXL*mL-^ zg#DWE+!AxUwVCDZF(2=VPbCC`#R`fn_sf_05|J`-VvZ289p{ z;~6kjL#LVqU#cx;;JV7APY<%V)JMU5iLU+#ExXIue{_ULSLE>B2eK{sf_W0V-?_;< zt)qN@sq@cVA)w*+#Vp%0kW}9au+uOM$FH(8{brVkkiz*gr(Ous`j0XG*)xvcCwTa4 zejfZv7JuAa$kMO+9rE{=`e^!dDPPK$DJ_yY@TU?MFZ4O;k3YAN@F$7GKmb%$R<1G( zW3><>*Zoz;3tlqu+P(bLf7d+_R4WoHP6JZ%k8tGFl3l_u>Qd3y6w#ZKR}@7R&x6sI zioT|RaSWbL*~to|sR?xAFmK?WIQkWTy$h$T*% TZT($D00000NkvXXu0mjfq_GY9 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8b45704fca650b7c19fbac4a4a11fbef1cfdfe GIT binary patch literal 2141 zcmV-j2%`6iP)$MH>1GhF_qXw1MN!mgawG=^H zQ@fR#h{qzKNUd7HsZ=FWE0>zc6;f65p+%umLK4ZuphzJ~LX;qk5-I}57%&etHmYb; zTPE=XZw)1JA-2KTJCA;FXEHN8`!Ke5jk14fG&A>}d+t5|Ip^FvcLp-ZAcG7t$RL9Z zzMv2Q?d|QWfbRkFbQN}VbOffW!;Xs=FOE%Dhpy|I5aL2(V`H1UPPDhT{|4C2|H+sx zIyyRLLd|UGGl1VVHa5OwFO+~kFhj=AaR^|$yHEm3Q$Lopp~PJvrDGIx2^j*?J%DuM z3r~QuW5*5v48w3#xbKrKyW4i@&;_8(=0N5u8FnbC8pvEB!wn@J1DQ)?M1_FYeKJhm zOJvw#g@Bi!uIpBvy=s!v}qI7)zuh=K}AIcJ9qBn%9SfflH}ZCq2-%G`?^RW$rly9f zsw%545C~9LS4S`yMAvn6U5^S8ue^~=Zkg$J$q*@d3F^9zrfDo+zMRU+N-GRZ+1%V* zwr$%+Sy`D?Znm3kqh^W>i-357*RNl97;eAL$;qLvu8y*@GBizdB+OJ9H66np1jK6G z-Q7(?LxaO``(b)Qd3kvhQK>X)I%G((1SLsAmSxVIIYUE31Dd8qd4aC$1OfrJZrw^S z7_?;UdeY(U$rx@C;1(7_Scjd2J$v?82HFpkgxcC#D=~T*KUc``Bp?<`CJBn7(AL&Q zV`HNg?`|S;a&oAxt+jH(-Q3d!tA&uLFSq_4V~ETed9j6q#-^ z90bG?w6E^WP%uf@w{KrmIM}DanwlDmghZZ@YBJo2yC2|Q^P8S<_Uu^C@Co+KR=&vI2@G_O)@TAxZpV>OviwkCSyulVy4ms3l^|;?OG&B zqOY$HP16VlgN{zmojb>oBS%maC9dx2lubvMiG*8X?mdCR!a_o!5I&y|P1ErCeDwA8 z0Z>{x`H`u$wUq-04&d|oX603RI=bA5yCr6S^17~5P*6Z96e2r28%@&ykYyR4&qr@> zFGE8^T)K3L)2C0PD2nyE?1i~PM$9cS`&rX8hw*l=5JI3R3Yw;I?AS57ySq^o1zDCY z2}@DB`;$b5hTvbKltbkQC(eac(G!{UH_p)6Fgt@De@fq4rO5MiV6X{d*|il5ekLK%F1#WZ6~6+xtYtCFC)vcr||$k zS$ccU84n;3Pk1hc&FiwVGJHOt^`z~GrfD=cH`CqSZ7q&s?GerkIQIu)LD!*sAi|}O z$GI^Q-R8!K&h0zy2R2zgHtsB+h!~zaTqyM15;LD}XJ;qZu3d8&ZJtk@IN?Zm$?g)z z!rLcDS^Rh(5B{Q`ho2arWL*y*_Dopy?Oo#(uj!%ccmH;@`*<)y@tPi1JUPHbd|2yTaim%K;rSLJi?!-oV<8DO76wlVmkXL*mL-^ zg#DWE+!AxUwVCDZF(2=VPbCC`#R`fn_sf_05|J`-VvZ289p{ z;~6kjL#LVqU#cx;;JV7APY<%V)JMU5iLU+#ExXIue{_ULSLE>B2eK{sf_W0V-?_;< zt)qN@sq@cVA)w*+#Vp%0kW}9au+uOM$FH(8{brVkkiz*gr(Ous`j0XG*)xvcCwTa4 zejfZv7JuAa$kMO+9rE{=`e^!dDPPK$DJ_yY@TU?MFZ4O;k3YAN@F$7GKmb%$R<1G( zW3><>*Zoz;3tlqu+P(bLf7d+_R4WoHP6JZ%k8tGFl3l_u>Qd3y6w#ZKR}@7R&x6sI zioT|RaSWbL*~to|sR?xAFmK?WIQkWTy$h$T*% TZT($D00000NkvXXu0mjfq_GY9 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..4d97ed69bd34fc76376e96fc818bcc58d26b7a49 GIT binary patch literal 1260 zcmV0AgC_v~0gyzc& z$N@k?Cm|%cUayze4$scc5Dte?Sy{E`ie;FgiNQD&e!P37oG2*(!mv0HW-a zkdgqR?30kP03vP@8XFsVPBk;6C4h*VgxcC#K6A|oX$y!(qhzz$SS2(!H}jb<@1!k& z=vji*YGsu$KR?e`zP$6M$Km_?dmJ1bupU=hTKbkSnI&Cwp-_lwYin7nKRG$cXTf+U zEdkjwc%n1}a2dQrBB80NsZ8X{J1Gg^GI*&}NfJv)K@hMw#~6*;#CFZ==1v9S;u=a5|lF=XZ8?!sqibn^LJv9*9d-{C+>l zb*_$|)Y}8UO$d z4GmaYT7uDNL^K-3;^HE2-+4AxR#pH2N~IDim5SNFx3`B-C4D%H8WyQACN+f+_Tnb-_atJPFkSjbf0<#G{G@-?8pzn>zJ2oWV; z1FEX3UX90T2E$ttWMN?A~G#U*6;Qag?J3Bi7 z0KHz1larH#^XlvCQB+j)aNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1OBOz#zOHgc*~c^&JKZvX^-Jy0Sm#5aTwGG=0DM6j0MdPZ!6KjC*fq`eq0_iX5+( zR_ef zCgR8<^`-2Ae;R_ATW2kCb^IrH=ZMN|li9P*$la-&erERE+Wp_3ot=HRS9MP=WB=RV z40a9t3@sj$9Irmi zdCU=?el#AidM1>>eRfZ2gxGmt>0+|UaygTgb{(H_@ z<@hw}40nUER!;LX#x#}9980^3Cq=R>*LvoVk)bywSjtsny;(;<<^c((r;6%_W<)j= zaR{d|>co~R^vq&$nARr4VOz?&qDW@@g;y79bN*z`m>fE*;D@~Fb{4lY;cNx7{x7}! z)jXc_KxSRs++KrtxdRJ+7BRS;wPxg*9loI?{NB>NEA}4{oVV-7+2c1^9lF?S7=GPg zIbbeS!+MBye$u(gYh&2$rdP-pzVx)Qf4;n`MeXz7HC8RZxZmwsm@sd#(6>ziPySAp z%DUysXK==7-OcsV%WoV{4lBAQSMSlnA^1#eeT(cVoy50~C;e)<<~_+QD<~-9#gZlA zWh`xXo-o)v*6?I1*%|fwQklezWU+?3Yt*=GHXZ*~Ed1k8>#KCf?rT?OoGtX!^!1+f ztGnpIv}xi`54_G2Wbn{GSkCcjZeiQb-0$x<&YipFLHk{QA?9b^X;(AtpBE{p*B2$s zO;ee>LYMv3&9qLT=T?guwg@vToC|ThvZ?oPSXF!6p_pf~7qaZ{_55c&5z%uYmfyMh z#wqE0&wtNdliDM)VdmZgBDX&!pPzZ|ua83q%gbX|f4Qoht#z9AfB!=M2h4xIGwf$H ziCLv4opt#DFukglxJHzuB$lLFB^RXvDF!10BV%0y3tc0l5JN*N6ALRNQ(Xg7D+7bt zE5VOZH00)|WTsW(*1*&GIv%J&5@bVgep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U& PKt&9mu6{1-oD!M<;i!#F literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..4d97ed69bd34fc76376e96fc818bcc58d26b7a49 GIT binary patch literal 1260 zcmV0AgC_v~0gyzc& z$N@k?Cm|%cUayze4$scc5Dte?Sy{E`ie;FgiNQD&e!P37oG2*(!mv0HW-a zkdgqR?30kP03vP@8XFsVPBk;6C4h*VgxcC#K6A|oX$y!(qhzz$SS2(!H}jb<@1!k& z=vji*YGsu$KR?e`zP$6M$Km_?dmJ1bupU=hTKbkSnI&Cwp-_lwYin7nKRG$cXTf+U zEdkjwc%n1}a2dQrBB80NsZ8X{J1Gg^GI*&}NfJv)K@hMw#~6*;#CFZ==1v9S;u=a5|lF=XZ8?!sqibn^LJv9*9d-{C+>l zb*_$|)Y}8UO$d z4GmaYT7uDNL^K-3;^HE2-+4AxR#pH2N~IDim5SNFx3`B-C4D%H8WyQACN+f+_Tnb-_atJPFkSjbf0<#G{G@-?8pzn>zJ2oWV; z1FEX3UX90T2E$ttWMN?A~G#U*6;Qag?J3Bi7 z0KHz1larH#^XlvCQB+j)aNS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+ z;1OBOz#zOHgc*~c^&JKZvX^-Jy0Sm#5aTwGG=0DM6j0MdPZ!6KjC*fq`eq0_iX5+( zR_ef zCgR8<^`-2Ae;R_ATW2kCb^IrH=ZMN|li9P*$la-&erERE+Wp_3ot=HRS9MP=WB=RV z40a9t3@sj$9Irmi zdCU=?el#AidM1>>eRfZ2gxGmt>0+|UaygTgb{(H_@ z<@hw}40nUER!;LX#x#}9980^3Cq=R>*LvoVk)bywSjtsny;(;<<^c((r;6%_W<)j= zaR{d|>co~R^vq&$nARr4VOz?&qDW@@g;y79bN*z`m>fE*;D@~Fb{4lY;cNx7{x7}! z)jXc_KxSRs++KrtxdRJ+7BRS;wPxg*9loI?{NB>NEA}4{oVV-7+2c1^9lF?S7=GPg zIbbeS!+MBye$u(gYh&2$rdP-pzVx)Qf4;n`MeXz7HC8RZxZmwsm@sd#(6>ziPySAp z%DUysXK==7-OcsV%WoV{4lBAQSMSlnA^1#eeT(cVoy50~C;e)<<~_+QD<~-9#gZlA zWh`xXo-o)v*6?I1*%|fwQklezWU+?3Yt*=GHXZ*~Ed1k8>#KCf?rT?OoGtX!^!1+f ztGnpIv}xi`54_G2Wbn{GSkCcjZeiQb-0$x<&YipFLHk{QA?9b^X;(AtpBE{p*B2$s zO;ee>LYMv3&9qLT=T?guwg@vToC|ThvZ?oPSXF!6p_pf~7qaZ{_55c&5z%uYmfyMh z#wqE0&wtNdliDM)VdmZgBDX&!pPzZ|ua83q%gbX|f4Qoht#z9AfB!=M2h4xIGwf$H ziCLv4opt#DFukglxJHzuB$lLFB^RXvDF!10BV%0y3tc0l5JN*N6ALRNQ(Xg7D+7bt zE5VOZH00)|WTsW(*1*&GIv%J&5@bVgep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U& PKt&9mu6{1-oD!M<;i!#F literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72.png new file mode 100644 index 0000000000000000000000000000000000000000..11c86c39b9c50c198a2d91dab63bf1c1231682b9 GIT binary patch literal 2433 zcmV-{34Zp8P)wY#K~#90?OS_n6vrL@W_IuJ^K4@taiRtn8;Lr_y7r(>RURr( zYGWxaeWXEAt3*&^DFh`Is+6`MWb=?C^Q3u1*1E!K;NakzblMc??CeapNeqyTpn z#2CY>RjaUN%N77YX=y2ni;L0R+zd_AKnMY)G-WAIHwkuJEc)>q6(jyK#;|hb$|yOa zp4aQejvYIqZpIihm8o4hTgACdwKoH`GQt=`MMXs%IpV3TtVDf%eN<(7VrMD|sk#|W zWJC~DQc?l{F$&Q?KA#Wu_4U#Enx@TaB^eR3Sv9MC=FAyfyLQc#9Pun)z8u@OZHtm9 zGG8W@WM$KCdws?hpjHhy=TKD@jg5^lB$=tn=ksCv_U+M_WTvEaOk&2k7;rajOOgat zRk3&PUUYSJSt?1yBw@#>q;yYWT3t*m1Vsv06b1YC?XykNM3s~Y66_EoVCrJ}6*8q1 zk|aTvWvHr({rmSPb+={9mZ7e$E^aWhQAu$wrZ=KjkRVD`RU9~QAZg&PtgJ+1Vx3?5)fuHUN>(@=Mn^{n#>U1h-7PyiJI2;bRFWAIx|o>}p(asIDMf8< zEfy|Z2*2MCRaH$D6bglK@ZiB{cFaVuO;@l^=wfC@jG3smwiab&WpKOQV2qiP6b^^6 zXU`rC4-e0{IFmLPGuvBj+_(|t<>k?sB+GJ?Bt=ocImgJz2zKq-g`uILs9RcLx?Rl7 z?Gv6T8jTC}Xqpx!M;H`A+vRdWRaIQLZ~-ot3oR`zP!t8SEJKo{galdjKhd&uvpneQ0lQM_ zLrqN$%F4=Ojy3cuqm*KBa4?1(p`6&Y6yPllqogQe+Gn~ZtKTvub8i6n^1_Jk$4zB* zoRN@3`8l$t`!lkB{d%4#wF}iS#!y{djq>vHxD2JQ)7#sNwzjqy<%rl5eZX+wr-N8g z9=1L4X~GES^!rI&jB}2fni?!#yg1>hB4Z4FeSK(cZH1<3rsN2Ft16V)Kn?)pQo^6J zbTP&lmM&dtNe<^6y}i9?ZEZy)5`iqsrsN2eE{J4idT9U$e<11IyrL*^yK%eZm?J(Y zCqm#>1SihkLHB>h;F1WwQtHOfzMBuXYuxsm=kC6LbVEMwn>YS>M>LM-UmrqF7R3{r z3ZNg(KQ4~Kevf|St3IKO*)diCm66c-o6>-CzFBVvx8905Dm(ur`aJ%}e> z=z}`OvG`sWTr$B+CvRipgE_eO3nE4pj=E?2FdSkCs~o!@n;UIE76E>B=yL#oTL$VL zDu}*2bs>c3|1xBUAnS#qNZseppNHS?PZ*E|If4+QYuQ<`>hbQsMzQtTUU-+x!KW>Y zaOLLu&;03g1a3{twPP+0^$;mi zECT(0Km2|_G)+Sw5HKZ2{|K9@5F^m=@&NL)DNZz$p!A;cUmcVb%gAz%6MyVf5UVP) zQUARH^aRH6{-x14TU%;9$jzpB^2NCCM%e zVl2Y(;pH&ac(ZZOTq$9_8~qWy(;31~*5+ZuH*!#vPw~p>U|hZ2Y>MAMJs+*_kKm27 zcarvRMBEbB=we2aIOmXM*^(SHn|75H3^7DBj*?>Q-@zO|9fU*)ey}DFb6fgPa{UiY-xSI#e56Z-ms|d!%jAdt|n2ab-E=87`;OY(4 zvfeB0LFCLK*#1I49@+ChT7DJ#&@3_~d*1`Mqxj&m`GWGJ|Ag`BXDX^HvQU&yQIt>dwIy>ELf|K1(0l`oT7x_&u`tvjs6IVJ@N7D(eLErwReJe`=bzC za`L^|7dFWSF^Oa@VpNh<-9%+}P*Li}+2;G;UE)T=(E(KN3}Dl*Z{mZ?VMH{J6Yt)^ z+OKEFkOKgl*XKcFz^i{ZR}lcb^os@Xcf*Vm^pBJ zj+jP7&Mw&u@Rm&XI}vc;lZ@VT_$&<{E2R{=gH zzl@#?DCPYW^lxNM*OloWivtG^95`^`V9M}+LEBZ4LQA9d00000NkvXXu0mjfSQ&Y; literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..11c86c39b9c50c198a2d91dab63bf1c1231682b9 GIT binary patch literal 2433 zcmV-{34Zp8P)wY#K~#90?OS_n6vrL@W_IuJ^K4@taiRtn8;Lr_y7r(>RURr( zYGWxaeWXEAt3*&^DFh`Is+6`MWb=?C^Q3u1*1E!K;NakzblMc??CeapNeqyTpn z#2CY>RjaUN%N77YX=y2ni;L0R+zd_AKnMY)G-WAIHwkuJEc)>q6(jyK#;|hb$|yOa zp4aQejvYIqZpIihm8o4hTgACdwKoH`GQt=`MMXs%IpV3TtVDf%eN<(7VrMD|sk#|W zWJC~DQc?l{F$&Q?KA#Wu_4U#Enx@TaB^eR3Sv9MC=FAyfyLQc#9Pun)z8u@OZHtm9 zGG8W@WM$KCdws?hpjHhy=TKD@jg5^lB$=tn=ksCv_U+M_WTvEaOk&2k7;rajOOgat zRk3&PUUYSJSt?1yBw@#>q;yYWT3t*m1Vsv06b1YC?XykNM3s~Y66_EoVCrJ}6*8q1 zk|aTvWvHr({rmSPb+={9mZ7e$E^aWhQAu$wrZ=KjkRVD`RU9~QAZg&PtgJ+1Vx3?5)fuHUN>(@=Mn^{n#>U1h-7PyiJI2;bRFWAIx|o>}p(asIDMf8< zEfy|Z2*2MCRaH$D6bglK@ZiB{cFaVuO;@l^=wfC@jG3smwiab&WpKOQV2qiP6b^^6 zXU`rC4-e0{IFmLPGuvBj+_(|t<>k?sB+GJ?Bt=ocImgJz2zKq-g`uILs9RcLx?Rl7 z?Gv6T8jTC}Xqpx!M;H`A+vRdWRaIQLZ~-ot3oR`zP!t8SEJKo{galdjKhd&uvpneQ0lQM_ zLrqN$%F4=Ojy3cuqm*KBa4?1(p`6&Y6yPllqogQe+Gn~ZtKTvub8i6n^1_Jk$4zB* zoRN@3`8l$t`!lkB{d%4#wF}iS#!y{djq>vHxD2JQ)7#sNwzjqy<%rl5eZX+wr-N8g z9=1L4X~GES^!rI&jB}2fni?!#yg1>hB4Z4FeSK(cZH1<3rsN2Ft16V)Kn?)pQo^6J zbTP&lmM&dtNe<^6y}i9?ZEZy)5`iqsrsN2eE{J4idT9U$e<11IyrL*^yK%eZm?J(Y zCqm#>1SihkLHB>h;F1WwQtHOfzMBuXYuxsm=kC6LbVEMwn>YS>M>LM-UmrqF7R3{r z3ZNg(KQ4~Kevf|St3IKO*)diCm66c-o6>-CzFBVvx8905Dm(ur`aJ%}e> z=z}`OvG`sWTr$B+CvRipgE_eO3nE4pj=E?2FdSkCs~o!@n;UIE76E>B=yL#oTL$VL zDu}*2bs>c3|1xBUAnS#qNZseppNHS?PZ*E|If4+QYuQ<`>hbQsMzQtTUU-+x!KW>Y zaOLLu&;03g1a3{twPP+0^$;mi zECT(0Km2|_G)+Sw5HKZ2{|K9@5F^m=@&NL)DNZz$p!A;cUmcVb%gAz%6MyVf5UVP) zQUARH^aRH6{-x14TU%;9$jzpB^2NCCM%e zVl2Y(;pH&ac(ZZOTq$9_8~qWy(;31~*5+ZuH*!#vPw~p>U|hZ2Y>MAMJs+*_kKm27 zcarvRMBEbB=we2aIOmXM*^(SHn|75H3^7DBj*?>Q-@zO|9fU*)ey}DFb6fgPa{UiY-xSI#e56Z-ms|d!%jAdt|n2ab-E=87`;OY(4 zvfeB0LFCLK*#1I49@+ChT7DJ#&@3_~d*1`Mqxj&m`GWGJ|Ag`BXDX^HvQU&yQIt>dwIy>ELf|K1(0l`oT7x_&u`tvjs6IVJ@N7D(eLErwReJe`=bzC za`L^|7dFWSF^Oa@VpNh<-9%+}P*Li}+2;G;UE)T=(E(KN3}Dl*Z{mZ?VMH{J6Yt)^ z+OKEFkOKgl*XKcFz^i{ZR}lcb^os@Xcf*Vm^pBJ zj+jP7&Mw&u@Rm&XI}vc;lZ@VT_$&<{E2R{=gH zzl@#?DCPYW^lxNM*OloWivtG^95`^`V9M}+LEBZ4LQA9d00000NkvXXu0mjfSQ&Y; literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..bb27466d5a137a9d572e4a55cf96a35f5f3d28dd GIT binary patch literal 1322 zcmV+_1=aeAP)%oh84b8MBmyc_nAf09nH+EUO!$v zKRA&09G~;_^W4XKj|wmt3li->q7fIk8Jmgm>0(9i?;9RMi+{F3Kn3YlLO04npHMFCT($ae;X4il6FCMXF^ zP!gD+Bya#I-$@iQ%BiWTtOeiR-iFa=gu!6QO8h_|faBw1baZrNB|ZRfM8sCm7qA+X z1STj6Oi&V-pd_R$$nW>V;c&q3_h%!Y!XuSkq>YUYQmfSjfOwv#_4V~^Y*rN%k_AP} zd2gj4Ns^$=&CNJD?@g^%lgs7GcAj}dk|2Y@@Tr_qkR+){*Vos7+5hR>@5#bPiRB#XuJat;m-#?)5gT}_jnB!)%8D6g!dc9ss7+FxMR2Ui>f>x^q03011#f{;49({d%;^T*hhnRki zMuYP5azoScY{?RGmt2! zSXfwqMx%)tV=|c#2m~Mq0=l}o(A3lv7pJGEC+4_P`GtmDtb$C4UO}_7v$VIj7q_SM zu}C|ePU1L@0MPRCvN-O210zI2g=IIaB*>gv9Yn3zKMwmR8&-mZDL{~VH^d3A6d>dFX(hS*zI-zz|ztZ zoKC0M7l}l0b#*1SkCy>a1UVx54&aZhXPhQPgzM{T0DxAj{j{LIyu9H4{vH5OR#ql{ zcpwM@Zfyj>f;zP>^bgt+H_`}Qra*#F0Oj)-^w(|G%7X6`R)3X>p!e*oC>oCkx! gU@#aA2BT2<7vR)Pu?BL(`~Uy|07*qoM6N<$f*k8%V*mgE literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7023b41c8b2f47efc0a328b73fea336ad9704e GIT binary patch literal 1027 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L91A~|<2s3&HseAwmvX^-Jy0Sm#5aTwGacy;X1!}tJ>EalYaqsO+UyqPLk>mBo zJUVQtCfEY_0|I;9zo zmlpZ`)H-gqMD*B1*(EihyStS=xnFJJ{Sq6;DRxre%3{mES@SMs8n2jbqH^=|B+r`* z&R2Rgwr-Ei6*DTLDKWpR`wLe%(t4$iao7@6&DD-T4=KO ziTJ(rX6u#3(aY6uuB*#EyKUQ=!yT8?wA+}DYx1QW`nlA0>Jd%0ltYqX!KsB@%#nu# z6%R+QKEW?@oTcPSgRzK6A7@5Mti{*7P=kf;Sprt?vg%$nzG?}udi^#wn{jXS^zv7> z+YYZPY&80G$bN$P%wKoc@LZ@1eK~1!4BLt*PhO8nWfl&1y)>6jnIu>3T;y9kLI2!m z4tI~~QG8^;qTTn(|BL)ou=Bm`kJtFvY*_UzJ*f#wTmvipZ(nT z=DxfaK5O#V(&1^n+rHE;xdq*m8LmuHay@0s z#xSGj;_o>zjCBnxbd8Kc z3=OSJEUb(ybPY_c3=CwLjg(O|)EAE-eRWJ7R%T1k0gQ7S`udAVL@ eUUqSEVnM22eo^}DcQ#T$MGT&%oh84b8MBmyc_nAf09nH+EUO!$v zKRA&09G~;_^W4XKj|wmt3li->q7fIk8Jmgm>0(9i?;9RMi+{F3Kn3YlLO04npHMFCT($ae;X4il6FCMXF^ zP!gD+Bya#I-$@iQ%BiWTtOeiR-iFa=gu!6QO8h_|faBw1baZrNB|ZRfM8sCm7qA+X z1STj6Oi&V-pd_R$$nW>V;c&q3_h%!Y!XuSkq>YUYQmfSjfOwv#_4V~^Y*rN%k_AP} zd2gj4Ns^$=&CNJD?@g^%lgs7GcAj}dk|2Y@@Tr_qkR+){*Vos7+5hR>@5#bPiRB#XuJat;m-#?)5gT}_jnB!)%8D6g!dc9ss7+FxMR2Ui>f>x^q03011#f{;49({d%;^T*hhnRki zMuYP5azoScY{?RGmt2! zSXfwqMx%)tV=|c#2m~Mq0=l}o(A3lv7pJGEC+4_P`GtmDtb$C4UO}_7v$VIj7q_SM zu}C|ePU1L@0MPRCvN-O210zI2g=IIaB*>gv9Yn3zKMwmR8&-mZDL{~VH^d3A6d>dFX(hS*zI-zz|ztZ zoKC0M7l}l0b#*1SkCy>a1UVx54&aZhXPhQPgzM{T0DxAj{j{LIyu9H4{vH5OR#ql{ zcpwM@Zfyj>f;zP>^bgt+H_`}Qra*#F0Oj)-^w(|G%7X6`R)3X>p!e*oC>oCkx! gU@#aA2BT2<7vR)Pu?BL(`~Uy|07*qoM6N<$f*k8%V*mgE literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7023b41c8b2f47efc0a328b73fea336ad9704e GIT binary patch literal 1027 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L91A~|<2s3&HseAwmvX^-Jy0Sm#5aTwGacy;X1!}tJ>EalYaqsO+UyqPLk>mBo zJUVQtCfEY_0|I;9zo zmlpZ`)H-gqMD*B1*(EihyStS=xnFJJ{Sq6;DRxre%3{mES@SMs8n2jbqH^=|B+r`* z&R2Rgwr-Ei6*DTLDKWpR`wLe%(t4$iao7@6&DD-T4=KO ziTJ(rX6u#3(aY6uuB*#EyKUQ=!yT8?wA+}DYx1QW`nlA0>Jd%0ltYqX!KsB@%#nu# z6%R+QKEW?@oTcPSgRzK6A7@5Mti{*7P=kf;Sprt?vg%$nzG?}udi^#wn{jXS^zv7> z+YYZPY&80G$bN$P%wKoc@LZ@1eK~1!4BLt*PhO8nWfl&1y)>6jnIu>3T;y9kLI2!m z4tI~~QG8^;qTTn(|BL)ou=Bm`kJtFvY*_UzJ*f#wTmvipZ(nT z=DxfaK5O#V(&1^n+rHE;xdq*m8LmuHay@0s z#xSGj;_o>zjCBnxbd8Kc z3=OSJEUb(ybPY_c3=CwLjg(O|)EAE-eRWJ7R%T1k0gQ7S`udAVL@ eUUqSEVnM22eo^}DcQ#T$MGT& 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..e636f00d75cae30fd20c202cab5db914bf0890a9 GIT binary patch literal 1563 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96.png new file mode 100644 index 0000000000000000000000000000000000000000..4d563ccfa366fdb5102dfeb070d03d3289a1984c GIT binary patch literal 3180 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 N;BOe?YO&5S{|77C+eH8X literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..4d563ccfa366fdb5102dfeb070d03d3289a1984c GIT binary patch literal 3180 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 N;BOe?YO&5S{|77C+eH8X literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..217ea2d31a00e2aeaa5095b4fe055e688784ecc6 GIT binary patch literal 1721 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-100_contrast-black.png b/res/terminal/images-Can/StoreLogo.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..433e4ff611b6bfbad5cfd3d8fb8ff72b66fc5bf2 GIT binary patch literal 780 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=0wlMXIamQHmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk>U|_uE>Eaj?aro^tM-P!eiR1OV zyLRmgXcF*{Eef^S)-B6*NWs>nQO>5-T=CVDwSI9&CUeGx?CNM_U#M5Ml10CBI$znz z$*Z{?TRdM2%ybc|=l9!T{5!2Yt^By&XX~@o?`OaNzVG|q!_6H)FiWVI@9If`(sv7X z_H8M>to5&Q@z>zjk7F)=i4RM%ZWaEzuAzKM*W|awId>i!{(iRBe#!0viE`cLD_CRC zCGOdzbI?M(bM?nsBi6SQRWfJTKM|kalX>6z)2{sKfk6lF$yWX^_b>Xx7rn_t-26m) z;EbGm8hR~8r&bHqgkJI!W{{VBps>bkNvUe9z^-NQL&F|vuJn5x9ke1d&`hUu%d(cW znS3RzHKzaMGxcuAyVtC%eXM%&Qt58KFFJD`TLqhBD$Y4JUvt)0v4DrcNt(f}hnA-- z?0M7~u-I*@Zj05{rJ0H*GEd5S4LDRP&dT<`?V0qLHTr=3u1KbLigA+3wko`PQs!*` zQhjaD*6f>*llps|9tF5)p`|PpD8vs@?|!7Nf$I)RsC?_pB%%hrg>a9tuZ#C zOTTC0L-yG9nO2EggZsiup!6mQvLQG>t)x7$D3zhSyj(9cFS|H7 au^?41zbJk7I~ysWA_h-aKbLh*2~7Z`rYqtA literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-100_contrast-white.png b/res/terminal/images-Can/StoreLogo.scale-100_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..830999c88ddc11017dc7a7436f9faffbeb7763e0 GIT binary patch literal 777 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=0wlMXIamQHmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk>U|_uN>Eaj?aro^tdk^VEiFWzq zpjBd5MAnEndi03gdNg5ScWaY#;U;hE4%V{@uDc_xrs~bLn#T8b!R!kMT@O03bGHVu z|4-Enxw4HziGy`woR7ev_061~dhxS%t6bOney8Yu`TM=+fB$XfuK@xnugPog9=DzK z*75ShCm&xG{yg72*WCL4AC1@NHl2tobiW|>p)sR!lb+KtF6WY&-t(92-yyl7w(k;0 z($cdIxsi81`m1bf{jU-nU@kUo;eVlA8Oh7{G*4#Vk9G6hd-VT%)6Rm6$}2yeZ`tJa zYSW6hg-ViPVUxD3+)zG=ap{sOlXJ^nZ>xK5V(L4~*mJJb@<~a(OYPRHWOyvM^gVU= z)|7y!25Ock_HMoO`jw7_oM-#BDbG92FNX#!nff8^A$Ql5wmmlw$=+Jy{?KRAibWNg znu{Zks(9|+wNBFWQq%0arsstwq_y;mu9~TQ$ilpV`^^V|3V-46y&FYU(rq5F9$ph7 zQ)J)oQOvPN^_-Ex3AW&4HxiZtrT)L(6Q_74wvJIf<>oW7+cuAvTYH+?eV*}I`buzB z#Zj@hM|PencV6>%n}yqrPNgGu_Zt|JTJ>WdYm4coM6lm@T3Wc}uaaQulx=^?7tURi84v1;)B+iEBhjN@7W>RdP`(kYX@0Ff!IPu+TL!3NbXaGO@5S zG}1LNwK6a;`;*m&q9HdwB{QuOw+8oxmq3Y45@bVgep*R+Vo@qXd3m{BW?pu2a$-TM XUVc&f>~}U&Kt&9mu6{1-oD!M+ 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-125_contrast-white.png b/res/terminal/images-Can/StoreLogo.scale-125_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..75836cb6ebddf1b879ba4b7c3213aa953e528117 GIT binary patch literal 878 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 YRWH9NefB#WDWD<-Pgg&ebxsLQ04`>jnE(I) literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-150_contrast-white.png b/res/terminal/images-Can/StoreLogo.scale-150_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..303c49854eea6b9326b81cd175d355798eab3ca2 GIT binary patch literal 975 zcmeAS@N?(olHy`uVBq!ia0vp^-XP4u0wgE7Z%PDGEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4?NMQuI$e_#JCLve2lsCfSTrdx;Tbp+i6%CKd&pl^HDEhV}y=a_tB(Iq2*^j z%a&|hS+jlNx$F0j*M;!arriE_P1;ygD&^;iJ@Pl}!~&Ooido*eaLz^J;MuOf?yyS5 z%#TwS`?O|<@Z(+bYv)QUv2HlsA9i+QR9!|%b;a2mYkO5>+sxQseh@ghw{>;cZo`OK zYRRi5GWjRoDZK9|Y7mvdR=R83ny?p9x;9BoE^A}E`LCq)dQWby^OS@r!@U=@ zioLFMhdH&nD2SB^<-2Q(g`CzDDB6ve}SUZiK4uQ%&VDYG=KnfcZW&q|%WuE$|}?z$AuO{2W!^CcBt8wRhQvwWst z)eFW2owho~THp2hnh(c2v+(c!CSfz)(MGH87Hq!EA)6?w|%qq2ivQ=R=y0~ zf4p_xlrJItQv#z3d%b2|l&=lWKd9m-F}-H7`FRmd`NdX8@`HFj{xA9}o4e;>=)1eF zM*sADv{O%4dwNNox$dTVw8Zn$)R=8iwz&e*pF(RVYp%P*dVnLj=VIoIhwDtGV;-tc zt~q<%&agA$nN0AU)Hcs;0_yw9w*Nco|HkezYlTN@W--t5bjJO}bw5wX;8c~vxSdwa$T$Bo=7>o>zjCBnxbd8Kc3=OSJEUXO8 zbPY_c3=9r!_M3~MAvZrIGp!Q0hRyH3DFQV}f@}!RPb(=;EJ|f4FE7{2%*!rLPAo{( X%P&fw{mw=TsEEPS)z4*}Q$iB}26mFd literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-200.png b/res/terminal/images-Can/StoreLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..aabec52a43a3cbbfd2c60eb15efb1129a1dbde84 GIT binary patch literal 2745 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 F|35M0@ZbOd literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-200_contrast-black.png b/res/terminal/images-Can/StoreLogo.scale-200_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..6d215086ff3d6ddb23c42f9513db151d7d5253e6 GIT binary patch literal 1135 zcmeAS@N?(olHy`uVBq!ia0vp^DIm?NMQuI$e_#JCM)TwC2;85o#tJzX3_DsH`rb5qU$iiA6O*$W=tg#bx8h z4HJHN1WY`VzR2xedg1G$={eW#?ya5k>;Jl)Gw06zTW@V_ZM-`{qAd}DT&G-@a*dwS3RfhD(BNu+bd>Td`Y63Srap_watt(R<@v~Fq{dv~Bq)#B9PkV{hE zHg=_~6x!@jrnRvEHL5PZ}E+&byHC z{Zht-$1J8of*&tsTn$(x#o3XvSa7#Tm{jN98jon@u!+%fQn{L$N3=HWQ0gszr&YPG z>~-%FJM;8;-(F8v$@KGlJAIN+WN={ip4nFvkH<`WlpkEB^tAiPhPkUWiu~?Pcuz@tx9QB!`=@eeDc@Zn`DPJg#k*%uZZaL%F?rqc ziEdIS+1G70EAVW;c_)S0Kl0St4k?E3KX|rStUT^>fp5#E6DM~b^JcJ`Dfm-L)#tkF zzE|6f4GsssiPGK@wbhCH%hsG8osw&rOOsP{3a)eARbu$MX7z(_HY^FE;w#IG#Ed)| zzv`5q&MEol&ZS?`?XvWS((^A4&3@?NMQuI$e_#JCM)TwC2;85o!)JzX3_DsH`|}IrYqFHe z-0sjKGu{{Fe=a4*sEJ>_vdm**_ncF^^lPmwdM!2=G#)!~x>QUx zQNQmW=>RJ4j!u!SU2}TZ(@u`NPrkfpUo!vJ^bF~zDmz6qGc=?{%a!gnwyFFL$lu#l z6Qmg$)~S%VVgLN%JuSDtEPKCVmEn4St1oV+@F?v_el^=#N`V zDLvP+wv0V1r?q)c{GOV*h9m6c;#~{N7T$F{CmtI*Ti}%V)w&%)O8%#$dPP;Q?+f)- z2zn6r)x}G^(^EavF=g|t(mVNSjwzi*yVN|Ro_2pZIa%F*=M5!J@!ra9pHBV%(hdZ*~dtB_qk;^RBMmDe8!kvd3N0tDTe3_X170!c}mmazzbuIX;hg4v;XHU(C>zPLlF9}s=smcj9 zXv$5!Xf4fHam;YppE>;{7f(3Hv82BHJ-^;kvR~r&L=)wrpxI5br*k&EzbNn7-Xv%_ z@rUK4gVL*?^PI2lt!{3uf$P z*z)M%L)Pud496~AuREFBV{&5h@@0$Wq*Z>pqR=As^yK9r-OuuE^K7K5$MtCZvD0^dhV<0G1Lhmm64!{5l*E!$tK_0oAjM#0U}UUo zV4-Vd6k=#-Wny7vWT9(dYGq&`!)&CCq9HdwB{QuOw}#xfuK7R>k{}y`^V3So6N^$A k%FE03GV`*FlM@S4_413-XTP(N0xDwgboFyt=akR{04QtDyZ`_I literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/StoreLogo.scale-400.png b/res/terminal/images-Can/StoreLogo.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..f95132214d87001fff93a7cc1cc49ab5b2792475 GIT binary patch literal 5571 zcmcIobyQT*w;m7>kPd0-P(g-9x&|1fM!K7!8-@;vp%D;JQc9$T8X7_AP&x-`5oG9Y z-uT{MZ>{(K{GGMV+3W0k_TBsL`+fVIbz-$NREP=b2>}2AF<4dU4Mq<9>-cz>vC06E zhY>hXd3AXJpf>r@jU_H-&SI_lMjZeM-~s@`A_0Iaj4Esk0Pq0<0NWM-fW!v?fW|$i z6(WUsfM@kuMF}G@?NYHV6e9@SRSmrW0N&UCI#!8vMj1x=&>O4{dbo-I_z{pT^0(0* z06>)pR+888L+$1UzNebb?eCmkJhu1@4f7u$dQeFiG0zBFxiA{U#se*KKJ@n++i?_5 zmCl-WPol#mAI)O)rLjC{Kyz_b_Tf-a=~-*M*MHj;w(>+YLeP~6$P$A+qAK@!b8%7k zTE$p;T10nR_qKJq_uNY(AFmD*@x@K>NuW4Q?>-fOEAscn~qlt5X3fo#K7Nvpir^ZWx!gj{Y2dTledM%3U?~U zB5OI3hP{IWxhYO{h9TC$YrwK;y`6gURu}E+aKraR2C=K|)q`qIa4V%h3rVUj2Vfcd z;OZ^zqnpA<)=DF|o`am3zlc}3#;LgxYoFud;@XsGx?*x3^=uvot9X~hUTaa=7HkR9 zrg(+(#7(fYPV}U!QJ4bQJztOk;A>=w~x-(3`PM4yVY=Ay*L`PG5^kdd19CVjQm@5xM_ zi4Gd{Z#@&%cR^Pa4)~2H_i@l&IBUK9DQ90-MUc(DkB7lV z5Ha#6#S<%fo2se52Q_~8O0*E`6ZAMlEP$&uxxuNnD7DP#irl%^9?{eYGF2{V<+^q% z5FJ};(6$my>#Yg935y29IM3HRA@<|~>Zq%hv;ut@A2xmzO%xhhi*)$eQ_x{PUlw4!oPol(@;%D?#HK83WVN1Oj+wN%2;V%?lBCGkF9s~h8OmOfTA@RfI zDqm@TR{K6xraeA26^TF~BE3!&1Sn&-k~|6)U=I*C-S1^;L@D4`G0H{FH%G&iJq4fX z#9SFO_>80L>I4Vsa@_M|WS?IT4rRT{75q5kbha2-fn!xle!6CFZ~rZVRECxYE=mf5 zXc-&Fm%Pqoqb1^^2LE^XFm?oaOs?Tm5M(XaBx3`|V=Jmay1KDZbPtSF7~9MpLV9pb z+Oca^&yR|9;aW|{*eSABiQj~D{&iQ&hN-I)KM6h=XJv!nLhUBYrOKwBE+Pwep!FKA z)DKy-Fe!teU%qJDscRNbI1-XXR8{qMlgcJcOuWIw4E#N@@Vcb3P%?!k>M1?B*ciBs zyrKUQ&5MNiZby6D-|BrBjBvir_=H4Gc{i7+=kwQ6?}Xe z_yj&MCDRGvB}*N}l&R*#D0*`r6$Xo~N`!Mb_1Fu?H8wUzo(x9oT?J8thHD@&*Jxh**i`qUT>k%UcyMhSNi(;@ZNZuQmQnYR*lPvSkUa#S$rDRXT%|E zJp`2|ErRkKSM57-%BLK9+TT!KN-(zZGte}c>fe9uW8PjLD^Mh01B*Pt3L7s{u|8bu z{mka^EXjF#+103eo+g$oym&GumE-&7F)?|p%vWu-^SYxyKe37-ac%-3sG+MjEM#q;{tUoiJ$ae##!%&k}&_eoB4oYOKq# zeYn~Uul3v*YF0&F8ZcE9R!n!_%t!P?Au>svPSH`l*AR6l?|TlWTb~VIyxSV zDUx!=^f;)cy53HzqeZUS$ez9PW*ArHfRiwFmFreS|8Ol?Q+IT78lRi{F#Ule_X^79 zA~Q!5TVq(ML$4y)E*NMJH%86#u`l>So9G!B(hzA|FkEQ)9VX*n>C>769>10>ej`8; zdu43%^d%$0_3W7%EGPA$yXio^sy4Z0*(f`g#_C+&aV&W ztdk2`TTu|T)Yz-iG*R=NHgbIoHrBU#3uL(C!Fd7OWeGoAFK8HNRr{i%8jm$ZAt@T4uL%fW<>@#AR*{ z6jh$|40IIo&ZIzSJtxX-rThw%0BITrcHoj0rXl#KS@ym|B1*tG>WH#b*2;AHDv zP_ExQ4Ap|K&**00-vjRAx8AnJLOnbL{*2_tV@i|~8+ndQF#Wha)#>Rec>UpRZ6S|A zXNdGjzAQ6y-~6F78#+JPKd$GrNp?Ud;(6TLwKc0BAB7%LQ&VGV-r@Pe!UF5C;5Hf! z|0UG3V~}C%JXeDic)l04yw6;>AWq{$vG}>AhM#_R>FyhteY+`e>lo52fZ1~r7++G< z@%CF&oKBQ5F)_WfrK9B5KvKmzx^+hr+q=4I956a26md?s8E$BEpJmOCvE7hpD;FPg zZm;9&G9Lf=si><G{lJS#h!7)H7I zidJLdPg=0p8ew?xRqq@%ycJayg4i7>Ki8t-4EefzcIFHCR+4F!#gcc5p0z&TTO_DB zw(A-I-3q0PnjcYxyN4H}R`i@m{(UCNt{$0-M6gyBH6a-#n1@Qi#f1+K53j~!T^ZTX zKujK;!aW`jTx^9tcafQ=0hx_heRNsXY6_&Hp;@0Xc1t9!IB}xet`G4mHucb_!j+bm z7WX?~@!k6sJ6Wp99n<|I(8gMtbvgIDJ&Dp?@UaB=A}cVp!KyMV)Ln zlUhEanl?CGxMtIkKBp7^5J%86SY39<9K5ZASn}G+ud?e5o6CF>t5oB%AetlL;}|18 zWnDEpp68@{X0zs94@7=BZK?-mRd;E5B+Bi?DRzzysxtaUTH5hb#99nz@QM2$6&Hw4 zdF{qn8x$nW>-(&4UxnyofFLWQF3>^SO*PmSF2E<9GO2GA_llE;o0Gse*z-`cgf4Vv z$j}yfI@Iv}qY#t}nYFkY4T0)ID{=xz>StHoX|u)LM5*A@S#ZcFnGUW5YFHk~$6t6> zeFr3yV!OQ4g(hVlXjpA;xeY&~qowNq)#3&Jp~o3$zgv;4%8|-KQsH1yvDh6~Xknw( zSKh{xL?k$~8Se`5RsS;5V5z^{qa7$98r-j;%c#so0PmhdI%vNYKVid4jaS?_>-?(# zdbc*f`^H5{@9`$8{P`VeVZWo19)t=98wfLe-y6D@z%7>R~|xzacV%w!AQd3$}B($kK2+~-e=nD&%GDqNs9B7)eCN- zSZ-}0!V0yTFBClJ%X8IvpG`_Q8?o?ht}*i~;~SdnrQPH9XFR-^mQ7Z; z{V)Z632UE?(3G$`9ISMDE#RO)j<_4P0?1vr@HFnCYtuC~I}=Jf^>G_wGpa6yWNv@8 zp=G+E(zr#87qtFxaZ zm2Agl7KK{2FmKgONUaaBbD^4)OX@+bWd>8gHMLs4Fwyx)($63t-{FfTp?qlZh^vw+ zQEF^xgG04yAtJf`{_fh|#RUYURf0mDefykMZB-cOFZiRFZ0P1LmsVWX|7enaPBE^e zytjinH~IOOk%q!to&r`H>EwC+?}%*Z{Jh-6Mtqea5ZA9mWoB=8Vy~aoUWT?Ff>m(b z_ICUFn%dkKPXDG)CN^K4Jv2HAM>bA6eT^vO!H-f1mT zcNwF460udg?XFy}esvvsaeH@B9{|=0Hj*@++077P$a$GRH?Evf+61%Ro%!C7v|#t1 zGwAFm1D<(>e7mWgsc~Frvw&~Nb&Gu$cCo&DqTCOO&*S`^kQzj?Q0i{=#OA&Z?L5)U zX}wPHgED?3(@VO(rTh4*U>)Lf>p!}&wLhw6aUlj|^-cSl!Qy^a-ynCRyyabAQUtK7 z7wNn9?`O?w^7)$;SOz7sJ17~d0?=?1y@i>?qXy#nCz~<~u7L0CQ7+WbG}fbTbAT{g(rRHG8m#?uuLZ^`WMwk3rsX#WK9+hWYs_ z>|oOW`c%ICNbl*=++?D4Y4^qVc<5nw6!n>qyMRB8QnTnnJ~W+}@2{DV{(D%8lDW4rOr9!!6 z8rpr0@r9~A|I}`mW81fO5}sG>$>}5!H^e z^Si&S`EqoV==6KIu$J7=qk$d(E30>8VS+`Z6Q=9}ud@?ZbzWRRtVN8HNj{z0%K}!a zGM<=(%o6-#SLp3DTLS*oxFk!_ZW_>cXTNt)hKyK83=WJ z`YO)4^Flh9&}?x&Dr%WW(@2>w73bz;H6G}{N}w6@fNWjf=+td)w`cy!XEgd&P0MJf zJL&$*Yy7cpa$L8D6N}zfFN32Sx|@~W`iqTy1@W?L<^iLhS$)48BD7oVaNw7~Yp0Uv zP07`Xaa@DD+@YKXNF#Y|>RsU<1N)jB(j{BJGSciU<@MQny?8Visn9LWvVI@++M%1i z#o8P;BL>!aq43%f4Mbx9yAu8tQPGxXyA>;H#y!=9th&z3~e^6u?X)h zDzX*|%ibv-Rj$dY0+^_`If3B2UO5!oDdT4jv`m|Y?>IiS_rR}ealg=qSeiU#J2+*3 zxm&M*P<7u(*U!L-_?{1Fo~$IA-|LzGA(V~XGST`@Pq0 z5mgPhf%6qUT2U{~X?l*EihBd8#2MpjWqeXJ%zvr(2gQTY6*D9;M)59^#BpeQV-2}X z_`s7kP{4>2-okbf%b=-b6~GN&n8d-uK>`S%6yOkVepE%pO7b7Lt3#~ZW7*Y%hnF&y zs4#bxoZcVsx6jUYKbKYlGyizph1->Do8M6i?z12MJcs}gHqdAOIx(m zAZQuUSgWXgn~J3hVU?MwmSc{cAU;XGT*`d}RG03cQg0B$D(z%kpp{Q&?XwE$q- z3jolC03a4xLc<%d2OQqE))oNpIP(&Wt$0E)Zs7o+Zu`B@)fg7kvrTXW*3J^V$$f!W zOGQ{Wi39*#CRhto=bQ7pOE>O0Uj-Nk`+>!h)x-h?4C`9oB1}&%52u)>Z~~UgvAIiI zsI>^VD8gOA`BG&4Rm6=Um|$$(ukmSTCRZ+~iYQvHs|Jg!|8(ZC6fB^>9JM!6ii#uc zp%_Igeh}81k&f^&L*qTe@O{H@F^hjMdXu{j+#31=X&^IIIw0+i_?Dt z*I;~vTxrQ4?U-cA;4Njx{f)f68)mk;_^VffH`XX^rE_R%-Cb!nIv#wdN27K&SkZ2p zcXvQ3i}v=l_k!ClsAw~;AxU>7%Id^p8lOUH(TIFDoaoa*h=R|D$cgvkm7iW`7zqw0 zbMR#R1HPVrJP;g}56uVT5b&#}(`PqVGR15=%vr0%6~$*sq9Wk(8e z(mS1m?rbkFIxQ%N^!d4O#&yFV@P3IM=t1U=Ra28p(m_`gwqOF)5lppQgg7{N`BQrJ z3mj;U@fR%fWqIcZR`*%&AcDH7hPbH*7sZ;@-ig2dJXsz)grG|PICMGJtO#_9BB`NK{81 zW&}lpRAO+bPLmqU2(l~r2?mGku5HAqh8ql+<~Xm$ze|fzXnWRYo^-T9xJV69V00vD zE?yghYciX0mr%n`Q|{9zO;DD(uNMaTEmIyzOG1UR&9P#txk6Hz?zomT`zje%y_RAZ zzf4Eo-^A(+z}}ESS4!h3hcwUU^olfo7s>n`mIvOt3p`=>0N?TO%sS(C3_tz!=VLfU zZ98Meh#t@Vwkj}kN?s=n61BadEV$X+Fn<>ny&SpyAL&#>hdY4embX}?E|&#Lfi|nZxigt7xkHlr1JRPxy%pGV#jI?76;`@ zS3gB4r%s4~kw^2tKp5z<&r1^LK1C^~nx!*Ka1O7&*%cF{B)2{-3F%x|+d35Me*HYv zy|?P_^N|fve!j~sU3DYKjko>vi4HURKx{)z(JLDx>_?b3|aLftoVj#>qT5->Zm z1;GoAWqWIX?6mth#-~--;>emHK70Y4fio?%M9$ zFa8WUD;h(_uF5T3tI0dchkmJ7^SAAqVZ2Y=?0a&gM-7nj_}v2|Y>P}=sroz_K|kE5 z=^regAjs*Uz%|TSe$5Mop^pTM-akDxnqs6&J)CYO)(0{<58+c&@;>Fl~RvW7;oqUuJ*LgRN$~9+33f0$2#4ir&s1QAg$dkf;O(5{S z5rpM9lfvz*tClh3F&c;4PzOqJOdEm7pmq$VLhW)>TvnMj6nD6f~I# zr8Mi7Q3{q54_R9B2X$W^T17EA;C4OZso~Fw(Z|S=n}v&QaiJeU2V~WEIWw!8J=c%u zdzZ3iM_ue>DUG3Mvf=RRmc9Lad~)Q@tN`dIw3nY`wG;VC2Fcl`2y|LhPRr-DdBf|o z_1d;Ht@_B;F9wKub)KX(rgm6G^76PjYEQDNmlQda}z zfN{YK@a$so?M^-pa>o9w&4UCf`G2p~m0Kmz#!J#qvvS$ri(-VOYlJT$f{6ABBeDsA zX~E!7EhH2MbJo^IBamo>HdIRot)=Dg7bW`t0zo0Z0i>9}4;aeY#G&E<07%(k ztz7xovzuUH{t0p+$@4)t5aWOWfCjq6w!a9!h7+)^4ge6R2>=O60I|VB#{IFqMCMA7`K@5FOxoG7!D@7a=0qZ&TfeOd_3F8-Q_AowiH{`{yvMmiqR%# zK*rfd)nyQKxed(yaZ9&HP^fE-o1B$4=YHzou;t;K74rKrgrOK(4*ArCd~nv{YANex z-lU!5>!rM}#b#4sO6azWq}jzEQLoP!ilqK~X!q)OlVH};HFr-ucD1h_W4<;-ztyhH z&ez|$djWOT?``Y#az(9RBadrBPmI#>$+6|`x9g)|tU41agDshGtR|!n1~@0)07%Y+ zAOW@c=hlsOhYkJ6cM4GlJ3~hC=;~NFr0v&mS)FO&KzlEmE_!sTQm|DQo%sH&vUZ`WvG)_-Y*;xlfqB8P|%kilLIm0eX% zt|K{KC3%)*{J+|Nw$OZRkwbuJB4}owOIt^8kmCPCaTz(`U9bEgadnEaFFuO*^h?$y zvw%?8s)yWdOL(#XA~RrENQd)1T|I5aB6EMyExADBn>ZB8sl54p(4*F0zqp=K0TY5! zoa?-Fb@q99&|cjREVov4Ez3sHMwYnoyk02f(XX5)RfxQXl7bS3J}h8nx;#_z<0HL$ zsCLkR;qG&(H7yZy9?r2*HI;Evy9GWw!w5O{3&|+llh6QPHwM?~_esFoEw9@RQGp7cV8AMZ?C3{;I9v4V%u^NV_-HjLs<6`Qf(OYBgGUqMDQ8T#N0#Jdo8r_!-p&p&O>Ezq;-aA{wQw_zY( zQ~Aw%o>|LK*#oWqR>kqsW3|OlX-GX(s*fvUM8jPumOxX!ZfVMiT6^3)Jv(~z^mkC; zTsy<|Syq1Do6jai&at3ETzhYvI|ftdOs_fKxw&=LcNmAs+t&%5!v)p$ZuuG8ToaY> z&EY?nWoq-Y^8FBD@fx#BVUNggXJ_T4<+==!waK$NVLe+SHJAvM-_`s5v1yP>eRh~z`p5=Rwobm3qC;=uONEYC7y*v}UjUi!}I`WK@jcz7R&2 zyG5X|WAt~0w9D=p?4AW3iGu5Hf&x)0^k!#Bv+c~|QAP7-3%#R(5I7xOYh}s+bH->A z4akt(^!CusDdIzdJq>6JS6HZ*D}GP$z9Bi8c2Kt?vc>crwGUrX67K?9as9lZn=x~_ zg1SVMk2|7$dK=5D9j`h+*QfId%Y#Z>^s25?$&|$RwiiP_JfTb%o6KEN&TMiymc&p! z{yus|njojq!cDz?|LGixMU{i2U^bgohjanUazo)WGRjQ^cN$)jF}`cwS3e-C2c4ra!EaD;l{9{&^3h_LpI2=I>}q6t(Y9{?l*X{d)l z=^>GB1_o#&6xzr@4`GZ(Ae`3YqyJ$D2@5z!iv9lv^J;c4-=MfV;dB@&BHEuy1frv( g_0NR{T_E^Vi27mFz=EY?P`(JTvv#y%SmLk#4V&yYt^fc4 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-100.png b/res/terminal/images-Can/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ab0c03aaf8ae565f393ff2a50fce09b80462bc GIT binary patch literal 1907 zcmZ`)2~bm67X1ha$P#;?*=#lm*e=i!3`;^}4}?`hSd$1Cf`nZNgiWxaQCo*lOfyE- zfb5BcJ%+_71&9cO2vPPOS!7W`Xtr@a-BVLFRnxcLzwg~w=iGbld;iz}^@@{?^a511FK|=8on7P@5rn;3o>k>LgpNBLKu}1Hi3c0ALT? zy0ruV(J%m5@dW_HLjX{S$ZK{n0f1xJD-Jko5YW@p^ZNDc-rnBq?CgL)EHE$-48i5W zbLi1%(*nJzsj0cSIp{&VeEBj8g)%WQK_Za|1mZuiXVCv9VYAsR7OS$dGBq_dB_)MO zBoYV&2L}gxdwV-OI}8S6Yiny`V}nMct*x!CtgI|8EiEi8!2D)rW?+6i9-oz!<>KN3 z7V`G?9vT{&pP%pR>%(HPI2`WXyLWSQbF;IvGcz;Y-QCX4&Y&1yU*GBJ>8YuyuC6X8 zC#U%M_{qu1iHV8v@$o-(#>U2ujg5_tj&^o-#>B*ojEoEq55Ir^zP-I2Y#};2S|AX- zdGp58)6>}4m`bG%4i3J3`_|CVFexc%U|@jH=hNwQ9*@`G-~aI8!=j=h27^&wU!R+s zTTxL_TwI)=pKoAbU}R*Jlao_cR>ov9%gf6P3JR`Xy_#?xK3{WY-o)=b0sAu ztBu;=p_P`F78Vx5;qWPzdU>*9aiYRvjYdovjk- zaWaJ@yViJmyG3(dWa(#6jWQMMDnd4$d z*M^sALpa4-H^w_}R=S_{^sedi&wvk|kpI;p%}Fjz4{Kv>RbiK1bnl+P*19Uix3iSP zD||GqtIMnY#xTU>28=58XF?|TXLfHc<>@mgH}jghZz4&@!J>a*-^%CcrWzkRg3gfh z6Ys7132v-BKIbS1@W?)!;2GV!2N?H|TqvMDYM4ZiaqOX5;M_(dNc zE-*N_#o^^bWSsfnB_d3!UG72~H?%7g^%zlx?$H`(faYYTqq&&P-vUFM5CgqXZ`|u> zGK5W^XJ|)Kb`#e&v(cJkB^BPf<;2OPG!A7X(6z7p?uWR#J8m{xS9+Eu5uDZ5S3_Iz z5V#=+&BYNyXCARzQ($@r|!Q+9$v)mn?7F|_5yuH3+}y3*Ow=dcUaRD9Pc zeQaZiFpY36-o-Q&{s@wwhVa3|Tmxb*LSkV5=<8dr&x4_;5`1?`+i8r)`_M z+qZ9X&p06urA9W-S>Wo4QG?UPvI3Kh+8Vw-xt}e5!y+_%W={AJ$!(D7610A{a9&OP zQVC0phe|QyVmx^JP4oC8Y6zC@F;Y(&o2!o%T#-k~aQ@S6|k*N>L2=6T>GNSrfIj13MrwU&{Zh zyISYtYE&P}*~ka{JMheS5wrB|_fv4)u)0MzkGW?A+9@)moO6GL7ToH&9q|_*5CCL- zf@C;Vk*FdCZboBIa7cU|%86jYB9T*$|gPDHD z75on&G~AyO5chuwm;NLyjV(R^2}(y5T*CvXG`~nPK%>!gDPh4;B) PBdHi`Co7JHZ_2*`@SR}* literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-black.png b/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..7b30c8a322cbf61d0d85bfee9dbaebbdf30cfe51 GIT binary patch literal 1053 zcmeAS@N?(olHy`uVBq!ia0y~yU^D}=r*W_V$$$59Du5JAx}&cn1H;CC?mvmFK)yn< zN02WALzNl>LqiJ#!!Mvv!wUw6QUeBtR|yOZRx=nF#0%!^3bX-AFeQ1ryD%`U>Qy!Z z@;D1TB8wRq_>O=u<5X=vX`mo`iKnkC`*RL4ZUeq;HfFp)O|Ly&978H@y}h%yTc%Wk z{X>2dQ&x@RhDlFsn97}6T(koh72S#nVr^M4*DTW|C^Ip6;f(r&5p3M?94suHNefwY zTpWWl9roPe)N0CC;c}|cG28y8vO0Um?Ys9?YV0nbG1z?gz2v?#n?FmJ2?Jd~0Nj{k z6{Ed%;qq5|rJlaEvda4IC7Tr+Z=REH+t6n*^RBS<_jhx0V&lv?qR&5BG)<*CYWj8F zL&vUgNCzwD_?+E#jrm2#tPek9pQ~6$@h^~0$(7m?xXkUw45`HAtd`u4rEgB})7dJ= zX1?rT_)PKa{G~z0UvqoCU;lbB;b6j@_?^4XTq{3h=636a)#krjm+E9J>$&|P_C=BI zm-o4kvzD!Wz1aDhTk@a6>{xl(PxlLYtn}`qzCmSF0TE zNL;Hw9aWooHTvB;d0Pboxy6giHvGF0bfu-2adPjh4TcGu0)@B9^*`~v=2t6o;^)6# z$v+i#oBg@+`lr#!c`~PKE$7KkI`i!3o!RD}11CKxn76lT$;$pCmtP2c|8m{UUDt9t z|7U-{rw{*3+x*ic{F9s5q?(DwYr7ZhW{bY}YIgJg`~}@x_piFQ^2*Iw>y<;l1}&{y znOwTu%&KMnr?th|_s$pZoD{75KFhZLdCj?$>Tha0CtUv&CU+{<^WT|^D*8_@UOQUS zwSjk|_WQWU6XTydls$8KHc98{)jLy)*RuOx&wVFZyz$v9r@wKbp`S~a>|1&J)l9A2 z66SX~(M8_R*tR|U>=k$Ba%Ape{wGhbF~1Q>eEqY|`2U4(32DDx_ot|x{8%&Z^3D0j zKW9(!UTqy7HQl}a%EBnQ!*|{moZfn4mmPs*1(aSF{hz5)wratmjG%`=u4;*EL`h0w zNvc(HQ7VvPFfuSQ)-|xuH8Kh@G_*3Yurf5!H88a@FbLI&--4ncH$NpatrE8emBQy7 yKn;>08-nxGO3D+9QW?t2%k?tzvWt@w3sUv+i_&MmvylQSV(@hJb6Mw<&;$U_O}H-r literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-white.png b/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1a9bd3f3a6efdac64ecc6b31659b543c57f0ac50 GIT binary patch literal 1014 zcmeAS@N?(olHy`uVBq!ia0y~yU^D}=r*W_V$$$59Du5JAx}&cn1H;CC?mvmFK)yn< zN02WALzNl>LqiJ#!!Mvv!wUw6QUeBtR|yOZRx=nF#0%!^3bX-AFeQ1ryD%`U>Qy!Z z@;D1TB8wRq_>O=u<5X=vX`mo`iKnkC`*RL4ZUeq;HfFp)O-DRk978H@y}h%sTP9S7 z{X@Qx=fo8=oSYO+DM_qxQV4U}!l@=Mz;x=Shu6pWfGryYk7#uqy_q2z!t~N-x&Z%_ zkSe9d(+kzgladz-B^?l`7dyU=`R47*r{A2Du$zBt@wTqlJ58=KI~+KW*o_<=6Rd@0N4$*Gc(*<=kF-?pXiz@-4Y{zbu-j(qC%3 zw@$z;&F#hv$;9NeZKbvibt`7Q`?-DRIZwG4RSbU>xHm88F)}`M>wa;%-gco_0)^lb-h*1F(E#gvB{#h zLd9Rn>xu?TtkgWt>y~w@uzBJvneFzkBNG>^ZjHQ?cfI(KoAxocl=9fTBX6f|xU1Z6 zr&eK7EgfyXOOEe*r{U_0AAAq(x}QCNYHwJM(AM~@_0lX+Muu74UnjWBZ%Nw~C~PLR zOxiTj`_bv|&b<~oPamxK@9OvDS(|*7a{Ht;PY>%nFZrgra~gkzUO_QmvAUQh^kMk%5u1u7QQFkx_`Dp_Pe+m7$5QfvJ^&L8wmr78DJ+ z`6-!cmAEyi6h7wwYLEok5S*V@Ql40p%1~Zju9umYU7Va)kgAtols@~NjTBH3gQu&X J%Q~loCIHiake>hm literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-125.png b/res/terminal/images-Can/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..ba080ed8f81e5724cb2f5acf6d93ce1f0238d192 GIT binary patch literal 2438 zcma)6c{r47AAW~12BAS{EXh_WjCB}W!;6^=*`iY#%h=|OeUGw?lvL-O5Lw2S{fMG0 zktLl-*~S)=Jzpi2O30G)INx92AK!JI=epkK`Tg$a_q*=A8vu%b2SMRD z2wHT5Ai`}3k_gIuWM>FLTx+%zM{`in-`~$>vs+qPZr!?d`SN8togN<_|MH&!z=((l zZ*OlOA0GgKz|+$c1R(YG^?mT*K}bjlh;40cVHiGs{J5c^p`M;zR*dRKo6^op>{h$t zR+|!-O-xL{u91<^e*;i9R(=@ZJ56f4H64mOPZW29b_FnR0=!ceuw-Rr0mjbG&h+$j zz)PditgWpn6pEFV)rk`)$Ye5!M6$HBw6L%+H#Y|Y%*@P8O-+eJBH#iPP8 z!oospYin(7?VC4m=I7@-Iy$PVs^;eAW@l%?4+2$PU0pLXGt<-4O-)T_&z>C}9i5t* znw*?$Y-|Klot>S5Kd)cE=5RO$1_n+}PJ@Gk6B85T z`T3cdnMFlKZfjTNrMbDe-QC^V+S=LK*=cEMfq{XVnwmN~It2voBe=0s8Ab`bUg@%S| zXlVGI)4g|jzxH8X6c2(QjqtG%Hr3?ADe*S2=a4@3hmxQGz#%ohP2*MG?OL@ZccD;@cLr~aSS)ZPT1LW zj~iLY!DNq$9BQ>$f#ISvkSAN6w7>B>mX`;ySbOkI^}!br;A`sB8XSL_5u{nRYT5?mv19RF9!Is0yr@% zYJVgI@raUPqGQy=$BD=wu~X9R!Ap&w&75#MVtb<8!XArdbhJu{M(SY= zF=ARd$xdQPJ_6?1SM=(h8+QzM%;$4E%WJEv9uwzsUlOhvO!x0`S^wM{dw%oNwla%= z0L1@l5ZoS)5JFd*3y55 zNhB4K1}{}h$``@r6paQx(8Bg+KJnvHwd5Llt@|$N3R#)h;K%&ruZ*kZE244@O{ndH zG_LdOKc?Vbk=x>!t@KHrET3slsdxI*aB4idwMQD2WS$+ITgWg!(}%aKLuObb51i*5 zAB&NWGhIurrhk-11#okenfb%#T~3$pKeW0u{z0*#;6u|CY%Tg~b(N@YNEwbSGF4?a z`_vd;+{mjk7o6;llWY{63zlCe=^SPkZAX89&uNSM0;ys(uD4uC$N{|e>UZQ-BYN6f zx3BREVh!QnpV#$AN_t`ylV=pdwFXvv=Y~_~h>2H(jQo$^+DKCJ-#x88>Sr;ioxmx0kd8JE zQSbihf7rXsB3G3UC1hAukaJhptPg)vQI1)oDdDM~d@3nasiWNP?Trk6?;-zzV+kBN zGVnsevoksS%S!TKYC*B*X%mN2-QSf{k!1PxBL;VpVGk$zj~_~@1w~a$%tkMrNmQ>$ zIqJC#6P%lN-(f{l56at~l$4a(N`WChPR|czkqR-)g>;r(H_^{kyMALXyRx!Uc0tAT z6OtW|j{RC%!fwanV@-RXR_kQ0*nQR6tVyTufzz`N3krS_b=4f+U-wNyEY(UnzR}ys8+?Am3I5a+S-llCZ79EPrGvX0wefRtzo?Yd)A@0lfn!SzTKmom^|7?~5|2v0hp5>7R>?-LiZ2RZ;Rkq6tcZEt~fnM5lbC zhPE#-X1_K_^roRdw`@*r!pk}Mdl@@BC8=^ZS$0td?tk?NzwQ^7dTOb{)QbDk1uu_( znrbI}TaoI5smQl!N%?AS%n7Z@XtSLC?ZeNlwz(6Q_#PA>eT8OBdk(Y48v9q55txd(Zv1%~+Kd@{uDDkq!Uz}2R1%)bG#N-*#M literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-black.png b/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..e55030a8aa299a37090a030fa371f08a16625913 GIT binary patch literal 1320 zcmeAS@N?(olHy`uVBq!ia0y~yU~B=h_i(TP$xE*Ub$}F0x}&cn1H;CC?mvmFK)yn< zN02WALzNl>LjzFpFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfgp@RW<_h zI14-?iy0V%N{XE)7O>#IfodxfrQhG<`@PB=5L-Zjv*Dd-rn`?2u+n> z|L|Ydb<(Ln3K<#0Azt6m-5wRV2m!hX9_%9yqbh=&Yd^o1L#Bk9Cpm7G@4P@(pnx&=m%mdQh64%@lW!d%mlXGdB zNY&Rqg$tqKQfu1NTpuz!E||T}Cg!_o(}XB(4&`IwS=|p(;^uuklbl=5+4lHT@we++ z(~ebbNKJU%DB37^xt?d9Oac!JkQL8U_4SQIvI0lXjDJZVZZ&{eWl11Dh-`ke&Vu=p z%%tU;6`wD+l{-EArrmo>yHoEy*PZ&+qyAIvzMAK?XY0O9s+l%_*{!>FZ=c1#%Rb_f zdG7N1b369E2#DYO`$x!c)vrFGZ}*k_kuBY}>fQb?KUlSy7O%2gqJL#Bci+MLjut`e zuWotO2JY70y1MU5o7dZ!T!;2qMX+Z(2}pz*vwzj!ns~6f`J>n0z|d&zuVru7U3s{w zYH!f(WlaW#uV-v~{f^6=QEt-IpBDVjPw$zS%Wdq( zZ{+Q=N`vh?pZ~Zo_J#98#nP})t*;Kgez%ov;;-IN_SvuQcDuB^RWxzMH2ohPyEhw2 zZ%%&oc#rJP3E!9BWqkbk&a4#I+o`1!6nbXxZ_c^3*tb$?`KLI!laD9)38p_939N%9{`d7HEQ-MSB~e-B(amUHfE_m8Ii&nDm7xNS8X>A4A5 zf(U*SXJ4kBU|Mmx2dH1Q#5JNMC9x#cD!C{XNHG{07#ZstSm+uVg%}!InOImEndlmr zS{WGJUK|mQq9HdwB{QuOw}w^!9=rx>kObKfoS#-wo>-L1P+nfHmzkGcoSayYs+V7s TKKq@G6v!K%u6{1-oD!MLjzFpFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfgp@RW<_h zI14-?iy0V%N{XE)7O>#IfodxfrQhG<`@PB=98W-jv*Dd-rjNS5D%4M z|L}g6i(p8Gn^0`ag2|_n0#hbz*Ne3hX$s=ZboG{fC7`I77`~8C_DENkRmlsWqD2M< z#)}pm2u|55>vesJfz$u}eCxLTzkBEJ`m^=vKYsr_lX=73_`Tu#-*WHmH%|sSfplQd zbnMo;0~|MJ+%CTP$MxI2k01N<|8DuPa@)NvACJkhi))mqu@^6R>*l<_^GgtS?)|He zxuNov)6(US^u$dA7>9WKG=4RjXF+DqZm1SHG&+@8#=xr?cYc z7Oy$R+UV^T|NTyoF|R3?$swskjeY+4$L@8#IbU%*v`N)4dr94nb6?JKIY(|CE-_vf_ilHgy*ULH@|8^0xY!NuC5U4LHQid|{@y#N2E zu+UeFKmYchE%;*1KE2w~+Kn$H=I|bRR<+45>}uPs;86XmÑ_5Iir!*kca-5MtF zVol`SU$?Bbyx4j$B{Vi}rTOYt3xVpQwYDx^6*rsj(5;>B7dhk^5C%Y6R*5B@W6rDXMT zqpABR*G#I7oHzHUa&Y8a)5yuM|LcA!zJ0c>PWUD7Q_-%U+wRTz|Kp)jrNjL>y*H<< zp7x=t{@LN0TV;zGNzWk+3=0nbV?5QeYl38_oFP!ZYKdz^NlIc#s#S7PDv)9@GB7gM zHL%b%G72#?v@)@(yEr+qAXP8FD1G)j8!3=CJYD@<);T3K0RS7L1+f4C literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-150.png b/res/terminal/images-Can/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ace2768a060a6d78c9ae1293dd221f0bd867e0 GIT binary patch literal 3430 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 A`Tzg` literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-black.png b/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..0b14eacfa2160d2b19574113674d49fc8b80b4b1 GIT binary patch literal 1496 zcmeAS@N?(olHy`uVBq!ia0y~yV7v%qKjdHmlD5a)Ie`>Qx}&cn1H;CC?mvmFK)yn< zN02WALzNl>LqiJ#!!Mvv!wUw6QUeBtR|yOZRx=nF#0%!^3bX-AFeQ1ryD%`U>Qy!Z z@;D1TB8wRq#8g3;(KATp15l8?#M9T6{W*siw}FgntGg@E!eyQ=jv*Dd-rn)=hz*xz z|L}fF2~VTLBwr7gSxaUmlsWC%py0Y;+ly7Nonm(gns*8O58Rk}Fyzc4>2ke0nyo=P zjcTSW+_6)Xrsc)#m=NOI;VAUqw0_p4c=6)r6D!}JO^Lfy`CR2^O`7rdbG7gP-g{X4 zd`|Vdo%)PG*H8`YFy0zrzrJ{@p1Ic7JHPm3ZKCK?2pMUb##_Y1TOy*q$o?u`*{{1g?oE$b@a{;8#hgNggE~{o zD>l*Wc~URVRqwdH%XWeB<)os0e10t@al-CvZ!=pN+iJXZx&@M0?YcD7BYHb~N!r&9 zTOjtW5uKi?7`sAQiR<&`vV>)=CSTWd9NKe7%jh#7*gbcwOfIjRyev-iV)dM>uI0UV zf1aH3b=LYVdfmKfs%h$Zck}0-2;OP6E#I(y=F8a(*UwAcjeFtxUMSy(_r+~#k${9V zx6<3x!|%-4R+Ib1U)n74NsP#`Gg24UetpNr7UGh4CWrBEe7d>G0wu2GUtB9X4$aud zq_xOk+4_bqjh>l2Cs%4R+8Qi}a?;;xED(1qKT@-6U+DAIua0D#Pk&YY_5P|=w@NQ> z{_p0JSUOqp_a)h{3&fAL9iQ$l82hWz$t6*2vSRekoVsg@?}A+Jui}3l?#wVXyYc?>kAD{1%@5u#c|oO8`@i?5@*7p=*FVQh`de9X z(@*uLrw|FV-IZb#L~0SAFRh zWuE%CcIy88By01l-*b}wv+FmF-rSsSymR9EC)|sZ_gVdUx0oS&M{i$|?A{mNU;XAv ze$(+6mtEiLwb$mAYrW;yk85u?r!MP$m)ZX5#1Yx0@#iml?web`O|P$1@Z#?a8*@(Y z|8zI;bwOE*d)bqM*PCW|xl|YC)#-0F)>rwN+o$fS_dH1N>7^G+K9ip0yndE2Gk?WP z7umD?_7|U@-ak2KZ>G?Ts(Qx}&cn1H;CC?mvmFK)yn< zN02WALzNl>LqiJ#!!Mvv!wUw6QUeBtR|yOZRx=nF#0%!^3bX-AFeQ1ryD%`U>Qy!Z z@;D1TB8wRq#8g3;(KATp15l8?#M9T6{W*siw}FgntGg@ELJvF!Z*Ti|OevK) z{_%X5-Y!MM2_8b4vb$#lEN}Xk<>czfGEeKpwWZx6vTx=uXl0rA>dMCA$-t*o3yz%+TKviS@?{BVSH+Zsh9s2>B&TWsbtz*CNAtNtCsCAjA$Sk$Mj9D%VGl3l6Qd@>+A*HrW ziNSV~y#+6wl<&>Sc+Fm3VY*3ueyJc6Wh=h^Y$!m@aLPMWt?RHt{oILSQ(BWy8 zK~fJ(0yp>d*u>54-?aC&<3Zl+9J$*8?5lUp_H0z-PT91tPNKWyrk@O>^Qw&=jfz(z zmK>Onr4y9k;Vbs?XqE4x4Lw)%@mY+LMAbigCZTzue_VsDyiz7v*9TlB)b6diKXqoOqMw!|HorgL&ng8y6w`*Rb zmx!B>$cu%y3Tm0BYV>4)<+d@wnHxRi;uJ14pB7!=l6WLS#eJRDh0ixA_UQ5&TE5FJLwbmPo-Mk)d+PqG>>fOR9V~>(^%Y|AydtA+GZvI&kU-(>T z@Aj~(+-nxI)s=U|gyEa!{T^;A^y~R^+asIWOcjx>6?&p8L@QmKeP`xM3 zem}PZiHi%l`k!Irqfp+>@H?`7>D8zNun}xwkUs?DL(*_D?6OvdvQYo|-!; z`LBEQCaruG%Q=hxn)*NcrgCy!%!^;V&WH6rx0WuGdo%ym%$%9O7gaCVw~{-wbWK`Y zpKJfO_3!t7i74sS-{ou6ZT2o_-=41Y`*T%(nw@{1Q-AjS%_*WkmG1Lz`?==-_xRZT zp&7SK|HT&PiPf%rCwV8(s(#mtjk_X3&-0{R6#uJt%Vc@?zw7%>H_FIIelN81`4o4p z+}1+Ob&>gkxj%Kcr_}$yxNpw6mvheTTi1THN#$18()pX_-dOp@Ym;)nx@X!YZ~eUo z`wcuNO^$ufmvQ!9cD0UG=-yXXPkCQkvnuXu)~cYDqQKC)8)E)lKl(-J`ZT@TfAiLr zvu{bWsxJN%HQyxaQjt=%(cQ1_sFxK%Caqgv&+tR(?$6zmEH(hSswJ)wB`Jv|saDBF zsX&Us$iT>0*T6#8$SB0n(8|QZ%E&_3z|_jXK!({!8AU^GeoAIqC2kG5ab5F)8YDqB t1m~xflqVLYGL)B>>t*I;7bhncr0V4trO$q6BL!5%;OXk;vd$@?2>_fqGKByD literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-200.png b/res/terminal/images-Can/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1b1f067858af4a1344078bb3fcf2315b7d2a9a GIT binary patch literal 4613 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-black.png b/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-black.png new file mode 100644 index 0000000000000000000000000000000000000000..8a1e60299d8e3d2ae7db8efbb8c17870ec84d921 GIT binary patch literal 1940 zcmd5+dsGtU8~!Y{)H%wt-L@`f!8*tOPon%0+RmDNkg09hL=LEe%SunAN{}YoaemX`+V>7KHvBK@t#Ud zz}efn*#ZFU@iEc+09Zo@z`}36rI~r+=R-0F%S=o>1_04{yA`sPIp08u*%uE$;bs7e zzXo8*OchT7a1;)}%QOH`l>oTpSIhTCm=D&Z$Kj#@__@>zY0hl&V-B(afPT9Qi}RCz zQ_bW$Ha;G^PPg9K4(_Qv>--J?>vViH=0Jf)|BP|t*-_}azg~4f4qT~XDfY7ZeaX?u zu{S2N=GP&%CblN7vD;X9LFtI!E6YKvyn^cLYwJDc``WRX3->HBIleno7fye&9=nyE zajrE>%5Yd5RU2eF?yOpN_js3XWiN2 z!Y2V~rA1e!2_lQ2zRd;JeyU2RBzpJ2Cm+OF{l!D&XN)m=BruA3t)1k-S zMM`xJSwjV)r+Jd$Ymv2>=e@}{l}X3m7F#b^_5P|z_uoXVe0^~f}QC=F)u9;_@(R)ZD30_ZD zo5Q~84USIHo7TEXm~@F}?tAZ?`^v(9yl>)gqyKo9I(qoy%!Limfb5L4_N>>tc3h}9 zc5ONF+6_XN9XtF!?!<{BBM~s{Xo!g@U9y7ssCeDkqg6h)r8?Ph*&PmVgR1iDtcKi@ z(o-`7&(+#FZD)~~!>gWLNh&$<7~N8^VdRDM;zRm?(sp4P3h^O`5Tcreo@8c%{!y61 zCUi4f;4#HJyd&}!(Y^afBMUJTHA|JxQI%Hmov`cMpISoTzZCXpD8uRTtrlLlLY}6Q z_(a5eT9cj|7oHgMe$Vc^lD2ktrGel`?_U2HN-MGdlq{#LO7=L@mT9`GG>Yr!N!O_} zCmEA0bc?Ebe8PBY?9a4C?)evUPY)qf#3gp>OOM>&M~#N|=4HdB87S%(9MdrLUHo6& z$m6g&DEg-K4;rBfEt!bXRRmrhiQV#J5qk>Sx$kXC?3PJ2b@pwE6QqM)lKBP0H5_Hi z$(4OMm%rm0MbYb^8`Bb(`!a2FZxMlhIf0`a8d5FwXqvSb1&0w`d&Jwi`upU6Z=bv0 zUfH6Pd6N`q-u`WT%;HC>$H@9@i|ZJ{^RS}F9nu9NATWaDhaC2zGTV?I&_+w@>@#{KLYuW^G1u^N)|xeeStiRMZ2t=V$-@>eu^)aSX_ zagv9Br0)n>IYPhQfzb(1fwHJw7Qc+vJ}QBR{>np~z;hY0b; zSB7!DiO(5%E2@erDYd2#eiaw6X6#~vDXy&IWS$F)+^I%uP=>m2m`&(b&86*P#3+** z>gU=Oc$d17G^i{>J*;6upM}e0oCkZXGO<5Rm1(OXb7OUcxH6gF1RDSG=5YZ1 ziP+tWR+mlW8{Bk}UCC|9nmVURVTWvbPsi|4_4gB36}=3_QGD~u&e8Iki7IcFn%y6u zbi{pX?5X;}H(3`*KqLkz7>yi;X`H#Bt;uo0z$En_4L}E+c#@8j>@RS}O2XRz##ja5 ze*>>Q+hhA4z7q=GnD4ZSjU})%$ZRT#!lar5K*Eth{_rq=B=P_PfeH>o1tI+5At*Q; zi{R${OOVUR$Ue;buppTJ@R3>Iy6Ui>ahS~|GpT^fB%>2O+;kNoU Ncx*y+b5vT%{{jD95hDNq literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-white.png b/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..71167720c7b0f66c8354c3679452b90f8cafe44e GIT binary patch literal 1847 zcmd5+do-Q&I{+%Ol$UUFP`(52?ClOfq9FkEGyoPLi2fOX zI3xgH!T_*h0H7Xyr{2pNI#_qy&B+0PN16KOP^K8|97q8`Cw*K%jiAi@T%z zCxvawNZo-e+g1U9$+dz8b~DUG8z;N%YPpQEQy=|;KB=jB3p#9A zL90CaR?sH;aM5X-^O8Vm)I+=gx~cvQb-cOdJ`(@mj5L*i{mp{ziO#A7^tW0#tjUnG zXh2Q>TB+Z^aGSUK2kaHfR}8y@rn7ceq5G<3W-tnZWDDW2c)Kb(OiA_p6t;)4D*gT2*oOovT${hKounwvis*Z;R9_TZTXog*>=;SPOCrC^SoUEYFPwdef z8ChjWYSNg_Uqcv^97QdpfJSuhqnnK$_=ixyk&yFIxCgC-7S8Q!7mto;slubtlWn@S zGuXOO#9+zl=wt#dHg1etS;no&E}ULmD&$h_H*zeSTM|+vcD%fEEJf5kz6YZu|E+)| zsq)tuW6#=#1Q&PPN3ez|HIKcE>tq|~}w!asCK^i?|6V9}O5?`j&q$bNaR z3ooik38X(oo*8~Jm)cbKOZ$h{-ZR{ru81CeU*FjeCwDJgcP*IfarO1(QL5E!K90!v zRHzY@j3`d5?Slt$WKqFshAtnoT(Y&qM=_Zn3LVTf4|>b z4;t5FL%#_5jiVU_MOF7Pt$|*eiF;b550(s#d2DPWW60ob9=R62lk&p*#<`%jLh0l2 z_C@J48o&QrhH2*Q@FhFr60Hc9n1NfcVO4#~zEhUp84JF}S1D6zN9 zIdSwVW||E>M??#5cl#qYA)%1}TJv{9k8PC0juY`RczaIci!oPH#iyVk1<2O~H zo(y6g17c6$VhL9G7y=Z4Ino@3K<-DFn`6z)toH4LUYq4aRr`M&|o CW88oM literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-400.png b/res/terminal/images-Can/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000000000000000000000000000000000000..9361fe998528b7d55a3a5c92f17cedc61c3cb5c7 GIT binary patch literal 10851 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 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-white.png b/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-white.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa1be7a035c362e727fec7e9e449c05f09a78cb GIT binary patch literal 3848 zcmeHJdo#Rfqw$_Dic+QZD%hRtEK@=tl3u*CqtBUvsiCvwcVqOMfA5Hm^&C7RxVyQr;N;ba zkoe6h??MLJtF*`!#@;Hgy3JKLq{O6baq$#x%ecDO2fEk>*DvXk>RXjF(^B5!xg!t&Nj*Rfr}1g4J}v4e#sJ}CBKfKpsC zYMdvS1*ECcE+qJ<8SYRU%vRKzA#zYk zOaAY$ddBYNz;ZD0uZ@;~`Eu=FG!T_=d1q(N-R!WlV$T`^pmKNoNos zuSY;5Q7QM^+!Z@3VoEW}T`KGjr{T%eF~G8w6@ z)~H~9fQwK~AtH6Q^@9RvA{B4Rytme}^z=1UDIpo9Ee8>8ZB$UbNJgH_BqCFtxd?NY zDufdTioe;BdOjjyWnpPnVVSUivf|9%@y;|e!(MzP*U4!l>e}`vvPRtP)!2JsRQDQ# zAsrO2F=r|_o-T8}t-1H`Ws#_HT z0}U!bFKG_2vO=3cvwp5YE?U2I6O`;`Jo?+4e&zvtz!P(yGEvQ!v~Q z&DC1wqN2Jv>}CbS7we`SYH!tTwf3)EHclhE|7SbfCTN?;pF4BR2FF+EgkHgzpR9cP z!lsY;XnP+sJDUPhG{YPI-eSRYTx&_(`c|jmS9x8m91TUpK^hHdBBbP$(Sa9w--|)4 zMtFlaibaVV`|We>>^SIwyawcTY21gcYfi$$-`Z#{_I+u=RD*%}4w}?j4NTD2z&KPQ z%<)x-D>)R<{$033(_0;fS1{a$P|S8U%#~lmj1hxWm{lkUu4zjjb3X+4hOk=h`#6c} z{>8i1qs(Yj9g(m>%S*+f7MC(*%nxPe;M0QwOOV#Q<3g4Gg0c@$i8q#>_?8rP{7khg zq~fm`_ZOwj{3)IPU?|4K%mgE!Bs?PP3 zJv#qr(`pg;l}u8EHB)M$qgMB_K2EnOJUlLkSzQTyEuLV|jq$9}v3LJ$UR+0ukY%6W zQ}xR%XsC=ln%VIaJt**C*C&wps^$&lkY<5+8AMu1m3M{nl zH4x7@DcT#H^J4B4Y*jWV2KtWPBlz{qybCBRqsK;sNfJqFMW>e^()TydX|kt5)vvJ5 zG^=+6a`?QV$C(_b5JE!S%}5EuMfT?Wnu9P%`BwcCqMEnanH)b6cai=1A(g_N{jDJx z*69n6lJFX70QUiDGN0gUP>?XdL!%m^;Es+@b2wju+=qJ~eSBtAAxRofG@ZYJM*IJE zj-%LFP9@wR2TN`q-7KDKnc#7n%LEPsVI}AGR1EOy&D#B91h{(HKwOU=t^m;q`x#2Y z$_l27+MKVE6GY`WF-t0m_ms?0?dk|4 zxIhrZ6)gKnchd!_uC$Qnj><+`anYc2Y)Rk1Ft0V2yRQ;~fp-8?Kj70TTrP(ml;9O1 zN-_JqEF0{pna*NQZ>hTUT{yrWdpY!J-}~aZ=|UQ5PGlp&6;(yPZ^@3Q4PemiGWN7q z6;a8O=rL4MmLs^@zM8zRY;hwK_MtJ?(u1z=ihFN^6-%}1d(AGOF~T zy^uon}IkXze0S z0?iZGRWo*kf44ci5H<<0J6%qdc(gfpEOGCST(pvZC#>x$eiyPkk|LU1P~gQ72D*>f z2QJnj{_XHYa@$idKeACnq*Yd=Jho zEIp5>#J=Xo?62!N{VaN&@czp5%$oJK#|`A8ZEV#;ZqI6vPEj&jFzhsNYUH|V^jXuT zQBQW7_F-{If=u)4TO$}B(UA`4oZ`mbS_ynrF6+a4=&9g(A=fYS_r|-L0|gZNy&xpx zcU4{N(r(e)9nI08_SXpexVt?r@eqgY33w z7yoGTiRfT!5-k`KV2U+0Gsf;UHZ}Fxv&Y(eueG_UG1kHwi%rxcPyCMpHIf__690b` z_Iy>q{q&*@Dr{V}a5yp~I_?B57{tZJnS@19=%f=-!6uQk(8Bk&I#3AUUA&wt_Ysr- E4LAb%7 literal 0 HcmV?d00001 diff --git a/res/terminal/images-Can/terminal.ico b/res/terminal/images-Can/terminal.ico new file mode 100644 index 0000000000000000000000000000000000000000..4449fc7252163f6479f768f7543b376f2c5fa95d GIT binary patch literal 45274 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