diff --git a/.clang-format b/.clang-format index a6db19a068..1add70421e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,57 +1,25 @@ - +--- +Language: Cpp +BasedOnStyle: Microsoft AccessModifierOffset: -4 -AlignAfterOpenBracket: Align -AllowAllArgumentsOnNextLine: true -AlignConsecutiveMacros: false -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AllowAllConstructorInitializersOnNextLine: true AlignEscapedNewlines: Left -AlignOperands: true AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Never AllowShortFunctionsOnASingleLine: All -AllowShortCaseLabelsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: Never -#AllowShortLambdasOnASingleLine: Inline -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BraceWrapping: AfterCaseLabel: true - AfterClass: true - AfterControlStatement: true - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterObjCDeclaration: true - AfterStruct: true AfterUnion: true AfterExternBlock: false - BeforeCatch: true - BeforeElse: true - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBinaryOperators: None -BreakBeforeBraces: Custom BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakInheritanceList: AfterColon ColumnLimit: 0 CommentPragmas: "suppress" -CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 Cpp11BracedListStyle: false -DeriveLineEnding: true -DerivePointerAlignment: false FixNamespaceComments: false IncludeBlocks: Regroup IncludeCategories: @@ -63,35 +31,13 @@ IncludeCategories: Priority: 2 - Regex: '.*' Priority: 3 -IndentCaseLabels: false -IndentPPDirectives: None -IndentWidth: 4 -IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: "BEGIN_TEST_METHOD_PROPERTIES|BEGIN_MODULE|BEGIN_TEST_CLASS|BEGIN_TEST_METHOD" MacroBlockEnd: "END_TEST_METHOD_PROPERTIES|END_MODULE|END_TEST_CLASS|END_TEST_METHOD" -MaxEmptyLinesToKeep: 1 NamespaceIndentation: All PointerAlignment: Left ReflowComments: false SortIncludes: false -SortUsingDeclarations: true -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false -SpaceBeforeAssignmentOperators: true -SpaceBeforeCpp11BracedList: false -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 SpacesInAngles: false -SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: Latest -TabWidth: 4 -UseTab: Never diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..f76ffa6c2d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Commits mentioned in this file will automatically +# be skipped by GitHub's blame view. +# To use it with "git", run +# > git blame --ignore-revs-file ./.git-blame-ignore-revs + +# Reformatted the entire codebase +9b92986b49bed8cc41fde4d6ef080921c41e6d9e + +# Line Endings changes +cb7a76d96c92aa9fc7b03f69148fb0c75dff191d +5bbf61af8c8f12e6c05d07a696bf7d411b330a67 +d07546a6fef73fa4e1fb1c2f01535843d1fcc212 + +# UTF-8 BOM changes +ddae2a1d49d604487d3c963e5eacbeb73861d986 diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.yml b/.github/ISSUE_TEMPLATE/Bug_Report.yml index dfe9c9271f..517f2bdc7e 100644 --- a/.github/ISSUE_TEMPLATE/Bug_Report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_Report.yml @@ -14,7 +14,7 @@ body: label: Windows Terminal version placeholder: "1.7.3651.0" description: | - You can find the version in the about dialog, or by running `wt -v` at the commandline (for the Stable/Preview version you’re reporting a bug about). + You can copy the version number from the About dialog. Open the About dialog by opening the menu with the "V" button (to the right of the "+" button that opens a new tab) and choosing About from the end of the list. validations: required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d131a7d1f1..d8735c42b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,20 +1,14 @@ - ## Summary of the Pull Request - -## References +## References and Relevant Issues - -## PR Checklist -* [ ] Closes #xxx -* [ ] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA -* [ ] Tests added/passed -* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx -* [ ] Schema updated. -* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx - - ## Detailed Description of the Pull Request / Additional comments - ## Validation Steps Performed + +## PR Checklist +- [ ] Closes #xxx +- [ ] Tests added/passed +- [ ] Documentation updated + - If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx +- [ ] Schema updated (if necessary) diff --git a/.github/actions/spelling/README.md b/.github/actions/spelling/README.md index 2b1b582859..4c40f7f02a 100644 --- a/.github/actions/spelling/README.md +++ b/.github/actions/spelling/README.md @@ -6,6 +6,7 @@ File | Purpose | Format | Info [reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject) [excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes) [patterns/*.txt](patterns/) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) +[candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns) [line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) [expect/*.txt](expect.txt) | Expected words that aren't in the dictionary | one word per line (sorted, alphabetically) | [expect](https://github.com/check-spelling/check-spelling/wiki/Configuration#expect) [advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice) diff --git a/.github/actions/spelling/advice.md b/.github/actions/spelling/advice.md index 2c4c5443f8..d82df49ee2 100644 --- a/.github/actions/spelling/advice.md +++ b/.github/actions/spelling/advice.md @@ -21,7 +21,7 @@ See the `README.md` in each directory for more information. :microscope: You can test your commits **without** *appending* to a PR by creating a new branch with that extra change and pushing it to your fork. The [check-spelling](https://github.com/marketplace/actions/check-spelling) action will run in response to your **push** -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. :wink: -
:clamp: If the flagged items are false positives +
If the flagged items are :exploding_head: false positives If items relate to a ... * binary file (or some other file you wouldn't want to check at all). diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index d26e2a56e3..28c4684020 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,7 +1,8 @@ admins allcolors -apc Apc +apc +backpressure breadcrumb breadcrumbs bsd @@ -12,18 +13,24 @@ clickable clig CMMI copyable +Counterintuitively +CtrlDToClose cybersecurity dalet -dcs Dcs +dcs +deselection dialytika +diffing dje downside downsides dze dzhe +DTo EDDB EDDC +Emacspeak Enum'd Fitt formattings @@ -34,10 +41,12 @@ gantt gcc geeksforgeeks ghe +github gje godbolt hostname hostnames +https hyperlink hyperlinking hyperlinks @@ -58,6 +67,7 @@ lol lorem Lorigin maxed +megathread minimalistic mkmk mnt @@ -69,6 +79,8 @@ ok'd overlined pipeline postmodern +Powerline +powerline ptys qof qps @@ -82,6 +94,9 @@ runtimes shcha slnt Sos +ssh +stakeholders +sxn timeline timelines timestamped @@ -89,13 +104,17 @@ TLDR tokenizes tonos toolset +truthiness tshe +ubuntu uiatextrange UIs und unregister versioned vsdevcmd +walkthrough +walkthroughs We'd wildcards XBox diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index e7b0749498..4ce5fe88d6 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -28,14 +28,15 @@ CYICON Dacl dataobject dcomp +debugbreak +delayimp DERR dlldata DNE DONTADDTORECENT -DWMWA -DWORDLONG DWMSBT DWMWA +DWORDLONG endfor ENDSESSION enumset @@ -52,9 +53,12 @@ futex GETDESKWALLPAPER GETHIGHCONTRAST GETMOUSEHOVERTIME +GETTEXTLENGTH Hashtable HIGHCONTRASTON HIGHCONTRASTW +hinternet +HINTERNET hotkeys href hrgn @@ -74,6 +78,7 @@ IConnection ICustom IDialog IDirect +Idn IExplorer IFACEMETHOD IFile @@ -85,6 +90,7 @@ IObject iosfwd IPackage IPeasant +isa ISetup isspace IStorage @@ -106,12 +112,14 @@ lsass LSHIFT LTGRAY MAINWINDOW +MAXIMIZEBOX memchr memicmp MENUCOMMAND MENUDATA -MENUITEMINFOW MENUINFO +MENUITEMINFOW +MINIMIZEBOX mmeapi MOUSELEAVE mov @@ -158,11 +166,13 @@ rcx REGCLS RETURNCMD rfind -roundf +RLO ROOTOWNER +roundf RSHIFT SACL schandle +SEH semver serializer SETVERSION @@ -178,6 +188,7 @@ snprintf spsc sregex SRWLOC +srwlock SRWLOCK STDCPP STDMETHOD @@ -194,6 +205,7 @@ TABROW TASKBARCREATED TBPF THEMECHANGED +THICKFRAME tlg TME tmp @@ -208,11 +220,19 @@ UFIELD ULARGE UOI UPDATEINIFILE +urlmon userenv USEROBJECTFLAGS +Vcpp Viewbox +virtualalloc +vsnwprintf +wcsnlen wcsstr wcstoui +WDJ +winhttp +wininet winmain winsta winstamin @@ -220,6 +240,7 @@ wmemcmp wpc WSF wsregex +WWH wwinmain xchg XDocument @@ -245,3 +266,4 @@ xtree xutility YIcon YMax +zwstring diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 1f4a28664f..1ed1c6da89 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -4,14 +4,17 @@ advapi altform altforms appendwttlogging +appinstaller appx appxbundle appxerror appxmanifest ATL +autoexec backplating bitmaps BOMs +COMPUTERNAME CPLs cpptools cppvsdbg @@ -26,6 +29,7 @@ dotnetfeed DTDs DWINRT enablewttlogging +HOMESHARE Intelli IVisual libucrt @@ -33,6 +37,7 @@ libucrtd LKG LOCKFILE Lxss +makepri mfcribbon microsoft microsoftonline @@ -50,14 +55,20 @@ pgo pgosweep powerrename powershell +priconfig +PRIINFO propkey pscustomobject QWORD +rdpclip regedit +resfiles robocopy SACLs sdkddkver +segoe Shobjidl +sid Skype SRW sxs @@ -70,6 +81,7 @@ tdbuildteamid ucrt ucrtd unvirtualized +USERDNSDOMAIN VCRT vcruntime Virtualization diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 1c6ef9a373..0409931d02 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -1,6 +1,6 @@ Anup -austdi arkthur +austdi Ballmer bhoj Bhojwani @@ -23,6 +23,7 @@ Griese Hernan Howett Illhardt +Imms iquilezles italo jantari @@ -30,10 +31,11 @@ jerrysh Kaiyu kimwalisch KMehrain -KODELIFE Kodelife +KODELIFE Kourosh kowalczyk +leonardder leonmsft Lepilleur lhecker @@ -52,6 +54,7 @@ mikegr mikemaccana miloush miniksa +nguyen niksa nvaccess nvda @@ -59,6 +62,7 @@ oising oldnewthing opengl osgwiki +Ottosson pabhojwa panos paulcam @@ -77,15 +81,18 @@ sonpham stakx talo thereses +Thysell Walisch WDX Wellons +Westerman Wirt Wojciech zadjii Zamor -Zamora zamora +Zamora +zljubisic Zoey zorio Zverovich diff --git a/.github/actions/spelling/candidate.patterns b/.github/actions/spelling/candidate.patterns new file mode 100644 index 0000000000..400f103ea2 --- /dev/null +++ b/.github/actions/spelling/candidate.patterns @@ -0,0 +1,521 @@ +# marker to ignore all code on line +^.*/\* #no-spell-check-line \*/.*$ +# marker for ignoring a comment to the end of the line +// #no-spell-check.*$ + +# patch hunk comments +^\@\@ -\d+(?:,\d+|) \+\d+(?:,\d+|) \@\@ .* +# git index header +index [0-9a-z]{7,40}\.\.[0-9a-z]{7,40} + +# cid urls +(['"])cid:.*?\g{-1} + +# data url in parens +\(data:[^)]*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\) +# data url in quotes +([`'"])data:.*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\g{-1} +# data url +data:[-a-zA-Z=;:/0-9+]*,\S* + +# mailto urls +mailto:[-a-zA-Z=;:/?%&0-9+@.]{3,} + +# magnet urls +magnet:[?=:\w]+ + +# magnet urls +"magnet:[^"]+" + +# obs: +"obs:[^"]*" + +# The `\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read +# In this examples content, I'm using a number of different ways to match things to show various approaches +# asciinema +\basciinema\.org/a/[0-9a-zA-Z]+ + +# apple +\bdeveloper\.apple\.com/[-\w?=/]+ +# Apple music +\bembed\.music\.apple\.com/fr/playlist/usr-share/[-\w.]+ + +# appveyor api +\bci\.appveyor\.com/api/projects/status/[0-9a-z]+ +# appveyor project +\bci\.appveyor\.com/project/(?:[^/\s"]*/){2}builds?/\d+/job/[0-9a-z]+ + +# Amazon + +# Amazon +\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|) +# AWS S3 +\b\w*\.s3[^.]*\.amazonaws\.com/[-\w/&#%_?:=]* +# AWS execute-api +\b[0-9a-z]{10}\.execute-api\.[-0-9a-z]+\.amazonaws\.com\b +# AWS ELB +\b\w+\.[-0-9a-z]+\.elb\.amazonaws\.com\b +# AWS SNS +\bsns\.[-0-9a-z]+.amazonaws\.com/[-\w/&#%_?:=]* +# AWS VPC +vpc-\w+ + +# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there +# YouTube url +\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]* +# YouTube music +\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*) +# YouTube tag +<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"] +# YouTube image +\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]* +# Google Accounts +\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]* +# Google Analytics +\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]* +# Google APIs +\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+ +# Google Storage +\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|) +# Google Calendar +\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+ +\w+\@group\.calendar\.google\.com\b +# Google DataStudio +\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|) +# The leading `/` here is as opposed to the `\b` above +# ... a short way to match `https://` or `http://` since most urls have one of those prefixes +# Google Docs +/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|)) +# Google Drive +\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]* +# Google Groups +\bgroups\.google\.com/(?:(?:forum/#!|d/)(?:msg|topics?|searchin)|a)/[^/\s"]+/[-a-zA-Z0-9$]+(?:/[-a-zA-Z0-9]+)* +# Google Maps +\bmaps\.google\.com/maps\?[\w&;=]* +# Google themes +themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+. +# Google CDN +\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]* +# Goo.gl +/goo\.gl/[a-zA-Z0-9]+ +# Google Chrome Store +\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|) +# Google Books +\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]* +# Google Fonts +\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]* +# Google Forms +\bforms\.gle/\w+ +# Google Scholar +\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+ +# Google Colab Research Drive +\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]* + +# GitHub SHAs (api) +\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b +# GitHub SHAs (markdown) +(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|) +# GitHub SHAs +\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b +# GitHub wiki +\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b +# githubusercontent +/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]* +# githubassets +\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+) +# gist github +\bgist\.github\.com/[^/\s"]+/[0-9a-f]+ +# git.io +\bgit\.io/[0-9a-zA-Z]+ +# GitHub JSON +"node_id": "[-a-zA-Z=;:/0-9+]*" +# Contributor +\[[^\]]+\]\(https://github\.com/[^/\s"]+\) +# GHSA +GHSA(?:-[0-9a-z]{4}){3} + +# GitLab commit +\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b +# GitLab merge requests +\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b +# GitLab uploads +\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]* +# GitLab commits +\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b + +# binanace +accounts.binance.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]* + +# bitbucket diff +\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}diff(?:stat|)(?:/[^/\s"]+){2}:[0-9a-f]+ +# bitbucket repositories commits +\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}commits?/[0-9a-f]+ +# bitbucket commits +\bbitbucket\.org/(?:[^/\s"]+/){2}commits?/[0-9a-f]+ + +# bit.ly +\bbit\.ly/\w+ + +# bitrise +\bapp\.bitrise\.io/app/[0-9a-f]*/[\w.?=&]* + +# bootstrapcdn.com +\bbootstrapcdn\.com/[-./\w]+ + +# cdn.cloudflare.com +\bcdnjs\.cloudflare\.com/[./\w]+ + +# circleci +\bcircleci\.com/gh(?:/[^/\s"]+){1,5}.[a-z]+\?[-0-9a-zA-Z=&]+ + +# gitter +\bgitter\.im(?:/[^/\s"]+){2}\?at=[0-9a-f]+ + +# gravatar +\bgravatar\.com/avatar/[0-9a-f]+ + +# ibm +[a-z.]*ibm\.com/[-_#=:%!?~.\\/\d\w]* + +# imgur +\bimgur\.com/[^.]+ + +# Internet Archive +\barchive\.org/web/\d+/(?:[-\w.?,'/\\+&%$#_:]*) + +# discord +/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,} + +# Disqus +\bdisqus\.com/[-\w/%.()!?&=_]* + +# medium link +\blink\.medium\.com/[a-zA-Z0-9]+ +# medium +\bmedium\.com/\@?[^/\s"]+/[-\w]+ + +# microsoft +\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]* +# powerbi +\bapp\.powerbi\.com/reportEmbed/[^"' ]* +# vs devops +\bvisualstudio.com(?::443|)/[-\w/?=%&.]* +# microsoft store +\bmicrosoft\.com/store/apps/\w+ + +# mvnrepository.com +\bmvnrepository\.com/[-0-9a-z./]+ + +# now.sh +/[0-9a-z-.]+\.now\.sh\b + +# oracle +\bdocs\.oracle\.com/[-0-9a-zA-Z./_?#&=]* + +# chromatic.com +/\S+.chromatic.com\S*[")] + +# codacy +\bapi\.codacy\.com/project/badge/Grade/[0-9a-f]+ + +# compai +\bcompai\.pub/v1/png/[0-9a-f]+ + +# mailgun api +\.api\.mailgun\.net/v3/domains/[0-9a-z]+\.mailgun.org/messages/[0-9a-zA-Z=@]* +# mailgun +\b[0-9a-z]+.mailgun.org + +# /message-id/ +/message-id/[-\w@./%]+ + +# Reddit +\breddit\.com/r/[/\w_]* + +# requestb.in +\brequestb\.in/[0-9a-z]+ + +# sched +\b[a-z0-9]+\.sched\.com\b + +# Slack url +slack://[a-zA-Z0-9?&=]+ +# Slack +\bslack\.com/[-0-9a-zA-Z/_~?&=.]* +# Slack edge +\bslack-edge\.com/[-a-zA-Z0-9?&=%./]+ +# Slack images +\bslack-imgs\.com/[-a-zA-Z0-9?&=%.]+ + +# shields.io +\bshields\.io/[-\w/%?=&.:+;,]* + +# stackexchange -- https://stackexchange.com/feeds/sites +\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/) + +# Sentry +[0-9a-f]{32}\@o\d+\.ingest\.sentry\.io\b + +# Twitter markdown +\[\@[^[/\]:]*?\]\(https://twitter.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)\) +# Twitter hashtag +\btwitter\.com/hashtag/[\w?_=&]* +# Twitter status +\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|) +# Twitter profile images +\btwimg\.com/profile_images/[_\w./]* +# Twitter media +\btwimg\.com/media/[-_\w./?=]* +# Twitter link shortened +\bt\.co/\w+ + +# facebook +\bfburl\.com/[0-9a-z_]+ +# facebook CDN +\bfbcdn\.net/[\w/.,]* +# facebook watch +\bfb\.watch/[0-9A-Za-z]+ + +# dropbox +\bdropbox\.com/sh?/[^/\s"]+/[-0-9A-Za-z_.%?=&;]+ + +# ipfs protocol +ipfs://[0-9a-z]* +# ipfs url +/ipfs/[0-9a-z]* + +# w3 +\bw3\.org/[-0-9a-zA-Z/#.]+ + +# loom +\bloom\.com/embed/[0-9a-f]+ + +# regex101 +\bregex101\.com/r/[^/\s"]+/\d+ + +# figma +\bfigma\.com/file(?:/[0-9a-zA-Z]+/)+ + +# freecodecamp.org +\bfreecodecamp\.org/[-\w/.]+ + +# image.tmdb.org +\bimage\.tmdb\.org/[/\w.]+ + +# mermaid +\bmermaid\.ink/img/[-\w]+|\bmermaid-js\.github\.io/mermaid-live-editor/#/edit/[-\w]+ + +# Wikipedia +\ben\.wikipedia\.org/wiki/[-\w%.#]+ + +# gitweb +[^"\s]+/gitweb/\S+;h=[0-9a-f]+ + +# HyperKitty lists +/archives/list/[^@/]+\@[^/\s"]*/message/[^/\s"]*/ + +# lists +/thread\.html/[^"\s]+ + +# list-management +\blist-manage\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+ + +# kubectl.kubernetes.io/last-applied-configuration +"kubectl.kubernetes.io/last-applied-configuration": ".*" + +# pgp +\bgnupg\.net/pks/lookup[?&=0-9a-zA-Z]* + +# Spotify +\bopen\.spotify\.com/embed/playlist/\w+ + +# Mastodon +\bmastodon\.[-a-z.]*/(?:media/|\@)[?&=0-9a-zA-Z_]* + +# scastie +\bscastie\.scala-lang\.org/[^/]+/\w+ + +# images.unsplash.com +\bimages\.unsplash\.com/(?:(?:flagged|reserve)/|)[-\w./%?=%&.;]+ + +# pastebin +\bpastebin\.com/[\w/]+ + +# heroku +\b\w+\.heroku\.com/source/archive/\w+ + +# quip +\b\w+\.quip\.com/\w+(?:(?:#|/issues/)\w+)? + +# badgen.net +\bbadgen\.net/badge/[^")\]'\s]+ + +# statuspage.io +\w+\.statuspage\.io\b + +# media.giphy.com +\bmedia\.giphy\.com/media/[^/]+/[\w.?&=]+ + +# tinyurl +\btinyurl\.com/\w+ + +# getopts +\bgetopts\s+(?:"[^"]+"|'[^']+') + +# ANSI color codes +(?:\\(?:u00|x)1b|\x1b)\[\d+(?:;\d+|)m + +# URL escaped characters +\%[0-9A-F][A-F] +# IPv6 +\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b +# c99 hex digits (not the full format, just one I've seen) +0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP] +# sha +sha\d+:[0-9]*[a-f]{3,}[0-9a-f]* +# sha-... -- uses a fancy capture +(['"]|")[0-9a-f]{40,}\g{-1} +# hex runs +\b[0-9a-fA-F]{16,}\b +# hex in url queries +=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?& +# ssh +(?:ssh-\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,} + +# PGP +\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\b +# GPG keys +\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\b +# Well known gpg keys +.well-known/openpgpkey/[\w./]+ + +# uuid: +\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b +# hex digits including css/html color classes: +(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b +# integrity +integrity="sha\d+-[-a-zA-Z=;:/0-9+]{40,}" + +# https://www.gnu.org/software/groff/manual/groff.html +# man troff content +\\f[BCIPR] +# ' +\\\(aq + +# .desktop mime types +^MimeTypes?=.*$ +# .desktop localized entries +^[A-Z][a-z]+\[[a-z]+\]=.*$ +# Localized .desktop content +Name\[[^\]]+\]=.* + +# IServiceProvider +\bI(?=(?:[A-Z][a-z]{2,})+\b) + +# crypt +"\$2[ayb]\$.{56}" + +# scrypt / argon +\$(?:scrypt|argon\d+[di]*)\$\S+ + +# Input to GitHub JSON +content: "[-a-zA-Z=;:/0-9+]*=" + +# Python stringprefix / binaryprefix +# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings +(?v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) +# Compiler flags (Scala) +(?:^|[\t ,>"'`=(])-J-[DPWXY](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) +# Compiler flags +#(?:^|[\t ,"'`=(])-[DPWXYLlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) + +# Compiler flags (linker) +,-B +# curl arguments +\b(?:\\n|)curl(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)* +# set arguments +\bset(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)* +# tar arguments +\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+ +# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long... +\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b +# macOS temp folders +/var/folders/\w\w/[+\w]+/(?:T|-Caches-)/ diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index b09b9a65f3..441d1f295c 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -2,14 +2,14 @@ (?:(?i)\.png$) (?:^|/)(?i)COPYRIGHT (?:^|/)(?i)LICEN[CS]E +(?:^|/)3rdparty/ (?:^|/)dirs$ (?:^|/)go\.mod$ (?:^|/)go\.sum$ (?:^|/)package(?:-lock|)\.json$ (?:^|/)sources(?:|\.dep)$ (?:^|/)vendor/ -ignore$ -SUMS$ +\.a$ \.ai$ \.avi$ \.bmp$ @@ -20,6 +20,8 @@ SUMS$ \.crt$ \.csr$ \.dll$ +\.docx?$ +\.drawio$ \.DS_Store$ \.eot$ \.eps$ @@ -31,6 +33,7 @@ SUMS$ \.icns$ \.ico$ \.jar$ +\.jks$ \.jpeg$ \.jpg$ \.key$ @@ -41,6 +44,7 @@ SUMS$ \.mod$ \.mp3$ \.mp4$ +\.o$ \.ocf$ \.otf$ \.pbxproj$ @@ -48,22 +52,41 @@ SUMS$ \.pem$ \.png$ \.psd$ +\.pyc$ \.runsettings$ +\.s$ \.sig$ \.so$ \.svg$ \.svgz$ +\.svgz?$ \.tar$ \.tgz$ +\.tiff?$ \.ttf$ \.vsdx$ \.wav$ +\.webm$ +\.webp$ \.woff +\.woff2?$ \.xcf$ \.xls +\.xlsx?$ \.xpm$ \.yml$ \.zip$ +^\.github/actions/spelling/ +^\.github/fabricbot.json$ +^\.gitignore$ +^\Q.git-blame-ignore-revs\E$ +^\Q.github/workflows/spelling.yml\E$ +^\Qdoc/reference/windows-terminal-logo.ans\E$ +^\Qsamples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters\E$ +^\Qsrc/host/exe/Host.EXE.vcxproj.filters\E$ +^\Qsrc/host/ft_host/chafa.txt\E$ +^\Qsrc/tools/closetest/CloseTest.vcxproj.filters\E$ +^\XamlStyler.json$ ^build/config/ ^consolegit2gitfilters\.json$ ^dep/ @@ -83,19 +106,15 @@ SUMS$ ^src/terminal/parser/ft_fuzzwrapper/run\.bat$ ^src/terminal/parser/ut_parser/Base64Test.cpp$ ^src/terminal/parser/ut_parser/run\.bat$ +^src/tools/benchcat ^src/tools/integrity/packageuwp/ConsoleUWP\.appxSources$ ^src/tools/lnkd/lnkd\.bat$ ^src/tools/pixels/pixels\.bat$ +^src/tools/RenderingTests/main.cpp$ ^src/tools/texttests/fira\.txt$ ^src/tools/U8U16Test/(?:fr|ru|zh)\.txt$ +^src/types/ColorFix.cpp ^src/types/ut_types/UtilsTests.cpp$ ^tools/ReleaseEngineering/ServicingPipeline.ps1$ -^\.github/actions/spelling/ -^\.github/fabricbot.json$ -^\.gitignore$ -^\Q.github/workflows/spelling.yml\E$ -^\Qsamples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters\E$ -^\Qsrc/host/exe/Host.EXE.vcxproj.filters\E$ -^\Qsrc/host/ft_host/chafa.txt\E$ -^\Qsrc/tools/closetest/CloseTest.vcxproj.filters\E$ -^\XamlStyler.json$ +ignore$ +SUMS$ diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt index 7275b200cb..97180b7211 100644 --- a/.github/actions/spelling/expect/alphabet.txt +++ b/.github/actions/spelling/expect/alphabet.txt @@ -5,31 +5,30 @@ AAAAAABBBBBBCCC AAAAABBBBBBCCC abcd abcd -abcde -abcdef -ABCDEFG -ABCDEFGH ABCDEFGHIJ abcdefghijk ABCDEFGHIJKLMNO abcdefghijklmnop +ABCDEFGHIJKLMNOPQRS ABCDEFGHIJKLMNOPQRST -abcdefghijklmnopqrstuvwxyz ABCG ABE abf BBBBB BBBBBBBB -BBBBBBBBBBBBBBDDDD BBBBBCCC BBBBCCCCC BBGGRR -CCE +efg EFG +efgh EFGh +KLMNOQQQQQQQQQQ QQQQQQQQQQABCDEFGHIJ +QQQQQQQQQQABCDEFGHIJKLMNOPQRS QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ +QQQQQQQQQQABCDEFGHIJPQRST QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ qrstuvwxyz qwerty diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 0c94916712..7505061fab 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1,24 +1,22 @@ +aabbcc +aarch ABANDONFONT +abbcc ABCDEFGHIJKLMNOPQRSTUVWXY +ABCF abgr abi -ACCESSTOKEN -acec -acf +ABORTIFHUNG acidev ACIOSS ACover actctx ACTCTXW activatable -ACTIVEBORDER -ACTIVECAPTION ADDALIAS ADDREF -addressof ADDSTRING ADDTOOL -AEnd AFew AFill AFX @@ -27,8 +25,8 @@ ahz AImpl AInplace ALIGNRIGHT -alloc allocing +allocs alpc ALTERNATENAME ALTF @@ -39,57 +37,46 @@ ansicpg ANSISYS ANSISYSRC ANSISYSSC -antialias +answerback antialiasing ANull anycpu APARTMENTTHREADED +APCA APCs -api APIENTRY apiset APPBARDATA appcontainer -APPICON appium -applet appletname +applets applicationmodel APPLMODAL appmodel -apps APPWINDOW APrep apsect APSTUDIO archeologists -architected argb -argc -args -argv ARRAYSIZE ARROWKEYS asan -ASBRST ASBSET ASDF asdfghjkl ASetting ASingle -asm -asmv asmx ASYNCWINDOWPOS atch ATest -attr ATTRCOLOR aumid Authenticode AUTOBUDDY AUTOCHECKBOX -autogenerated autohide AUTOHSCROLL automagically @@ -98,51 +85,43 @@ AUTORADIOBUTTON autoscrolling Autowrap AVerify -AVI AVX awch azzle -backend backgrounded Backgrounder backgrounding -backport +backported backstory barbaz Batang -baz Bazz BBDM bbwe bcount -bcrypt bcx bcz BEFOREPARENT beginthread -bgcolor +benchcat bgfx bgidx Bgk BGR -BGRA +bgra BHID bigobj binplace binplaced bitcoin bitcrazed -bitflag bitmask BITOPERATION -bitset BKCOLOR BKGND Bksp -blog Blt BLUESCROLL -bmp BODGY BOLDFONT BOOLIFY @@ -158,44 +137,33 @@ bpp BPPF branchconfig brandings -BRK Browsable -bsearch Bspace bstr BTNFACE -buf bufferout buffersize buflen -bugfix buildtransitive -BUILDURI burriter BValue -byref -bytearray bytebuffer cac cacafire -callee +CALLCONV capslock CARETBLINKINGENABLED CARRIAGERETURN cascadia -cassert -castsi catid cazamor CBash -cbegin cbiex CBN CBoolean cbt cbuffer CCCBB -ccf cch CCHAR cci @@ -206,17 +174,10 @@ CComp CConsole CConversion CCRT -cctype -CDATA cdd -cdecl -CDeclaration CEdit CELLSIZE -cend -cerr cfae -Cfg cfie cfiex cfte @@ -226,61 +187,47 @@ chafa changelist chaof charinfo -charset CHARSETINFO -chcp -checkbox -checkboxes chh chk -chrono CHT Cic -cjk -CLA -Clcompile +cielab +Cielab CLE cleartype CLICKACTIVE clickdown -climits clipbrd CLIPCHILDREN CLIPSIBLINGS -clocale closetest cloudconsole cls CLSCTX -clsid +clsids CLUSTERMAP -cmath cmatrix cmder CMDEXT -Cmdlet -cmdline cmh CMOUSEBUTTONS -cmp -cmpeq cmt cmw cmyk CNL +cnn cnt CNTRL -codebase Codeflow codepage codepath -codepoint +codepoints coinit -COLLECTIONURI colorizing COLORMATRIX -colorref -colorscheme +COLORREFs +colorschemes colorspaces colorspec colortable @@ -289,33 +236,28 @@ colortest colortool COLR combaseapi -combobox comctl COMDAT commandline commctrl commdlg COMMITID -compat componentization conapi conareainfo conattrs conbufferout -concat concfg conclnt +concretizations conddkrefs condrv conechokey conemu -config configurability conhost -conhostv conime conimeinfo -conint conintegrity conintegrityuwp coninteractivitybase @@ -343,102 +285,72 @@ consoleinternal Consoleroot CONSOLESETFOREGROUND consoletaeftemplates -CONSOLEV +consoleuwp Consolewait CONSOLEWINDOWOWNER consrv -constexpr constexprable constness contentfiles conterm -CONTEXTMENU contsf contypes convarea conwinuserrefs -coord coordnew COPYCOLOR -copymode CORESYSTEM cotaskmem -countof -cout CPG cpinfo CPINFOEX CPLINFO cplusplus -cpp CPPCORECHECK cppcorecheckrules -cpprest cpprestsdk cppwinrt CProc cpx -crbegin CREATESCREENBUFFER CREATESTRUCT CREATESTRUCTW cred -cref -crend -Crisman +crisman CRLFs crloew Crt CRTLIBS csbi csbiex -csharp CSHORT -CSIDL Cspace -csproj -Csr csrmsg CSRSS csrutil -cstdarg -cstddef -cstdio -cstdlib -cstr -cstring -cstyle -csv +CSTYLE CSwitch CTerminal CText -ctime ctl ctlseqs -Ctlv -ctor CTRLEVENT CTRLFREQUENCY CTRLKEYSHORTCUTS CTRLVOLUME -Ctx Ctxt -ctype CUF cupxy -curated CURRENTFONT currentmode CURRENTPAGE CURSORCOLOR CURSORSIZE CURSORTYPE +CUsers CUU Cwa cwch -cwchar -cwctype -cwd CXFRAME CXFULLSCREEN CXHSCROLL @@ -448,14 +360,11 @@ CXSIZE CXSMICON CXVIRTUALSCREEN CXVSCROLL -cxx CYFRAME CYFULLSCREEN -cygwin CYHSCROLL CYMIN CYPADDEDBORDER -CYRL CYSIZE CYSIZEFRAME CYSMICON @@ -463,11 +372,8 @@ CYVIRTUALSCREEN CYVSCROLL dai DATABLOCK -DATAVIEW -DATAW DBatch dbcs -DBCSCHAR DBCSFONT dbg DBGALL @@ -477,7 +383,6 @@ DBGOUTPUT dbh dblclk DBlob -DBUILD DColor DCOLORVALUE dcommon @@ -494,38 +399,55 @@ debugtype DECAC DECALN DECANM +DECARM DECAUPSS DECAWM +DECBI DECBKM +DECCARA +DECCIR DECCKM +DECCKSR DECCOLM DECCRA DECCTR +DECDC DECDHL decdld +DECDMAC DECDWL +DECECM DECEKBD +DECERA +DECFI +DECFRA +DECIC DECID +DECINVM DECKPAM DECKPM DECKPNM DECLRMM -decls -declspec -decltype -declval +DECMSR DECNKM DECNRCM DECOM +decommit DECPCTERM DECPS +DECRARA DECRC DECREQTPARM DECRLM +DECRPM +DECRQCRA DECRQM +DECRQPSR DECRQSS DECRQTSR +DECRSPS decrst +DECSACE DECSASD DECSC DECSCA @@ -534,18 +456,19 @@ DECSCPP DECSCUSR DECSED DECSEL +DECSERA DECSET DECSLPP DECSLRM DECSMKR DECSR DECSTBM +DECSTGLT DECSTR DECSWL +DECTABSR DECTCEM -Dedupe -deduplicate -deduplicated +DECXCPR DEFAPP DEFAULTBACKGROUND DEFAULTFOREGROUND @@ -560,38 +483,21 @@ DEFFACE defing DEFPUSHBUTTON defterm -deiconify DELAYLOAD -deletable DELETEONRELEASE -delims Delt -demoable depersist deprioritized -deps -deque -deref -deserialization -deserialize -deserialized -deserializer -deserializing +deserializers desktopwindowxamlsource -dest DESTINATIONNAME devicecode -Devops Dext DFactory DFF -DFMT -dhandler dialogbox -DINLINE directio DIRECTX -Dirs DISABLEDELAYEDEXPANSION DISABLENOSCROLL DISPLAYATTRIBUTE @@ -600,27 +506,20 @@ DISPLAYCHANGE distro dlg DLGC -dll -dllexport DLLGETVERSIONPROC -dllimport dllinit dllmain DLLVERSIONINFO DLOAD DLOOK -dmp -DOCTYPE DONTCARE doskey dotnet -doubleclick -downlevel DPG -dpi DPIAPI DPICHANGE DPICHANGED +DPIs dpix dpiy dpnx @@ -628,8 +527,6 @@ DRAWFRAME DRAWITEM DRAWITEMSTRUCT drcs -dropdown -DROPDOWNLIST DROPFILES drv DSBCAPS @@ -640,21 +537,16 @@ DSBVOLUME dsm dsound DSSCL -dst DSwap DTest -dtor DTTERM -DUMMYUNIONNAME -DUNICODE -DUNIT dup'ed dvi dwl DWLP dwm dwmapi -dword +DWORDs dwrite dxgi dxgidwm @@ -663,7 +555,6 @@ dxinterop dxsm dxttbmp Dyreen -eaf EASTEUROPE ECH echokey @@ -676,59 +567,42 @@ EDITTEXT EDITUPDATE edputil Efast +efghijklmn EHsc EINS EJO ELEMENTNOTAVAILABLE elems -elif -elseif emacs EMPTYBOX enabledelayedexpansion -endian -endif -endl -endlocal endptr endregion -ENQ -enqueuing ENTIREBUFFER -entrypoint +entrypoints ENU -enum ENUMLOGFONT ENUMLOGFONTEX enumranges -envvar -eol +EOK eplace EPres EQU ERASEBKGND -errorlevel -ETB -etcoreapp ETW -ETX EUDC EVENTID eventing -everytime evflags evt -ewdelete -exe execd -executables executionengine exemain EXETYPE +exeuwp exewin exitwin expectedinput -expr EXPUNGECOMMANDHISTORY EXSTYLE EXTENDEDEDITKEY @@ -737,43 +611,32 @@ EXTTEXTOUT facename FACENODE FACESIZE -failfast FAILIFTHERE -fallthrough fastlink fcharset -fclose -fcntl -fdc -FDD +FDEA fdw +FECF +FEEF fesb +FFAF FFDE FFrom fgbg FGCOLOR -fgetc -fgetwc FGHIJ fgidx FGs FILEDESCRIPTION -fileno -filepath FILESUBTYPE FILESYSPATH -filesystem -FILETYPE -fileurl FILEW FILLATTR FILLCONSOLEOUTPUT FILTERONPASTE -finalizer FINDCASE FINDDLG FINDDOWN -FINDSTR FINDSTRINGEXACT FINDUP FIter @@ -784,7 +647,6 @@ flyout fmodern fmtarg fmtid -FNV FOLDERID FONTCHANGE fontdlg @@ -793,8 +655,7 @@ FONTENUMPROC FONTFACE FONTFAMILY FONTHEIGHT -FONTINFO -fontlist +fontinfo FONTOK FONTSIZE FONTSTRING @@ -804,27 +665,20 @@ FONTWEIGHT FONTWIDTH FONTWINDOW fooo -forceinline FORCEOFFFEEDBACK FORCEONFEEDBACK -FORCEV -foreach -fprintf framebuffer FRAMECHANGED fre -freopen -frontend +frontends fsanitize Fscreen FSCTL FSINFOCLASS -fstream fte Ftm -fullscreen -fullwidth -func +Fullscreens +Fullwidth FUNCTIONCALL fuzzer fuzzmain @@ -836,10 +690,10 @@ fwlink GAUSSIAN gci gcx -gcy gdi gdip gdirenderer +Geddy geopol GETALIAS GETALIASES @@ -849,7 +703,6 @@ GETALIASEXESLENGTH GETAUTOHIDEBAREX GETCARETWIDTH getch -getchar GETCLIENTAREAANIMATION GETCOMMANDHISTORY GETCOMMANDHISTORYLENGTH @@ -871,9 +724,9 @@ GETHUNGAPPTIMEOUT GETICON GETITEMDATA GETKEYBOARDLAYOUTNAME +GETKEYSTATE GETLARGESTWINDOWSIZE GETLBTEXT -getline GETMINMAXINFO GETMOUSEINFO GETMOUSEVANISH @@ -883,8 +736,6 @@ GETOBJECT GETPOS GETSELECTIONINFO getset -GETSTATE -GETTEXT GETTEXTLEN GETTITLE GETWAITTOKILLSERVICETIMEOUT @@ -892,33 +743,24 @@ GETWAITTOKILLTIMEOUT GETWHEELSCROLLCHARACTERS GETWHEELSCROLLCHARS GETWHEELSCROLLLINES -getwriter GFEh Gfun gfx GGI GHIJK GHIJKL -GHIJKLM gitfilters -github -gitlab +gitmodules gle GLOBALFOCUS -globals GLYPHENTRY -gmail GMEM GNUC Goldmine gonce -Google goutput -GPUs -grayscale GREENSCROLL Grehan -grep Greyscale gridline groupbox @@ -927,7 +769,6 @@ gsl GTP GTR guc -gui guidatom GValue GWL @@ -937,8 +778,6 @@ HABCDEF Hackathon HALTCOND HANGEUL -hardcoded -hardcodes hashalg HASSTRINGS hbitmap @@ -953,7 +792,6 @@ hdr HDROP hdrstop HEIGHTSCROLL -hfile hfont hfontresource hglobal @@ -961,12 +799,9 @@ hhh hhook hhx HIBYTE -HICON +hicon HIDEWINDOW -HIGHLIGHTTEXT hinst -HINSTANCE -Hirots HISTORYBUFS HISTORYNODUP HISTORYSIZE @@ -975,35 +810,30 @@ HIWORD HKCU hkey hkl -HKLM hlocal -HLS hlsl -HMENU +HMB +HMK hmod hmodule hmon -HMONITOR -horiz +homoglyph HORZ hostable hostlib -HOTFIX HPA +hpcon HPCON hpj -hpp HPR HProvider HREDRAW hresult hrottled -HRSRC hscroll hsl hstr hstring -hsv HTBOTTOMLEFT HTBOTTOMRIGHT HTCAPTION @@ -1011,7 +841,6 @@ HTCLIENT HTLEFT HTMAXBUTTON HTMINBUTTON -html HTMLTo HTRIGHT HTTOP @@ -1022,37 +851,17 @@ HVP hwheel hwnd HWNDPARENT -hxx -IAccessibility -IAction -IApi -IApplication -IBase -ICache -icacls iccex -IChar icket -ico -IComponent ICONERROR Iconified ICONINFORMATION IConsole ICONSTOP -IControl ICONWARNING -ICore -IData IDCANCEL IDD -IDefault -IDesktop -IDevice -IDictionary IDISHWND -IDispatch -IDisposable idl idllib IDOK @@ -1060,35 +869,18 @@ IDR idth idx IDXGI -IDynamic IEnd IEnum -IEnumerable IFACEMETHODIMP -ifdef ification -ifndef -IFont -ifstream IGNOREEND IGNORELANGUAGE -IHigh IHosted iid -IInitialize -IInput -IInspectable -IInteract -IInteractivity IIo -IList -imagemagick -Imatch ime Imm -IMouse IMPEXP -impl inbox inclusivity INCONTEXT @@ -1096,97 +888,57 @@ INFOEX inheritcursor inheritdoc inheritfrom -ini INITCOMMONCONTROLSEX INITDIALOG initguid INITMENU inkscape -inl INLINEPREFIX inlines -INotify -inout -inplace inproc Inputkeyinfo INPUTPROCESSORPROFILE inputrc Inputreadhandledata INSERTMODE -intellisense INTERACTIVITYBASE INTERCEPTCOPYPASTE INTERNALNAME -interop -interoperability -inthread +Interner intsafe INVALIDARG INVALIDATERECT -IObservable ioctl -iomanip -iostream -iot ipch -ipconfig -IPersist ipp IProperty IPSINK ipsp -IRaw -IRead -IReference -IRender -IScheme -ISelection IShell -IState -IStoryboard -isupper ISwap -iswdigit -iswspace -ISystem iterm itermcolors ITerminal -IText itf Ith -itoa IUI -IUia IUnknown ivalid -IValue -IVector -IWait -IWeb -IWin -IWindow -IXaml -IXMP +IWIC IXP jconcpp JOBOBJECT JOBOBJECTINFOCLASS jpe -jpeg -jpg JPN -json -jsonc jsoncpp +Jsons jsprovider jumplist KAttrs kawa Kazu kazum -kbd kcub kcud kcuf @@ -1194,13 +946,10 @@ kcuu kernelbase kernelbasestaging KEYBDINPUT -keybinding keychord keydown -keyevent KEYFIRST KEYLAST -keymap Keymapping keyscan keystate @@ -1213,29 +962,28 @@ KLF KLMNO KLMNOPQRST KLMNOPQRSTQQQQQ +KLMNOPQRSTUVWXY +KLMNOPQRSTY KOK KPRIORITY KVM langid LANGUAGELIST lasterror -lastexitcode -LATN +LASTEXITCODE LAYOUTRTL +lbl LBN -LBound LBUTTON LBUTTONDBLCLK LBUTTONDOWN LBUTTONUP lcb +lci LCONTROL LCTRL lcx LEFTALIGN -LEFTSHIFT -len -lhs libpopcnt libsancov libtickit @@ -1245,56 +993,42 @@ LINESELECTION LINEWRAP LINKERRCAP LINKERROR -linkpath linputfile -Linq -linux -listbox -listproperties listptr listptrsize lld llx LMENU -LMNOP lnk lnkd lnkfile LNM LOADONCALL -loadu LOBYTE localappdata -localhost locsrc Loewen LOGFONT LOGFONTA LOGFONTW logissue -lowercased +losslessly loword lparam -LPBYTE LPCCH lpch -LPCOLORREF LPCPLINFO LPCREATESTRUCT lpcs -LPCSTR LPCTSTR -LPCWSTR lpdata LPDBLIST lpdis LPDRAWITEMSTRUCT lpdw -LPDWORD lpelfe lpfn LPFNADDPROPSHEETPAGE -LPINT lpl LPMEASUREITEMSTRUCT LPMINMAXINFO @@ -1304,18 +1038,15 @@ LPNEWCPLINFOA LPNEWCPLINFOW LPNMHDR lpntme -LPPOINT LPPROC LPPROPSHEETPAGE LPPSHNOTIFY lprc -LPRECT lpstr lpsz LPTSTR LPTTFONTLIST lpv -LPVOID LPW LPWCH lpwfx @@ -1323,7 +1054,6 @@ LPWINDOWPOS lpwpos lpwstr LRESULT -lru lsb lsconfig lss @@ -1332,8 +1062,6 @@ lstrcmp lstrcmpi LTEXT LTLTLTLTL -ltrim -ltype LUID luma lval @@ -1342,7 +1070,6 @@ LVERTICAL LWA LWIN lwkmvj -mailto majorly makeappx MAKEINTRESOURCE @@ -1351,12 +1078,11 @@ MAKELANGID MAKELONG MAKELPARAM MAKELRESULT -malloc MAPBITMAP +MAPVIRTUALKEY MAPVK MAXDIMENSTRING maxing -MAXLENGTH MAXSHORT maxval maxversiontested @@ -1372,87 +1098,66 @@ MDs MEASUREITEM megamix memallocator -memcmp -memcpy -memmove -memset +meme MENUCHAR MENUCONTROL MENUDROPALIGNMENT -MENUITEM MENUITEMINFO MENUSELECT -Mersenne messageext -metadata metaproj midl mii MIIM milli -mimetype mincore mindbogglingly -mingw -minimizeall minkernel MINMAXINFO minwin minwindef Mip -mkdir MMBB mmcc MMCPL +MMIX mmsystem MNC MNOPQ MNOPQR +MNOPQRSTUVWXY MODALFRAME MODERNCORE MONITORINFO MONITORINFOEXW MONITORINFOF -monospaced -monostate MOUSEACTIVATE MOUSEFIRST MOUSEHWHEEL MOUSEMOVE -mousewheel -movemask MOVESTART msb -msbuild msctf msctls msdata -msdn msft MSGCMDLINEF MSGF MSGFILTER MSGFLG MSGMARKMODE -MSGS MSGSCROLLMODE MSGSELECTMODE msiexec MSIL msix msrc -msvcrt MSVCRTD msys MTSM -mui -Mul -multiline munged munges murmurhash -mutex -mutexes muxes myapplet mydir @@ -1461,12 +1166,8 @@ Mypair Myval NAMELENGTH nameof -namespace -namespaced namestream -nano natvis -nbsp NCCALCSIZE NCCREATE NCLBUTTONDOWN @@ -1478,9 +1179,7 @@ NCRBUTTONDOWN NCRBUTTONUP NCXBUTTONDOWN NCXBUTTONUP -NDEBUG NEL -NEQ netcoreapp netstandard NEWCPLINFO @@ -1495,21 +1194,16 @@ NEWTEXTMETRICEX Newtonsoft NEXTLINE nfe -Nls NLSMODE nnn NOACTIVATE NOAPPLYNOW NOCLIP -NOCOLOR NOCOMM NOCONTEXTHELP NOCOPYBITS -nodiscard NODUP -noexcept -NOHELP -noinline +noexcepts NOINTEGRALHEIGHT NOINTERFACE NOLINKINFO @@ -1525,13 +1219,12 @@ NONINFRINGEMENT NONPREROTATED nonspace NOOWNERZORDER +Nop NOPAINT -NOPQRST noprofile NOREDRAW NOREMOVE NOREPOSITION -noreturn NORMALDISPLAY NOSCRATCH NOSEARCH @@ -1540,23 +1233,17 @@ NOSENDCHANGING NOSIZE NOSNAPSHOT NOTHOUSANDS -nothrow NOTICKS +NOTIMEOUTIFNOTHUNG NOTIMPL -notin -NOTNULL NOTOPMOST NOTRACK NOTSUPPORTED nouicompat nounihan NOUPDATE -novtable -nowait NOYIELD NOZORDER -NPM -npos nrcs NSTATUS ntapi @@ -1568,41 +1255,36 @@ ntdll ntifs ntlpcapi ntm +nto ntrtl ntstatus -ntsubauth NTSYSCALLAPI nttree nturtl ntuser NTVDM ntverp -NTWIN -nuget nugetversions nullability nullness nullonfailure -nullopt -nullptr -NULs +nullopts numlock numpad NUMSCROLL nupkg -nuspec NVIDIA -NVR OACR -oauth objbase ocolor odl -oem oemcp OEMFONT OEMFORMAT +OEMs offboarded +oklab +Oklab OLEAUT OLECHAR onecore @@ -1615,39 +1297,26 @@ onecoreuuid ONECOREWINDOWS onehalf oneseq -ONLCR +OOM openbash opencode opencon openconsole openconsoleproxy -OPENIF -OPENLINK openps openvt ORIGINALFILENAME osc -OSCBG -OSCCT -OSCFG -OSCRCC -OSCSCB -OSCSCC -OSCWT OSDEPENDSROOT OSG OSGENG osign oss -ostream -ostringstream +otepad ouicompat OUnter outdir -outfile -Outof OUTOFCONTEXT -OUTOFMEMORY Outptr outstr OVERLAPPEDWINDOW @@ -1663,15 +1332,12 @@ PAINTPARAMS PAINTSTRUCT PALPC pankaj -params parentable parms passthrough PATCOPY pathcch PATTERNID -PBOOL -PBYTE pcat pcb pcch @@ -1680,12 +1346,9 @@ PCCONSOLE PCD pcg pch -PCHAR PCIDLIST PCIS -PCLIENT PCLONG -PCOBJECT pcon PCONSOLE PCONSOLEENDTASK @@ -1697,17 +1360,14 @@ pcshell PCSHORT PCSR PCSTR -PCUNICODE PCWCH PCWCHAR PCWSTR pda -pdb -pdbonly +Pdbs pdbstr pdtobj pdw -PDWORD pdx peb PEMAGIC @@ -1726,12 +1386,11 @@ pgdn PGONu pguid pgup -PHANDLE phhook phwnd -pid pidl PIDLIST +pids pii pinvoke pipename @@ -1740,36 +1399,24 @@ pixelheight PIXELSLIST PJOBOBJECT pkey -placeholders platforming playsound -plist ploc -PLOC ploca -PLOCA plocm -PLOCM PLOGICAL -plugin -PMv -png pnm PNMLINK pntm -PNTSTATUS POBJECT Podcast POINTSLIST POLYTEXTW -popd -POPF poppack -popup POPUPATTR +popups PORFLG positionals -posix POSTCHARBREAKS POSX POSXSCROLL @@ -1778,9 +1425,7 @@ PPEB ppf ppguid ppidl -pplx PPROC -PPROCESS ppropvar ppsi ppsl @@ -1789,18 +1434,12 @@ ppsz ppv ppwch PQRST -pragma prc prealigned -prebuilt -precomp prect prefast -prefilled +preflighting prefs -preinstalled -PRELOAD -PREMULTIPLIED prepopulated presorted PREVENTPINNING @@ -1809,14 +1448,10 @@ PREVIEWWINDOW PREVLINE prg pri -printf prioritization processenv processhost PROCESSINFOCLASS -procs -Progman -proj PROPERTYID PROPERTYKEY PROPERTYVAL @@ -1830,20 +1465,16 @@ propvar propvariant propvarutil psa -psd PSECURITY -pseudocode pseudoconsole pseudoterminal psh pshn PSHNOTIFY -PSHORT pshpack PSINGLE psl psldl -psm PSNRET PSobject psp @@ -1852,47 +1483,34 @@ psr PSTR psz ptch -ptr -ptrdiff +ptrs ptsz PTYIn PUCHAR -PULONG -PUNICODE -pushd -putchar -putwchar -PVOID pwch -PWCHAR PWDDMCONSOLECONTEXT -PWORD pws -pwsh pwstr pwsz pythonw +Qaabbcc qos -QRSTU -qsort -queryable QUERYOPEN QUESTIONMARK quickedit QUZ QWER +Qxxxxxxxxxxxxxxx qzmp RAII RALT rasterbar rasterfont rasterization -rawinput RAWPATH raytracers razzlerc rbar -rbegin RBUTTON RBUTTONDBLCLK RBUTTONDOWN @@ -1908,38 +1526,27 @@ RCOCW RCONTROL RCOW rcv -rdbuf readback READCONSOLE READCONSOLEOUTPUT READCONSOLEOUTPUTSTRING -Readline -readme READMODE -readonly -READWRITE -realloc +reallocs reamapping rects redef redefinable Redir -redirector redist REDSCROLL -refactor -refactoring REFCLSID -refcount REFGUID REFIID REFPROPERTYKEY -regex REGISTEROS REGISTERVDM regkey REGSTR -reingest RELBINPATH remoting renamer @@ -1951,22 +1558,20 @@ replatformed Replymessage repositorypath Requiresx +rerasterize rescap Resequence RESETCONTENT resheader -resizable resmimetype resw resx -retval rfa rfid rftp -rgb -rgba RGBCOLOR rgbi +rgbs rgci rgfae rgfte @@ -1978,30 +1583,24 @@ rgrc rgs rgui rgw -rgwch -rhs RIGHTALIGN RIGHTBUTTON riid Rike RIPMSG RIS -RMENU -rng roadmap robomac -roundtrip +rosetta +roundtrips RRF RRRGGGBB rsas rtcore RTEXT -rtf RTFTo -Rtl RTLREADING Rtn -rtrim RTTI ruleset runas @@ -2009,19 +1608,18 @@ RUNDLL runformat runft RUNFULLSCREEN +runfuzz runsettings -runtests +runtest runtimeclass runuia runut runxamlformat -rvalue RVERTICAL rvpa RWIN rxvt safearray -SAFECAST safemath sapi sba @@ -2039,12 +1637,10 @@ SCRBUFSIZE screenbuffer SCREENBUFFERINFO screeninfo -screenshot +screenshots scriptload -Scrollable scrollback -scrollbar -Scroller +scrollbars SCROLLFORWARD SCROLLINFO scrolllock @@ -2054,18 +1650,14 @@ SCROLLSCREENBUFFER scursor sddl sdeleted -sdk SDKDDK -searchbox securityappcontainer segfault SELCHANGE SELECTALL -selectany SELECTEDFONT SELECTSTRING Selfhosters -SERIALIZERS SERVERDLL SETACTIVE SETBUDDYINT @@ -2076,7 +1668,6 @@ SETCURSOR SETCURSORINFO SETCURSORPOSITION SETDISPLAYMODE -setfill SETFOCUS SETFONT SETFOREGROUND @@ -2087,30 +1678,24 @@ setintegritylevel SETITEMDATA SETITEMHEIGHT SETKEYSHORTCUTS -setlocal -setlocale SETMENUCLOSE -setmode SETNUMBEROFCOMMANDS SETOS SETPALETTE -SETPOS SETRANGE SETSCREENBUFFERSIZE SETSEL SETTEXTATTRIBUTE SETTINGCHANGE -SETTITLE -setw Setwindow SETWINDOWINFO SFGAO SFGAOF sfi SFINAE +SFolder SFUI sgr -SHANDLE SHCo shcore shellapi @@ -2118,7 +1703,6 @@ shellex shellscalingapi SHFILEINFO SHGFI -SHGFP SHIFTJIS Shl shlguid @@ -2133,7 +1717,6 @@ SHOWNA SHOWNOACTIVATE SHOWNORMAL SHOWWINDOW -SHRT sidebyside SIF SIGDN @@ -2142,7 +1725,6 @@ SINGLETHREADED siup sixel SIZEBOX -sizeof SIZESCROLL SKIPFONT SKIPOWNPROCESS @@ -2161,28 +1743,22 @@ SND SOLIDBOX Solutiondir somefile -SOURCEBRANCH sourced spammy -spand -sprintf -srand -src SRCCODEPAGE SRCCOPY SRCINVERT srcsrv SRCSRVTRG srctool -sre srect +srgb +Srgb srv srvinit srvpipe ssa -ssh -sstream -standalone +startdir STARTF STARTUPINFO STARTUPINFOEX @@ -2192,66 +1768,45 @@ STARTWPARMS STARTWPARMSA STARTWPARMSW Statusline +stb stdafx STDAPI stdc -stdcall stdcpp -stderr -stdexcept -stdin -STDIO STDMETHODCALLTYPE STDMETHODIMP -stdout STGM stl -stoi -stol -stoul -stoutapot Stri -strikethrough -stringstream +Stringable STRINGTABLE -strlen strrev strsafe -strtok -structs STUBHEAD STUVWX -STX stylecop SUA subcompartment -subfolder +subfolders subkey SUBLANG -submenu subresource -subspan -substr subsystemconsole subsystemwindows -suiteless -svg swapchain swapchainpanel swappable SWMR SWP -swprintf SYMED -symlink SYNCPAINT -sys syscalls SYSCHAR SYSCOMMAND SYSDEADCHAR SYSKEYDOWN SYSKEYUP +SYSLIB SYSLINK SYSMENU sysparams @@ -2265,8 +1820,6 @@ TARG targetentrypoint TARGETLIBS TARGETNAME -targetnametoken -targetsize targetver taskbar tbar @@ -2278,47 +1831,37 @@ TBM tchar TCHFORMAT TCI -tcome tcommandline tcommands +Tdd TDelegated TDP -TEAMPROJECT tearoff Teb +Techo tellp -telnet -telnetd -templated teraflop terminalcore +terminalinput +terminalrenderdata TERMINALSCROLLING terminfo TEs -testapp -testbuildplatform testcon testd -testdlls testenv testlab testlist testmd -testmode testname -testnameprefix TESTNULL testpass testpasses -testtestabc -testtesttesttesttest -testtimeout TEXCOORD texel TExpected textattribute TEXTATTRIBUTEID -textbox textboxes textbuffer TEXTINCLUDE @@ -2326,15 +1869,14 @@ textinfo TEXTMETRIC TEXTMETRICW textmode +texttests TFCAT tfoo TFunction tga -threadpool THUMBPOSITION THUMBTRACK TIcon -tif tilunittests titlebar TITLEISLINKNAME @@ -2342,40 +1884,33 @@ TJson TLambda TLDP TLEN -Tlgdata TMAE TMPF TMult tmultiple -tmux -todo +TODOs tofrom tokenhelpers -tokenized -tokenizing -toolbar +toolbars TOOLINFO -tooltip TOOLWINDOW TOPDOWNDIB TOPLEFT -toplevel TOPRIGHT TOpt tosign touchpad -towlower -towupper Tpp Tpqrst tprivapi +tput tracelog tracelogging traceloggingprovider +traceviewpp trackbar TRACKCOMPOSITION trackpad -transcoder transitioning Trd TREX @@ -2384,99 +1919,76 @@ triaging TRIANGLESTRIP Tribool TRIMZEROHEADINGS -truetype trx tsattrs tsf +tsgr +tsm TStr TSTRFORMAT TSub TTBITMAP -ttf TTFONT TTFONTLIST tthe tthis TTM TTo -TVPP +tvpp Txtev typechecked -typechecking -typedef -typeid -typeinfo typelib -typename -typeof typeparam TYUI UAC uap uapadmin UAX -ubuntu +UBool ucd uch -UCHAR -ucs +UChars udk UDM uer +UError uget uia UIACCESS uiacore uiautomationcore -Uid uielem UIELEMENTENABLEDONLY -uint -uintptr +UINTs ulcch -ulong umul umulh Unadvise unattend -uncomment UNCPRIORITY -undef -Unescape unexpand -Unfocus unhighlighting unhosted -unicode -UNICODESTRING UNICODETEXT UNICRT -uninit uninitialize -uninstall Unintense Uniscribe -unittest unittesting +unittests unk unknwn -unmark UNORM unparseable -unpause unregistering -untests untextured untimes UPDATEDISPLAY UPDOWN UPKEY UPSS -upvote -uri -url -urlencoded -USASCII +uregex +URegular usebackq USECALLBACK USECOLOR @@ -2490,41 +2002,33 @@ USEPOSITION userbase USERDATA userdpiapi -username Userp userprivapi -userprofile USERSRV USESHOWWINDOW USESIZE USESTDHANDLES -ushort usp USRDLL -utf -utils +utext +UText +UTEXT utr -uuid -uuidof -uuidv -UVWX UVWXY +UVWXYZ uwa uwp uxtheme -vals Vanara vararg vclib -Vcount vcpkg vcprintf vcxitems -vcxproj vec +vectorize vectorized VERCTRL -versioning VERTBAR VFT vga @@ -2533,24 +2037,22 @@ viewkind viewports Virt VIRTTERM -Virtualizing vkey +VKKEYSCAN VMs VPA -VPATH VPR VProc VRaw VREDRAW vsc +vsconfig vscprintf VSCROLL vsdevshell vsinfo -vsnprintf vso vspath -vsprintf VSTAMP vstest VSTS @@ -2570,58 +2072,43 @@ VTRGBTo vtseq vtterm vttest -VWX -waaay waitable WANSUNG WANTARROWS WANTTAB wapproj -wav WAVEFORMATEX wbuilder wch -wchar +wchars WCIA WCIW -WClass -wcout -wcschr -wcscmp -wcscpy WCSHELPER wcsicmp -wcslen wcsnicmp -wcsrchr wcsrev -wcstod -wcstoul wddm wddmcon WDDMCONSOLECONTEXT wdm webpage -website -websocket +websites wekyb -WEOF wex wextest wextestclass WFill wfopen WHelper -whitelisting +wic WIDTHSCROLL Widthx -Wiki -Wikipedia wil WImpl WINAPI winbase winbasep +wincodec wincon winconp winconpty @@ -2631,14 +2118,11 @@ wincontypes WINCORE windbg WINDEF -WINDIR windll WINDOWALPHA -Windowbuffer windowdpiapi WINDOWEDGE windowext -WINDOWFRAME windowime WINDOWINFO windowio @@ -2652,9 +2136,9 @@ windowrect windowsapp windowsinternalstring WINDOWSIZE +windowsshell windowsterminal windowsx -WINDOWTEXT windowtheme WINDOWTITLE winevent @@ -2684,6 +2168,7 @@ wnd WNDALLOC WNDCLASS WNDCLASSEX +WNDCLASSEXW WNDCLASSW Wndproc WNegative @@ -2691,9 +2176,6 @@ WNull wnwb workarea workaround -workflow -WORKITEM -wostream WOutside WOWARM WOWx @@ -2703,12 +2185,10 @@ wpf WPR WPrep WPresent -wprintf wprp wprpi wregex writeback -writechar WRITECONSOLE WRITECONSOLEINPUT WRITECONSOLEOUTPUT @@ -2720,11 +2200,8 @@ WRunoff WScript wsl WSLENV -wsmatch -wss wstr -wstring -wstringstream +wstrings wsz wtd WTest @@ -2745,9 +2222,8 @@ wyhash wymix wyr xact -xaml Xamlmeta -xargs +xamls xaz xbf xbutton @@ -2771,19 +2247,12 @@ xinxinchaof XManifest XMath XMFLOAT -xml -xmlns -xor xorg -XPosition XResource -xsd xsi -xsize xstyler XSubstantial xtended -xterm XTest XTPOPSGR XTPUSHSGR @@ -2791,24 +2260,24 @@ xtr XTWINOPS xunit xutr -xvalue XVIRTUALSCREEN XWalk yact -YAML YCast YCENTER YCount YDPI -yml +YLimit YOffset -YPosition -YSize YSubstantial YVIRTUALSCREEN YWalk +Zab +zabcd +Zabcdefghijklmn Zabcdefghijklmnopqrstuvwxyz ZCmd ZCtrl -zsh zxcvbnm +ZYXWVU +ZYXWVUTd diff --git a/.github/actions/spelling/expect/web.txt b/.github/actions/spelling/expect/web.txt index 47ce4058d2..8f5b59ac73 100644 --- a/.github/actions/spelling/expect/web.txt +++ b/.github/actions/spelling/expect/web.txt @@ -1,8 +1,7 @@ -http -www WCAG winui appshellintegration mdtauk gfycat Guake +xkcd diff --git a/.github/actions/spelling/line_forbidden.patterns b/.github/actions/spelling/line_forbidden.patterns index 0120c775a6..31ad2ddcd2 100644 --- a/.github/actions/spelling/line_forbidden.patterns +++ b/.github/actions/spelling/line_forbidden.patterns @@ -1,3 +1,11 @@ +# reject `m_data` as there's a certain OS which has evil defines that break things if it's used elsewhere +# \bm_data\b + +# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test, +# you might not want to check in code where you were debugging w/ `fit()`, in which case, you might want +# to use this: +#\bfit\( + # s.b. GitHub \bGithub\b @@ -16,6 +24,12 @@ # s.b. greater than \bgreater then\b +# s.b. into +#\sin to\s + +# s.b. opt-in +\sopt in\s + # s.b. less than \bless then\b @@ -27,10 +41,22 @@ \b[Nn]o[nt][- ]existent\b # s.b. preexisting -[Pp]re-existing +[Pp]re[- ]existing + +# s.b. preempt +[Pp]re[- ]empt\b # s.b. preemptively -[Pp]re-emptively +[Pp]re[- ]emptively + +# s.b. reentrancy +[Rr]e[- ]entrancy + +# s.b. reentrant +[Rr]e[- ]entrant + +# s.b. workaround(s) +#\bwork[- ]arounds?\b # Reject duplicate words \s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s diff --git a/.github/actions/spelling/patterns/0_t.txt b/.github/actions/spelling/patterns/0_t.txt index 0e44139a32..519c4ec067 100644 --- a/.github/actions/spelling/patterns/0_t.txt +++ b/.github/actions/spelling/patterns/0_t.txt @@ -10,4 +10,4 @@ \\tests(?![a-z]) \\thread(?![a-z]) \\tools(?![a-z]) -\\types(?![a-z]) +\\types?(?![a-z]) diff --git a/.github/actions/spelling/patterns/patterns.txt b/.github/actions/spelling/patterns/patterns.txt index 7360e920f7..6e4a39485d 100644 --- a/.github/actions/spelling/patterns/patterns.txt +++ b/.github/actions/spelling/patterns/patterns.txt @@ -27,13 +27,74 @@ ROY\sG\.\sBIV # Python stringprefix / binaryprefix \b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)' +# SSE intrinsics like "_mm_subs_epu16" +\b_mm(?:|256|512)_\w+\b + +# ARM NEON intrinsics like "vsubq_u16" +\bv\w+_[fsu](?:8|16|32|64)\b + +# Automatically suggested patterns +# hit-count: 3831 file-count: 582 +# IServiceProvider +\bI(?=(?:[A-Z][a-z]{2,})+\b) + +# hit-count: 71 file-count: 35 +# Compiler flags +(?:^|[\t ,"'`=(])-[D](?=[A-Z]{2,}|[A-Z][a-z]) +(?:^|[\t ,"'`=(])-[X](?!aml)(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) + +# hit-count: 41 file-count: 28 +# version suffix v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) + +# hit-count: 20 file-count: 9 +# hex runs +\b[0-9a-fA-F]{16,}\b + +# hit-count: 10 file-count: 7 +# uuid: +\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b + +# hit-count: 4 file-count: 4 +# mailto urls +mailto:[-a-zA-Z=;:/?%&0-9+@.]{3,} + +# hit-count: 4 file-count: 1 +# ANSI color codes +(?:\\(?:u00|x)1b|\x1b)\[\d+(?:;\d+|)m + +# hit-count: 2 file-count: 1 +# latex +\\(?:n(?:ew|ormal|osub)|r(?:enew)|t(?:able(?:of|)|he|itle))(?=[a-z]+) + +# hit-count: 1 file-count: 1 +# hex digits including css/html color classes: +(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b + +# hit-count: 1 file-count: 1 +# Non-English +[a-zA-Z]*[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]* + +# hit-count: 1 file-count: 1 +# French +# This corpus only had capital letters, but you probably want lowercase ones as well. +\b[LN]'+[a-z]{2,}\b + # acceptable duplicates # ls directory listings [-bcdlpsw](?:[-r][-w][-sx]){3}\s+\d+\s+(\S+)\s+\g{-1}\s+\d+\s+ # C/idl types + English ... \s(Guid|long|LONG|that) \g{-1}\s + # javadoc / .net -(?:\@(?:groupname|param)|(?:public|private)(?:\s+static|\s+readonly)*)\s+(\w+)\s+\g{-1}\s +(?:[\\@](?:groupname|param)|(?:public|private)(?:\s+static|\s+readonly)*)\s+(\w+)\s+\g{-1}\s + +# Commit message -- Signed-off-by and friends +^\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\s*$ + +# Autogenerated revert commit message +^This reverts commit [0-9a-f]{40}\.$ + # vtmode --vtmode\s+(\w+)\s+\g{-1}\s diff --git a/.github/actions/spelling/reject.txt b/.github/actions/spelling/reject.txt index 4d43bc53ee..301719de47 100644 --- a/.github/actions/spelling/reject.txt +++ b/.github/actions/spelling/reject.txt @@ -1,31 +1,12 @@ -benefitting -occurences? -Sorce ^attache$ ^attacher$ ^attachers$ +benefitting +occurences? ^dependan.* ^oer$ -^spae$ -^spae-man$ -^spaebook$ -^spaecraft$ -^spaed$ -^spaedom$ -^spaeing$ -^spaeings$ -^spaeman$ -^spaer$ -^Spaerobee$ -^spaes$ -^spaewife$ -^spaewoman$ -^spaework$ -^spaewright$ +Sorce +^[Ss]pae.* ^untill$ ^untilling$ -^wether$ ^wether.* -^wethers$ -^wetherteg$ -^[Ss]pae.* diff --git a/.github/fabricbot.json b/.github/fabricbot.json deleted file mode 100644 index 500878b868..0000000000 --- a/.github/fabricbot.json +++ /dev/null @@ -1,3156 +0,0 @@ -{ - "version": "1.0", - "tasks": [ - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "taskName": "Add needs triage label to new issue", - "conditions": { - "operator": "or", - "operands": [ - { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "opened" - } - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "⛺ Reserved" - } - } - ] - } - ] - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Triage" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ] - }, - "id": "aZ_SNTI7W" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "config": { - "taskName": "Replace needs author feedback label with needs attention label when the author comments on an issue", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "created" - } - }, - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Attention" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issue_comment" - ] - }, - "id": "_X5OkVx4JU" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label from issues", - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ] - }, - "id": "dER5Fu3ekK" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label when an issue is commented on", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issue_comment" - ] - }, - "id": "cyoTuRwP9P" - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Close stale issues", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 3 - } - } - ], - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - }, - "id": "oc-LHz1wag" - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Add no recent activity label to issues", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 4 - } - }, - { - "name": "noLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "No-Recent-Activity" - } - }, - { - "name": "addReply", - "parameters": { - "comment": "This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**." - } - } - ] - }, - "id": "t0Eu4GMbGx" - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Close duplicate issues", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 0, - 3, - 6, - 9, - 12, - 15, - 18, - 21 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 1 - } - } - ], - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes." - } - }, - { - "name": "closeIssue", - "parameters": {} - } - ] - }, - "id": "CSDw0s8LGP" - }, - { - "taskType": "trigger", - "capabilityId": "CodeFlowLink", - "subCapability": "CodeFlowLink", - "version": "1.0", - "config": { - "taskName": "Add a CodeFlow link to new pull requests" - }, - "id": "w1aPb06aR", - "disabled": true - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Add needs author feedback label to pull requests when changes are requested", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "submitted" - } - }, - { - "name": "isReviewState", - "parameters": { - "state": "changes_requested" - } - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - }, - "id": "NooCyW2RPs" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author responds to a pull request", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "operator": "not", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ] - }, - "id": "YSz7PmirbZ" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestCommentResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author comments on a pull request", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "issue_comment" - ] - }, - "id": "JRxI_S1cjy" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author responds to a pull request review comment", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - }, - "id": "T8n8me4yLo" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label from pull requests", - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ] - }, - "id": "nAs8e47Tod" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestCommentResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label when a pull request is commented on", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "issue_comment" - ] - }, - "id": "BwFyZ6jhbC" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label when a pull request is reviewed", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - }, - "id": "JV-Wq2rwx2" - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Close stale pull requests", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isPr", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "No-Recent-Activity" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - }, - "id": "Y6XW51lcUD" - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Add no recent activity label to pull requests", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 1, - 4, - 7, - 10, - 13, - 16, - 19, - 22 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isPr", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - }, - { - "name": "noLabel", - "parameters": { - "label": "No-Recent-Activity" - } - } - ], - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "No-Recent-Activity" - } - }, - { - "name": "addReply", - "parameters": { - "comment": "This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **7 days**. It will be closed if no further activity occurs **within 7 days of this comment**." - } - } - ] - }, - "id": "I6gc1et842" - }, - { - "taskType": "trigger", - "capabilityId": "AutoMerge", - "subCapability": "AutoMerge", - "version": "1.0", - "config": { - "taskName": "Automatically merge pull requests", - "label": "AutoMerge", - "silentMode": false, - "minMinutesOpen": 480, - "mergeType": "squash", - "deleteBranches": true, - "removeLabelOnPush": true, - "allowAutoMergeInstructionsWithoutLabel": true, - "enforceDMPAsStatus": true, - "usePrDescriptionAsCommitMessage": true - }, - "id": "Q0_X6ws7SR" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "tv-BontEW", - "config": { - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "or", - "operands": [ - { - "operator": "and", - "operands": [ - { - "name": "isLabeled", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Mass-Chaos" - } - } - ] - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "isLabeled", - "parameters": {} - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Accessibility" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Build" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Extensibility" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Fonts" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Input" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Interaction" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Interop" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Output" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Performance" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Rendering" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Server" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Settings" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-TerminalConnection" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-TerminalControl" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-User Interface" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-VT" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-CodeHealth" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Quality" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-AzureShell" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Schema" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Commandline" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-ShellExtension" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-WPFControl" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Settings UI" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-DefApp" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Remoting" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Windowing" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Theming" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Localization" - } - } - ] - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Bug" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Docs" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Feature" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Question" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Samples" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Task" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Scenario" - } - } - ] - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Cmd.exe" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Colortool" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Conhost" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Conpty" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Meta" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Powershell" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Terminal" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-WSL" - } - } - ] - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isOpen", - "parameters": {} - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Answered" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-By-Design" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-External" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fix-Available" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fix-Committed" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Won't-Fix" - } - } - ] - } - ] - } - ] - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Triage" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "⛺ Reserved" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Tracking-External" - } - } - ] - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - } - ], - "taskName": "Mark issues that need tagging fixed" - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "L78SZN8PE", - "config": { - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "and", - "operands": [ - { - "name": "isLabeled", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - } - ] - }, - { - "operator": "and", - "operands": [ - { - "operator": "or", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Area-Accessibility" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Build" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Extensibility" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Fonts" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Input" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Interaction" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Interop" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Output" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Performance" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Rendering" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Server" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Settings" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-TerminalConnection" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-TerminalControl" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-User Interface" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-VT" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-CodeHealth" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Quality" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Schema" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-AzureShell" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Commandline" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-ShellExtension" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-WPFControl" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Settings UI" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-DefApp" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Localization" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Windowing" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-Theming" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-AtlasEngine" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Area-CmdPal" - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Bug" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Docs" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Feature" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Question" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Samples" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Task" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Issue-Scenario" - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Product-Cmd.exe" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Colortool" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Conhost" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Conpty" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Meta" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Powershell" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-Terminal" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Product-WSL" - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isOpen", - "parameters": {} - } - ] - }, - { - "operator": "or", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Answered" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-By-Design" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-External" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fix-Available" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fix-Committed" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Won't-Fix" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Tracking-External" - } - } - ] - } - ] - } - ] - } - ] - } - ] - }, - "taskName": "Remove tag fix label from issues with fixed tags", - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "InPrLabel", - "subCapability": "InPrLabel", - "version": "1.0", - "id": "SRYfNSWer", - "config": { - "fixedLabelEnabled": true, - "taskName": "Label PRs and mark fix committed automatically", - "label_inPr": "In-PR", - "label_fixed": "Resolution-Fix-Committed" - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "17D1zI_5Q", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "remove tag fix from duplicates", - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "-6xYBV_-e", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "or", - "operands": [ - { - "name": "titleContains", - "parameters": { - "isRegex": true, - "titlePattern": "^\\s*Bug Report \\(IF I DO NOT CHANGE THIS THE ISSUE WILL BE AUTO-CLOSED\\)\\s*$" - } - }, - { - "name": "titleContains", - "parameters": { - "titlePattern": "^\\s*Bug Report\\s*$", - "isRegex": true - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "opened" - } - }, - { - "name": "isAction", - "parameters": { - "action": "reopened" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "write" - } - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "admin" - } - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "Close issues with no title fix", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "addReply", - "parameters": { - "comment": "Hi! Thanks for attempting to open an issue. Unfortunately, your title wasn't changed from the original template which makes it very hard for us to track and triage. You are welcome to fix up the title and try again with a new issue." - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "_Mpyf9AhA", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "or", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "opened" - } - }, - { - "name": "isAction", - "parameters": { - "action": "reopened" - } - } - ] - }, - { - "operator": "or", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "bodyContains", - "parameters": { - "bodyPattern": ".+", - "isRegex": true - } - } - ] - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "Close issues with nothing in the body", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "addReply", - "parameters": { - "comment": "Hi! Thanks for attempting to open an issue. Unfortunately, you didn't write anything in the body which makes it impossible to understand your concern. You are welcome to fix up the issue and try again by opening another issue with the body filled out. " - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "id": "__Wpe__Ye", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isLabeled", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Second" - } - }, - { - "name": "isOpen", - "parameters": {} - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ], - "taskName": "add team when a second is needed", - "actions": [ - { - "name": "requestReviewer", - "parameters": { - "reviewer": "zadjii-msft" - } - }, - { - "name": "requestReviewer", - "parameters": { - "reviewer": "PankajBhojwani" - } - }, - { - "name": "requestReviewer", - "parameters": { - "reviewer": "carlos-zamora" - } - }, - { - "name": "requestReviewer", - "parameters": { - "reviewer": "dhowett" - } - }, - { - "name": "requestReviewer", - "parameters": { - "reviewer": "lhecker" - } - } - ] - }, - "disabled": false - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "id": "XutI7oBsy", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isOpen", - "parameters": {} - } - ] - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Second" - } - } - ] - }, - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ], - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Second" - } - } - ], - "taskName": "Remove Needs-Second from closed PRs." - } - }, - { - "taskType": "trigger", - "capabilityId": "ReleaseAnnouncement", - "subCapability": "ReleaseAnnouncement", - "version": "1.0", - "config": { - "taskName": "Announce a fix has been released", - "prReply": ":tada:`${pkgName} ${version}` has been released which incorporates this pull request.:tada:\n\nHandy links:\n* [Release Notes](https://github.com/${owner}/${repo}/releases/tag/${version})\n* [Store Download](https://www.microsoft.com/store/apps/9n8g5rfz9xk3?cid=storebadge&ocid=badge)\n", - "issueReply": ":tada:This issue was addressed in #${prNumber}, which has now been successfully released as `${pkgName} ${version}`.:tada:\n\nHandy links:\n* [Release Notes](https://github.com/${owner}/${repo}/releases/tag/${version})\n* [Store Download](https://www.microsoft.com/store/apps/9n8g5rfz9xk3?cid=storebadge&ocid=badge)", - "referencedPrsRegex": "\\#([0-9]+)\\s*\\)" - }, - "id": "jERDgOu8l" - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "8rtmfy0pc", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isLabeled", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Feedback-Hub" - } - }, - { - "name": "", - "parameters": {} - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "Ask for Feedback Hub link", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "Hi there!

Can you please send us feedback with the Feedback Hub with this issue and paste the link here so we can more easily find your crash information on the back end?

Thanks!

![image](https://user-images.githubusercontent.com/18221333/62478757-b69d0d00-b760-11e9-9626-1fa33c91e7c5.png) ![image](https://user-images.githubusercontent.com/18221333/62478649-6de55400-b760-11e9-806e-5aab7e085a9f.png)" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Feedback-Hub" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - }, - "disabled": true - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "id": "1DaxcVAAB", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "In-PR" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Help Wanted" - } - }, - { - "name": "isLabeled", - "parameters": {} - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "Pull the Help-Wanted tag if something goes into PR", - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Help-Wanted" - } - } - ], - "dangerZone": { - "respondToBotActions": false, - "acceptRespondToBotActions": true - } - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "id": "NtTsTnnPU", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "commentContains", - "parameters": { - "bodyPattern": "/dup", - "isRegex": true, - "commentPattern": "\\/dup(licate|e)?(\\s+of)?\\s+#[\\d]+" - } - }, - { - "operator": "or", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "admin" - } - }, - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "write" - } - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issue_comment" - ], - "taskName": "Helper to mark as duplicate", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!" - } - }, - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Triage" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Tag-Fix" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Attention" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Repro" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Second" - } - } - ] - }, - "disabled": false - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "id": "P_CqIudDp", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "commentContains", - "parameters": { - "commentPattern": "\\/feedback", - "isRegex": true - } - }, - { - "operator": "or", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "admin" - } - }, - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "write" - } - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issue_comment" - ], - "taskName": "Ask for Feedback Hub link via comment trigger", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "\nHi there!

Can you please send us feedback with the [Feedback Hub](https://support.microsoft.com/en-us/windows/send-feedback-to-microsoft-with-the-feedback-hub-app-f59187f8-8739-22d6-ba93-f66612949332) with this issue? Make sure to click the \"Start recording\" button, then reproduce the issue before submitting the feedback. Once it's submitted, paste the link here so we can more easily find your crash information on the back end?

Thanks!

![image](https://user-images.githubusercontent.com/18356694/140811502-a068f78b-89d2-4587-925a-73e19652b830.png)

![image](https://user-images.githubusercontent.com/18356694/140811557-cdc22a0f-fa6a-4f6a-953e-73b51f5548a3.png)

![image](https://user-images.githubusercontent.com/18221333/62478649-6de55400-b760-11e9-806e-5aab7e085a9f.png)" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "EmailCleanser", - "subCapability": "EmailCleanser", - "version": "1.0", - "id": "yt42fShpQ", - "config": { - "taskName": "Scrub-a-dub-dub email replies" - } - }, - { - "taskType": "trigger", - "capabilityId": "LabelSync", - "subCapability": "LabelSync", - "version": "1.0", - "id": "PoWEf56Be", - "config": { - "taskName": "Syncy dinky doo", - "labelPatterns": [ - { - "pattern": "Issue-" - }, - { - "pattern": "Area-" - }, - { - "pattern": "Priority-" - }, - { - "pattern": "Product-" - }, - { - "pattern": "Severity-" - }, - { - "pattern": "Impact-" - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "id": "y6DOsjvqw", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "commentContains", - "parameters": { - "commentPattern": "\\/dup(licate|e)?(\\s+of)?\\s+https", - "isRegex": true - } - }, - { - "operator": "or", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "admin" - } - }, - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "write" - } - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issue_comment" - ], - "taskName": "Helper to mark as EXTERNAL duplicate", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "Hi! We've identified this issue as a duplicate of one that exists on somebody else's Issue Tracker. Please make sure you subscribe to the referenced external issue for future updates. Thanks for your report!" - } - }, - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Triage" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Resolution-External" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Attention" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Bisect" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Repro" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Second" - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "id": "XENeaGGwPr8hJK0FgfzMo", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "commentContains", - "parameters": { - "commentPattern": "/?" - } - }, - { - "operator": "or", - "operands": [ - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "admin" - } - }, - { - "name": "activitySenderHasPermissions", - "parameters": { - "permissions": "write" - } - } - ] - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issue_comment" - ], - "taskName": "Auto add feedback label", - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Attention" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author-Feedback" - } - } - ] - } - } - ], - "userGroups": [] -} diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 0000000000..695df7162d --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,751 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isIssue + - isOpen + - hasLabel: + label: Needs-Author-Feedback + - hasLabel: + label: No-Recent-Activity + - noActivitySince: + days: 3 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isIssue + - isOpen + - hasLabel: + label: Needs-Author-Feedback + - noActivitySince: + days: 4 + - isNotLabeledWith: + label: No-Recent-Activity + actions: + - addLabel: + label: No-Recent-Activity + - addReply: + reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isIssue + - isOpen + - hasLabel: + label: Resolution-Duplicate + - noActivitySince: + days: 1 + actions: + - addReply: + reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. + - closeIssue + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isPullRequest + - isOpen + - hasLabel: + label: Needs-Author-Feedback + - hasLabel: + label: No-Recent-Activity + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isPullRequest + - isOpen + - hasLabel: + label: Needs-Author-Feedback + - noActivitySince: + days: 7 + - isNotLabeledWith: + label: No-Recent-Activity + actions: + - addLabel: + label: No-Recent-Activity + - addReply: + reply: This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **7 days**. It will be closed if no further activity occurs **within 7 days of this comment**. + eventResponderTasks: + - if: + - payloadType: Issues + - or: + - and: + - isAction: + action: Opened + - not: + hasLabel: + label: ⛺ Reserved + then: + - addLabel: + label: Needs-Triage + description: + - if: + - payloadType: Issue_Comment + - isAction: + action: Created + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author-Feedback + then: + - addLabel: + label: Needs-Attention + - removeLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Issues + - not: + isAction: + action: Closed + - hasLabel: + label: No-Recent-Activity + then: + - removeLabel: + label: No-Recent-Activity + description: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: No-Recent-Activity + then: + - removeLabel: + label: No-Recent-Activity + description: + - if: + - payloadType: Pull_Request_Review + - isAction: + action: Submitted + - isReviewState: + reviewState: Changes_requested + then: + - addLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Pull_Request + - isActivitySender: + issueAuthor: True + - not: + isAction: + action: Closed + - hasLabel: + label: Needs-Author-Feedback + then: + - removeLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Issue_Comment + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author-Feedback + then: + - removeLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Pull_Request_Review + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author-Feedback + then: + - removeLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Pull_Request + - not: + isAction: + action: Closed + - hasLabel: + label: No-Recent-Activity + then: + - removeLabel: + label: No-Recent-Activity + description: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: No-Recent-Activity + then: + - removeLabel: + label: No-Recent-Activity + description: + - if: + - payloadType: Pull_Request_Review + - hasLabel: + label: No-Recent-Activity + then: + - removeLabel: + label: No-Recent-Activity + description: + - if: + - payloadType: Pull_Request + - hasLabel: + label: AutoMerge + then: + - enableAutoMerge: + mergeMethod: Squash + description: + - if: + - payloadType: Pull_Request + - labelRemoved: + label: AutoMerge + then: + - disableAutoMerge + description: + - if: + - payloadType: Issues + - or: + - and: + - isLabeled + - hasLabel: + label: Mass-Chaos + - isOpen + - isLabeled + - not: + hasLabel: + label: Needs-Tag-Fix + - or: + - and: + - not: + hasLabel: + label: Area-Accessibility + - not: + hasLabel: + label: Area-Build + - not: + hasLabel: + label: Area-Extensibility + - not: + hasLabel: + label: Area-Fonts + - not: + hasLabel: + label: Area-Input + - not: + hasLabel: + label: Area-Interaction + - not: + hasLabel: + label: Area-Interop + - not: + hasLabel: + label: Area-Output + - not: + hasLabel: + label: Area-Performance + - not: + hasLabel: + label: Area-Rendering + - not: + hasLabel: + label: Area-Server + - not: + hasLabel: + label: Area-Settings + - not: + hasLabel: + label: Area-TerminalConnection + - not: + hasLabel: + label: Area-TerminalControl + - not: + hasLabel: + label: Area-User Interface + - not: + hasLabel: + label: Area-VT + - not: + hasLabel: + label: Area-CodeHealth + - not: + hasLabel: + label: Area-Quality + - not: + hasLabel: + label: Area-AzureShell + - not: + hasLabel: + label: Area-Schema + - not: + hasLabel: + label: Area-Commandline + - not: + hasLabel: + label: Area-ShellExtension + - not: + hasLabel: + label: Area-WPFControl + - not: + hasLabel: + label: Area-Settings UI + - not: + hasLabel: + label: Area-DefApp + - not: + hasLabel: + label: Area-Remoting + - not: + hasLabel: + label: Area-Windowing + - not: + hasLabel: + label: Area-Theming + - not: + hasLabel: + label: Area-Localization + - and: + - not: + hasLabel: + label: Issue-Bug + - not: + hasLabel: + label: Issue-Docs + - not: + hasLabel: + label: Issue-Feature + - not: + hasLabel: + label: Issue-Question + - not: + hasLabel: + label: Issue-Samples + - not: + hasLabel: + label: Issue-Task + - not: + hasLabel: + label: Issue-Scenario + - and: + - not: + hasLabel: + label: Product-Cmd.exe + - not: + hasLabel: + label: Product-Colortool + - not: + hasLabel: + label: Product-Conhost + - not: + hasLabel: + label: Product-Conpty + - not: + hasLabel: + label: Product-Meta + - not: + hasLabel: + label: Product-Powershell + - not: + hasLabel: + label: Product-Terminal + - not: + hasLabel: + label: Product-WSL + - and: + - not: isOpen + - and: + - not: + hasLabel: + label: Resolution-Answered + - not: + hasLabel: + label: Resolution-By-Design + - not: + hasLabel: + label: Resolution-Duplicate + - not: + hasLabel: + label: Resolution-External + - not: + hasLabel: + label: Resolution-Fix-Available + - not: + hasLabel: + label: Resolution-Fix-Committed + - not: + hasLabel: + label: Resolution-Won't-Fix + - not: + hasLabel: + label: Needs-Triage + - not: + hasLabel: + label: Resolution-Duplicate + - not: + hasLabel: + label: ⛺ Reserved + - not: + hasLabel: + label: Tracking-External + then: + - addLabel: + label: Needs-Tag-Fix + description: + - if: + - payloadType: Issues + - and: + - isLabeled + - hasLabel: + label: Needs-Tag-Fix + - and: + - or: + - hasLabel: + label: Area-Accessibility + - hasLabel: + label: Area-Build + - hasLabel: + label: Area-Extensibility + - hasLabel: + label: Area-Fonts + - hasLabel: + label: Area-Input + - hasLabel: + label: Area-Interaction + - hasLabel: + label: Area-Interop + - hasLabel: + label: Area-Output + - hasLabel: + label: Area-Performance + - hasLabel: + label: Area-Rendering + - hasLabel: + label: Area-Server + - hasLabel: + label: Area-Settings + - hasLabel: + label: Area-TerminalConnection + - hasLabel: + label: Area-TerminalControl + - hasLabel: + label: Area-User Interface + - hasLabel: + label: Area-VT + - hasLabel: + label: Area-CodeHealth + - hasLabel: + label: Area-Quality + - hasLabel: + label: Area-Schema + - hasLabel: + label: Area-AzureShell + - hasLabel: + label: Area-Commandline + - hasLabel: + label: Area-ShellExtension + - hasLabel: + label: Area-WPFControl + - hasLabel: + label: Area-Settings UI + - hasLabel: + label: Area-DefApp + - hasLabel: + label: Area-Localization + - hasLabel: + label: Area-Windowing + - hasLabel: + label: Area-Theming + - hasLabel: + label: Area-AtlasEngine + - hasLabel: + label: Area-CmdPal + - or: + - hasLabel: + label: Issue-Bug + - hasLabel: + label: Issue-Docs + - hasLabel: + label: Issue-Feature + - hasLabel: + label: Issue-Question + - hasLabel: + label: Issue-Samples + - hasLabel: + label: Issue-Task + - hasLabel: + label: Issue-Scenario + - or: + - hasLabel: + label: Product-Cmd.exe + - hasLabel: + label: Product-Colortool + - hasLabel: + label: Product-Conhost + - hasLabel: + label: Product-Conpty + - hasLabel: + label: Product-Meta + - hasLabel: + label: Product-Powershell + - hasLabel: + label: Product-Terminal + - hasLabel: + label: Product-WSL + - or: + - isOpen + - and: + - not: isOpen + - or: + - hasLabel: + label: Resolution-Answered + - hasLabel: + label: Resolution-By-Design + - hasLabel: + label: Resolution-Duplicate + - hasLabel: + label: Resolution-External + - hasLabel: + label: Resolution-Fix-Available + - hasLabel: + label: Resolution-Fix-Committed + - hasLabel: + label: Resolution-Won't-Fix + - hasLabel: + label: Tracking-External + then: + - removeLabel: + label: Needs-Tag-Fix + description: + - if: + - payloadType: Pull_Request + then: + - inPrLabel: + label: In-PR + description: + - if: + - payloadType: Issues + - hasLabel: + label: Needs-Tag-Fix + - hasLabel: + label: Resolution-Duplicate + then: + - removeLabel: + label: Needs-Tag-Fix + description: + - if: + - payloadType: Issues + - or: + - titleContains: + pattern: ^\s*Bug Report \(IF I DO NOT CHANGE THIS THE ISSUE WILL BE AUTO-CLOSED\)\s*$ + isRegex: True + - titleContains: + pattern: ^\s*Bug Report\s*$ + isRegex: True + - or: + - isAction: + action: Opened + - isAction: + action: Reopened + - not: + activitySenderHasPermission: + permission: Write + - not: + activitySenderHasPermission: + permission: Admin + then: + - closeIssue + - addLabel: + label: Needs-Author-Feedback + - addReply: + reply: Hi! Thanks for attempting to open an issue. Unfortunately, your title wasn't changed from the original template which makes it very hard for us to track and triage. You are welcome to fix up the title and try again with a new issue. + description: + - if: + - payloadType: Issues + - or: + - isAction: + action: Opened + - isAction: + action: Reopened + - or: + - not: + bodyContains: + pattern: .+ + isRegex: True + then: + - closeIssue + - addLabel: + label: Needs-Author-Feedback + - addReply: + reply: "Hi! Thanks for attempting to open an issue. Unfortunately, you didn't write anything in the body which makes it impossible to understand your concern. You are welcome to fix up the issue and try again by opening another issue with the body filled out. " + description: + - if: + - payloadType: Pull_Request + - isLabeled + - hasLabel: + label: Needs-Second + - isOpen + then: + - requestReview: + reviewer: zadjii-msft + - requestReview: + reviewer: PankajBhojwani + - requestReview: + reviewer: carlos-zamora + - requestReview: + reviewer: dhowett + - requestReview: + reviewer: lhecker + description: + - if: + - payloadType: Pull_Request_Review + - not: isOpen + - hasLabel: + label: Needs-Second + then: + - removeLabel: + label: Needs-Second + description: + - if: + - payloadType: Issues + - hasLabel: + label: In-PR + - hasLabel: + label: Help Wanted + - isLabeled + then: + - removeLabel: + label: Help-Wanted + description: + - if: + - payloadType: Issue_Comment + - commentContains: + pattern: '\/dup(licate|e)?(\s+of)?\s+\#[\d]+' + isRegex: True + - or: + - activitySenderHasPermission: + permission: Admin + - activitySenderHasPermission: + permission: Write + then: + - addReply: + reply: Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report! + - closeIssue + - removeLabel: + label: Needs-Triage + - addLabel: + label: Resolution-Duplicate + - removeLabel: + label: Needs-Tag-Fix + - removeLabel: + label: Needs-Attention + - removeLabel: + label: Needs-Author-Feedback + - removeLabel: + label: Needs-Repro + - removeLabel: + label: Needs-Second + description: + - if: + - payloadType: Issue_Comment + - commentContains: + pattern: '\/feedback' + isRegex: True + - or: + - activitySenderHasPermission: + permission: Admin + - activitySenderHasPermission: + permission: Write + then: + - addReply: + reply: >2- + + Hi there!

Can you please send us feedback with the [Feedback Hub](https://support.microsoft.com/en-us/windows/send-feedback-to-microsoft-with-the-feedback-hub-app-f59187f8-8739-22d6-ba93-f66612949332) with this issue? Make sure to click the "Start recording" button, then reproduce the issue before submitting the feedback. Once it's submitted, paste the link here so we can more easily find your crash information on the back end?

Thanks!

![image](https://user-images.githubusercontent.com/18356694/140811502-a068f78b-89d2-4587-925a-73e19652b830.png)

![image](https://user-images.githubusercontent.com/18356694/140811557-cdc22a0f-fa6a-4f6a-953e-73b51f5548a3.png)

![image](https://user-images.githubusercontent.com/18221333/62478649-6de55400-b760-11e9-806e-5aab7e085a9f.png) + - addLabel: + label: Needs-Author-Feedback + description: + - if: + - payloadType: Issue_Comment + then: + - cleanEmailReply + description: + - if: + - payloadType: Pull_Request + then: + - labelSync: + pattern: Issue- + - labelSync: + pattern: Area- + - labelSync: + pattern: Priority- + - labelSync: + pattern: Product- + - labelSync: + pattern: Severity- + - labelSync: + pattern: Impact- + description: + - if: + - payloadType: Issue_Comment + - commentContains: + pattern: '\/dup(licate|e)?(\s+of)?\s+https' + isRegex: True + - or: + - activitySenderHasPermission: + permission: Admin + - activitySenderHasPermission: + permission: Write + then: + - addReply: + reply: Hi! We've identified this issue as a duplicate of one that exists on somebody else's Issue Tracker. Please make sure you subscribe to the referenced external issue for future updates. Thanks for your report! + - closeIssue + - removeLabel: + label: Needs-Triage + - addLabel: + label: Resolution-External + - removeLabel: + label: Needs-Attention + - removeLabel: + label: Needs-Author-Feedback + - removeLabel: + label: Needs-Bisect + - removeLabel: + label: Needs-Repro + - removeLabel: + label: Needs-Second + description: + - if: + - payloadType: Issue_Comment + - commentContains: + pattern: /? + isRegex: False + - or: + - activitySenderHasPermission: + permission: Admin + - activitySenderHasPermission: + permission: Write + then: + - removeLabel: + label: Needs-Attention + - addLabel: + label: Needs-Author-Feedback + description: +onFailure: +onSuccess: diff --git a/.github/workflows/addToProject.yml b/.github/workflows/addToProject.yml index 286cacb7bf..3fbb2a7b2c 100644 --- a/.github/workflows/addToProject.yml +++ b/.github/workflows/addToProject.yml @@ -7,6 +7,7 @@ on: - labeled - unlabeled +permissions: {} jobs: add-to-project: name: Add issue to project diff --git a/.github/workflows/spelling2.yml b/.github/workflows/spelling2.yml index d8369ca0cd..446b24343e 100644 --- a/.github/workflows/spelling2.yml +++ b/.github/workflows/spelling2.yml @@ -1,10 +1,57 @@ # spelling.yml is blocked per https://github.com/check-spelling/check-spelling/security/advisories/GHSA-g86g-chm8-7r2p name: Spell checking + +# Comment management is handled through a secondary job, for details see: +# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions +# +# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment +# (in odd cases, it might actually run just to collapse a commment, but that's fairly rare) +# it needs `contents: write` in order to add a comment. +# +# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment +# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment) +# it needs `pull-requests: write` in order to manipulate those comments. + +# Updating pull request branches is managed via comment handling. +# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list +# +# These elements work together to make it happen: +# +# `on.issue_comment` +# This event listens to comments by users asking to update the metadata. +# +# `jobs.update` +# This job runs in response to an issue_comment and will push a new commit +# to update the spelling metadata. +# +# `with.experimental_apply_changes_via_bot` +# Tells the action to support and generate messages that enable it +# to make a commit to update the spelling metadata. +# +# `with.ssh_key` +# In order to trigger workflows when the commit is made, you can provide a +# secret (typically, a write-enabled github deploy key). +# +# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key + on: - pull_request_target: push: - branches: ["**"] - tags-ignore: ["**"] + branches: + - "**" + tags-ignore: + - "**" + pull_request_target: + branches: + - "**" + tags-ignore: + - "**" + types: + - 'opened' + - 'reopened' + - 'synchronize' + issue_comment: + types: + - 'created' jobs: spelling: @@ -24,23 +71,64 @@ jobs: steps: - name: check-spelling id: spelling - uses: check-spelling/check-spelling@v0.0.20 + uses: check-spelling/check-spelling@v0.0.21 with: suppress_push_for_open_pull_request: 1 checkout: true + check_file_names: 1 + spell_check_this: check-spelling/spell-check-this@prerelease post_comment: 0 + use_magic_file: 1 + extra_dictionary_limit: 10 + extra_dictionaries: + cspell:software-terms/src/software-terms.txt + cspell:python/src/python/python-lib.txt + cspell:node/node.txt + cspell:cpp/src/stdlib-c.txt + cspell:cpp/src/stdlib-cpp.txt + cspell:fullstack/fullstack.txt + cspell:filetypes/filetypes.txt + cspell:html/html.txt + cspell:cpp/src/compiler-msvc.txt + cspell:python/src/common/extra.txt + cspell:powershell/powershell.txt + cspell:aws/aws.txt + cspell:cpp/src/lang-keywords.txt + cspell:npm/npm.txt + cspell:dotnet/dotnet.txt + cspell:python/src/python/python.txt + cspell:css/css.txt + cspell:cpp/src/stdlib-cmath.txt + check_extra_dictionaries: '' - comment: - name: Report + comment-push: + name: Report (Push) + # If your workflow isn't running on push, you can remove this job runs-on: ubuntu-latest needs: spelling permissions: contents: write - pull-requests: write - if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name != 'push' + if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push' steps: - name: comment - uses: check-spelling/check-spelling@v0.0.20 + uses: check-spelling/check-spelling@v0.0.21 with: checkout: true + spell_check_this: check-spelling/spell-check-this@prerelease + task: ${{ needs.spelling.outputs.followup }} + + comment-pr: + name: Report (PR) + # If you workflow isn't running on pull_request*, you can remove this job + runs-on: ubuntu-latest + needs: spelling + permissions: + pull-requests: write + if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request') + steps: + - name: comment + uses: check-spelling/check-spelling@v0.0.21 + with: + checkout: true + spell_check_this: check-spelling/spell-check-this@prerelease task: ${{ needs.spelling.outputs.followup }} diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 0000000000..63464ea4fe --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,24 @@ +name: Publish to Winget + +on: + release: + types: [published] + +env: + REGEX: 'Microsoft\.WindowsTerminal(?:Preview)?_([\d.]+)_8wekyb3d8bbwe\.msixbundle$' + +jobs: + publish: + runs-on: windows-latest # Action can only run on Windows + steps: + - name: Publish Windows Terminal ${{ github.event.release.prerelease && 'Preview' || 'Stable' }} + run: | + $assets = '${{ toJSON(github.event.release.assets) }}' | ConvertFrom-Json + $wingetRelevantAsset = $assets | Where-Object { $_.name -like '*.msixbundle' } | Select-Object -First 1 + $regex = [Regex]::New($env:REGEX) + $version = $regex.Match($wingetRelevantAsset.name).Groups[1].Value + + $wingetPackage = "Microsoft.WindowsTerminal${{ github.event.release.prerelease && '.Preview' || '' }}" + + & curl.exe -JLO https://aka.ms/wingetcreate/latest + & .\wingetcreate.exe update $wingetPackage -s -v $version -u $wingetRelevantAsset.browser_download_url -t "${{ secrets.WINGET_TOKEN }}" diff --git a/.vscode/settings.json b/.vscode/settings.json index 604f91797d..e7802ba358 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "C_Cpp.loggingLevel": "None", "files.associations": { "xstring": "cpp", - "*.idl": "cpp", + "*.idl": "midl3", "array": "cpp", "future": "cpp", "istream": "cpp", @@ -103,7 +103,9 @@ "files.exclude": { "**/bin/**": true, "**/obj/**": true, - "**/packages/**": true, "**/Generated Files/**": true + }, + "search.exclude": { + "**/packages/**": true } -} \ No newline at end of file +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3e0a6b0a8..c7f3b1a710 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 ["Easy Starter"](https://github.com/microsoft/terminal/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22+label%3A%22Easy+Starter%22+) issues. 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 your 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. diff --git a/NOTICE.md b/NOTICE.md index e1488fd4a2..fb48315d25 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -276,41 +276,53 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ``` -## ConEmu -**Source**: [https://github.com/Maximus5/ConEmu](https://github.com/Maximus5/ConEmu) +## stb + +**Source**: [https://github.com/nothings/stb](https://github.com/nothings/stb) ### License ``` -BSD 3-Clause License +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` -Copyright (c) 2009-2017, Maximus5 -All rights reserved. +## Oklab +**Source**: [https://bottosson.github.io/posts/oklab/](https://bottosson.github.io/posts/oklab/) -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +### License -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` +Copyright (c) 2020 Björn Ottosson +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ``` # Microsoft Open Source diff --git a/OpenConsole.sln b/OpenConsole.sln index d7cc1264cb..5c2b55ccd4 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -239,8 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{89CDCC EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Types.Unit.Tests", "src\types\ut_types\Types.Unit.Tests.vcxproj", "{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PublicTerminalCore", "src\cascadia\PublicTerminalCore\PublicTerminalCore.vcxproj", "{84848BFA-931D-42CE-9ADF-01EE54DE7890}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfTerminalControl", "src\cascadia\WpfTerminalControl\WpfTerminalControl.csproj", "{376FE273-6B84-4EB5-8B30-8DE9D21B022C}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests_TerminalApp", "src\cascadia\ut_app\TerminalApp.UnitTests.vcxproj", "{CA5CAD1A-9333-4D05-B12A-1905CBF112F9}" @@ -326,12 +324,15 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winconpty.Tests.Feature", "src\winconpty\ft_pty\winconpty.FeatureTests.vcxproj", "{024052DE-83FB-4653-AEA4-90790D29D5BD}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAzBridge", "src\cascadia\TerminalAzBridge\TerminalAzBridge.vcxproj", "{067F0A06-FCB7-472C-96E9-B03B54E8E18D}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} + EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "src\dep\fmt\fmt.vcxproj", "{6BAE5851-50D5-4934-8D5E-30361A8A40F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfTerminalTestNetCore", "src\cascadia\WpfTerminalTestNetCore\WpfTerminalTestNetCore.csproj", "{1588FD7C-241E-4E7D-9113-43735F3E6BAD}" ProjectSection(ProjectDependencies) = postProject - {84848BFA-931D-42CE-9ADF-01EE54DE7890} = {84848BFA-931D-42CE-9ADF-01EE54DE7890} + {CA5CAD1A-F542-4635-A069-7CAEFB930070} = {CA5CAD1A-F542-4635-A069-7CAEFB930070} {A22EC5F6-7851-4B88-AC52-47249D437A52} = {A22EC5F6-7851-4B88-AC52-47249D437A52} EndProjectSection EndProject @@ -413,34 +414,34 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audio", "Audio", "{40BD8415 EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MidiAudio", "src\audio\midi\lib\midi.vcxproj", "{3C67784E-1453-49C2-9660-483E2CC7F7AD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerminalStress", "src\tools\TerminalStress\TerminalStress.csproj", "{613CCB57-5FA9-48EF-80D0-6B1E319E20C4}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenderingTests", "src\tools\RenderingTests\RenderingTests.vcxproj", "{37C995E0-2349-4154-8E77-4A52C0C7F46D}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "benchcat", "src\tools\benchcat\benchcat.vcxproj", "{2C836962-9543-4CE5-B834-D28E1F124B66}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleMonitor", "src\tools\ConsoleMonitor\ConsoleMonitor.vcxproj", "{328729E9-6723-416E-9C98-951F1473BBE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution AuditMode|Any CPU = AuditMode|Any CPU AuditMode|ARM = AuditMode|ARM AuditMode|ARM64 = AuditMode|ARM64 - AuditMode|DotNet_x64Test = AuditMode|DotNet_x64Test - AuditMode|DotNet_x86Test = AuditMode|DotNet_x86Test AuditMode|x64 = AuditMode|x64 AuditMode|x86 = AuditMode|x86 Debug|Any CPU = Debug|Any CPU Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 - Debug|DotNet_x64Test = Debug|DotNet_x64Test - Debug|DotNet_x86Test = Debug|DotNet_x86Test Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Fuzzing|Any CPU = Fuzzing|Any CPU Fuzzing|ARM = Fuzzing|ARM Fuzzing|ARM64 = Fuzzing|ARM64 - Fuzzing|DotNet_x64Test = Fuzzing|DotNet_x64Test - Fuzzing|DotNet_x86Test = Fuzzing|DotNet_x86Test Fuzzing|x64 = Fuzzing|x64 Fuzzing|x86 = Fuzzing|x86 Release|Any CPU = Release|Any CPU Release|ARM = Release|ARM Release|ARM64 = Release|ARM64 - Release|DotNet_x64Test = Release|DotNet_x64Test - Release|DotNet_x86Test = Release|DotNet_x86Test Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection @@ -450,8 +451,6 @@ Global {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM.Build.0 = Debug|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM.Deploy.0 = Debug|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|x86 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x86.ActiveCfg = Release|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|Any CPU.ActiveCfg = Debug|x86 @@ -459,8 +458,6 @@ Global {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.Build.0 = Debug|ARM64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|DotNet_x64Test.ActiveCfg = Debug|x86 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|DotNet_x86Test.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Deploy.0 = Debug|x64 @@ -470,8 +467,6 @@ Global {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|Any CPU.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|ARM.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|DotNet_x64Test.ActiveCfg = Debug|x86 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|x64.ActiveCfg = Debug|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Fuzzing|x86.ActiveCfg = Debug|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|Any CPU.ActiveCfg = Release|x86 @@ -479,8 +474,6 @@ Global {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.Build.0 = Release|ARM64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.Deploy.0 = Release|ARM64 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|DotNet_x64Test.ActiveCfg = Release|x86 - {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|DotNet_x86Test.ActiveCfg = Release|x86 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.Deploy.0 = Release|x64 @@ -490,16 +483,12 @@ Global {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.ActiveCfg = Release|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.ActiveCfg = Release|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|Any CPU.ActiveCfg = Debug|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM.ActiveCfg = Debug|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.Build.0 = Debug|ARM64 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.ActiveCfg = Debug|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Build.0 = Debug|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.ActiveCfg = Debug|Win32 @@ -507,16 +496,12 @@ Global {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|Any CPU.ActiveCfg = Release|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM.ActiveCfg = Release|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.ActiveCfg = Release|ARM64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.Build.0 = Release|ARM64 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.ActiveCfg = Release|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.Build.0 = Release|x64 {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.ActiveCfg = Release|Win32 @@ -524,16 +509,12 @@ Global {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x64.ActiveCfg = Release|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x86.ActiveCfg = Release|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|Any CPU.ActiveCfg = Debug|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|ARM.ActiveCfg = Debug|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|ARM64.ActiveCfg = Debug|ARM64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|ARM64.Build.0 = Debug|ARM64 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x64.ActiveCfg = Debug|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x64.Build.0 = Debug|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x86.ActiveCfg = Debug|Win32 @@ -541,8 +522,6 @@ Global {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|x64.Build.0 = Fuzzing|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -550,8 +529,6 @@ Global {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|ARM.ActiveCfg = Release|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|ARM64.ActiveCfg = Release|ARM64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|ARM64.Build.0 = Release|ARM64 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x64.ActiveCfg = Release|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x64.Build.0 = Release|x64 {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x86.ActiveCfg = Release|Win32 @@ -559,16 +536,12 @@ Global {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x64.ActiveCfg = Release|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x86.ActiveCfg = Release|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|Any CPU.ActiveCfg = Debug|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|ARM.ActiveCfg = Debug|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|ARM64.ActiveCfg = Debug|ARM64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|ARM64.Build.0 = Debug|ARM64 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x64.ActiveCfg = Debug|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x64.Build.0 = Debug|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x86.ActiveCfg = Debug|Win32 @@ -576,8 +549,6 @@ Global {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|x64.Build.0 = Fuzzing|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -585,8 +556,6 @@ Global {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|ARM.ActiveCfg = Release|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|ARM64.ActiveCfg = Release|ARM64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|ARM64.Build.0 = Release|ARM64 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x64.ActiveCfg = Release|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x64.Build.0 = Release|x64 {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x86.ActiveCfg = Release|Win32 @@ -594,8 +563,6 @@ Global {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x64.ActiveCfg = AuditMode|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x64.Build.0 = AuditMode|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -603,10 +570,6 @@ Global {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|ARM.ActiveCfg = Debug|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|ARM64.ActiveCfg = Debug|ARM64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|ARM64.Build.0 = Debug|ARM64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x64.ActiveCfg = Debug|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x64.Build.0 = Debug|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x86.ActiveCfg = Debug|Win32 @@ -614,8 +577,6 @@ Global {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|x64.Build.0 = Fuzzing|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -623,10 +584,6 @@ Global {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|ARM.ActiveCfg = Release|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|ARM64.ActiveCfg = Release|ARM64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|ARM64.Build.0 = Release|ARM64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|DotNet_x64Test.Build.0 = Release|x64 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|DotNet_x86Test.Build.0 = Release|Win32 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x64.ActiveCfg = Release|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x64.Build.0 = Release|x64 {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x86.ActiveCfg = Release|Win32 @@ -634,8 +591,6 @@ Global {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.ActiveCfg = AuditMode|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.Build.0 = AuditMode|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -643,9 +598,6 @@ Global {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM.ActiveCfg = Debug|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.Build.0 = Debug|ARM64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.ActiveCfg = Debug|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.Build.0 = Debug|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.ActiveCfg = Debug|Win32 @@ -653,8 +605,6 @@ Global {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x64.Build.0 = Fuzzing|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -662,9 +612,6 @@ Global {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM.ActiveCfg = Release|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.ActiveCfg = Release|ARM64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.Build.0 = Release|ARM64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.Build.0 = Release|x64 - {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.ActiveCfg = Release|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.Build.0 = Release|x64 {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.ActiveCfg = Release|Win32 @@ -672,8 +619,6 @@ Global {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.ActiveCfg = AuditMode|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.Build.0 = AuditMode|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -681,10 +626,6 @@ Global {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM.ActiveCfg = Debug|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.Build.0 = Debug|ARM64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.ActiveCfg = Debug|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.Build.0 = Debug|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.ActiveCfg = Debug|Win32 @@ -692,8 +633,6 @@ Global {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x64.Build.0 = Fuzzing|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -701,10 +640,6 @@ Global {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM.ActiveCfg = Release|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.ActiveCfg = Release|ARM64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.Build.0 = Release|ARM64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x64Test.Build.0 = Release|x64 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|DotNet_x86Test.Build.0 = Release|Win32 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.ActiveCfg = Release|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.Build.0 = Release|x64 {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.ActiveCfg = Release|Win32 @@ -712,8 +647,6 @@ Global {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x64.ActiveCfg = Release|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x64.Build.0 = Release|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -721,10 +654,6 @@ Global {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|ARM.ActiveCfg = Debug|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|ARM64.ActiveCfg = Debug|ARM64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|ARM64.Build.0 = Debug|ARM64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x64.ActiveCfg = Debug|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x64.Build.0 = Debug|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x86.ActiveCfg = Debug|Win32 @@ -732,8 +661,6 @@ Global {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|x64.Build.0 = Fuzzing|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -741,10 +668,6 @@ Global {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|ARM.ActiveCfg = Release|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|ARM64.ActiveCfg = Release|ARM64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|ARM64.Build.0 = Release|ARM64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|DotNet_x64Test.Build.0 = Release|x64 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|DotNet_x86Test.Build.0 = Release|Win32 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x64.ActiveCfg = Release|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x64.Build.0 = Release|x64 {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x86.ActiveCfg = Release|Win32 @@ -752,16 +675,12 @@ Global {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x64.ActiveCfg = Release|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x86.ActiveCfg = Release|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|Any CPU.ActiveCfg = Debug|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|ARM.ActiveCfg = Debug|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|ARM64.Build.0 = Debug|ARM64 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x64.ActiveCfg = Debug|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x64.Build.0 = Debug|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -769,8 +688,6 @@ Global {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|x64.Build.0 = Fuzzing|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -778,8 +695,6 @@ Global {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|ARM.ActiveCfg = Release|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|ARM64.ActiveCfg = Release|ARM64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|ARM64.Build.0 = Release|ARM64 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x64.ActiveCfg = Release|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x64.Build.0 = Release|x64 {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x86.ActiveCfg = Release|Win32 @@ -787,16 +702,12 @@ Global {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x86.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|Any CPU.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|ARM.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|ARM64.ActiveCfg = Debug|ARM64 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|ARM64.Build.0 = Debug|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x64.ActiveCfg = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x64.Build.0 = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x86.ActiveCfg = Debug|Win32 @@ -804,8 +715,6 @@ Global {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|x64.Build.0 = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -813,8 +722,6 @@ Global {06EC74CB-9A12-429C-B551-8562EC954746}.Release|ARM.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Release|ARM64.ActiveCfg = Release|ARM64 {06EC74CB-9A12-429C-B551-8562EC954746}.Release|ARM64.Build.0 = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954746}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {06EC74CB-9A12-429C-B551-8562EC954746}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x64.Build.0 = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x86.ActiveCfg = Release|Win32 @@ -822,16 +729,12 @@ Global {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x86.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|Any CPU.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|ARM.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|ARM64.ActiveCfg = Debug|ARM64 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|ARM64.Build.0 = Debug|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x64.ActiveCfg = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x64.Build.0 = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x86.ActiveCfg = Debug|Win32 @@ -839,16 +742,12 @@ Global {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|Any CPU.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|ARM.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|ARM64.ActiveCfg = Release|ARM64 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|ARM64.Build.0 = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC954747}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {06EC74CB-9A12-429C-B551-8562EC954747}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x64.Build.0 = Release|x64 {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x86.ActiveCfg = Release|Win32 @@ -856,16 +755,12 @@ Global {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x64.ActiveCfg = Release|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x86.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|Any CPU.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|ARM.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|ARM64.Build.0 = Debug|ARM64 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x64.ActiveCfg = Debug|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x64.Build.0 = Debug|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -873,16 +768,12 @@ Global {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|Any CPU.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|ARM.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|ARM64.ActiveCfg = Release|ARM64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|ARM64.Build.0 = Release|ARM64 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x64.ActiveCfg = Release|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x64.Build.0 = Release|x64 {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x86.ActiveCfg = Release|Win32 @@ -890,16 +781,12 @@ Global {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x64.ActiveCfg = Release|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x86.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|Any CPU.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|ARM.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|ARM64.Build.0 = Debug|ARM64 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x64.ActiveCfg = Debug|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x64.Build.0 = Debug|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -907,16 +794,12 @@ Global {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|Any CPU.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|ARM.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|ARM64.ActiveCfg = Release|ARM64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|ARM64.Build.0 = Release|ARM64 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x64.ActiveCfg = Release|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x64.Build.0 = Release|x64 {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x86.ActiveCfg = Release|Win32 @@ -924,48 +807,36 @@ Global {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x64.ActiveCfg = Release|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x86.ActiveCfg = Release|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|Any CPU.ActiveCfg = Debug|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|ARM.ActiveCfg = Debug|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|ARM64.ActiveCfg = Debug|ARM64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|ARM64.Build.0 = Debug|ARM64 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x64.ActiveCfg = Debug|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x64.Build.0 = Debug|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x86.ActiveCfg = Debug|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|Any CPU.ActiveCfg = Release|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|ARM.ActiveCfg = Release|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|ARM64.ActiveCfg = Release|ARM64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|ARM64.Build.0 = Release|ARM64 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x64.ActiveCfg = Release|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x64.Build.0 = Release|x64 {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x86.ActiveCfg = Release|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x64.ActiveCfg = Release|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x86.ActiveCfg = Release|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|Any CPU.ActiveCfg = Debug|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|ARM.ActiveCfg = Debug|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|ARM64.ActiveCfg = Debug|ARM64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|ARM64.Build.0 = Debug|ARM64 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x64.ActiveCfg = Debug|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x64.Build.0 = Debug|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x86.ActiveCfg = Debug|Win32 @@ -973,16 +844,12 @@ Global {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|Any CPU.ActiveCfg = Release|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|ARM.ActiveCfg = Release|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|ARM64.ActiveCfg = Release|ARM64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|ARM64.Build.0 = Release|ARM64 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x64.ActiveCfg = Release|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x64.Build.0 = Release|x64 {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x86.ActiveCfg = Release|Win32 @@ -990,16 +857,12 @@ Global {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x64.ActiveCfg = Release|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x86.ActiveCfg = Release|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|Any CPU.ActiveCfg = Debug|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|ARM.ActiveCfg = Debug|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|ARM64.ActiveCfg = Debug|ARM64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|ARM64.Build.0 = Debug|ARM64 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x64.ActiveCfg = Debug|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x64.Build.0 = Debug|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1007,16 +870,12 @@ Global {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|Any CPU.ActiveCfg = Release|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|ARM.ActiveCfg = Release|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|ARM64.ActiveCfg = Release|ARM64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|ARM64.Build.0 = Release|ARM64 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x64.ActiveCfg = Release|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x64.Build.0 = Release|x64 {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x86.ActiveCfg = Release|Win32 @@ -1024,16 +883,12 @@ Global {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x64.ActiveCfg = Release|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x86.ActiveCfg = Release|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|Any CPU.ActiveCfg = Debug|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|ARM.ActiveCfg = Debug|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|ARM64.ActiveCfg = Debug|ARM64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|ARM64.Build.0 = Debug|ARM64 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x64.ActiveCfg = Debug|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x64.Build.0 = Debug|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1041,16 +896,12 @@ Global {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|Any CPU.ActiveCfg = Release|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|ARM.ActiveCfg = Release|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|ARM64.ActiveCfg = Release|ARM64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|ARM64.Build.0 = Release|ARM64 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x64.ActiveCfg = Release|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x64.Build.0 = Release|x64 {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x86.ActiveCfg = Release|Win32 @@ -1058,16 +909,12 @@ Global {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x64.ActiveCfg = Release|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x86.ActiveCfg = Release|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|Any CPU.ActiveCfg = Debug|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|ARM.ActiveCfg = Debug|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|ARM64.Build.0 = Debug|ARM64 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x64.ActiveCfg = Debug|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x64.Build.0 = Debug|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1075,16 +922,12 @@ Global {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|Any CPU.ActiveCfg = Release|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|ARM.ActiveCfg = Release|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|ARM64.ActiveCfg = Release|ARM64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|ARM64.Build.0 = Release|ARM64 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x64.ActiveCfg = Release|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x64.Build.0 = Release|x64 {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x86.ActiveCfg = Release|Win32 @@ -1092,16 +935,12 @@ Global {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x64.ActiveCfg = Release|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x86.ActiveCfg = Release|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|Any CPU.ActiveCfg = Debug|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|ARM.ActiveCfg = Debug|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|ARM64.ActiveCfg = Debug|ARM64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|ARM64.Build.0 = Debug|ARM64 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x64.ActiveCfg = Debug|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x64.Build.0 = Debug|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1109,8 +948,6 @@ Global {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|x64.Build.0 = Fuzzing|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1118,8 +955,6 @@ Global {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|ARM.ActiveCfg = Release|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|ARM64.ActiveCfg = Release|ARM64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|ARM64.Build.0 = Release|ARM64 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x64.ActiveCfg = Release|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x64.Build.0 = Release|x64 {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x86.ActiveCfg = Release|Win32 @@ -1127,16 +962,12 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x64.ActiveCfg = Release|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x86.ActiveCfg = Release|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|Any CPU.ActiveCfg = Debug|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|ARM.ActiveCfg = Debug|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|ARM64.ActiveCfg = Debug|ARM64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|ARM64.Build.0 = Debug|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x64.ActiveCfg = Debug|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x64.Build.0 = Debug|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1144,8 +975,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|x64.Build.0 = Fuzzing|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1153,8 +982,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|ARM.ActiveCfg = Release|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|ARM64.ActiveCfg = Release|ARM64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|ARM64.Build.0 = Release|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x64.ActiveCfg = Release|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x64.Build.0 = Release|x64 {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x86.ActiveCfg = Release|Win32 @@ -1163,15 +990,11 @@ Global {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|ARM.ActiveCfg = Release|ARM64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|ARM.Build.0 = Release|ARM64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|ARM64.ActiveCfg = Debug|Win32 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|ARM64 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|ARM64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x64.ActiveCfg = Release|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x86.ActiveCfg = Release|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|Any CPU.ActiveCfg = Debug|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|ARM.ActiveCfg = Debug|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|ARM64.ActiveCfg = Debug|Win32 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x64.ActiveCfg = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x64.Build.0 = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1179,15 +1002,11 @@ Global {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|Any CPU.ActiveCfg = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|ARM.ActiveCfg = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|DotNet_x64Test.ActiveCfg = Debug|x64 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|x64.ActiveCfg = Debug|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Fuzzing|x86.ActiveCfg = Release|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|Any CPU.ActiveCfg = Release|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|ARM.ActiveCfg = Release|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|ARM64.ActiveCfg = Release|ARM64 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x64.ActiveCfg = Release|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x64.Build.0 = Release|x64 {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x86.ActiveCfg = Release|Win32 @@ -1196,15 +1015,11 @@ Global {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|ARM.ActiveCfg = Release|ARM64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|ARM.Build.0 = Release|ARM64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|ARM64.ActiveCfg = Debug|Win32 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|ARM64 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|ARM64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x64.ActiveCfg = Release|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x86.ActiveCfg = Release|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|Any CPU.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|ARM.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|ARM64.ActiveCfg = Debug|Win32 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x64.ActiveCfg = Debug|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x64.Build.0 = Debug|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1212,15 +1027,11 @@ Global {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|Any CPU.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|ARM.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|DotNet_x64Test.ActiveCfg = Debug|Win32 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|x64.ActiveCfg = Debug|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Fuzzing|x86.ActiveCfg = Debug|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|Any CPU.ActiveCfg = Release|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|ARM.ActiveCfg = Release|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|ARM64.ActiveCfg = Release|ARM64 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x64.ActiveCfg = Release|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x64.Build.0 = Release|x64 {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x86.ActiveCfg = Release|Win32 @@ -1228,16 +1039,12 @@ Global {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x64.ActiveCfg = Release|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x86.ActiveCfg = Release|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|Any CPU.ActiveCfg = Debug|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|ARM.ActiveCfg = Debug|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|ARM64.Build.0 = Debug|ARM64 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x64.ActiveCfg = Debug|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x64.Build.0 = Debug|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1245,16 +1052,12 @@ Global {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|Any CPU.ActiveCfg = Release|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|ARM.ActiveCfg = Release|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|ARM64.ActiveCfg = Release|ARM64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|ARM64.Build.0 = Release|ARM64 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x64.ActiveCfg = Release|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x64.Build.0 = Release|x64 {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x86.ActiveCfg = Release|Win32 @@ -1262,16 +1065,12 @@ Global {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x64.ActiveCfg = Release|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x86.ActiveCfg = Release|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|Any CPU.ActiveCfg = Debug|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|ARM.ActiveCfg = Debug|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|ARM64.ActiveCfg = Debug|ARM64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|ARM64.Build.0 = Debug|ARM64 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x64.ActiveCfg = Debug|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x64.Build.0 = Debug|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1279,16 +1078,12 @@ Global {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|Any CPU.ActiveCfg = Release|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|ARM.ActiveCfg = Release|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|ARM64.ActiveCfg = Release|ARM64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|ARM64.Build.0 = Release|ARM64 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x64.ActiveCfg = Release|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x64.Build.0 = Release|x64 {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x86.ActiveCfg = Release|Win32 @@ -1296,16 +1091,12 @@ Global {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x64.ActiveCfg = Release|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x86.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|Any CPU.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|ARM.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|ARM64.ActiveCfg = Debug|ARM64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|ARM64.Build.0 = Debug|ARM64 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x64.ActiveCfg = Debug|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x64.Build.0 = Debug|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1313,16 +1104,12 @@ Global {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|Any CPU.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|ARM.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|ARM64.ActiveCfg = Release|ARM64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|ARM64.Build.0 = Release|ARM64 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x64.ActiveCfg = Release|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x64.Build.0 = Release|x64 {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x86.ActiveCfg = Release|Win32 @@ -1330,16 +1117,12 @@ Global {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x86.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|Any CPU.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|ARM.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|ARM64.ActiveCfg = Debug|ARM64 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|ARM64.Build.0 = Debug|ARM64 - {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x64.ActiveCfg = Debug|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x64.Build.0 = Debug|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1347,8 +1130,6 @@ Global {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|x64.Build.0 = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1356,8 +1137,6 @@ Global {06EC74CB-9A12-429C-B551-8532EC964726}.Release|ARM.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Release|ARM64.ActiveCfg = Release|ARM64 {06EC74CB-9A12-429C-B551-8532EC964726}.Release|ARM64.Build.0 = Release|ARM64 - {06EC74CB-9A12-429C-B551-8532EC964726}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {06EC74CB-9A12-429C-B551-8532EC964726}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x64.Build.0 = Release|x64 {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x86.ActiveCfg = Release|Win32 @@ -1365,16 +1144,12 @@ Global {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x64.ActiveCfg = Release|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x86.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|Any CPU.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|ARM.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|ARM64.ActiveCfg = Debug|ARM64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|ARM64.Build.0 = Debug|ARM64 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x64.ActiveCfg = Debug|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x64.Build.0 = Debug|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1382,8 +1157,6 @@ Global {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|x64.Build.0 = Fuzzing|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1391,8 +1164,6 @@ Global {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|ARM.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|ARM64.ActiveCfg = Release|ARM64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|ARM64.Build.0 = Release|ARM64 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x64.ActiveCfg = Release|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x64.Build.0 = Release|x64 {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x86.ActiveCfg = Release|Win32 @@ -1400,16 +1171,12 @@ Global {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x86.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|Any CPU.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|ARM.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|ARM64.ActiveCfg = Debug|ARM64 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|ARM64.Build.0 = Debug|ARM64 - {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x64.ActiveCfg = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x64.Build.0 = Debug|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1417,8 +1184,6 @@ Global {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|x64.Build.0 = Fuzzing|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1426,8 +1191,6 @@ Global {06EC74CB-9A12-429C-B551-8562EC964846}.Release|ARM.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Release|ARM64.ActiveCfg = Release|ARM64 {06EC74CB-9A12-429C-B551-8562EC964846}.Release|ARM64.Build.0 = Release|ARM64 - {06EC74CB-9A12-429C-B551-8562EC964846}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {06EC74CB-9A12-429C-B551-8562EC964846}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x64.Build.0 = Release|x64 {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x86.ActiveCfg = Release|Win32 @@ -1435,16 +1198,12 @@ Global {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x64.ActiveCfg = Release|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x86.ActiveCfg = Release|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|Any CPU.ActiveCfg = Debug|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|ARM.ActiveCfg = Debug|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|ARM64.Build.0 = Debug|ARM64 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x64.ActiveCfg = Debug|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x64.Build.0 = Debug|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1452,16 +1211,12 @@ Global {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|Any CPU.ActiveCfg = Release|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|ARM.ActiveCfg = Release|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|ARM64.ActiveCfg = Release|ARM64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|ARM64.Build.0 = Release|ARM64 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x64.ActiveCfg = Release|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x64.Build.0 = Release|x64 {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x86.ActiveCfg = Release|Win32 @@ -1469,16 +1224,12 @@ Global {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x64.ActiveCfg = Release|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x86.ActiveCfg = Release|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|Any CPU.ActiveCfg = Debug|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|ARM.ActiveCfg = Debug|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|ARM64.ActiveCfg = Debug|ARM64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|ARM64.Build.0 = Debug|ARM64 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x64.ActiveCfg = Debug|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x64.Build.0 = Debug|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1486,16 +1237,12 @@ Global {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|Any CPU.ActiveCfg = Release|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|ARM.ActiveCfg = Release|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|ARM64.ActiveCfg = Release|ARM64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|ARM64.Build.0 = Release|ARM64 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x64.ActiveCfg = Release|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x64.Build.0 = Release|x64 {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x86.ActiveCfg = Release|Win32 @@ -1503,16 +1250,12 @@ Global {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x64.ActiveCfg = Release|x64 {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x86.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Debug|Any CPU.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Debug|ARM.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Debug|ARM64.ActiveCfg = Debug|ARM64 {990F2657-8580-4828-943F-5DD657D11842}.Debug|ARM64.Build.0 = Debug|ARM64 - {990F2657-8580-4828-943F-5DD657D11842}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {990F2657-8580-4828-943F-5DD657D11842}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Debug|x64.ActiveCfg = Debug|x64 {990F2657-8580-4828-943F-5DD657D11842}.Debug|x64.Build.0 = Debug|x64 {990F2657-8580-4828-943F-5DD657D11842}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1520,8 +1263,6 @@ Global {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|x64.Build.0 = Fuzzing|x64 {990F2657-8580-4828-943F-5DD657D11842}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1529,8 +1270,6 @@ Global {990F2657-8580-4828-943F-5DD657D11842}.Release|ARM.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Release|ARM64.ActiveCfg = Release|ARM64 {990F2657-8580-4828-943F-5DD657D11842}.Release|ARM64.Build.0 = Release|ARM64 - {990F2657-8580-4828-943F-5DD657D11842}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {990F2657-8580-4828-943F-5DD657D11842}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11842}.Release|x64.ActiveCfg = Release|x64 {990F2657-8580-4828-943F-5DD657D11842}.Release|x64.Build.0 = Release|x64 {990F2657-8580-4828-943F-5DD657D11842}.Release|x86.ActiveCfg = Release|Win32 @@ -1538,16 +1277,12 @@ Global {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x64.ActiveCfg = Release|x64 {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x86.ActiveCfg = Release|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|Any CPU.ActiveCfg = Debug|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|ARM.ActiveCfg = Debug|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|ARM64.ActiveCfg = Debug|ARM64 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|ARM64.Build.0 = Debug|ARM64 - {814DBDDE-894E-4327-A6E1-740504850098}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {814DBDDE-894E-4327-A6E1-740504850098}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x64.ActiveCfg = Debug|x64 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x64.Build.0 = Debug|x64 {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1555,16 +1290,12 @@ Global {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {814DBDDE-894E-4327-A6E1-740504850098}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Release|Any CPU.ActiveCfg = Release|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Release|ARM.ActiveCfg = Release|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Release|ARM64.ActiveCfg = Release|ARM64 {814DBDDE-894E-4327-A6E1-740504850098}.Release|ARM64.Build.0 = Release|ARM64 - {814DBDDE-894E-4327-A6E1-740504850098}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {814DBDDE-894E-4327-A6E1-740504850098}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {814DBDDE-894E-4327-A6E1-740504850098}.Release|x64.ActiveCfg = Release|x64 {814DBDDE-894E-4327-A6E1-740504850098}.Release|x64.Build.0 = Release|x64 {814DBDDE-894E-4327-A6E1-740504850098}.Release|x86.ActiveCfg = Release|Win32 @@ -1572,16 +1303,12 @@ Global {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x64.ActiveCfg = Release|x64 {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x86.ActiveCfg = Release|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|Any CPU.ActiveCfg = Debug|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|ARM.ActiveCfg = Debug|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|ARM64.ActiveCfg = Debug|ARM64 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|ARM64.Build.0 = Debug|ARM64 - {814CBEEE-894E-4327-A6E1-740504850098}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {814CBEEE-894E-4327-A6E1-740504850098}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x64.ActiveCfg = Debug|x64 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x64.Build.0 = Debug|x64 {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1589,16 +1316,12 @@ Global {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {814CBEEE-894E-4327-A6E1-740504850098}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Release|Any CPU.ActiveCfg = Release|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Release|ARM.ActiveCfg = Release|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Release|ARM64.ActiveCfg = Release|ARM64 {814CBEEE-894E-4327-A6E1-740504850098}.Release|ARM64.Build.0 = Release|ARM64 - {814CBEEE-894E-4327-A6E1-740504850098}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {814CBEEE-894E-4327-A6E1-740504850098}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {814CBEEE-894E-4327-A6E1-740504850098}.Release|x64.ActiveCfg = Release|x64 {814CBEEE-894E-4327-A6E1-740504850098}.Release|x64.Build.0 = Release|x64 {814CBEEE-894E-4327-A6E1-740504850098}.Release|x86.ActiveCfg = Release|Win32 @@ -1607,8 +1330,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.ActiveCfg = AuditMode|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.Build.0 = AuditMode|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -1617,10 +1338,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM.ActiveCfg = Debug|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.ActiveCfg = Debug|ARM64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.Build.0 = Debug|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.ActiveCfg = Debug|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.Build.0 = Debug|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1628,8 +1345,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x64.Build.0 = Fuzzing|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1637,10 +1352,6 @@ Global {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM.ActiveCfg = Release|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.ActiveCfg = Release|ARM64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.Build.0 = Release|ARM64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|DotNet_x64Test.Build.0 = Release|x64 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|DotNet_x86Test.Build.0 = Release|Win32 {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.ActiveCfg = Release|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.Build.0 = Release|x64 {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x86.ActiveCfg = Release|Win32 @@ -1648,16 +1359,12 @@ Global {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x64.ActiveCfg = Release|x64 {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x86.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Debug|Any CPU.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Debug|ARM.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Debug|ARM64.ActiveCfg = Debug|ARM64 {990F2657-8580-4828-943F-5DD657D11843}.Debug|ARM64.Build.0 = Debug|ARM64 - {990F2657-8580-4828-943F-5DD657D11843}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {990F2657-8580-4828-943F-5DD657D11843}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Debug|x64.ActiveCfg = Debug|x64 {990F2657-8580-4828-943F-5DD657D11843}.Debug|x64.Build.0 = Debug|x64 {990F2657-8580-4828-943F-5DD657D11843}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1665,16 +1372,12 @@ Global {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {990F2657-8580-4828-943F-5DD657D11843}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Release|Any CPU.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Release|ARM.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Release|ARM64.ActiveCfg = Release|ARM64 {990F2657-8580-4828-943F-5DD657D11843}.Release|ARM64.Build.0 = Release|ARM64 - {990F2657-8580-4828-943F-5DD657D11843}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {990F2657-8580-4828-943F-5DD657D11843}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {990F2657-8580-4828-943F-5DD657D11843}.Release|x64.ActiveCfg = Release|x64 {990F2657-8580-4828-943F-5DD657D11843}.Release|x64.Build.0 = Release|x64 {990F2657-8580-4828-943F-5DD657D11843}.Release|x86.ActiveCfg = Release|Win32 @@ -1683,8 +1386,6 @@ Global {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x64.ActiveCfg = AuditMode|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x64.Build.0 = AuditMode|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -1693,10 +1394,6 @@ Global {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|ARM.ActiveCfg = Debug|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|ARM64.Build.0 = Debug|ARM64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x64.ActiveCfg = Debug|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x64.Build.0 = Debug|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1704,8 +1401,6 @@ Global {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|x64.Build.0 = Fuzzing|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1713,10 +1408,6 @@ Global {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|ARM.ActiveCfg = Release|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|ARM64.ActiveCfg = Release|ARM64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|ARM64.Build.0 = Release|ARM64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|DotNet_x64Test.Build.0 = Release|x64 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|DotNet_x86Test.Build.0 = Release|Win32 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x64.ActiveCfg = Release|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x64.Build.0 = Release|x64 {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x86.ActiveCfg = Release|Win32 @@ -1725,8 +1416,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x64.ActiveCfg = AuditMode|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x64.Build.0 = AuditMode|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -1735,10 +1424,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|ARM.ActiveCfg = Debug|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|ARM64.ActiveCfg = Debug|ARM64 {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|ARM64.Build.0 = Debug|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x64.ActiveCfg = Debug|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x64.Build.0 = Debug|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1746,8 +1431,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|x64.Build.0 = Fuzzing|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -1755,10 +1438,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF62}.Release|ARM.ActiveCfg = Release|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Release|ARM64.ActiveCfg = Release|ARM64 {48D21369-3D7B-4431-9967-24E81292CF62}.Release|ARM64.Build.0 = Release|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Release|DotNet_x64Test.Build.0 = Release|x64 - {48D21369-3D7B-4431-9967-24E81292CF62}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {48D21369-3D7B-4431-9967-24E81292CF62}.Release|DotNet_x86Test.Build.0 = Release|Win32 {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x64.ActiveCfg = Release|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x64.Build.0 = Release|x64 {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x86.ActiveCfg = Release|Win32 @@ -1766,8 +1445,6 @@ Global {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x64.Build.0 = AuditMode|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -1775,8 +1452,6 @@ Global {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1784,16 +1459,12 @@ Global {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x86.ActiveCfg = Release|Win32 @@ -1801,8 +1472,6 @@ Global {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x64.Build.0 = AuditMode|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -1810,10 +1479,6 @@ Global {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1821,18 +1486,12 @@ Global {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|DotNet_x64Test.Build.0 = Release|x64 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|DotNet_x86Test.Build.0 = Release|Win32 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x86.ActiveCfg = Release|Win32 @@ -1840,16 +1499,12 @@ Global {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x86.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1857,16 +1512,12 @@ Global {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x86.ActiveCfg = Release|Win32 @@ -1874,16 +1525,12 @@ Global {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.AuditMode|x86.ActiveCfg = Release|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1891,16 +1538,12 @@ Global {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-F542-4635-A069-7CAEFB930070}.Release|x86.ActiveCfg = Release|Win32 @@ -1908,16 +1551,12 @@ Global {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|Any CPU.ActiveCfg = Debug|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|x64 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.ActiveCfg = Release|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1925,16 +1564,12 @@ Global {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.ActiveCfg = Release|Win32 @@ -1942,16 +1577,12 @@ Global {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x86.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1959,16 +1590,12 @@ Global {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x86.ActiveCfg = Release|Win32 @@ -1976,8 +1603,6 @@ Global {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|Any CPU.ActiveCfg = Release|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|x64.ActiveCfg = Release|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|x64.Build.0 = Release|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -1985,8 +1610,6 @@ Global {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|ARM.ActiveCfg = Debug|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|ARM64.Build.0 = Debug|ARM64 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|x64.ActiveCfg = Debug|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|x64.Build.0 = Debug|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Debug|x86.ActiveCfg = Debug|Win32 @@ -1994,16 +1617,12 @@ Global {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|Any CPU.ActiveCfg = Release|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|ARM.ActiveCfg = Release|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|ARM64.ActiveCfg = Release|ARM64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|ARM64.Build.0 = Release|ARM64 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|x64.ActiveCfg = Release|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|x64.Build.0 = Release|x64 {F2ED628A-DB22-446F-A081-4CC845B51A2B}.Release|x86.ActiveCfg = Release|Win32 @@ -2011,16 +1630,12 @@ Global {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x64.ActiveCfg = AuditMode|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|Any CPU.ActiveCfg = Debug|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|ARM.ActiveCfg = Debug|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.Build.0 = Debug|ARM64 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x64.ActiveCfg = Debug|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x64.Build.0 = Debug|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2028,16 +1643,12 @@ Global {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|Any CPU.ActiveCfg = Release|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|ARM.ActiveCfg = Release|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.ActiveCfg = Release|ARM64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.Build.0 = Release|ARM64 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x64.ActiveCfg = Release|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x64.Build.0 = Release|x64 {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x86.ActiveCfg = Release|Win32 @@ -2046,8 +1657,6 @@ Global {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x64.ActiveCfg = AuditMode|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x64.Build.0 = AuditMode|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2056,8 +1665,6 @@ Global {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|ARM.ActiveCfg = Debug|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|ARM64.ActiveCfg = Debug|ARM64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|ARM64.Build.0 = Debug|ARM64 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x64.ActiveCfg = Debug|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x64.Build.0 = Debug|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2065,8 +1672,6 @@ Global {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|x64.Build.0 = Fuzzing|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -2074,8 +1679,6 @@ Global {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|ARM.ActiveCfg = Release|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|ARM64.ActiveCfg = Release|ARM64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|ARM64.Build.0 = Release|ARM64 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x64.ActiveCfg = Release|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x64.Build.0 = Release|x64 {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x86.ActiveCfg = Release|Win32 @@ -2083,16 +1686,12 @@ Global {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x64.ActiveCfg = AuditMode|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|Any CPU.ActiveCfg = Debug|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|ARM.ActiveCfg = Debug|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|ARM64.ActiveCfg = Debug|ARM64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|ARM64.Build.0 = Debug|ARM64 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x64.ActiveCfg = Debug|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x64.Build.0 = Debug|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2100,67 +1699,21 @@ Global {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|Any CPU.ActiveCfg = Release|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|ARM.ActiveCfg = Release|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|ARM64.ActiveCfg = Release|ARM64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|ARM64.Build.0 = Release|ARM64 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x64.ActiveCfg = Release|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x64.Build.0 = Release|x64 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.ActiveCfg = Release|Win32 {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.Build.0 = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x64.ActiveCfg = AuditMode|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x64.Build.0 = AuditMode|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x86.ActiveCfg = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.AuditMode|x86.Build.0 = AuditMode|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|Any CPU.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|ARM64.Build.0 = Debug|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x64.ActiveCfg = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x64.Build.0 = Debug|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x86.ActiveCfg = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Debug|x86.Build.0 = Debug|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|Any CPU.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM64.ActiveCfg = Release|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|ARM64.Build.0 = Release|ARM64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|DotNet_x64Test.Build.0 = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|DotNet_x86Test.Build.0 = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x64.ActiveCfg = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x64.Build.0 = Release|x64 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x86.ActiveCfg = Release|Win32 - {84848BFA-931D-42CE-9ADF-01EE54DE7890}.Release|x86.Build.0 = Release|Win32 {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|Any CPU.ActiveCfg = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM64.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|ARM64.Build.0 = Debug|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|DotNet_x64Test.ActiveCfg = Release|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|DotNet_x86Test.ActiveCfg = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|x64.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|x64.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.AuditMode|x86.ActiveCfg = Debug|Any CPU @@ -2170,17 +1723,14 @@ Global {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|ARM.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|ARM.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|DotNet_x64Test.ActiveCfg = Debug|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|DotNet_x64Test.Build.0 = Debug|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|DotNet_x86Test.ActiveCfg = Debug|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|DotNet_x86Test.Build.0 = Debug|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|ARM64.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|x64.ActiveCfg = Debug|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|x64.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|x86.ActiveCfg = Debug|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Debug|x86.Build.0 = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|Any CPU.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|ARM.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|ARM64.ActiveCfg = Fuzzing|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|x64.ActiveCfg = Debug|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Fuzzing|x86.ActiveCfg = Fuzzing|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -2188,25 +1738,20 @@ Global {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|ARM.ActiveCfg = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|ARM.Build.0 = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|ARM64.ActiveCfg = Release|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|DotNet_x64Test.ActiveCfg = Release|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|DotNet_x64Test.Build.0 = Release|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|DotNet_x86Test.ActiveCfg = Release|Any CPU - {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|DotNet_x86Test.Build.0 = Release|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|ARM64.Build.0 = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|x64.ActiveCfg = Release|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|x64.Build.0 = Release|Any CPU {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|x86.ActiveCfg = Release|Any CPU + {376FE273-6B84-4EB5-8B30-8DE9D21B022C}.Release|x86.Build.0 = Release|Any CPU {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2214,16 +1759,12 @@ Global {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-9333-4D05-B12A-1905CBF112F9}.Release|x86.ActiveCfg = Release|Win32 @@ -2231,16 +1772,12 @@ Global {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.AuditMode|x86.ActiveCfg = Release|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2248,16 +1785,12 @@ Global {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-9A12-429C-B551-8562EC954746}.Release|x86.ActiveCfg = Release|Win32 @@ -2265,16 +1798,12 @@ Global {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|Any CPU.ActiveCfg = Debug|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2282,16 +1811,12 @@ Global {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-B11C-4DDB-A4FE-C3AFAE9B5506}.Release|x86.ActiveCfg = Release|Win32 @@ -2300,8 +1825,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|x64.ActiveCfg = AuditMode|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|x64.Build.0 = AuditMode|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2310,9 +1833,6 @@ Global {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM.ActiveCfg = Debug|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM64.ActiveCfg = Debug|ARM64 {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|ARM64.Build.0 = Debug|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|x64.ActiveCfg = Debug|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|x64.Build.0 = Debug|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2320,17 +1840,12 @@ Global {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|Any CPU.ActiveCfg = Release|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM.ActiveCfg = Release|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM64.ActiveCfg = Release|ARM64 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|ARM64.Build.0 = Release|ARM64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x64Test.Build.0 = Release|x64 - {48D21369-3D7B-4431-9967-24E81292CF63}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|x64.ActiveCfg = Release|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|x64.Build.0 = Release|x64 {48D21369-3D7B-4431-9967-24E81292CF63}.Release|x86.ActiveCfg = Release|Win32 @@ -2338,8 +1853,6 @@ Global {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|Any CPU.ActiveCfg = Release|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|DotNet_x64Test.ActiveCfg = Release|x64 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|DotNet_x86Test.ActiveCfg = Release|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|x64.Build.0 = AuditMode|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -2347,8 +1860,6 @@ Global {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2356,16 +1867,12 @@ Global {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}.Release|x86.ActiveCfg = Release|Win32 @@ -2374,8 +1881,6 @@ Global {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|x64.ActiveCfg = AuditMode|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|x64.Build.0 = AuditMode|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2384,8 +1889,6 @@ Global {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|ARM.ActiveCfg = Debug|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|ARM64.Build.0 = Debug|ARM64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|DotNet_x86Test.ActiveCfg = Debug|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|x64.ActiveCfg = Debug|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|x64.Build.0 = Debug|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2393,16 +1896,12 @@ Global {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|Any CPU.ActiveCfg = Release|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|ARM.ActiveCfg = Release|Win32 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|ARM64.ActiveCfg = Release|ARM64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|ARM64.Build.0 = Release|ARM64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|DotNet_x86Test.ActiveCfg = Release|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|x64.ActiveCfg = Release|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|x64.Build.0 = Release|x64 {58A03BB2-DF5A-4B66-91A0-7EF3BA01269A}.Release|x86.ActiveCfg = Release|Win32 @@ -2411,8 +1910,6 @@ Global {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|x64.ActiveCfg = Release|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2420,8 +1917,6 @@ Global {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|ARM.ActiveCfg = Debug|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|ARM64.Build.0 = Debug|ARM64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|DotNet_x86Test.ActiveCfg = Debug|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|x64.ActiveCfg = Debug|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|x64.Build.0 = Debug|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2429,16 +1924,12 @@ Global {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|Any CPU.ActiveCfg = Release|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|ARM.ActiveCfg = Release|Win32 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|ARM64.ActiveCfg = Release|ARM64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|ARM64.Build.0 = Release|ARM64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|DotNet_x86Test.ActiveCfg = Release|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|x64.ActiveCfg = Release|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|x64.Build.0 = Release|x64 {A22EC5F6-7851-4B88-AC52-47249D437A52}.Release|x86.ActiveCfg = Release|Win32 @@ -2446,8 +1937,6 @@ Global {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|Any CPU.ActiveCfg = AuditMode|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|x64 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|x64.ActiveCfg = AuditMode|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|Any CPU.ActiveCfg = Debug|Win32 @@ -2455,8 +1944,6 @@ Global {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|ARM64.Build.0 = Debug|ARM64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|x64.ActiveCfg = Debug|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|x64.Build.0 = Debug|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Debug|x64.Deploy.0 = Debug|x64 @@ -2466,8 +1953,6 @@ Global {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|Any CPU.ActiveCfg = Release|Win32 @@ -2475,8 +1960,6 @@ Global {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|ARM64.ActiveCfg = Release|ARM64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|ARM64.Build.0 = Release|ARM64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|ARM64.Deploy.0 = Release|ARM64 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|x64.ActiveCfg = Release|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|x64.Build.0 = Release|x64 {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8}.Release|x64.Deploy.0 = Release|x64 @@ -2487,8 +1970,6 @@ Global {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|x64.ActiveCfg = Release|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2496,8 +1977,6 @@ Global {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|ARM.ActiveCfg = Debug|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|ARM64.ActiveCfg = Debug|ARM64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|ARM64.Build.0 = Debug|ARM64 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|x64.ActiveCfg = Debug|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|x64.Build.0 = Debug|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2505,16 +1984,12 @@ Global {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|Any CPU.ActiveCfg = Release|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|ARM.ActiveCfg = Release|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|ARM64.ActiveCfg = Release|ARM64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|ARM64.Build.0 = Release|ARM64 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|x64.ActiveCfg = Release|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|x64.Build.0 = Release|x64 {767268EE-174A-46FE-96F0-EEE698A1BBC9}.Release|x86.ActiveCfg = Release|Win32 @@ -2524,8 +1999,6 @@ Global {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|ARM64.ActiveCfg = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|ARM64.Build.0 = Release|x64 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|DotNet_x64Test.ActiveCfg = Release|x64 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|DotNet_x86Test.ActiveCfg = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|x64.ActiveCfg = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|x64.Build.0 = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -2533,8 +2006,6 @@ Global {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|Any CPU.ActiveCfg = Debug|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|ARM.ActiveCfg = Debug|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|ARM64.ActiveCfg = Debug|Win32 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|x64.ActiveCfg = Debug|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|x64.Build.0 = Debug|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2542,15 +2013,11 @@ Global {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|Any CPU.ActiveCfg = Release|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|ARM.ActiveCfg = Release|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|ARM64.ActiveCfg = Release|Win32 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|x64.ActiveCfg = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|x64.Build.0 = Release|x64 {A602A555-BAAC-46E1-A91D-3DAB0475C5A1}.Release|x86.ActiveCfg = Release|Win32 @@ -2559,8 +2026,6 @@ Global {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|x64.ActiveCfg = Release|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2568,8 +2033,6 @@ Global {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|ARM.ActiveCfg = Debug|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|ARM64.Build.0 = Debug|ARM64 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|x64.ActiveCfg = Debug|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|x64.Build.0 = Debug|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2577,16 +2040,12 @@ Global {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|Any CPU.ActiveCfg = Release|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|ARM.ActiveCfg = Release|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|ARM64.ActiveCfg = Release|ARM64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|ARM64.Build.0 = Release|ARM64 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|x64.ActiveCfg = Release|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|x64.Build.0 = Release|x64 {95B136F9-B238-490C-A7C5-5843C1FECAC4}.Release|x86.ActiveCfg = Release|Win32 @@ -2595,8 +2054,6 @@ Global {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|x64.ActiveCfg = Release|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2604,8 +2061,6 @@ Global {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|ARM.ActiveCfg = Debug|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|ARM64.Build.0 = Debug|ARM64 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|x64.ActiveCfg = Debug|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|x64.Build.0 = Debug|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2613,16 +2068,12 @@ Global {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|Any CPU.ActiveCfg = Release|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|ARM.ActiveCfg = Release|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|ARM64.ActiveCfg = Release|ARM64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|ARM64.Build.0 = Release|ARM64 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x64.ActiveCfg = Release|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x64.Build.0 = Release|x64 {024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x86.ActiveCfg = Release|Win32 @@ -2631,8 +2082,6 @@ Global {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|x64.ActiveCfg = Release|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2640,8 +2089,6 @@ Global {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM.ActiveCfg = Debug|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.Build.0 = Debug|ARM64 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.ActiveCfg = Debug|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.Build.0 = Debug|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2649,16 +2096,12 @@ Global {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|Any CPU.ActiveCfg = Release|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM.ActiveCfg = Release|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.ActiveCfg = Release|ARM64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.Build.0 = Release|ARM64 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.ActiveCfg = Release|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.Build.0 = Release|x64 {067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x86.ActiveCfg = Release|Win32 @@ -2667,8 +2110,6 @@ Global {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x64.ActiveCfg = AuditMode|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x64.Build.0 = AuditMode|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2677,10 +2118,6 @@ Global {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|ARM.ActiveCfg = Debug|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|ARM64.ActiveCfg = Debug|ARM64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|ARM64.Build.0 = Debug|ARM64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|DotNet_x86Test.Build.0 = Debug|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x64.ActiveCfg = Debug|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x64.Build.0 = Debug|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2688,8 +2125,6 @@ Global {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x64.Build.0 = Fuzzing|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -2697,10 +2132,6 @@ Global {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|ARM.ActiveCfg = Release|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|ARM64.ActiveCfg = Release|ARM64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|ARM64.Build.0 = Release|ARM64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|DotNet_x64Test.Build.0 = Release|x64 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 - {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|DotNet_x86Test.Build.0 = Release|Win32 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x64.ActiveCfg = Release|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x64.Build.0 = Release|x64 {6BAE5851-50D5-4934-8D5E-30361A8A40F3}.Release|x86.ActiveCfg = Release|Win32 @@ -2709,43 +2140,35 @@ Global {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|ARM.ActiveCfg = Release|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|ARM.Build.0 = Release|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|ARM64.ActiveCfg = Debug|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|x64.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.AuditMode|x86.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|ARM.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|ARM.Build.0 = Debug|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|DotNet_x86Test.ActiveCfg = Debug|x86 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|DotNet_x86Test.Build.0 = Debug|x86 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x64.ActiveCfg = Debug|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x86.ActiveCfg = Debug|Any CPU + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|ARM64.Build.0 = Debug|ARM64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x64.ActiveCfg = Debug|x64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x64.Build.0 = Debug|x64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x86.ActiveCfg = Debug|x86 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Debug|x86.Build.0 = Debug|x86 {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|Any CPU.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|ARM.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|ARM64.ActiveCfg = Fuzzing|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|x64.ActiveCfg = Debug|x64 {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Fuzzing|x86.ActiveCfg = Fuzzing|x86 {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|ARM.ActiveCfg = Release|Any CPU {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|ARM.Build.0 = Release|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|ARM64.ActiveCfg = Release|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|DotNet_x64Test.Build.0 = Release|x64 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|DotNet_x86Test.ActiveCfg = Release|x86 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|DotNet_x86Test.Build.0 = Release|x86 - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x64.ActiveCfg = Release|Any CPU - {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x86.ActiveCfg = Release|Any CPU + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|ARM64.ActiveCfg = Release|ARM64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|ARM64.Build.0 = Release|ARM64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x64.ActiveCfg = Release|x64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x64.Build.0 = Release|x64 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x86.ActiveCfg = Release|x86 + {1588FD7C-241E-4E7D-9113-43735F3E6BAD}.Release|x86.Build.0 = Release|x86 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.ActiveCfg = AuditMode|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.Build.0 = AuditMode|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2754,8 +2177,6 @@ Global {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM.ActiveCfg = Debug|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.ActiveCfg = Debug|ARM64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.Build.0 = Debug|ARM64 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.ActiveCfg = Debug|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.Build.0 = Debug|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2763,16 +2184,12 @@ Global {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|Any CPU.ActiveCfg = Release|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM.ActiveCfg = Release|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.ActiveCfg = Release|ARM64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.Build.0 = Release|ARM64 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.ActiveCfg = Release|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.Build.0 = Release|x64 {506FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.ActiveCfg = Release|Win32 @@ -2781,8 +2198,6 @@ Global {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.ActiveCfg = AuditMode|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x64.Build.0 = AuditMode|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -2791,8 +2206,6 @@ Global {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM.ActiveCfg = Debug|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.ActiveCfg = Debug|ARM64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|ARM64.Build.0 = Debug|ARM64 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.ActiveCfg = Debug|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x64.Build.0 = Debug|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2800,16 +2213,12 @@ Global {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|Any CPU.ActiveCfg = Release|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM.ActiveCfg = Release|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.ActiveCfg = Release|ARM64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|ARM64.Build.0 = Release|ARM64 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.ActiveCfg = Release|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x64.Build.0 = Release|x64 {416FD703-BAA7-4F6E-9361-64F550EC8FCA}.Release|x86.ActiveCfg = Release|Win32 @@ -2821,12 +2230,6 @@ Global {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|ARM64.ActiveCfg = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|ARM64.Build.0 = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|ARM64.Deploy.0 = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x64Test.ActiveCfg = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x64Test.Build.0 = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x64Test.Deploy.0 = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x86Test.ActiveCfg = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x86Test.Build.0 = Release|x64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|DotNet_x86Test.Deploy.0 = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|x64.Deploy.0 = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.AuditMode|x86.ActiveCfg = Release|Win32 @@ -2836,8 +2239,6 @@ Global {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Debug|x64.Deploy.0 = Debug|x64 @@ -2847,16 +2248,12 @@ Global {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-0B5E-45C3-96A8-BB496BFE4E32}.Release|x64.Deploy.0 = Release|x64 @@ -2867,8 +2264,6 @@ Global {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2876,8 +2271,6 @@ Global {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2885,16 +2278,12 @@ Global {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x86.ActiveCfg = Release|Win32 @@ -2903,8 +2292,6 @@ Global {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|x64.ActiveCfg = Release|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2912,8 +2299,6 @@ Global {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2921,16 +2306,12 @@ Global {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-082C-4476-9F33-94B339494076}.Release|x86.ActiveCfg = Release|Win32 @@ -2939,8 +2320,6 @@ Global {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|x64.ActiveCfg = AuditMode|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -2948,8 +2327,6 @@ Global {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|ARM.ActiveCfg = Debug|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|ARM64.ActiveCfg = Debug|ARM64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|x64.ActiveCfg = Debug|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|x64.Build.0 = Debug|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2957,16 +2334,12 @@ Global {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|Any CPU.ActiveCfg = Release|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|ARM.ActiveCfg = Release|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|ARM64.ActiveCfg = Release|ARM64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|ARM64.Build.0 = Release|ARM64 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|x64.ActiveCfg = Release|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|x64.Build.0 = Release|x64 {CA5CAD1A-9B68-456A-B13E-C8218070DC42}.Release|x86.ActiveCfg = Release|Win32 @@ -2974,16 +2347,12 @@ Global {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|ARM64.ActiveCfg = Release|ARM64 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|Win32 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|x64.ActiveCfg = Release|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.AuditMode|x86.ActiveCfg = Release|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|Any CPU.ActiveCfg = Debug|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|ARM.ActiveCfg = Debug|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|ARM64.Build.0 = Debug|ARM64 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|x64.ActiveCfg = Debug|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|x64.Build.0 = Debug|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Debug|x86.ActiveCfg = Debug|Win32 @@ -2991,16 +2360,12 @@ Global {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|Any CPU.ActiveCfg = Release|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|ARM.ActiveCfg = Release|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|ARM64.ActiveCfg = Release|ARM64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|ARM64.Build.0 = Release|ARM64 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|x64.ActiveCfg = Release|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|x64.Build.0 = Release|x64 {21B7EA5E-1EF8-49B6-AC07-11714AF0E37D}.Release|x86.ActiveCfg = Release|Win32 @@ -3012,8 +2377,6 @@ Global {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|ARM64.ActiveCfg = Debug|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|ARM64.Build.0 = Debug|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|ARM64.Deploy.0 = Debug|ARM64 - {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|DotNet_x64Test.ActiveCfg = Release|Any CPU - {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|DotNet_x86Test.ActiveCfg = Release|Any CPU {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|x64.ActiveCfg = Debug|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|x64.Build.0 = Debug|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.AuditMode|x64.Deploy.0 = Debug|x64 @@ -3027,8 +2390,6 @@ Global {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|ARM64.Build.0 = Debug|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|DotNet_x64Test.ActiveCfg = Debug|Any CPU - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|DotNet_x86Test.ActiveCfg = Debug|Any CPU {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|x64.ActiveCfg = Debug|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|x64.Build.0 = Debug|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Debug|x64.Deploy.0 = Debug|x64 @@ -3038,8 +2399,6 @@ Global {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|Any CPU.ActiveCfg = Release|Any CPU {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|ARM.ActiveCfg = Fuzzing|ARM {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Any CPU - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|DotNet_x86Test.ActiveCfg = Release|Any CPU {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|x64.ActiveCfg = Release|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Fuzzing|x86.ActiveCfg = Release|x86 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -3049,8 +2408,6 @@ Global {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|ARM64.ActiveCfg = Release|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|ARM64.Build.0 = Release|ARM64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|ARM64.Deploy.0 = Release|ARM64 - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|DotNet_x64Test.ActiveCfg = Release|Any CPU - {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|DotNet_x86Test.ActiveCfg = Release|Any CPU {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|x64.ActiveCfg = Release|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|x64.Build.0 = Release|x64 {F75E29D0-D288-478B-8D83-2C190F321A3F}.Release|x64.Deploy.0 = Release|x64 @@ -3061,8 +2418,6 @@ Global {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|x64.ActiveCfg = Release|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|x64.Build.0 = Release|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -3071,8 +2426,6 @@ Global {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|ARM.ActiveCfg = Debug|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|ARM64.ActiveCfg = Debug|ARM64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|ARM64.Build.0 = Debug|ARM64 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|x64.ActiveCfg = Debug|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|x64.Build.0 = Debug|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3080,16 +2433,12 @@ Global {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|Any CPU.ActiveCfg = Release|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|ARM.ActiveCfg = Release|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|ARM64.ActiveCfg = Release|ARM64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|ARM64.Build.0 = Release|ARM64 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|x64.ActiveCfg = Release|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|x64.Build.0 = Release|x64 {43CE4CE5-0010-4B99-9569-672670D26E26}.Release|x86.ActiveCfg = Release|Win32 @@ -3098,8 +2447,6 @@ Global {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|x64.ActiveCfg = Release|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -3107,8 +2454,6 @@ Global {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|ARM.ActiveCfg = Debug|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|ARM64.ActiveCfg = Debug|ARM64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|ARM64.Build.0 = Debug|ARM64 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|x64.ActiveCfg = Debug|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|x64.Build.0 = Debug|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3116,16 +2461,12 @@ Global {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|Any CPU.ActiveCfg = Release|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|ARM.ActiveCfg = Release|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|ARM64.ActiveCfg = Release|ARM64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|ARM64.Build.0 = Release|ARM64 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|x64.ActiveCfg = Release|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|x64.Build.0 = Release|x64 {27B5AAEB-A548-44CF-9777-F8BAA32AF7AE}.Release|x86.ActiveCfg = Release|Win32 @@ -3134,8 +2475,6 @@ Global {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|x64.ActiveCfg = AuditMode|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -3143,8 +2482,6 @@ Global {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|ARM.ActiveCfg = Debug|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|ARM64.ActiveCfg = Debug|ARM64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|ARM64.Build.0 = Debug|ARM64 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|x64.ActiveCfg = Debug|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|x64.Build.0 = Debug|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3152,16 +2489,12 @@ Global {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|Any CPU.ActiveCfg = Release|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|ARM.ActiveCfg = Release|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|ARM64.ActiveCfg = Release|ARM64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|ARM64.Build.0 = Release|ARM64 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x64.ActiveCfg = Release|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x64.Build.0 = Release|x64 {68A10CD3-AA64-465B-AF5F-ED4E9700543C}.Release|x86.ActiveCfg = Release|Win32 @@ -3170,8 +2503,6 @@ Global {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x64.ActiveCfg = AuditMode|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x64.Build.0 = AuditMode|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -3180,8 +2511,6 @@ Global {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM.ActiveCfg = Debug|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM64.ActiveCfg = Debug|ARM64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|ARM64.Build.0 = Debug|ARM64 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x64.ActiveCfg = Debug|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x64.Build.0 = Debug|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3189,8 +2518,6 @@ Global {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|x64.Build.0 = Fuzzing|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -3198,8 +2525,6 @@ Global {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM.ActiveCfg = Release|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM64.ActiveCfg = Release|ARM64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|ARM64.Build.0 = Release|ARM64 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x64.ActiveCfg = Release|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x64.Build.0 = Release|x64 {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}.Release|x86.ActiveCfg = Release|Win32 @@ -3207,38 +2532,28 @@ Global {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|x64.ActiveCfg = AuditMode|x64 {05D9052F-D78F-478F-968A-2DE38A6DB996}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|Any CPU.ActiveCfg = Debug|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|ARM.ActiveCfg = Debug|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|x64.ActiveCfg = Debug|x64 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Debug|x86.ActiveCfg = Debug|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|x64.Build.0 = Fuzzing|x64 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|Any CPU.ActiveCfg = Release|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|ARM.ActiveCfg = Release|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|ARM64.ActiveCfg = Release|ARM64 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|x64.ActiveCfg = Release|x64 {05D9052F-D78F-478F-968A-2DE38A6DB996}.Release|x86.ActiveCfg = Release|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|x64.ActiveCfg = Release|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.AuditMode|x86.Build.0 = AuditMode|Win32 @@ -3246,8 +2561,6 @@ Global {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|ARM.ActiveCfg = Debug|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|ARM64.Build.0 = Debug|ARM64 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|x64.ActiveCfg = Debug|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|x64.Build.0 = Debug|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3255,16 +2568,12 @@ Global {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|Any CPU.ActiveCfg = Release|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|ARM.ActiveCfg = Release|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|ARM64.ActiveCfg = Release|ARM64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|ARM64.Build.0 = Release|ARM64 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|x64.ActiveCfg = Release|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|x64.Build.0 = Release|x64 {C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B}.Release|x86.ActiveCfg = Release|Win32 @@ -3272,16 +2581,12 @@ Global {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|ARM.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|ARM64.ActiveCfg = Debug|ARM64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|DotNet_x64Test.ActiveCfg = Debug|x64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|DotNet_x86Test.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|x64.ActiveCfg = Debug|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.AuditMode|x86.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|Any CPU.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|ARM.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|ARM64.Build.0 = Debug|ARM64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|x64.ActiveCfg = Debug|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|x64.Build.0 = Debug|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3289,16 +2594,12 @@ Global {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|Any CPU.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|ARM.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|DotNet_x64Test.ActiveCfg = Debug|x64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|DotNet_x86Test.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|x64.ActiveCfg = Debug|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Fuzzing|x86.ActiveCfg = Debug|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|Any CPU.ActiveCfg = Release|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|ARM.ActiveCfg = Release|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|ARM64.ActiveCfg = Release|ARM64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|ARM64.Build.0 = Release|ARM64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|x64.ActiveCfg = Release|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|x64.Build.0 = Release|x64 {F19DACD5-0C6E-40DC-B6E4-767A3200542C}.Release|x86.ActiveCfg = Release|Win32 @@ -3307,8 +2608,6 @@ Global {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x64.ActiveCfg = AuditMode|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x64.Build.0 = AuditMode|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -3317,9 +2616,6 @@ Global {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM.ActiveCfg = Debug|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.ActiveCfg = Debug|ARM64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.Build.0 = Debug|ARM64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.Build.0 = Debug|x64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.ActiveCfg = Debug|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.Build.0 = Debug|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3328,8 +2624,6 @@ Global {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x64.Build.0 = Fuzzing|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -3338,9 +2632,6 @@ Global {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM.ActiveCfg = Release|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.ActiveCfg = Release|ARM64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.Build.0 = Release|ARM64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.Build.0 = Release|x64 - {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.ActiveCfg = Release|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.Build.0 = Release|x64 {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x86.ActiveCfg = Release|Win32 @@ -3348,8 +2639,6 @@ Global {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|Any CPU.ActiveCfg = AuditMode|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|ARM.ActiveCfg = AuditMode|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|x64 - {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|x64.ActiveCfg = AuditMode|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|x64.Build.0 = AuditMode|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -3357,8 +2646,6 @@ Global {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|ARM.ActiveCfg = Debug|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|ARM64.ActiveCfg = Debug|ARM64 {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|ARM64.Build.0 = Debug|ARM64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|DotNet_x86Test.ActiveCfg = Debug|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|x64.ActiveCfg = Debug|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|x64.Build.0 = Debug|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3366,16 +2653,12 @@ Global {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|ARM.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|x64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|Any CPU.ActiveCfg = Release|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|ARM.ActiveCfg = Release|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|ARM64.ActiveCfg = Release|ARM64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|ARM64.Build.0 = Release|ARM64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {06EC74CB-9A12-428C-B551-8537EC964726}.Release|DotNet_x86Test.ActiveCfg = Release|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|x64.ActiveCfg = Release|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|x64.Build.0 = Release|x64 {06EC74CB-9A12-428C-B551-8537EC964726}.Release|x86.ActiveCfg = Release|Win32 @@ -3383,16 +2666,12 @@ Global {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|Any CPU.ActiveCfg = AuditMode|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|ARM.ActiveCfg = AuditMode|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|x64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|x64.ActiveCfg = AuditMode|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.AuditMode|x86.ActiveCfg = AuditMode|Win32 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|Any CPU.ActiveCfg = Debug|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|ARM.ActiveCfg = Debug|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|ARM64.ActiveCfg = Debug|ARM64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|ARM64.Build.0 = Debug|ARM64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|DotNet_x64Test.ActiveCfg = Debug|x64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|DotNet_x86Test.ActiveCfg = Debug|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|x64.ActiveCfg = Debug|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|x64.Build.0 = Debug|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3400,8 +2679,6 @@ Global {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|ARM.ActiveCfg = Fuzzing|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|x64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|x64.Build.0 = Fuzzing|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -3409,8 +2686,6 @@ Global {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|ARM.ActiveCfg = Release|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|ARM64.ActiveCfg = Release|ARM64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|ARM64.Build.0 = Release|ARM64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|DotNet_x64Test.ActiveCfg = Release|x64 - {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|DotNet_x86Test.ActiveCfg = Release|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x64.ActiveCfg = Release|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x64.Build.0 = Release|x64 {75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x86.ActiveCfg = Release|Win32 @@ -3419,8 +2694,6 @@ Global {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x64.ActiveCfg = AuditMode|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x64.Build.0 = AuditMode|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x86.ActiveCfg = AuditMode|Win32 @@ -3429,8 +2702,6 @@ Global {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM.ActiveCfg = Debug|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM64.Build.0 = Debug|ARM64 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x64.ActiveCfg = Debug|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x64.Build.0 = Debug|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x86.ActiveCfg = Debug|Win32 @@ -3439,8 +2710,6 @@ Global {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x64.Build.0 = Fuzzing|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 @@ -3449,12 +2718,94 @@ Global {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM.ActiveCfg = Release|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM64.ActiveCfg = Release|ARM64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM64.Build.0 = Release|ARM64 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|DotNet_x64Test.ActiveCfg = Release|Win32 - {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|DotNet_x86Test.ActiveCfg = Release|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x64.ActiveCfg = Release|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x64.Build.0 = Release|x64 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x86.ActiveCfg = Release|Win32 {3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x86.Build.0 = Release|Win32 + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.AuditMode|Any CPU.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.AuditMode|ARM.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.AuditMode|ARM64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.AuditMode|x64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.AuditMode|x86.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|x64.Build.0 = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Fuzzing|Any CPU.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Fuzzing|ARM.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Fuzzing|x64.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Fuzzing|x86.ActiveCfg = Debug|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|Any CPU.Build.0 = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|ARM.ActiveCfg = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|ARM64.ActiveCfg = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x64.ActiveCfg = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x64.Build.0 = Release|Any CPU + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4}.Release|x86.ActiveCfg = Release|Any CPU + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|x64.ActiveCfg = Release|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.AuditMode|x86.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x64.ActiveCfg = Debug|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x86.ActiveCfg = Debug|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|Any CPU.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM.ActiveCfg = Release|Win32 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM64.ActiveCfg = Release|ARM64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x64.ActiveCfg = Release|x64 + {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x86.ActiveCfg = Release|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|x64.ActiveCfg = Release|x64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|x86.ActiveCfg = Release|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|ARM.ActiveCfg = Debug|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|x64.ActiveCfg = Debug|x64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|x86.ActiveCfg = Debug|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|Any CPU.ActiveCfg = Release|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|ARM.ActiveCfg = Release|Win32 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|ARM64.ActiveCfg = Release|ARM64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|x64.ActiveCfg = Release|x64 + {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|x86.ActiveCfg = Release|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.AuditMode|ARM.ActiveCfg = AuditMode|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {328729E9-6723-416E-9C98-951F1473BBE1}.AuditMode|x64.ActiveCfg = Release|x64 + {328729E9-6723-416E-9C98-951F1473BBE1}.AuditMode|x86.ActiveCfg = Release|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Debug|ARM.ActiveCfg = Debug|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Debug|x64.ActiveCfg = Debug|x64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Debug|x86.ActiveCfg = Debug|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Release|Any CPU.ActiveCfg = Release|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Release|ARM.ActiveCfg = Release|Win32 + {328729E9-6723-416E-9C98-951F1473BBE1}.Release|ARM64.ActiveCfg = Release|ARM64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Release|x64.ActiveCfg = Release|x64 + {328729E9-6723-416E-9C98-951F1473BBE1}.Release|x86.ActiveCfg = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3513,7 +2864,6 @@ Global {05500DEF-2294-41E3-AF9A-24E580B82836} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {1E4A062E-293B-4817-B20D-BF16B979E350} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} - {84848BFA-931D-42CE-9ADF-01EE54DE7890} = {4DAF0299-495E-4CD1-A982-9BAC16A45932} {376FE273-6B84-4EB5-8B30-8DE9D21B022C} = {4DAF0299-495E-4CD1-A982-9BAC16A45932} {CA5CAD1A-9333-4D05-B12A-1905CBF112F9} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E} {CA5CAD1A-9A12-429C-B551-8562EC954746} = {59840756-302F-44DF-AA47-441A9D673202} @@ -3559,6 +2909,10 @@ Global {75C6F576-18E9-4566-978A-F0A301CAC090} = {05500DEF-2294-41E3-AF9A-24E580B82836} {40BD8415-DD93-4200-8D82-498DDDC08CC8} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} {3C67784E-1453-49C2-9660-483E2CC7F7AD} = {40BD8415-DD93-4200-8D82-498DDDC08CC8} + {613CCB57-5FA9-48EF-80D0-6B1E319E20C4} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {37C995E0-2349-4154-8E77-4A52C0C7F46D} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {2C836962-9543-4CE5-B834-D28E1F124B66} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {328729E9-6723-416E-9C98-951F1473BBE1} = {A10C4720-DCA4-4640-9749-67F4314F527C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271} diff --git a/README.md b/README.md index 08819d607a..b174aeaa5a 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,6 +72,9 @@ package: winget install --id Microsoft.WindowsTerminal -e ``` +> **Note**\ +> 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) [Chocolatey](https://chocolatey.org) users can download and install the latest @@ -115,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 @@ -235,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 @@ -271,10 +277,10 @@ similar open/closed preexisting issues before creating a new issue.** If you would like to ask a question that you feel doesn't warrant an issue (yet), please reach out to us via Twitter: -* Kayla Cinnamon, Program Manager: - [@cinnamon\_msft](https://twitter.com/cinnamon_msft) +* Christopher Nguyen, Product Manager: + [@nguyen_dows](https://twitter.com/nguyen_dows) * Dustin Howett, Engineering Lead: [@dhowett](https://twitter.com/DHowett) -* Mike Griese, Senior Developer: [@zadjii](https://twitter.com/zadjii) +* Mike Griese, Senior Developer: [@zadjii@mastodon.social](https://mastodon.social/@zadjii) * Carlos Zamora, Developer: [@cazamor_msft](https://twitter.com/cazamor_msft) * Pankaj Bhojwani, Developer * Leonard Hecker, Developer: [@LeonardHecker](https://twitter.com/LeonardHecker) diff --git a/build/Helix/AzurePipelinesHelperScripts.ps1 b/build/Helix/AzurePipelinesHelperScripts.ps1 deleted file mode 100644 index 8934a8548d..0000000000 --- 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/ConvertWttLogToXUnit.ps1 b/build/Helix/ConvertWttLogToXUnit.ps1 index 13d333d3e7..c17d86e8ec 100644 --- a/build/Helix/ConvertWttLogToXUnit.ps1 +++ b/build/Helix/ConvertWttLogToXUnit.ps1 @@ -20,9 +20,7 @@ Param( $helixResultsContainerUri = $Env:HELIX_RESULTS_CONTAINER_URI $helixResultsContainerRsas = $Env:HELIX_RESULTS_CONTAINER_RSAS -$rerunPassesRequiredToAvoidFailure = $env:rerunPassesRequiredToAvoidFailure - 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) -$testResultParser.ConvertWttLogToXUnitLog($WttInputPath, $WttSingleRerunInputPath, $WttMultipleRerunInputPath, $XUnitOutputPath, $rerunPassesRequiredToAvoidFailure) \ No newline at end of file +$testResultParser.ConvertWttLogToXUnitLog($WttInputPath, $WttSingleRerunInputPath, $WttMultipleRerunInputPath, $XUnitOutputPath) diff --git a/build/Helix/EnsureMachineState.ps1 b/build/Helix/EnsureMachineState.ps1 deleted file mode 100644 index 6f26da63a5..0000000000 --- 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 07a3550a79..0000000000 --- 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/HelixTestHelpers.cs b/build/Helix/HelixTestHelpers.cs index d691026a41..67f13937c0 100644 --- a/build/Helix/HelixTestHelpers.cs +++ b/build/Helix/HelixTestHelpers.cs @@ -20,32 +20,13 @@ namespace HelixTestHelpers public string Name { get; set; } public string SourceWttFile { get; set; } public bool Passed { get; set; } + public bool Skipped { get; set; } public bool CleanupPassed { get; set; } public TimeSpan ExecutionTime { get; set; } public string Details { get; set; } public List Screenshots { get; private set; } public List RerunResults { get; private set; } - - // Returns true if the test pass rate is sufficient to avoid being counted as a failure. - public bool PassedOrUnreliable(int requiredNumberOfPasses) - { - if(Passed) - { - return true; - } - else - { - if(RerunResults.Count == 1) - { - return RerunResults[0].Passed; - } - else - { - return RerunResults.Where(r => r.Passed).Count() >= requiredNumberOfPasses; - } - } - } } // @@ -221,7 +202,9 @@ namespace HelixTestHelpers testsExecuting--; // If any inner test fails, we'll still fail the outer - currentResult.Passed &= element.Attribute("Result").Value == "Pass"; + var value = element.Attribute("Result").Value; + currentResult.Passed = value == "Pass"; + currentResult.Skipped = value == "Skipped"; // Only gather execution data if this is the outer test we ran initially if (testsExecuting == 0) @@ -498,7 +481,7 @@ namespace HelixTestHelpers return subResultsJsonByMethod; } - public void ConvertWttLogToXUnitLog(string wttInputPath, string wttSingleRerunInputPath, string wttMultipleRerunInputPath, string xunitOutputPath, int requiredPassRateThreshold) + public void ConvertWttLogToXUnitLog(string wttInputPath, string wttSingleRerunInputPath, string wttMultipleRerunInputPath, string xunitOutputPath) { TestPass testPass = TestPass.ParseTestWttFileWithReruns(wttInputPath, wttSingleRerunInputPath, wttMultipleRerunInputPath, cleanupFailuresAreRegressions: true, truncateTestNames: false); var results = testPass.TestResults; @@ -510,8 +493,8 @@ namespace HelixTestHelpers // If the test failed sufficiently often enough for it to count as a failed test (determined by a property on the // Azure DevOps job), we'll later mark it as failed during test results processing. - int failedCount = results.Where(r => !r.PassedOrUnreliable(requiredPassRateThreshold)).Count(); - int skippedCount = results.Where(r => !r.Passed && r.PassedOrUnreliable(requiredPassRateThreshold)).Count(); + int failedCount = results.Where(r => !r.Passed).Count(); + int skippedCount = results.Where(r => (!r.Passed && r.Skipped)).Count(); var root = new XElement("assemblies"); @@ -557,12 +540,13 @@ namespace HelixTestHelpers string resultString = string.Empty; - if (result.Passed) + if (result.Passed && !result.Skipped) { resultString = "Pass"; } - else if(result.PassedOrUnreliable(requiredPassRateThreshold)) + else if (result.Skipped) { + resultString = "Skip"; } else @@ -571,31 +555,25 @@ namespace HelixTestHelpers } - test.SetAttributeValue("result", resultString); - if (!result.Passed) { - // If a test failed, we'll have rerun it multiple times. - // We'll save the subresults to a JSON text file that we'll upload to the helix results container - - // this allows it to be as long as we want, whereas the reason field in Azure DevOps has a 4000 character limit. - string subResultsFileName = methodName + "_subresults.json"; - string subResultsFilePath = Path.Combine(Path.GetDirectoryName(wttInputPath), subResultsFileName); - - if (result.PassedOrUnreliable(requiredPassRateThreshold)) + if (result.Skipped) { var reason = new XElement("reason"); - reason.Add(new XCData(GetUploadedFileUrl(subResultsFileName, helixResultsContainerUri, helixResultsContainerRsas))); + reason.Add(new XCData("Test skipped")); test.Add(reason); } - else - { + else { var failure = new XElement("failure"); var message = new XElement("message"); - message.Add(new XCData(GetUploadedFileUrl(subResultsFileName, helixResultsContainerUri, helixResultsContainerRsas))); + message.Add(new XCData("Test failed")); failure.Add(message); test.Add(failure); } } + + test.SetAttributeValue("result", resultString); + collection.Add(test); } diff --git a/build/Helix/InstallTestAppDependencies.ps1 b/build/Helix/InstallTestAppDependencies.ps1 deleted file mode 100644 index da0a049d27..0000000000 --- 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 3ea7abf80a..0000000000 --- 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 476502e987..0000000000 --- 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/OutputTestErrorsForAzureDevops.ps1 b/build/Helix/OutputTestErrorsForAzureDevops.ps1 new file mode 100644 index 0000000000..863db32afe --- /dev/null +++ b/build/Helix/OutputTestErrorsForAzureDevops.ps1 @@ -0,0 +1,38 @@ +Param( + [Parameter(Mandatory = $true)] + [string]$XUnitOutputPath +) + +# This script is used to parse the XUnit output from the test runs and print out +# the tests that failed. +# +# Why you might ask? Well, it sure seems like Azure DevOps doesn't like the fact +# that we just call our tests in a powershell script. It can't seemingly find +# the actual errors in the TAEF logs. That means when you just go to the +# "Checks" page on GitHub, the Azure DevOps integration doesn't have anything +# meaningful to say other than "PowerShell exited with code '1'". If we however, +# just manually emit the test names formatted with "#[error]" in front of them, +# well, then the integration will all work like magic. + +# Load the test results as a XML object +$testResults = [xml](Get-Content -Path $XUnitOutputPath) + +# Our XML looks like: +# +# +# +# + +# Iterate over all the assemblies and print all the tests that failed +foreach ($assembly in $testResults.assemblies.assembly) { + foreach ($collection in $assembly.collection) { + foreach ($test in $collection.test) { + if ($test.result -eq "Fail") { + # This particular format is taken from the Azure DevOps documentation: + # https://github.com/microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md + # This will treat this line as an error message + Write-Output "##vso[task.logissue type=error]$($test.name) Failed" + } + } + } +} diff --git a/build/Helix/OutputTestResults.ps1 b/build/Helix/OutputTestResults.ps1 deleted file mode 100644 index a23b456afe..0000000000 --- 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 3f9600492e..0000000000 --- a/build/Helix/PrepareHelixPayload.ps1 +++ /dev/null @@ -1,68 +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" - -# Copy the APPX package from the 'drop' artifact dir and Windows Kits -Copy-Item "$repoDirectory\Artifacts\$ArtifactName\appx\CascadiaPackage_0.0.1.0_$Platform.msix" $payloadDir\CascadiaPackage.zip -Copy-Item "C:\program files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.VCLibs.Desktop\14.0\Appx\Retail\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" $payloadDir\VCLibs.zip - -# Rename it to extension of ZIP because Expand-Archive is real sassy on the build machines -# and refuses to unzip it because of its file extension while on a desktop, it just -# does the job without complaining. - -# Extract the APPX package -Expand-Archive -LiteralPath $payloadDir\CascadiaPackage.zip -DestinationPath $payloadDir\appx -Expand-Archive -LiteralPath $payloadDir\VCLibs.zip -DestinationPath $payloadDir\appx -Force diff --git a/build/Helix/ProcessHelixFiles.ps1 b/build/Helix/ProcessHelixFiles.ps1 deleted file mode 100644 index dcd3608a5d..0000000000 --- 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 $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 db00314ff0..0000000000 --- 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 ecf4e8bf7a..0000000000 --- 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 53d8ed33c4..0000000000 --- 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 b101a50d6f..0000000000 --- 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 d51fc973c1..0000000000 --- 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 5c543a141b..0000000000 --- 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 d01e9c9eab..0000000000 --- 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/esrp.build.batch.conpty.json b/build/config/esrp.build.batch.conpty.json new file mode 100644 index 0000000000..b0097056ec --- /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 0000000000..c33123a16d --- /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 0000000000..6243204666 --- /dev/null +++ b/build/config/esrp.build.batch.wpf.json @@ -0,0 +1,46 @@ +[ + { + "MatchedPath": [ + "Microsoft.Terminal.Control/Microsoft.Terminal.Control.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 0000000000..0699353f64 --- /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/config/release.gdnbaselines b/build/config/release.gdnbaselines new file mode 100644 index 0000000000..b8f686e3a9 --- /dev/null +++ b/build/config/release.gdnbaselines @@ -0,0 +1,1127 @@ +{ + "hydrated": false, + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines", + "hydrationStatus": "This file does not contain identifying data. It is safe to check into your repo. To hydrate this file with identifying data, run `guardian hydrate --help` and follow the guidance." + }, + "version": "1.0.0", + "baselines": { + "default": { + "name": "default", + "createdDate": "2023-09-29 19:39:12Z", + "lastUpdatedDate": "2023-09-29 19:39:12Z" + } + }, + "results": { + "b1e592dc96e9286d806682680cffcb44b7ba2f7ea883b022371037689fe2bb5b": { + "signature": "b1e592dc96e9286d806682680cffcb44b7ba2f7ea883b022371037689fe2bb5b", + "alternativeSignatures": [ + "25d5f2d86325f3e6666cf720c3b0827c7b5773c42f3770a6e83ab8bc0f716686" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "fd608e2146a69491516290ae3b65d4d094e5f9ba53c6624ff457c3b21f67e433": { + "signature": "fd608e2146a69491516290ae3b65d4d094e5f9ba53c6624ff457c3b21f67e433", + "alternativeSignatures": [ + "528526d5f81c7387e9eca88c1dc1e7886bb3189d9c53f7678c63a44a7e78d2b8" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "104eb68a26c9a422f9973b85603046c42bb6894f398e1ebd454d829a386c47cd": { + "signature": "104eb68a26c9a422f9973b85603046c42bb6894f398e1ebd454d829a386c47cd", + "alternativeSignatures": [ + "33b75434f899dc72a71c55e6667420b5f3406641eae1a784bad8632fe80eccc6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "fa8ab295467d81fd138402f12899da84cdf312e2e84712a539e08361b6900651": { + "signature": "fa8ab295467d81fd138402f12899da84cdf312e2e84712a539e08361b6900651", + "alternativeSignatures": [ + "b0ef4eae3f158488f99f7705cc93d037d55e8bd39b173c40e25581a35c6889d2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2d5cf239e62a256384275134ea1e1180b927f4cd53e51193751b0b5671045be8": { + "signature": "2d5cf239e62a256384275134ea1e1180b927f4cd53e51193751b0b5671045be8", + "alternativeSignatures": [ + "bd73f2f051d6525c01db61a4c4a6c3b9a80083da83ed602eae2a70e17286a7ed" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e88b3902e68d0315f7bef3a24860a7d3d9abd2d960fde06a0c650c6bb569df74": { + "signature": "e88b3902e68d0315f7bef3a24860a7d3d9abd2d960fde06a0c650c6bb569df74", + "alternativeSignatures": [ + "143ade735d3d6d19f999555f2ce706f647a64688054b5f19052f99992ab325fd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "01306239b92e4508b0ab60e025cadc4c974c33c29f1eb28d2b3fdda8d4019e09": { + "signature": "01306239b92e4508b0ab60e025cadc4c974c33c29f1eb28d2b3fdda8d4019e09", + "alternativeSignatures": [ + "dca24210a3064ca7069a737f9a88f4a25b1cc1cd4c9e2a095156297246611de5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "675ea9bd7809b2f210105e9703dffe8e48ee134ab21677e7cc4556bf22782e8d": { + "signature": "675ea9bd7809b2f210105e9703dffe8e48ee134ab21677e7cc4556bf22782e8d", + "alternativeSignatures": [ + "7d5287a33645767969627a20e3a65fb2f1bb2db0e79a689b5a804a98f65d01fa" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6e040bc57aa08165f8af6007e3794149d3a2c0325021e746e9705ada7c05b30c": { + "signature": "6e040bc57aa08165f8af6007e3794149d3a2c0325021e746e9705ada7c05b30c", + "alternativeSignatures": [ + "218c0d7d5332113e33632d06eb0f209bac711ac515bd76a0fcfda2a0cbed885d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "73e85b3af96a27c797fbe9f36f00feccd4118204615b9477da67e5868dd5dfb9": { + "signature": "73e85b3af96a27c797fbe9f36f00feccd4118204615b9477da67e5868dd5dfb9", + "alternativeSignatures": [ + "49c44c3dc27233eba172214cb302de4b5c9b2a845e8595e7930fad74805183ac" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "15bd7ecdb19cdd263867db23c12a98029cede5476c89e870b811fce45abb8db4": { + "signature": "15bd7ecdb19cdd263867db23c12a98029cede5476c89e870b811fce45abb8db4", + "alternativeSignatures": [ + "2705f7f6c9e83d2a812e264f39878e0e0173a1a5efea16e7206c4d02dc419af8" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3f7d3360f0b185361b1e67b94c22b2a319b7c2d5aa2c8fd7fc3e4626394cba54": { + "signature": "3f7d3360f0b185361b1e67b94c22b2a319b7c2d5aa2c8fd7fc3e4626394cba54", + "alternativeSignatures": [ + "71f05133f86c279daee4f1b85f997955e36857bcc5945dbae71f8bbb91b5df6d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3bcb157fd01d81513a15da41b4fc8b2820247331689fef5d0026578d95b85716": { + "signature": "3bcb157fd01d81513a15da41b4fc8b2820247331689fef5d0026578d95b85716", + "alternativeSignatures": [ + "67e145d1f3bf6eb205e385884ca6356decffc3cac3e15339cc7ca43441cff99b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "27ae6a9a7047c5ca98d1dc3a9b0885733875387d475f1426afc4d031b357fc1b": { + "signature": "27ae6a9a7047c5ca98d1dc3a9b0885733875387d475f1426afc4d031b357fc1b", + "alternativeSignatures": [ + "b87d72abe37f6144614469221a0ce60e389792843f602805afe149b3ad1dd098" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "bd2aea1d24e36c1dcc128eb87a2c27292ed251d95aedf5f11204647bc68719f8": { + "signature": "bd2aea1d24e36c1dcc128eb87a2c27292ed251d95aedf5f11204647bc68719f8", + "alternativeSignatures": [ + "ca29329aae819543d357e86acf0ccce914dbc3a15e0f91016091c3079638ad34" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b836a6e0d58634003e054c88f3ed615d8e92182ebc78e9dd2168d8126c33e694": { + "signature": "b836a6e0d58634003e054c88f3ed615d8e92182ebc78e9dd2168d8126c33e694", + "alternativeSignatures": [ + "850aa5d2e92dac06fc119ed297e84dfde9fdb3f9b116bdb2c83e8d66ea442865" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e54972bb37d35b3b9c7c764cdf5351f0254ae2b8526465fe03c9ba8a04b06651": { + "signature": "e54972bb37d35b3b9c7c764cdf5351f0254ae2b8526465fe03c9ba8a04b06651", + "alternativeSignatures": [ + "9e8723695743edaddfc5b188ec6bde240ddde68516f5c2342bd1c4d2a8c68430" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3786d03543e25eec1f68beb21a3884953460239c269ba4071baf9fd4f564e763": { + "signature": "3786d03543e25eec1f68beb21a3884953460239c269ba4071baf9fd4f564e763", + "alternativeSignatures": [ + "05e0c9bc16b8ade4d40d4e4371864da4ee7d9c8d68b19c3319543dbebea97a39" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5f7c1c0c1f9290d3afad4bb3a17d5e5d33f2176e18b8ec406df71e8245ec0b7f": { + "signature": "5f7c1c0c1f9290d3afad4bb3a17d5e5d33f2176e18b8ec406df71e8245ec0b7f", + "alternativeSignatures": [ + "952d91003467bdf2dbf5e72f56b9c69de52c4abf44cc82c0833ba8d89d9befac" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "605037e76782777be2f16c12c023247671ebad0afea868ed0c4bf939c9ff443a": { + "signature": "605037e76782777be2f16c12c023247671ebad0afea868ed0c4bf939c9ff443a", + "alternativeSignatures": [ + "4f77f69716e098fec8214948428cfe9d2c2324d0719250b25e5c084a21d05e69" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4a173deb8aeab5f4f2463774f117b909dee0168c99bc54b29b1141fca64598bd": { + "signature": "4a173deb8aeab5f4f2463774f117b909dee0168c99bc54b29b1141fca64598bd", + "alternativeSignatures": [ + "66526149b57bbdaa8d4e3437a0099a4e3b7818c911b2110fa40d7d0264ce99f6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e557e9c4d1c36f69fd4a2310d22bbeadc1dff3dd2404449b8424662bb1c6adc7": { + "signature": "e557e9c4d1c36f69fd4a2310d22bbeadc1dff3dd2404449b8424662bb1c6adc7", + "alternativeSignatures": [ + "6b93c58cd83ba89289c12b408c2988dfd54d7936c0920981cf690725155caf7a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "70eb589101b9b57d2e47f6879348a8b93cd0c2f223e2c20d2289c5e1e8be0b9d": { + "signature": "70eb589101b9b57d2e47f6879348a8b93cd0c2f223e2c20d2289c5e1e8be0b9d", + "alternativeSignatures": [ + "e9fe7fc2a31de263521f635058984dd81b4bab9cba174949fc5582fbeef6f693" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3e5c0b1cad192bf5fa00d6b0f46f4cfaa7251ebaea30177d2d6891890f0dafab": { + "signature": "3e5c0b1cad192bf5fa00d6b0f46f4cfaa7251ebaea30177d2d6891890f0dafab", + "alternativeSignatures": [ + "9a1dd0e934ddd3a33f3cf37372052731f5c157c13cac05ef0539815f845db9bf" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "dd3b2ddb278bb70559efcfc10e19c2f3a3bfe805cbbecd6a16dd141433f0b433": { + "signature": "dd3b2ddb278bb70559efcfc10e19c2f3a3bfe805cbbecd6a16dd141433f0b433", + "alternativeSignatures": [ + "3fcb3a01715c6d244d26ef49fa4c06f8a92616a9c6465c26c4398c554f7fd67f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a635cb613925ab6c9f6b133890169bdf77acee212b6ac8f83cca7253faafaec7": { + "signature": "a635cb613925ab6c9f6b133890169bdf77acee212b6ac8f83cca7253faafaec7", + "alternativeSignatures": [ + "2ff5cc7a3bf6cd63862cffc5088690fc824eb1d2f9e42c732fcd1611e9e25bfc" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4ecdfc33e6267bfeadb1507d5304a9737f3ba0ee3132e6d7bc6f3fd68a47824e": { + "signature": "4ecdfc33e6267bfeadb1507d5304a9737f3ba0ee3132e6d7bc6f3fd68a47824e", + "alternativeSignatures": [ + "23dc3273d54a3db12ad729569696b875523201dc13d4cbba9e3642dde17492bb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a9b9f171556057eaf197eb19780abfac5df82154f6c06c07240ce73970127df7": { + "signature": "a9b9f171556057eaf197eb19780abfac5df82154f6c06c07240ce73970127df7", + "alternativeSignatures": [ + "0a9f4d3a4aa85e918744a5af36851791eb4b2f44fc132560cc56cf5661ba28fb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "ad9da2b392f1622c6cff4a2b63eadf0da2ff31811dbbf8c9f2b2c4e8d9fefa1f": { + "signature": "ad9da2b392f1622c6cff4a2b63eadf0da2ff31811dbbf8c9f2b2c4e8d9fefa1f", + "alternativeSignatures": [ + "b1fafebeb213b04346d56182ce395ccb9a0ad866dc28bda801c222499e550396" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b8f591f33a84c38402239941484ead425c3e37bfcfd486b9be3a15d2213d9fa3": { + "signature": "b8f591f33a84c38402239941484ead425c3e37bfcfd486b9be3a15d2213d9fa3", + "alternativeSignatures": [ + "de56fbe02702da318ea6a113e592b2efee3f942e04e31c5e9300cf6798abea0c" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "37adbb986601cf75f2d1eaebbe6d430a70a0886e60685a880e7d1f2c48f9b125": { + "signature": "37adbb986601cf75f2d1eaebbe6d430a70a0886e60685a880e7d1f2c48f9b125", + "alternativeSignatures": [ + "7ebe5c4d5a144a3f356114bf5e062295ee90d5cdc7ca615dc729b7dcba8bed33" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e0de291c50fb0cae3bdaf75237ac8e3f34acc6a8de18ba589dde4e76352ab849": { + "signature": "e0de291c50fb0cae3bdaf75237ac8e3f34acc6a8de18ba589dde4e76352ab849", + "alternativeSignatures": [ + "52c0187472041933b5d850e47cf28ac6a7ef5ba0a6a85bca65af3b3531ddb06a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6b163fce31228367e0323768ab0dcc1224f8416cc856ddc03ccc2b8a2cf9a08e": { + "signature": "6b163fce31228367e0323768ab0dcc1224f8416cc856ddc03ccc2b8a2cf9a08e", + "alternativeSignatures": [ + "0e6f9f2e1008091bcfbf520d7407c3690564ea5118580fd8cfca965946f53a78" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "25012b8cab7c6dddc276e2c0d7888554e8542dd686891914d2c2e82dfd3f697a": { + "signature": "25012b8cab7c6dddc276e2c0d7888554e8542dd686891914d2c2e82dfd3f697a", + "alternativeSignatures": [ + "ea05d52f5ee2b483e83c14216a3e56bc8a4474396b71cbb426ced61242d81919" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "bfc7ac3b82698c138a970968cce2c849ed0e45bb8930d42215f6704659f63762": { + "signature": "bfc7ac3b82698c138a970968cce2c849ed0e45bb8930d42215f6704659f63762", + "alternativeSignatures": [ + "c59c7529464c717d1909058d1f4fbc5109881effafab3c67bbbf02ccdffddd4e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "25582fa1ab3be3a375a0d053304e65b50ebfa590f81a5d957759304ba29e3053": { + "signature": "25582fa1ab3be3a375a0d053304e65b50ebfa590f81a5d957759304ba29e3053", + "alternativeSignatures": [ + "e2628baf1e09340a2063cd39c238e541014a65f4433018cc36cd25c5c8c3043d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "38b73c3711c0c9cd958195f318fd2e6ab1f10cfa94abdfb7505d784ba022cfae": { + "signature": "38b73c3711c0c9cd958195f318fd2e6ab1f10cfa94abdfb7505d784ba022cfae", + "alternativeSignatures": [ + "6688a9492755e33e541513bf297d0b4f459f70a43486fd8411749a2caf5cb91c" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "250a0184e9feee525e9a54930f5714965bd2bc29d0fe91c502c560443fa0e10a": { + "signature": "250a0184e9feee525e9a54930f5714965bd2bc29d0fe91c502c560443fa0e10a", + "alternativeSignatures": [ + "d7b1083c67959b8f095acd532a10244a0a5afcbc93eefc66be20c19841b2588e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2c64c980db98b04a7daeedf4c5d358b8ecaecca81a8e0f70911fd211e06c8e20": { + "signature": "2c64c980db98b04a7daeedf4c5d358b8ecaecca81a8e0f70911fd211e06c8e20", + "alternativeSignatures": [ + "7778c4704d515b92d4dc77a9b612c151e6bb9e7cd88e100c7d0419fb150f34e6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "3bb5f422737103df8f018ec0789d2b7793ce5b1641927d5646e963126e73e1b5": { + "signature": "3bb5f422737103df8f018ec0789d2b7793ce5b1641927d5646e963126e73e1b5", + "alternativeSignatures": [ + "225e02e47ea3f999bc43279246ee51b4287e4e2b4b7dac899da91c365d8acd0e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6e39bdcfe28e2cb6868a5dcbe12fa712d2a60762bb57fd82734089e19ca85b52": { + "signature": "6e39bdcfe28e2cb6868a5dcbe12fa712d2a60762bb57fd82734089e19ca85b52", + "alternativeSignatures": [ + "f6ebab2bea88d78f69c8ae78f4ec641edd0f0796b9ca193260c44cee9d543a18" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e9a5cff46470c62be3e1db1e43785b71daa88e466c802d4393bf9248a9e6fb56": { + "signature": "e9a5cff46470c62be3e1db1e43785b71daa88e466c802d4393bf9248a9e6fb56", + "alternativeSignatures": [ + "3bdabc64cff83a1322f107a97ce6eab6e8cb41b8492643e9a0df3561d0169b95" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9f19392621589641c13fc5762e36429cc4d38fc04f98297006be89e5ca3e5631": { + "signature": "9f19392621589641c13fc5762e36429cc4d38fc04f98297006be89e5ca3e5631", + "alternativeSignatures": [ + "f448a0535740dcb9bddebe7765ea55c819472efdb9cd63761dc156b8af11a260" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "60b306912ae3399a167514dabf495a7f57e0ff17ec9a9491e66cf39c1ecbf60b": { + "signature": "60b306912ae3399a167514dabf495a7f57e0ff17ec9a9491e66cf39c1ecbf60b", + "alternativeSignatures": [ + "fbd4d1fe2e8e21ed5fe000fc592ef9fa98ff52f529b849c9ceca12c85c3cf4f3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2498be9bbe27f393fad6af89f23cf228fe5c263e38fa3d20d3652b68c356243b": { + "signature": "2498be9bbe27f393fad6af89f23cf228fe5c263e38fa3d20d3652b68c356243b", + "alternativeSignatures": [ + "750b9e1ff2f1b443502036d226678bcdda2f22a3879e36b61e3bcec24c0eec3e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "147fa7e9b0187ab163dfe5049876c838b2b599a8ac2c243226ab4fed8d9e3513": { + "signature": "147fa7e9b0187ab163dfe5049876c838b2b599a8ac2c243226ab4fed8d9e3513", + "alternativeSignatures": [ + "5fe9a0ee3ada52e4dbee1aa6f45683a82d14ed812d9d7c1634b786f28fc2ca51" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a2fe2c57ef4efc03e72dfd1fa653fc9d731b69bda91711f8ab8d46edc3b12667": { + "signature": "a2fe2c57ef4efc03e72dfd1fa653fc9d731b69bda91711f8ab8d46edc3b12667", + "alternativeSignatures": [ + "42ab6b70626c246ddea96b060cb7a79248576d19f003c4f4eedf171f4f5992f4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "e7973a3b90d74d4ff6ee544dee45bb35ac66214a3e92eadecbf8fafccca9abb1": { + "signature": "e7973a3b90d74d4ff6ee544dee45bb35ac66214a3e92eadecbf8fafccca9abb1", + "alternativeSignatures": [ + "3359d44d5bbf533487fc65e4a98220af62eef0cc6b883bc35b8286ee1a31ca4f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "190440674b3fec2fcc77b341a684a9e9100c8dec0911a8c83f19f6ccedcb2b78": { + "signature": "190440674b3fec2fcc77b341a684a9e9100c8dec0911a8c83f19f6ccedcb2b78", + "alternativeSignatures": [ + "c7aab0deb9b97032353766e961f4b23bc694e85b8b06709beaddfa2e5743e742" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4d258afa7d015f4fc96c8866e2e43cb00290526d56394eec3848ed650cec840d": { + "signature": "4d258afa7d015f4fc96c8866e2e43cb00290526d56394eec3848ed650cec840d", + "alternativeSignatures": [ + "e23b41b521b849057b8c5ba0594af11fc5ceec59714f3d0ebbeda708511d5803" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "530d02de7ec8a3c9f575cf3770e34665a58b127e01555000e611ee4c5e37e24d": { + "signature": "530d02de7ec8a3c9f575cf3770e34665a58b127e01555000e611ee4c5e37e24d", + "alternativeSignatures": [ + "23f217577bcf9d3a6f87c0ac259cf891a80e78f2b77dd58e631816f3183101bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "69a35d8bd5d4766e5dc981526229ccc0d27f238b06357bdd8e0469cd35b47d08": { + "signature": "69a35d8bd5d4766e5dc981526229ccc0d27f238b06357bdd8e0469cd35b47d08", + "alternativeSignatures": [ + "cab1cab825e5846acc097221236feaf3a06895e648c2ef16a4c285616b714987" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c78a17b7f8d46b9ddedd34240d76adf92e4f1bf27a978c022e4599fe6a7b150d": { + "signature": "c78a17b7f8d46b9ddedd34240d76adf92e4f1bf27a978c022e4599fe6a7b150d", + "alternativeSignatures": [ + "fa75ccd56697c3073a6ef17e2d959ee24b6afc11eabc4bd5dbea1d74ecdb81cb" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "7bd1757994e7f1909ffd25e56393f991d436232150cff2555a97e9b08b347b99": { + "signature": "7bd1757994e7f1909ffd25e56393f991d436232150cff2555a97e9b08b347b99", + "alternativeSignatures": [ + "dbdb5c5ebe211b21d9b25e346dfbc58e6b32500258a1efed248e4630216ce57e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "cef01bb2a477968c8cbc997179d04b90c36febd9cfd21ca973d4a22b3efc4caa": { + "signature": "cef01bb2a477968c8cbc997179d04b90c36febd9cfd21ca973d4a22b3efc4caa", + "alternativeSignatures": [ + "9ec52d86604abde8a13124534ec587d3a1686630ea7bb2f736a66963349abdbd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "11026da7398e29b59d98ffee5ed5317c243407638a77d7a09637ebe2d8bb09f3": { + "signature": "11026da7398e29b59d98ffee5ed5317c243407638a77d7a09637ebe2d8bb09f3", + "alternativeSignatures": [ + "a642f1f37cf06c75c2082d720e317481bb7e73249dcda33dba0b5cdd6933926f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8f2550b4f39a798d217679c04dd7cb21b72ef63e6f6d63b06aadb1c40d8c2546": { + "signature": "8f2550b4f39a798d217679c04dd7cb21b72ef63e6f6d63b06aadb1c40d8c2546", + "alternativeSignatures": [ + "10c95aac8513a0d6cc2c0ef08b51f7f4ac3be4b23ddd90d68d06fc536421acc3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "022eb0065260832f06f12313f8bf99765488c1db9ff9ef8ecf8a0cbf393e878d": { + "signature": "022eb0065260832f06f12313f8bf99765488c1db9ff9ef8ecf8a0cbf393e878d", + "alternativeSignatures": [ + "4801e53b7e46778aa21a8c9a0ec2725e73746fb648e76c9359f6db0f32ac2963" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "36f66ad8b00e978f29a7b1c09c7942247e3b068d131431c3b321190aa7cb2978": { + "signature": "36f66ad8b00e978f29a7b1c09c7942247e3b068d131431c3b321190aa7cb2978", + "alternativeSignatures": [ + "7d3ee020a3a1601c49be0c5d687f46858f679ecf6d4150b97ba9ae5abfa742e4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c50b78c8370aa9d767f783bddf8c57b2f1dbd749073ccde60b3b210b116b6951": { + "signature": "c50b78c8370aa9d767f783bddf8c57b2f1dbd749073ccde60b3b210b116b6951", + "alternativeSignatures": [ + "87568cc368760fe9f38d9fb95e93297d8bb5a83587d20d071f97713443df2e04" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8e2be08973d6028cf58e152fba8dcd5616b7938591d2292eaf3bb5f80bdcf004": { + "signature": "8e2be08973d6028cf58e152fba8dcd5616b7938591d2292eaf3bb5f80bdcf004", + "alternativeSignatures": [ + "d9df5f887062a990099a2cb70e46778aad25304d9758db556040beb4b23a8776" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "be36363b685b28e3ac4457b7a415120cab5a01c14d5c9a19192ef5ebd71c176a": { + "signature": "be36363b685b28e3ac4457b7a415120cab5a01c14d5c9a19192ef5ebd71c176a", + "alternativeSignatures": [ + "13779d67c6e5f5eba285b83093a2c9eec2d79b3b36ee86400873191310088255" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a6e48721d67f94b2876a97fe4b385ee37adc1b345825f751e87d6bd05095bfb9": { + "signature": "a6e48721d67f94b2876a97fe4b385ee37adc1b345825f751e87d6bd05095bfb9", + "alternativeSignatures": [ + "f6a5898d9652b23b269283d4bc1a8e95865ee9f7125479ab78087e3dd3b3b12f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b1b1aba985591c904b0faf09b54ddc4763890d25e5dcfa2d3ca758165a616970": { + "signature": "b1b1aba985591c904b0faf09b54ddc4763890d25e5dcfa2d3ca758165a616970", + "alternativeSignatures": [ + "b7ad084f971c1fae38509e87d50fd4984185ea6915d501d6cc0d1eac8bd93abf" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "078e8e5c43a51876fb09d7fab78a07fe896bbbb74c9507f3b6313ed3fe7586d4": { + "signature": "078e8e5c43a51876fb09d7fab78a07fe896bbbb74c9507f3b6313ed3fe7586d4", + "alternativeSignatures": [ + "7046bab04be2bfce75f0a34461871d3dccddab009c2a223a78c08696610c7a55" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9443465290e747bbefb4435296af78a82a948222097df6559db0d0488f7df81a": { + "signature": "9443465290e747bbefb4435296af78a82a948222097df6559db0d0488f7df81a", + "alternativeSignatures": [ + "47a1795431bb9f78383c54fd33cf3427a774ae9802aa994cbd688f81286904bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "01a56e39bd6a3652d95b21035efc6d165eb16451f9558d79ff13d297cd345cd9": { + "signature": "01a56e39bd6a3652d95b21035efc6d165eb16451f9558d79ff13d297cd345cd9", + "alternativeSignatures": [ + "2169e62964a8fa3bb4ccde41e33232efa8b7ddfb6d5a45688c28318c96ccbe52" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a9c67896453ec9e7fb742e05ec0a47f13c5c6b7bd3886497afe3c3bb860ef006": { + "signature": "a9c67896453ec9e7fb742e05ec0a47f13c5c6b7bd3886497afe3c3bb860ef006", + "alternativeSignatures": [ + "5973211461980400fe2aeea7dafe6079e6f771d799c347a0bcb7c631dc379d5e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2689cc88d4721d762e3783920aa7150cd00e4b5f7f15bdd81476dbb56876aa08": { + "signature": "2689cc88d4721d762e3783920aa7150cd00e4b5f7f15bdd81476dbb56876aa08", + "alternativeSignatures": [ + "ba41ff939940ab49d1a4b39dfb5fac6108111616c0d7fb87e6f64f1b82cbfbd2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "64d08c256034d4907fa6655e4914850c945bf1f6db4e14f52a8032eefe8e1245": { + "signature": "64d08c256034d4907fa6655e4914850c945bf1f6db4e14f52a8032eefe8e1245", + "alternativeSignatures": [ + "639e5b7c91137d49bd041d7f241bd2403067f445de5058048c4c6c926af226bd" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "689ed96056f498ca67a5a056efa247ee9aea196362e693a0b196e6e9eb2cba0f": { + "signature": "689ed96056f498ca67a5a056efa247ee9aea196362e693a0b196e6e9eb2cba0f", + "alternativeSignatures": [ + "9473554d81655d5cf548e8033fc5f34cd42cec8697306b75fc2ec31e8c19b29e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f497cbbab92fbbd65c27b122c7ceaf0d181859365ea121a885d32898dec963c6": { + "signature": "f497cbbab92fbbd65c27b122c7ceaf0d181859365ea121a885d32898dec963c6", + "alternativeSignatures": [ + "2fe25bb29ed8a90e5dba2e8e899f42713b61d178fd18231d560d4873656fe8a4" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "669b421bcaea46b4f3a2b63097a357920df16b02075d1b86a476f9ee6fc4314a": { + "signature": "669b421bcaea46b4f3a2b63097a357920df16b02075d1b86a476f9ee6fc4314a", + "alternativeSignatures": [ + "5647958533f690de004b8f82064ed0fc779fe1206b64a1813f90988481dfb72e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8bb84450c75dcbee365ee2b2eb2ff83ef91925afb50d9c96aec845729f3a9be1": { + "signature": "8bb84450c75dcbee365ee2b2eb2ff83ef91925afb50d9c96aec845729f3a9be1", + "alternativeSignatures": [ + "85d7ccfcf73dbbc98b78cbd81581f3e81e9bc9a632a589fd79abb6f9cdd3236d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "9beca9c8ed341ed7dbc682811ca43ec7174d945df99613c85e76e45ad55ac7f3": { + "signature": "9beca9c8ed341ed7dbc682811ca43ec7174d945df99613c85e76e45ad55ac7f3", + "alternativeSignatures": [ + "147836cc11afe71d92bdf75ef6d5b1b97ae5f5709d07a9d8c58ef8bce0631a86" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "a0f61b31ca15ee653301086835bfb62889b4f4b537fd703ccfc21df9aa280a59": { + "signature": "a0f61b31ca15ee653301086835bfb62889b4f4b537fd703ccfc21df9aa280a59", + "alternativeSignatures": [ + "62828b4278eba310bc7982e5afdac6b0a865bb647f8d3cb078a6391e1a48054f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "235159b32d9022a9e8bf8e37fd5489bb11ca6f1bf56e8ee8737e63358c3610e5": { + "signature": "235159b32d9022a9e8bf8e37fd5489bb11ca6f1bf56e8ee8737e63358c3610e5", + "alternativeSignatures": [ + "ddb5cc4ca95b12e6fcccfd2c5089c798307d3bb664d91e9a87f6efbd1ea78a19" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "48738e93956293a663a2d1f6933bd6856afc4699c22d45da51ca02ffe4781230": { + "signature": "48738e93956293a663a2d1f6933bd6856afc4699c22d45da51ca02ffe4781230", + "alternativeSignatures": [ + "e83017371567ceabc524ef3e26a5520be1aac07e75b1f02a173f2eb847fbc3e5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "6d0c84954b066bde9f165d3e7471f621d824bf0f9d920125f2188196c3e7be99": { + "signature": "6d0c84954b066bde9f165d3e7471f621d824bf0f9d920125f2188196c3e7be99", + "alternativeSignatures": [ + "07e93499ac1047ac94bc10474ba57d397bf395f28f8d6e0dd08eeab03f17a539" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b033555adb13ce0c0ec9e65689e4be5f9856f49f9e201f3dbbb24d430d76ec15": { + "signature": "b033555adb13ce0c0ec9e65689e4be5f9856f49f9e201f3dbbb24d430d76ec15", + "alternativeSignatures": [ + "0a59d205aee1fdc5cb027a46c424e4a2da06c5650aadbfdfec77d744278d6b7b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c522ca7bfcad2bc36cfb9567a5bbe36395bb3a5969ce411832197ff9ccfd4b32": { + "signature": "c522ca7bfcad2bc36cfb9567a5bbe36395bb3a5969ce411832197ff9ccfd4b32", + "alternativeSignatures": [ + "32eb8b00cbe99b945a02337c349a0fb97e1763fa9782469531a6859714a7160f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1c58db43c898e119381ecd4a9bb9701f2bbbffd3d0889ca734a905c151b168db": { + "signature": "1c58db43c898e119381ecd4a9bb9701f2bbbffd3d0889ca734a905c151b168db", + "alternativeSignatures": [ + "a87bb1c17c23667355c9b8e17f84f37671b65040a3aa7870a7d169165583211e" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "ef7ba70ebe8984da430b1787f439496e14810afd9848a851a3d4cacc37ae7eac": { + "signature": "ef7ba70ebe8984da430b1787f439496e14810afd9848a851a3d4cacc37ae7eac", + "alternativeSignatures": [ + "bbff3ea19b6dd5baa7d56928526a38313685c1d81a30c7c2e7c31d7105d8287f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5bb44f02281666fb7ef18753b4ed0e08923d741efe0361ab1336c18dd0086111": { + "signature": "5bb44f02281666fb7ef18753b4ed0e08923d741efe0361ab1336c18dd0086111", + "alternativeSignatures": [ + "a4f1b29f64aa99073ad3678c41b29ab15bd3af2cbc87ae6b05e6568265734146" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1621b0375d4c98eb6822a7ae37feccda594bbd0137cba47b721f9f9288929947": { + "signature": "1621b0375d4c98eb6822a7ae37feccda594bbd0137cba47b721f9f9288929947", + "alternativeSignatures": [ + "05396ecc761c29e0e0b0a1508c1ed091b213ea18917b9f0ba118c1e7032c51df" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c566d2437202984d3a9d2682ca810d7c52eb78c7b6236c7b9c82db6f7ba63ac8": { + "signature": "c566d2437202984d3a9d2682ca810d7c52eb78c7b6236c7b9c82db6f7ba63ac8", + "alternativeSignatures": [ + "2bcbfb431661bacfa69c2c43c8f09f47444b22353d9e3f7db28f98c7b8126bd6" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "8f8856d0c4f85bb601ea84e7b9da0d5d636573c772951c4e85bf4cf61fef72bc": { + "signature": "8f8856d0c4f85bb601ea84e7b9da0d5d636573c772951c4e85bf4cf61fef72bc", + "alternativeSignatures": [ + "769ffcc5ae3360b1017647384c184dc56574e0d9ddd00f4083e1c986e9f616b2" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b4c31c908facbb22d23b15c3280359184d736901c154d516be16495dbb35ccb0": { + "signature": "b4c31c908facbb22d23b15c3280359184d736901c154d516be16495dbb35ccb0", + "alternativeSignatures": [ + "d4603cccc6c303b5f16f1120b15dc74e8bdbef5877922fdcf57f3b29423c027b" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "7e3401dab114451f8ccc26a6ee0ac27639a41cef983b5c7ca7b22f8e03748b53": { + "signature": "7e3401dab114451f8ccc26a6ee0ac27639a41cef983b5c7ca7b22f8e03748b53", + "alternativeSignatures": [ + "32964f7bed67a8e77a61cdc1cdedfb55c8800e04f16e9673dbc1c9dda0d2fcf0" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "1599eec39bd8bfe41754c1b87f6d4cf2517107bf55a31fd6f06e57853f0edcb3": { + "signature": "1599eec39bd8bfe41754c1b87f6d4cf2517107bf55a31fd6f06e57853f0edcb3", + "alternativeSignatures": [ + "1d0195a64263b4be03267b1ce650ac0ad6b3589f32639ecd3792df0a39134d2a" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c7e28dfe20e2c60575af07324e9960283b6ab567f4d0877763109330077b535d": { + "signature": "c7e28dfe20e2c60575af07324e9960283b6ab567f4d0877763109330077b535d", + "alternativeSignatures": [ + "22da5e4da2ab3befa6aea6c7b562de7057d367a0bf4f05a5791a02c6d7de5886" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "da7db308719d405aa91b08fcda683bc73d9fb73906ad14857b07f1487e2538fb": { + "signature": "da7db308719d405aa91b08fcda683bc73d9fb73906ad14857b07f1487e2538fb", + "alternativeSignatures": [ + "e8a05e57577cfff0f0614b0e2ac77ef86185b636ac9a85322decda6d93bb1025" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "406449365cf5b8a747cda6bcb9f698ab04e5f23c76ea26b15ae5f6dd199e200a": { + "signature": "406449365cf5b8a747cda6bcb9f698ab04e5f23c76ea26b15ae5f6dd199e200a", + "alternativeSignatures": [ + "ab85af1c9f00c18837eddc1a658df6f16759b57337f0c576979a7ead9ca5aea5" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "2d3985d721950fdc54d6dd67f6da4e2f26da1de752237d6eff2bee79e76e74cf": { + "signature": "2d3985d721950fdc54d6dd67f6da4e2f26da1de752237d6eff2bee79e76e74cf", + "alternativeSignatures": [ + "eb8fe28224b13b24a5fd5dab39c73356ab0e958242fefd3468a632b6f1fc8468" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "73a9b54e10ff814734690c8e99319851d3caffb8d7ee02237ccef08057623c57": { + "signature": "73a9b54e10ff814734690c8e99319851d3caffb8d7ee02237ccef08057623c57", + "alternativeSignatures": [ + "a8395f556a6edc7f633b06820ab22d4ae275f1e2f0fb9f0389520cba4a3fa038" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f5e7a576035cfef04eaefb5c3d64e7575c80aea1449c6528d16bd18f19601938": { + "signature": "f5e7a576035cfef04eaefb5c3d64e7575c80aea1449c6528d16bd18f19601938", + "alternativeSignatures": [ + "e3bf1d972a8173cd7c9e853facdd28b184614cffc18ce2a47e00114894a9a0f3" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "85150297f02b3d19a5a032b1ec6b083b4bc2eec2643f60cceac18fd07551134e": { + "signature": "85150297f02b3d19a5a032b1ec6b083b4bc2eec2643f60cceac18fd07551134e", + "alternativeSignatures": [ + "56a1cb19171420c70451d831140f44f8beace9ee605870a54998170e9b9eea44" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "db5e6cce78b5039be2e56eb828caea7fbc1b2c0ddf2204f3c95e90805f480025": { + "signature": "db5e6cce78b5039be2e56eb828caea7fbc1b2c0ddf2204f3c95e90805f480025", + "alternativeSignatures": [ + "38588e5b5cbf9208115ae67488467545f67a83e5ea18d08f65e76318ba2809c1" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "87cbf986f8699bddcd94445159a5772945dd63bb4f55587afaecb5be2846e3cb": { + "signature": "87cbf986f8699bddcd94445159a5772945dd63bb4f55587afaecb5be2846e3cb", + "alternativeSignatures": [ + "229e52e7992660959c3cc4581b73623f6807f87a266ecced4af4343f1f8a2c06" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "c3cc891778043c08b6177ace3e90a4af4be52f7936d9df603efcfa77f11df9cf": { + "signature": "c3cc891778043c08b6177ace3e90a4af4be52f7936d9df603efcfa77f11df9cf", + "alternativeSignatures": [ + "4d98cab41917051c77b027daaa0ebaad30ae99b738bb3721331ddacb3d13e990" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "dbd00c32ac388db25b7b4095d242411fb192ecc6d471c80d72f6c83bc5790f5d": { + "signature": "dbd00c32ac388db25b7b4095d242411fb192ecc6d471c80d72f6c83bc5790f5d", + "alternativeSignatures": [ + "5d3a76ad12be60e1fc117dae08b4e56f70e0e905000e6bac82b7c0d7d47efa12" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b68b91f6e33cf24cb0a6b106bad2cccc5f7191510437ce703a42f1fefbb1d13": { + "signature": "5b68b91f6e33cf24cb0a6b106bad2cccc5f7191510437ce703a42f1fefbb1d13", + "alternativeSignatures": [ + "cbd85190a9dc566b475d9d0da29ef2890469d545129695dccd33e12b042c7f87" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "48a3d50b09e89367e161fa2e843b605dbf840ac3264623bad804634b9e17f0b1": { + "signature": "48a3d50b09e89367e161fa2e843b605dbf840ac3264623bad804634b9e17f0b1", + "alternativeSignatures": [ + "ecf7b4e72800b2f2881b924e7c602984c0101738bfe7d901ef9bf68d54f22acc" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b641532bc10265a307b0984e2c1cf7478d345aa73e0b9f52f57de6b6139db0d": { + "signature": "5b641532bc10265a307b0984e2c1cf7478d345aa73e0b9f52f57de6b6139db0d", + "alternativeSignatures": [ + "93f72434bf401ab42b1478aae49174f0f231385f2d190646de90b2f321bf0405" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f5bbdce1f432ce0cc0f8eab2a548e86389c11972a0fd807d5b0218929dc0667f": { + "signature": "f5bbdce1f432ce0cc0f8eab2a548e86389c11972a0fd807d5b0218929dc0667f", + "alternativeSignatures": [ + "c0dd620ed7794312e1f535291893133d2983ef6af266bf1261e08f5a40030184" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "b48abf4f4fc439b180232125d632b47c3ca44cde8f518e08d38905578255a76e": { + "signature": "b48abf4f4fc439b180232125d632b47c3ca44cde8f518e08d38905578255a76e", + "alternativeSignatures": [ + "8b1d9c30d610788b227bcf7b1a02f715887adb26d9d476705a864bfbd3eec42d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "5b91412070b80c595c93a131c6423a78aca0eb174e67b49a72f3a28b2f2e04fc": { + "signature": "5b91412070b80c595c93a131c6423a78aca0eb174e67b49a72f3a28b2f2e04fc", + "alternativeSignatures": [ + "3b52062806f35937f2372d4497d5679d95e9c28d9cef710ac9f0551b3bf6fa69" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4e6e328cd795ad8ffa3d409d0e0313ae91f87124a058e5dfe64dfbc93e236f78": { + "signature": "4e6e328cd795ad8ffa3d409d0e0313ae91f87124a058e5dfe64dfbc93e236f78", + "alternativeSignatures": [ + "656279220733cb5f45e3ef13a50ae9174c9763f7c24ff163245f6bea398d0794" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "4dd68b37dee025fa0a77ac00e6e85ce7e1ad8add70a2226e10a6423534359a9b": { + "signature": "4dd68b37dee025fa0a77ac00e6e85ce7e1ad8add70a2226e10a6423534359a9b", + "alternativeSignatures": [ + "c63dd7cfcff47e1a2f686035319925e8b2d3b0f1ac8db86b39d4b1821ae4797d" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "62a125c376534b19e9083efbad081f32bc294602b7f19a5210ad940e1c450a5f": { + "signature": "62a125c376534b19e9083efbad081f32bc294602b7f19a5210ad940e1c450a5f", + "alternativeSignatures": [ + "2b2732249979c5e87cc79cbdec660e2b1c8eba1f93a362e5595ec03cdf1e4951" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + }, + "f918c167a1a70deba61361fd3138b76fba86387a1f41237efeda8f01d464457e": { + "signature": "f918c167a1a70deba61361fd3138b76fba86387a1f41237efeda8f01d464457e", + "alternativeSignatures": [ + "959e93d957e0e5ec521d2f3b9dae02db123812fb8d523d8175329b08694d65d9" + ], + "memberOf": [ + "default" + ], + "createdDate": "2023-09-29 19:39:12Z" + } + } +} \ No newline at end of file diff --git a/build/config/template.appinstaller b/build/config/template.appinstaller new file mode 100644 index 0000000000..7977fd950b --- /dev/null +++ b/build/config/template.appinstaller @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/build/config/tsa.json b/build/config/tsa.json new file mode 100644 index 0000000000..89874d37fc --- /dev/null +++ b/build/config/tsa.json @@ -0,0 +1,6 @@ +{ + "instanceUrl": "https://microsoft.visualstudio.com", + "projectName": "OS", + "areaPath": "OS\\Windows Client and Services\\ADEPT\\E4D-Engineered for Developers\\SHINE\\Terminal", + "notificationAliases": ["condev@microsoft.com", "duhowett@microsoft.com"] +} diff --git a/build/packages.config b/build/packages.config index 9648184131..ceadd04ac2 100644 --- a/build/packages.config +++ b/build/packages.config @@ -3,8 +3,6 @@ - - diff --git a/build/pgo/PGO.DB.proj b/build/pgo/PGO.DB.proj index f89eff931a..ce7e1c6ebc 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 d49574423a..b1437af2a0 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 55a5e39f4d..b3735920c2 100644 --- a/build/pgo/Terminal.PGO.props +++ b/build/pgo/Terminal.PGO.props @@ -9,21 +9,21 @@ - main + release-1.19 Microsoft.Internal.Windows.Terminal.PGODatabase - + $(VersionMajor) - + $(VersionMinor) - + 0 - + @@ -46,6 +46,5 @@ true - - + diff --git a/build/pipelines/ci.yml b/build/pipelines/ci.yml index 7e225d7cb8..4ad72566a1 100644 --- a/build/pipelines/ci.yml +++ b/build/pipelines/ci.yml @@ -29,76 +29,80 @@ variables: # 0.0.1904.0900 name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) +parameters: + - name: auditMode + displayName: "Build in Audit Mode (x64)" + type: boolean + default: true + - name: runTests + displayName: "Run Tests" + type: boolean + default: true + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + stages: - - stage: Audit_x64 - displayName: Audit Mode - dependsOn: [] - condition: succeeded() - jobs: - - template: ./templates/build-console-audit-job.yml - parameters: - platform: x64 + - ${{ if eq(parameters.auditMode, true) }}: + - stage: Audit_x64 + displayName: Audit Mode + dependsOn: [] + jobs: + - 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: [AuditMode] + buildEverything: true + keepAllExpensiveBuildOutputs: false - - 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 + - stage: CodeHealth + displayName: Code Health 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 + - template: ./templates-v2/job-check-code-format.yml - - 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 + - ${{ each platform in parameters.buildPlatforms }}: + - stage: Build_${{ platform }} + displayName: Build ${{ platform }} + dependsOn: [] + jobs: + - 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: + - ${{ platform }} + buildConfigurations: [Release] + buildEverything: true + keepAllExpensiveBuildOutputs: false - - 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 + - ${{ if eq(parameters.runTests, true) }}: + - stage: Test_${{ platform }} + displayName: Test ${{ platform }} + dependsOn: + - Build_${{ platform }} + condition: succeeded() + jobs: + - 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) - - stage: Scripts - displayName: Code Health Scripts - dependsOn: [] - condition: succeeded() - jobs: - - template: ./templates/check-formatting.yml - - - - 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 ne(variables['Build.Reason'], 'PullRequest') }}: + - stage: CodeIndexer + displayName: Github CodeNav Indexer + dependsOn: [] + jobs: + - 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 126b2e1035..890c1f86a4 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 0ca4400463..ead379dbf3 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 b5604b5f22..75b781d33c 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/nightly.yml b/build/pipelines/nightly.yml new file mode 100644 index 0000000000..6ec737157f --- /dev/null +++ b/build/pipelines/nightly.yml @@ -0,0 +1,43 @@ +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) + +parameters: + - name: publishToAzure + displayName: "Deploy to **PUBLIC** Azure Storage" + type: boolean + default: true + +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! + ${{ if eq(true, parameters.publishToAzure) }}: + extraPublishJobs: + - template: job-deploy-to-azure-storage.yml + parameters: + pool: + name: SHINE-INT-S + dependsOn: [PublishSymbols] + storagePublicRootURL: $(AppInstallerRootURL) + subscription: $(AzureSubscriptionName) + storageAccount: $(AzureStorageAccount) + storageContainer: $(AzureStorageContainer) + buildConfiguration: Release + environment: production-canary + diff --git a/build/pipelines/ob-nightly.yml b/build/pipelines/ob-nightly.yml new file mode 100644 index 0000000000..c8c62e278f --- /dev/null +++ b/build/pipelines/ob-nightly.yml @@ -0,0 +1,47 @@ +trigger: none +pr: none +schedules: + - cron: "30 3 * * 2-6" # Run at 03:30 UTC Tuesday through Saturday (After the work day in Pacific, Mon-Fri) + displayName: "Nightly Terminal Build" + branches: + include: + - main + always: false # only run if there's code changes! + +parameters: + - name: publishToAzure + displayName: "Deploy to **PUBLIC** Azure Storage" + type: boolean + default: true + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +variables: + - template: templates-v2/variables-nuget-package-version.yml + - template: templates-v2/variables-onebranch-config.yml + +extends: + template: templates-v2/pipeline-onebranch-full-release-build.yml + parameters: + official: true + branding: Canary + buildTerminal: true + pgoBuildMode: Optimize + codeSign: true + publishSymbolsToPublic: true + publishVpackToWindows: false + symbolExpiryTime: 15 + ${{ if eq(true, parameters.publishToAzure) }}: + extraPublishJobs: + - template: job-deploy-to-azure-storage.yml + parameters: + pool: { type: windows } + dependsOn: [PublishSymbols] + storagePublicRootURL: $(AppInstallerRootURL) + subscription: $(AzureSubscriptionName) + storageAccount: $(AzureStorageAccount) + storageContainer: $(AzureStorageContainer) + buildConfiguration: Release + buildPlatforms: [x64, x86, arm64] + environment: production-canary + diff --git a/build/pipelines/ob-release.yml b/build/pipelines/ob-release.yml new file mode 100644 index 0000000000..9c81f57066 --- /dev/null +++ b/build/pipelines/ob-release.yml @@ -0,0 +1,81 @@ +trigger: none +pr: none + +# Expose all of these parameters for user configuration. +parameters: + - name: branding + displayName: "Branding (Build Type)" + type: string + default: Release + values: + - Release + - Preview + - Canary + - Dev + - name: buildTerminal + displayName: "Build Windows Terminal MSIX" + type: boolean + default: true + - name: buildConPTY + displayName: "Build ConPTY NuGet" + type: boolean + default: false + - name: buildWPF + displayName: "Build Terminal WPF Control" + type: boolean + default: false + - name: pgoBuildMode + displayName: "PGO Build Mode" + type: string + default: Optimize + values: + - Optimize + - Instrument + - None + - name: buildConfigurations + displayName: "Build Configurations" + type: object + default: + - Release + - name: buildPlatforms + displayName: "Build Platforms" + type: object + default: + - x64 + - x86 + - arm64 + - name: terminalInternalPackageVersion + displayName: "Terminal Internal Package Version" + type: string + default: '0.0.8' + + - name: publishSymbolsToPublic + displayName: "Publish Symbols to MSDL" + type: boolean + default: true + - name: publishVpackToWindows + displayName: "Publish VPack to Windows" + type: boolean + default: false + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +variables: + - template: templates-v2/variables-nuget-package-version.yml + - template: templates-v2/variables-onebranch-config.yml + +extends: + template: templates-v2/pipeline-onebranch-full-release-build.yml + parameters: + official: true + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + codeSign: true + terminalInternalPackageVersion: ${{ parameters.terminalInternalPackageVersion }} + publishSymbolsToPublic: ${{ parameters.publishSymbolsToPublic }} + publishVpackToWindows: ${{ parameters.publishVpackToWindows }} diff --git a/build/pipelines/pgo.yml b/build/pipelines/pgo.yml index 1c93a56d8c..d659c4e252 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: - - template: ./templates/pgo-build-and-publish-nuget-job.yml + - ${{ 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: + # This job takes multiple platforms and fans them back in to a single artifact. + - template: ./templates-v2/job-pgo-merge-pgd.yml parameters: - pgoArtifact: 'PGO' + jobName: MergePGD + pool: + vmImage: 'windows-2022' + buildConfiguration: Release + buildPlatforms: ${{ parameters.buildPlatforms }} + artifactStem: -instrumentation + + - template: ./templates-v2/job-pgo-build-nuget-and-publish.yml + parameters: + pool: + vmImage: 'windows-2022' + dependsOn: MergePGD + buildConfiguration: Release + artifactStem: -instrumentation diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 053dc03a73..e016270e87 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: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-latest - +# Expose all of these parameters for user configuration. parameters: - name: branding displayName: "Branding (Build Type)" @@ -14,22 +10,12 @@ parameters: values: - Release - Preview + - Canary + - 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,677 +33,53 @@ parameters: - Instrument - None - name: buildConfigurations + displayName: "Build Configurations" type: object default: - Release - name: buildPlatforms + displayName: "Build Platforms" type: object default: - x64 - x86 - arm64 - - name: buildWindowsVersions - type: object - default: - - Win10 - - Win11 + - 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' -variables: - TerminalInternalPackageVersion: "0.0.7" - # 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 - # The NuGet packages have to use *somebody's* DLLs. We used to force them to - # use the Win10 build outputs, but if there isn't a Win10 build we should use - # the Win11 one. - ${{ if containsValue(parameters.buildWindowsVersions, 'Win10') }}: - TerminalBestVersionForNuGetPackages: Win10 - ${{ else }}: - TerminalBestVersionForNuGetPackages: Win11 + - name: publishSymbolsToPublic + displayName: "Publish Symbols to MSDL" + type: boolean + default: true + - name: publishVpackToWindows + displayName: "Publish VPack to Windows" + type: boolean + default: false name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) -resources: - repositories: - - repository: self - type: git - ref: main -jobs: -- job: Build - strategy: - matrix: - ${{ each config in parameters.buildConfigurations }}: - ${{ each platform in parameters.buildPlatforms }}: - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - ${{ config }}_${{ platform }}_${{ windowsVersion }}: - BuildConfiguration: ${{ config }} - BuildPlatform: ${{ platform }} - TerminalTargetWindowsVersion: ${{ windowsVersion }} - 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) }}: - - pwsh: |- - ./build/scripts/Patch-ManifestsToWindowsVersion.ps1 -NewWindowsVersion "10.0.22000.0" - displayName: Update manifest target version to Win11 (if necessary) - condition: and(succeeded(), eq(variables['TerminalTargetWindowsVersion'], 'Win11')) - - 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)-$(TerminalTargetWindowsVersion) - - 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 *.appx/*.msix to Artifacts - inputs: - Contents: >- - **/*.appx - - **/*.msix - - **/*.appxsym - - !**/Microsoft.VCLibs*.appx - TargetFolder: $(Build.ArtifactStagingDirectory)/appx - OverWrite: true - flattenFolders: true - - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 - displayName: 'Generate SBOM manifest' - inputs: - BuildDropPath: '$(System.ArtifactsDirectory)/appx' - - task: PublishBuildArtifacts@1 - displayName: Publish Artifact (appx) - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/appx - ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration)-$(TerminalTargetWindowsVersion) - - ${{ 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)-$(TerminalTargetWindowsVersion) - - ${{ 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)-$(TerminalTargetWindowsVersion) - - - 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 - strategy: - matrix: - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - ${{ windowsVersion }}: - TerminalTargetWindowsVersion: ${{ windowsVersion }} - 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@0 - displayName: Download Artifacts ${{ platform }} $(TerminalTargetWindowsVersion) - inputs: - artifactName: appx-${{ platform }}-Release-$(TerminalTargetWindowsVersion) - # 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 "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(TerminalTargetWindowsVersion)_$(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: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: appxbundle-signed' - inputs: - PathtoPublish: $(System.ArtifactsDirectory)\bundle - ArtifactName: appxbundle-signed-$(TerminalTargetWindowsVersion) - -- ${{ 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@0 - displayName: Download ${{ platform }} ConPTY binaries - inputs: - artifactName: conpty-dll-${{ platform }}-$(BuildConfiguration)-$(TerminalBestVersionForNuGetPackages) - 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@0 - displayName: Download ${{ platform }} PublicTerminalCore - inputs: - artifactName: wpf-dll-${{ platform }}-$(BuildConfiguration)-$(TerminalBestVersionForNuGetPackages) - 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 appx-PLATFORM-CONFIG-VERSION artifact for every platform/version combo - - ${{ each platform in parameters.buildPlatforms }}: - - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - - task: DownloadBuildArtifacts@0 - displayName: Download Symbols ${{ platform }} ${{ windowsVersion }} - inputs: - artifactName: appx-${{ platform }}-Release-${{ windowsVersion }} - - # 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@0 - displayName: Download Build Artifacts - inputs: - artifactName: appxbundle-signed-Win11 - 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_Win11_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } - - - # Create vpack directory and place item inside - - mkdir WindowsTerminal.app - - mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ - workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed-Win11 - - task: PkgESVPack@12 - displayName: 'Package ES - VPack' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - inputs: - sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed-Win11\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 -... +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-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml new file mode 100644 index 0000000000..9d64e1c7cf --- /dev/null +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -0,0 +1,144 @@ +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 + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + +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) + JobOutputDirectory: $(Build.ArtifactStagingDirectory)\nupkg + JobOutputArtifactName: wpf-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} + 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@3 + 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' + + - ${{ 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 new file mode 100644 index 0000000000..d04561230f --- /dev/null +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -0,0 +1,293 @@ +parameters: + - name: branding + type: string + default: 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: [] + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + - name: removeAllNonSignedFiles + type: boolean + default: false + +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: ' ' + JobOutputDirectory: $(Terminal.BinDir) + JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} + 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\Control\Microsoft_Terminal_Control" + $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 + + - ${{ 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: + # - 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. + # - console.dll, which apparently breaks XFGCheck? lol. + - pwsh: |- + $binDir = '$(Terminal.BinDir)' + $ImportLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.exp' | ForEach-Object { $_.FullName -Replace "exp$","lib" } + $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 += Get-ChildItem $binDir -Filter 'console.dll' + + $Items | Remove-Item -Recurse -Force -Verbose -ErrorAction:Ignore + displayName: Clean up static libs and extra symbols + errorActionPreference: silentlyContinue # It's OK if this silently fails + + # 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@3 + 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 + + # Some of our governed pipelines explicitly fail builds that have *any* non-codesigned filed (!) + - ${{ if eq(parameters.removeAllNonSignedFiles, true) }}: + - pwsh: |- + Get-ChildItem "$(Terminal.BinDir)" -Recurse -Include "*.dll","*.exe" | + Where-Object { (Get-AuthenticodeSignature $_).Status -Ne "Valid" } | + Remove-Item -Verbose -Force + displayName: Remove all non-signed output files + + - ${{ else }}: # No Signing + - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: + - pwsh: |- + $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' + + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(Terminal.BinDir) + artifact: $(JobOutputArtifactName) + 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 0000000000..e889fc8756 --- /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-v2/job-deploy-to-azure-storage.yml b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml new file mode 100644 index 0000000000..140a513ee1 --- /dev/null +++ b/build/pipelines/templates-v2/job-deploy-to-azure-storage.yml @@ -0,0 +1,70 @@ +parameters: + - name: buildConfiguration + type: string + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: artifactStem + type: string + default: '' + - name: variables + type: object + default: {} + - name: environment + type: string + - name: storagePublicRootURL + type: string + - name: subscription + type: string + - name: storageAccount + type: string + - name: storageContainer + type: string + +jobs: +- deployment: DeployAzure + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + displayName: Publish to Azure Storage (Prod) + dependsOn: ${{ parameters.dependsOn }} + variables: + ${{ insert }}: ${{ parameters.variables }} + environment: ${{ parameters.environment }} + strategy: + runOnce: + deploy: + steps: + - download: none + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here + submodules: true + persistCredentials: True + + - task: DownloadPipelineArtifact@2 + displayName: Download MSIX Bundle Artifact + inputs: + artifactName: appxbundle-${{ parameters.buildConfiguration }}${{ parameters.artifactStem }} + downloadPath: '$(Build.SourcesDirectory)/_out' + itemPattern: '**/*.msixbundle' + + - pwsh: |- + $b = Get-Item _out/*.msixbundle + ./build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 -BundlePath $b.FullName -AppInstallerTemplatePath ./build/config/template.appinstaller -AppInstallerRoot "${{ parameters.storagePublicRootURL }}" -OutputPath _out/Microsoft.WindowsTerminalCanary.appinstaller + displayName: "Produce AppInstaller for MSIX bundle" + + - task: AzureFileCopy@5 + displayName: Publish to Storage Account + inputs: + sourcePath: _out/* + Destination: AzureBlob + azureSubscription: ${{ parameters.subscription }} + storage: ${{ parameters.storageAccount }} + ContainerName: ${{ parameters.storageContainer }} + AdditionalArgumentsForBlobCopy: "--content-type application/octet-stream" + diff --git a/build/pipelines/templates/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 04e018ed2a..e2edf55e65 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 0000000000..6d6ad09cda --- /dev/null +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -0,0 +1,152 @@ +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 + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + - name: afterBuildSteps + type: stepList + default: [] + +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 + ${{ elseif eq(parameters.branding, 'Canary') }}: + BundleStemName: Microsoft.WindowsTerminalCanary + ${{ else }}: + BundleStemName: WindowsTerminalDev + JobOutputDirectory: '$(System.ArtifactsDirectory)/bundle' + JobOutputArtifactName: appxbundle-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} + 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" + $BundlePath = "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + .\build\scripts\Create-AppxBundle.ps1 -InputPath 'bin/' -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath $BundlePath + Write-Host "##vso[task.setvariable variable=MsixBundlePath]${BundlePath}" + displayName: Create msixbundle + + - ${{ if eq(parameters.codeSign, true) }}: + - task: EsrpCodeSigning@3 + 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' + + - ${{ parameters.afterBuildSteps }} + + - ${{ 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 new file mode 100644 index 0000000000..2f777cdf4f --- /dev/null +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -0,0 +1,129 @@ +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 + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + +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 }} + variables: + JobOutputDirectory: $(Build.ArtifactStagingDirectory)\nupkg + JobOutputArtifactName: conpty-nupkg-$(BuildConfiguration)${{ parameters.artifactStem }} + ${{ insert }}: ${{ parameters.variables }} + 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@3 + 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' + + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + 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 2c36f7cf15..b61a22c4fc 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 0000000000..8b341b463e --- /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 0000000000..83c472872b --- /dev/null +++ b/build/pipelines/templates-v2/job-publish-symbols.yml @@ -0,0 +1,97 @@ +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 + - name: symbolExpiryTime + type: string + default: 36530 # This is the default from PublishSymbols@2 + - name: variables + type: object + default: {} + - name: symbolPatGoesInTaskInputs + type: boolean + default: false + +jobs: +- job: ${{ parameters.jobName }} + ${{ 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 }} + variables: + ${{ insert }}: ${{ parameters.variables }} + 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)' + SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} + 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)' + SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} + ${{ if eq(parameters.symbolPatGoesInTaskInputs, true) }}: + Pat: $(ADO_microsoftpublicsymbols_PAT) + # The ADO task does not support indexing of GitHub sources. + # There is a bug which causes this task to fail if LIB includes an inaccessible path (even though it does not depend on it). + # To work around this issue, we just force LIB to be any dir that we know exists. + # Copied from https://github.com/microsoft/icu/blob/f869c214adc87415dfe751d81f42f1bca55dcf5f/build/azure-nuget.yml#L564-L583 + env: + LIB: $(Build.SourcesDirectory) + ArtifactServices_Symbol_AccountName: microsoftpublicsymbols + ${{ if ne(parameters.symbolPatGoesInTaskInputs, true) }}: + 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 0000000000..817d97ff9c --- /dev/null +++ b/build/pipelines/templates-v2/job-run-pgo-tests.yml @@ -0,0 +1,82 @@ +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)' + + - 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 0000000000..2e9cb1e1fe --- /dev/null +++ b/build/pipelines/templates-v2/job-submit-windows-vpack.yml @@ -0,0 +1,81 @@ +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: '' + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + +jobs: +- job: VPack + ${{ if ne(length(parameters.pool), 0) }}: + 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 + 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) + + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + 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 50% rename from build/pipelines/templates/test-console-ci.yml rename to build/pipelines/templates-v2/job-test-project.yml index b2eea094f1..1cd8e2bef6 100644 --- a/build/pipelines/templates/test-console-ci.yml +++ b/build/pipelines/templates-v2/job-test-project.yml @@ -1,9 +1,9 @@ parameters: configuration: 'Release' platform: '' - additionalBuildArguments: '' - artifactName: 'drop' testLogPath: '$(Build.BinariesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\testsOnBuildMachine.wtl' + inputArtifactStem: '' + outputArtifactStem: '' jobs: - job: Test${{ parameters.platform }}${{ parameters.configuration }} @@ -11,72 +11,80 @@ 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/') }}: - name: WinDevPoolOSS-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: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-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 + 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.inputArtifactStem }} + 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" - condition: and(and(succeeded(), ne(variables['PGOBuildMode'], 'Instrument')), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + arguments: -MatchPattern '*unit.test*.dll' -Platform '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" - - 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 '$(OutputBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' -Root "$(Terminal.BinDir)" - 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: and(ne(variables['PGOBuildMode'], 'Instrument'),or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) + + - task: PowerShell@2 + displayName: 'Manually log test failures' + condition: always() + inputs: + targetType: filePath + filePath: build\Helix\OutputTestErrorsForAzureDevops.ps1 + arguments: -XUnitOutputPath 'onBuildMachineResults.xml' - 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' - #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' + condition: always() inputs: Contents: | **/*.wtl @@ -87,4 +95,5 @@ jobs: flattenFolders: true - publish: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test-logs' - artifact: TestLogs$(BuildPlatform)$(BuildConfiguration) + artifact: test-logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.outputArtifactStem }} + condition: always() 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 0000000000..03826cac85 --- /dev/null +++ b/build/pipelines/templates-v2/pipeline-full-release-build.yml @@ -0,0 +1,179 @@ +# 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: extraPublishJobs + type: object + default: [] + - 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: + - template: variables-nuget-package-version.yml + +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 }} + + - ${{ parameters.extraPublishJobs }} +... diff --git a/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml new file mode 100644 index 0000000000..a466e987f0 --- /dev/null +++ b/build/pipelines/templates-v2/pipeline-onebranch-full-release-build.yml @@ -0,0 +1,256 @@ +parameters: + - name: official + type: boolean + default: false + - name: branding + type: string + default: Release + values: + - Release + - Preview + - Canary + - Dev + - name: buildTerminal + type: boolean + default: true + - name: buildConPTY + type: boolean + default: false + - name: buildWPF + type: boolean + default: false + - name: pgoBuildMode + type: string + default: Optimize + values: + - Optimize + - Instrument + - None + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - x86 + - arm64 + - name: codeSign + type: boolean + default: true + - name: terminalInternalPackageVersion + type: string + default: '0.0.8' + + - name: publishSymbolsToPublic + type: boolean + default: true + - name: publishVpackToWindows + type: boolean + default: false + + - name: extraPublishJobs + type: object + default: [] + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + ${{ if eq(parameters.official, true) }}: + template: v2/Microsoft.Official.yml@templates # https://aka.ms/obpipelines/templates + ${{ else }}: + template: v2/Microsoft.NonOfficial.yml@templates + parameters: + featureFlags: + WindowsHostVersion: 1ESWindows2022 + platform: + name: 'windows_undocked' + product: 'Windows Terminal' + cloudvault: # https://aka.ms/obpipelines/cloudvault + enabled: false + globalSdl: # https://aka.ms/obpipelines/sdl + tsa: + enabled: true + configFile: '$(Build.SourcesDirectory)\build\config\tsa.json' + binskim: + break: false + scanOutputDirectoryOnly: true + policheck: + break: false + severity: Note + baseline: + baselineFile: '$(Build.SourcesDirectory)\build\config\release.gdnbaselines' + suppressionSet: default + + stages: + - stage: Build + displayName: Build + dependsOn: [] + jobs: + - template: ./build/pipelines/templates-v2/job-build-project.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + branding: ${{ parameters.branding }} + buildTerminal: ${{ parameters.buildTerminal }} + buildConPTY: ${{ parameters.buildConPTY }} + buildWPF: ${{ parameters.buildWPF }} + pgoBuildMode: ${{ parameters.pgoBuildMode }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + removeAllNonSignedFiles: true # appease the overlords + codeSign: ${{ parameters.codeSign }} + beforeBuildSteps: # Right before we build, lay down the universal package and localizations + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + + - task: UniversalPackages@0 + displayName: Download terminal-internal Universal Package + inputs: + feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48 + packageListDownload: e82d490c-af86-4733-9dc4-07b772033204 + versionListDownload: ${{ parameters.terminalInternalPackageVersion }} + + - template: ./build/pipelines/templates-v2/steps-fetch-and-prepare-localizations.yml@self + parameters: + includePseudoLoc: true + + - ${{ if eq(parameters.buildWPF, true) }}: + # Add an Any CPU build flavor for the WPF control bits + - template: ./build/pipelines/templates-v2/job-build-project.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + jobName: BuildWPF + branding: ${{ parameters.branding }} + buildTerminal: false + buildWPFDotNetComponents: true + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: + - Any CPU + generateSbom: false # this is handled by onebranch + removeAllNonSignedFiles: true # appease the overlords + codeSign: ${{ parameters.codeSign }} + beforeBuildSteps: + - task: PkgESSetupBuild@12 + displayName: Package ES - Setup Build + inputs: + disableOutputRedirect: true + # WPF doesn't need the localizations or the universal package, but if it does... put them here. + + - stage: Package + displayName: Package + dependsOn: [Build] + jobs: + - ${{ if eq(parameters.buildTerminal, true) }}: + - template: ./build/pipelines/templates-v2/job-merge-msix-into-bundle.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + ### This job is also in charge of submitting the vpack to Windows if it's enabled + ob_createvpack_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }} + ob_updateOSManifest_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }} + ### If enabled above, these options are in play. + ob_createvpack_packagename: 'WindowsTerminal.app' + ob_createvpack_owneralias: 'conhost@microsoft.com' + ob_createvpack_description: 'VPack for the Windows Terminal Application' + ob_createvpack_targetDestinationDirectory: '$(Destination)' + ob_createvpack_propsFile: false + ob_createvpack_provData: true + ob_createvpack_metadata: '$(Build.SourceVersion)' + ob_createvpack_topLevelRetries: 0 + ob_createvpack_failOnStdErr: true + ob_createvpack_taskLogVerbosity: Detailed + ob_createvpack_verbose: true + ob_createvpack_vpackdirectory: '$(JobOutputDirectory)\vpack' + ob_updateOSManifest_gitcheckinConfigPath: '$(Build.SourcesDirectory)\build\config\GitCheckin.json' + # We're skipping the 'fetch' part of the OneBranch rules, but that doesn't mean + # that it doesn't expect to have downloaded a manifest directly to some 'destination' + # folder that it can then update and upload. + # Effectively: it says "destination" but it means "source" + # DH: Don't ask why. + ob_updateOSManifest_destination: $(XES_VPACKMANIFESTDIRECTORY) + ob_updateOSManifest_skipFetch: true + publishArtifacts: false # Handled by OneBranch + jobName: Bundle + branding: ${{ parameters.branding }} + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # Handled by onebranch + codeSign: ${{ parameters.codeSign }} + afterBuildSteps: + - pwsh: |- + $d = New-Item "$(JobOutputDirectory)/vpack" -Type Directory + Copy-Item -Verbose -Path "$(MsixBundlePath)" -Destination (Join-Path $d 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle') + displayName: Stage msixbundle for vpack + + - ${{ if eq(parameters.buildConPTY, true) }}: + - template: ./build/pipelines/templates-v2/job-package-conpty.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + codeSign: ${{ parameters.codeSign }} + + - ${{ if eq(parameters.buildWPF, true) }}: + - template: ./build/pipelines/templates-v2/job-build-package-wpf.yml@self + parameters: + pool: { type: windows } + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(JobOutputDirectory) + ob_artifactBaseName: $(JobOutputArtifactName) + publishArtifacts: false # Handled by OneBranch + buildConfigurations: ${{ parameters.buildConfigurations }} + buildPlatforms: ${{ parameters.buildPlatforms }} + generateSbom: false # this is handled by onebranch + codeSign: ${{ parameters.codeSign }} + + - stage: Publish + displayName: Publish + dependsOn: [Build, Package] + jobs: + - template: ./build/pipelines/templates-v2/job-publish-symbols.yml@self + parameters: + pool: { type: windows } + includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} + symbolPatGoesInTaskInputs: true # onebranch tries to muck with the PAT variable, so we need to change how it get the PAT + variables: + ob_git_checkout: false # This job checks itself out + ob_git_skip_checkout_none: true + ob_outputDirectory: $(Build.ArtifactStagingDirectory) + # Without this, OneBranch will nerf our symbol tasks + ob_symbolsPublishing_enabled: true + + - ${{ parameters.extraPublishJobs }} diff --git a/build/pipelines/templates-v2/steps-create-signing-config.yml b/build/pipelines/templates-v2/steps-create-signing-config.yml new file mode 100644 index 0000000000..790d7a3fa8 --- /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 0000000000..e5e90cc8ea --- /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 0000000000..fb5cd75e4c --- /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 0000000000..8ace357aad --- /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 75% rename from build/pipelines/templates/restore-nuget-steps.yml rename to build/pipelines/templates-v2/steps-restore-nuget.yml index f92c7aa5cc..bd0c067531 100644 --- a/build/pipelines/templates/restore-nuget-steps.yml +++ b/build/pipelines/templates-v2/steps-restore-nuget.yml @@ -1,11 +1,13 @@ 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` - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 diff --git a/build/pipelines/templates-v2/variables-nuget-package-version.yml b/build/pipelines/templates-v2/variables-nuget-package-version.yml new file mode 100644 index 0000000000..af195c043f --- /dev/null +++ b/build/pipelines/templates-v2/variables-nuget-package-version.yml @@ -0,0 +1,23 @@ +variables: + # If we are building a branch called "release-*", change the NuGet suffix + # to "preview". If we don't do that, XES will set the suffix to "release1" + # because it truncates the value after the first period. + # We also want to disable the suffix entirely if we're Release branded while + # on a release branch. + # main is special, however. XES ignores main. Since we never produce actual + # shipping builds from main, we want to force it to have a beta label as + # well. + # + # In effect: + # BRANCH / BRANDING | Release | Preview + # ------------------|----------------------------|----------------------------- + # release-* | 1.12.20220427 | 1.13.20220427-preview + # main | 1.14.20220427-experimental | 1.14.20220427-experimental + # all others | 1.14.20220427-mybranch | 1.14.20220427-mybranch + ${{ if startsWith(variables['Build.SourceBranchName'], 'release-') }}: + ${{ if eq(parameters.branding, 'Release') }}: + NoNuGetPackBetaVersion: true + ${{ else }}: + NuGetPackBetaVersion: preview + ${{ elseif eq(variables['Build.SourceBranchName'], 'main') }}: + NuGetPackBetaVersion: experimental diff --git a/build/pipelines/templates-v2/variables-onebranch-config.yml b/build/pipelines/templates-v2/variables-onebranch-config.yml new file mode 100644 index 0000000000..d7180e3e09 --- /dev/null +++ b/build/pipelines/templates-v2/variables-onebranch-config.yml @@ -0,0 +1,2 @@ +variables: + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml deleted file mode 100644 index 1ed556eff8..0000000000 --- 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: WinDevPoolOSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-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 0ccf90f10f..0000000000 --- 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: WinDevPoolOSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-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 d5042cdb17..0000000000 --- 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;-:file|**\cpprest*.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 e115fbb375..0000000000 --- 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: WinDevPoolOSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-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 1187146ccf..0000000000 --- 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: WinDevPoolOSS-L - ${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}: - name: WinDevPool-L - demands: ImageOverride -equals WinDevVS17-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 05cdf667c7..0000000000 --- a/build/pipelines/templates/build-console-steps.yml +++ /dev/null @@ -1,130 +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 *.appx/*.msix to Artifacts (Non-PR builds only)' - inputs: - Contents: | - **/*.appx - **/*.msix - **/*.appxsym - !**/Microsoft.VCLibs*.appx - TargetFolder: '$(Build.ArtifactStagingDirectory)/appx' - OverWrite: true - flattenFolders: true - condition: succeeded() - -- 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 46abca0324..0000000000 --- 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 07476712a0..0000000000 --- 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 3f7dc8c097..0000000000 --- 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 bd8e967de9..0000000000 --- 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 2036857cac..0000000000 --- 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.10.amd64.client21h1.open.xaml' - closedHelixTargetQueues: 'windows.10.amd64.client21h1.xaml' - -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 89cf5319cc..0000000000 --- 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/rules/Branding.targets b/build/rules/Branding.targets index 0ce12dfca5..57216032d6 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/build/rules/GenerateSxsManifestsFromWinmds.targets b/build/rules/GenerateSxsManifestsFromWinmds.targets index 600f9980ce..6e17cc50dc 100644 --- a/build/rules/GenerateSxsManifestsFromWinmds.targets +++ b/build/rules/GenerateSxsManifestsFromWinmds.targets @@ -30,7 +30,7 @@ DependsOnTargets="_ConsoleMapWinmdsToManifestFiles"> - + diff --git a/build/rules/Microsoft.UI.Xaml.Additional.targets b/build/rules/Microsoft.UI.Xaml.Additional.targets new file mode 100644 index 0000000000..dbd6eb3f35 --- /dev/null +++ b/build/rules/Microsoft.UI.Xaml.Additional.targets @@ -0,0 +1,16 @@ + + + + + + + diff --git a/build/scripts/Invoke-FormattingCheck.ps1 b/build/scripts/Invoke-FormattingCheck.ps1 index ef780b5f48..d1110943d1 100644 --- a/build/scripts/Invoke-FormattingCheck.ps1 +++ b/build/scripts/Invoke-FormattingCheck.ps1 @@ -10,7 +10,7 @@ function Invoke-CheckBadCodeFormatting() { # returns a non-zero exit code if there are any diffs in the tracked files in the repo git diff-index --quiet HEAD -- - if ($lastExitCode -eq 1) { + if ($LASTEXITCODE -eq 1) { # Write the list of files that need updating to the log git diff-index --name-only HEAD diff --git a/build/scripts/Merge-PriFiles.ps1 b/build/scripts/Merge-PriFiles.ps1 new file mode 100644 index 0000000000..3a9985936a --- /dev/null +++ b/build/scripts/Merge-PriFiles.ps1 @@ -0,0 +1,78 @@ +Param( + [Parameter(Mandatory, + HelpMessage="List of PRI files or XML dumps (detailed only) to merge")] + [string[]] + $Path, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Name of index in output file; defaults to 'Application'")] + [string] + $IndexName = "Application", + + [Parameter(HelpMessage="Path to makepri.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakePriPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakePri.exe" +) + +$ErrorActionPreference = 'Stop' + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null +$priConfig = Join-Path $tempDir "priconfig.xml" +$priListFile = Join-Path $tempDir "pri.resfiles" +$dumpListFile = Join-Path $tempDir "dump.resfiles" + +@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ | Out-File -Encoding:utf8NoBOM $priConfig + +$Path | Where { $_ -Like "*.pri" } | ForEach-Object { + Get-Item $_ | Select -Expand FullName +} | Out-File -Encoding:utf8NoBOM $priListFile + +$Path | Where { $_ -Like "*.xml" } | ForEach-Object { + Get-Item $_ | Select -Expand FullName +} | Out-File -Encoding:utf8NoBOM $dumpListFile + +& $MakePriPath new /pr $tempDir /cf $priConfig /o /in $IndexName /of $OutputPath + +Remove-Item -Recurse -Force $tempDir diff --git a/build/scripts/Merge-TerminalAndXamlResources.ps1 b/build/scripts/Merge-TerminalAndXamlResources.ps1 new file mode 100644 index 0000000000..92c8bad556 --- /dev/null +++ b/build/scripts/Merge-TerminalAndXamlResources.ps1 @@ -0,0 +1,47 @@ +Param( + [Parameter(Mandatory, + HelpMessage="Root directory of extracted Terminal AppX")] + [string[]] + $TerminalRoot, + + [Parameter(Mandatory, + HelpMessage="Root directory of extracted Xaml AppX")] + [string[]] + $XamlRoot, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Path to makepri.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakePriPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakePri.exe" +) + +$ErrorActionPreference = 'Stop' + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null + +$terminalDump = Join-Path $tempDir "terminal.pri.xml" + +& $MakePriPath dump /if (Join-Path $TerminalRoot "resources.pri") /of $terminalDump /dt detailed + +Write-Verbose "Removing Microsoft.UI.Xaml node from Terminal to prevent a collision with XAML" +$terminalXMLDocument = [xml](Get-Content $terminalDump) +$resourceMap = $terminalXMLDocument.PriInfo.ResourceMap +$fileSubtree = $resourceMap.ResourceMapSubtree | Where-Object { $_.Name -eq "Files" } +$subtrees = $fileSubtree.ResourceMapSubtree +$xamlSubtreeChild = ($subtrees | Where-Object { $_.Name -eq "Microsoft.UI.Xaml" }) +if ($Null -Ne $xamlSubtreeChild) { + $null = $fileSubtree.RemoveChild($xamlSubtreeChild) + $terminalXMLDocument.Save($terminalDump) +} + +$indexName = $terminalXMLDocument.PriInfo.ResourceMap.name + +& (Join-Path $PSScriptRoot "Merge-PriFiles.ps1") -Path $terminalDump, (Join-Path $XamlRoot "resources.pri") -IndexName $indexName -OutputPath $OutputPath -MakePriPath $MakePriPath + +Remove-Item -Recurse -Force $tempDir diff --git a/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 b/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 new file mode 100644 index 0000000000..b49ad244ff --- /dev/null +++ b/build/scripts/New-AppInstallerFromTemplateAndBundle.ps1 @@ -0,0 +1,42 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory, + HelpMessage="Path to the .msixbundle")] + [string] + $BundlePath, + + [Parameter(Mandatory, + HelpMessage="Path to the .appinstaller template")] + [string] + $AppInstallerTemplatePath, + + [string] + $AppInstallerRoot, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath +) + +$ErrorActionPreference = "Stop" + +$sentinelFile = New-TemporaryFile +$directory = New-Item -Type Directory "$($sentinelFile.FullName)_Package" +Remove-Item $sentinelFile -Force -EA:Ignore + +$bundle = (Get-Item $BundlePath) +& tar.exe -x -f $bundle.FullName -C $directory AppxMetadata/AppxBundleManifest.xml + +$xml = [xml](Get-Content (Join-Path $directory "AppxMetadata\AppxBundleManifest.xml")) +$name = $xml.Bundle.Identity.Name +$version = $xml.Bundle.Identity.Version + +$doc = (Get-Content -ReadCount 0 $AppInstallerTemplatePath) +$doc = $doc -Replace '\$\$ROOT\$\$',$AppInstallerRoot +$doc = $doc -Replace '\$\$NAME\$\$',$name +$doc = $doc -Replace '\$\$VERSION\$\$',$version +$doc = $doc -Replace '\$\$PACKAGE\$\$',$bundle.Name +$doc | Out-File -Encoding utf8NoBOM $OutputPath + +Get-Item $OutputPath diff --git a/build/scripts/New-UnpackagedTerminalDistribution.ps1 b/build/scripts/New-UnpackagedTerminalDistribution.ps1 new file mode 100644 index 0000000000..b9ae23a295 --- /dev/null +++ b/build/scripts/New-UnpackagedTerminalDistribution.ps1 @@ -0,0 +1,140 @@ +[CmdletBinding(DefaultParameterSetName = 'AppX')] +Param( + [Parameter(Mandatory, HelpMessage="Path to Terminal AppX", ParameterSetName = 'AppX')] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $TerminalAppX, + + [Parameter(Mandatory, HelpMessage="Path to Terminal Layout Deployment", ParameterSetName='Layout')] + [ValidateScript({Test-Path $_ -Type Container})] + [string] + $TerminalLayout, + + [Parameter(Mandatory, HelpMessage="Path to Xaml AppX", ParameterSetName='AppX')] + [Parameter(Mandatory, HelpMessage="Path to Xaml AppX", ParameterSetName='Layout')] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $XamlAppX, + + [Parameter(HelpMessage="Output Directory", ParameterSetName='AppX')] + [Parameter(HelpMessage="Output Directory", ParameterSetName='Layout')] + [string] + $Destination = ".", + + [Parameter(HelpMessage="Path to makeappx.exe", ParameterSetName='AppX')] + [Parameter(HelpMessage="Path to makeappx.exe", ParameterSetName='Layout')] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakeAppx.exe" +) + +$filesToRemove = @("*.xml", "*.winmd", "Appx*", "Images/*Tile*", "Images/*Logo*") # Remove from Terminal +$filesToKeep = @("Microsoft.Terminal.Remoting.winmd") # ... except for these +$filesToCopyFromXaml = @("Microsoft.UI.Xaml.dll", "Microsoft.UI.Xaml") # We don't need the .winmd + +$ErrorActionPreference = 'Stop' + +If ($null -Eq (Get-Item $MakeAppxPath -EA:SilentlyContinue)) { + Write-Error "Could not find MakeAppx.exe at `"$MakeAppxPath`".`nMake sure that -MakeAppxPath points to a valid SDK." + Exit 1 +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null + +$XamlAppX = Get-Item $XamlAppX | Select-Object -Expand FullName + +######## +# Reading the AppX Manifest for preliminary info +######## + +If ($TerminalAppX) { + $appxManifestPath = Join-Path $tempDir AppxManifest.xml + & tar.exe -x -f "$TerminalAppX" -C $tempDir AppxManifest.xml +} ElseIf($TerminalLayout) { + $appxManifestPath = Join-Path $TerminalLayout AppxManifest.xml +} +$manifest = [xml](Get-Content $appxManifestPath) +$pfn = $manifest.Package.Identity.Name +$version = $manifest.Package.Identity.Version +$architecture = $manifest.Package.Identity.ProcessorArchitecture + +$distributionName = "{0}_{1}_{2}" -f ($pfn, $version, $architecture) +$terminalDir = "terminal-{0}" -f ($version) + +######## +# Unpacking Terminal and XAML +######## + +$terminalAppPath = Join-Path $tempdir $terminalDir + +If ($TerminalAppX) { + $TerminalAppX = Get-Item $TerminalAppX | Select-Object -Expand FullName + New-Item -ItemType Directory -Path $terminalAppPath | Out-Null + & $MakeAppxPath unpack /p $TerminalAppX /d $terminalAppPath /o | Out-Null + If ($LASTEXITCODE -Ne 0) { + Throw "Unpacking $TerminalAppX failed" + } +} ElseIf ($TerminalLayout) { + Copy-Item -Recurse -Path $TerminalLayout -Destination $terminalAppPath +} + +$xamlAppPath = Join-Path $tempdir "xaml" +New-Item -ItemType Directory -Path $xamlAppPath | Out-Null +& $MakeAppxPath unpack /p $XamlAppX /d $xamlAppPath /o | Out-Null +If ($LASTEXITCODE -Ne 0) { + Throw "Unpacking $XamlAppX failed" +} + +######## +# Some sanity checking +######## + +$xamlManifest = [xml](Get-Content (Join-Path $xamlAppPath "AppxManifest.xml")) +If ($xamlManifest.Package.Identity.Name -NotLike "Microsoft.UI.Xaml*") { + Throw "$XamlAppX is not a XAML package (instead, it looks like $($xamlManifest.Package.Identity.Name))" +} +If ($xamlManifest.Package.Identity.ProcessorArchitecture -Ne $architecture) { + Throw "$XamlAppX is not built for $architecture (instead, it is built for $($xamlManifest.Package.Identity.ProcessorArchitecture))" +} + +######## +# Preparation of source files +######## + +$itemsToRemove = $filesToRemove | ForEach-Object { + Get-Item (Join-Path $terminalAppPath $_) -EA:SilentlyContinue | Where-Object { + $filesToKeep -NotContains $_.Name + } +} | Sort-Object FullName -Unique +$itemsToRemove | Remove-Item -Recurse + +$filesToCopyFromXaml | ForEach-Object { + Get-Item (Join-Path $xamlAppPath $_) +} | Copy-Item -Recurse -Destination $terminalAppPath + +######## +# Resource Management +######## + +$finalTerminalPriFile = Join-Path $terminalAppPath "resources.pri" +& (Join-Path $PSScriptRoot "Merge-TerminalAndXamlResources.ps1") ` + -TerminalRoot $terminalAppPath ` + -XamlRoot $xamlAppPath ` + -OutputPath $finalTerminalPriFile ` + -Verbose:$Verbose | Out-Host + +######## +# Packaging +######## + +If ($PSCmdlet.ParameterSetName -Eq "AppX") { + # We only produce a ZIP when we're combining two AppX directories. + New-Item -ItemType Directory -Path $Destination -ErrorAction:SilentlyContinue | Out-Null + $outputZip = (Join-Path $Destination ("{0}.zip" -f ($distributionName))) + & tar -c --format=zip -f $outputZip -C $tempDir $terminalDir + Remove-Item -Recurse -Force $tempDir -EA:SilentlyContinue + Get-Item $outputZip +} ElseIf ($PSCmdlet.ParameterSetName -Eq "Layout") { + Get-Item $terminalAppPath +} diff --git a/build/scripts/Patch-ManifestsToWindowsVersion.ps1 b/build/scripts/Patch-ManifestsToWindowsVersion.ps1 deleted file mode 100644 index fe86f24fd8..0000000000 --- a/build/scripts/Patch-ManifestsToWindowsVersion.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -Param( - [string]$NewWindowsVersion = "10.0.22000.0" -) - -Get-ChildItem src/cascadia/CascadiaPackage -Recurse -Filter *.appxmanifest | ForEach-Object { - $xml = [xml](Get-Content $_.FullName) - $xml.Package.Dependencies.TargetDeviceFamily | Where-Object Name -Like "Windows*" | ForEach-Object { - $_.MinVersion = $NewWindowsVersion - } - $xml.Save($_.FullName) -} diff --git a/build/scripts/Run-Tests.ps1 b/build/scripts/Run-Tests.ps1 index ea69396569..bd4cf28fc5 100644 --- a/build/scripts/Run-Tests.ps1 +++ b/build/scripts/Run-Tests.ps1 @@ -1,27 +1,37 @@ [CmdLetBinding()] Param( - [Parameter(Mandatory=$true, Position=0)][string]$MatchPattern, - [Parameter(Mandatory=$true, Position=1)][string]$Platform, - [Parameter(Mandatory=$true, Position=2)][string]$Configuration, - [Parameter(Mandatory=$false, Position=3)][string]$LogPath, - [Parameter(Mandatory=$false)][string]$Root = ".\bin\$Platform\$Configuration" + [Parameter(Mandatory=$true, Position=0)] + [string]$MatchPattern, + [Parameter(Mandatory=$true, Position=1)] + [string]$Platform, + [Parameter(Mandatory=$true, Position=2)] + [string]$Configuration, + [Parameter(Mandatory=$false, Position=3)] + [string]$LogPath, + [Parameter(Mandatory=$false)] + [string]$Root = ".\bin\$Platform\$Configuration", + [string[]]$AdditionalTaefArguments ) -$testdlls = Get-ChildItem -Path "$Root" -Recurse -Filter $MatchPattern +# Find test DLLs based on the provided root, match pattern, and recursion +$testDlls = Get-ChildItem -Path $Root -Recurse -Filter $MatchPattern +$args = @() -$args = @(); - -if ($LogPath) -{ - $args += '/enablewttlogging'; - $args += '/appendwttlogging'; - $args += "/logFile:$LogPath"; - Write-Host "Wtt Logging Enabled"; +# Check if the LogPath parameter is provided and enable WTT logging +if ($LogPath) { + $args += '/enablewttlogging' + $args += '/appendwttlogging' + $args += "/logFile:$LogPath" + Write-Host "WTT Logging Enabled" } -&"$Root\te.exe" $args $testdlls.FullName +# Invoke the te.exe executable with arguments and test DLLs +& "$Root\te.exe" $args $testDlls.FullName $AdditionalTaefArguments -if ($lastexitcode -Ne 0) { Exit $lastexitcode } +# Check the exit code of the te.exe process and exit accordingly +if ($LASTEXITCODE -ne 0) { + Exit $LASTEXITCODE +} Exit 0 diff --git a/build/scripts/Test-WindowsTerminalPackage.ps1 b/build/scripts/Test-WindowsTerminalPackage.ps1 index aa9cca6d10..b405d98c21 100644 --- a/build/scripts/Test-WindowsTerminalPackage.ps1 +++ b/build/scripts/Test-WindowsTerminalPackage.ps1 @@ -70,23 +70,24 @@ Try { $dependencies = $Manifest.Package.Dependencies.PackageDependency.Name $depsHasVclibsDesktop = ("Microsoft.VCLibs.140.00.UWPDesktop" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug.UWPDesktop" -in $dependencies) - $depsHasVcLibsAppX = ("Microsoft.VCLibs.140.00" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug" -in $dependencies) + $depsHasVclibsAppX = ("Microsoft.VCLibs.140.00" -in $dependencies) -or ("Microsoft.VCLibs.140.00.Debug" -in $dependencies) $filesHasVclibsDesktop = ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140.dll" -EA:Ignore)) -or ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140d.dll" -EA:Ignore)) $filesHasVclibsAppX = ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140_app.dll" -EA:Ignore)) -or ($null -ne (Get-Item "$AppxPackageRootPath\vcruntime140d_app.dll" -EA:Ignore)) - If ($depsHasVclibsDesktop -Eq $filesHasVclibsDesktop) { - $eitherBoth = if ($depsHasVclibsDesktop) { "both" } else { "neither" } - $neitherNor = if ($depsHasVclibsDesktop) { "and" } else { "nor" } - Throw "Package has $eitherBoth Dependency $neitherNor Integrated Desktop VCLibs" + If ($filesHasVclibsDesktop) { + Throw "Package contains the desktop VCLibs" } - If ($depsHasVclibsAppx -Eq $filesHasVclibsAppx) { - if ($depsHasVclibsAppx) { - # We've shipped like this forever, so downgrade to warning. - Write-Warning "Package has both Dependency and Integrated AppX VCLibs" - } else { - Throw "Package has neither Dependency nor Integrated AppX VCLibs" - } + If ($depsHasVclibsDesktop) { + Throw "Package has a dependency on the desktop VCLibs" + } + + If ($filesHasVclibsAppX) { + Throw "Package contains the AppX VCLibs" + } + + If ($depsHasVclibsAppX) { + Throw "Package has a dependency on the AppX VCLibs" } ### Check that we have an App.xbf (which is a proxy for our resources having been merged) @@ -96,11 +97,6 @@ Try { Throw "Failed to find App.xbf (TerminalApp project) in resources.pri" } - If (($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10.dll" -EA:Ignore)) -And - ($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10d.dll" -EA:Ignore))) { - Throw "Failed to find cpprest142_2_10.dll -- check the WAP packaging project" - } - If (($null -eq (Get-Item "$AppxPackageRootPath\wtd.exe" -EA:Ignore)) -And ($null -eq (Get-Item "$AppxPackageRootPath\wt.exe" -EA:Ignore))) { Throw "Failed to find wt.exe/wtd.exe -- check the WAP packaging project" diff --git a/common.openconsole.props b/common.openconsole.props index f6eb4c7c94..c8f43e3602 100644 --- a/common.openconsole.props +++ b/common.openconsole.props @@ -10,22 +10,4 @@ $(MSBuildThisFileDirectory) - - - 2.7.3-prerelease.220816001 - - 2.7.3 - - diff --git a/custom.props b/custom.props index 16a9b72910..6027b31193 100644 --- a/custom.props +++ b/custom.props @@ -2,22 +2,10 @@ - - $([MSBuild]::Add($(VersionBuildRevision), 1)) - true - 2022 + 2023 1 - 17 + 19 Windows Terminal diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index ffa247ab5d..d0c670dcd5 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -2,15 +2,13 @@ - - - - - + + - + + diff --git a/doc/ConsoleHostSettings.md b/doc/ConsoleHostSettings.md index 0a4d84847b..7eef405c2d 100644 --- a/doc/ConsoleHostSettings.md +++ b/doc/ConsoleHostSettings.md @@ -59,7 +59,7 @@ To modify settings specific to the current application, invoke the `Properties` When console applications are launched, the Windows Console Host determines which settings to use by overlaying settings from the above locations. -1. Initialize settings based on hardcoded defaults +1. Initialize settings based on hard-coded defaults 2. Overlay settings specified by the user's configured defaults 3. Overlay application-specific settings from either the registry or the shortcut file, depending on how the application was launched diff --git a/doc/Niksa.md b/doc/Niksa.md index c3f8a1fa1a..5efcdda0ab 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! @@ -145,7 +147,7 @@ Our only backport successes really come from corporations with massive addressab It's also costly in terms of time, effort, and testing for us to validate a modification to a released OS. We have a mindbogglingly massive amount of automated machinery dedicated to processing and validating the things that we check in while developing the current OS builds. But it's a special costly ask to spin up some to all of those activities to validate backported fixes. We do it all the time for Patch Tuesday, but in those patches, they only pass through the minimum number of fixes required to maximize the restoration of productivity/security/revenue/etc. because every additional fix adds additional complexity and additional risk. -So from our little team working hard to make developers happy, we virtually never make the cut for servicing. We're sorry, but we hope you can understand. It's just the reality of the situation to say "nope" when people ask for a backport. In our team's ideal world, you would all be running the latest console bits everywhere everytime we make a change. But that's just not how it is today. +So from our little team working hard to make developers happy, we virtually never make the cut for servicing. We're sorry, but we hope you can understand. It's just the reality of the situation to say "nope" when people ask for a backport. In our team's ideal world, you would all be running the latest console bits everywhere every time we make a change. But that's just not how it is today. Original Source: https://github.com/microsoft/terminal/issues/279#issuecomment-439179675 diff --git a/doc/ORGANIZATION.md b/doc/ORGANIZATION.md index d220c9034e..07a6e41ee7 100644 --- a/doc/ORGANIZATION.md +++ b/doc/ORGANIZATION.md @@ -32,7 +32,6 @@ * `/src/cascadia/TerminalApp` - This DLL represents the implementation of the Windows Terminal application. This includes parsing settings, hosting tabs & panes with Terminals in them, and displaying other UI elements. This DLL is almost entirely UWP-like code, and shouldn't be doing any Win32-like UI work. * `/src/cascadia/WindowsTerminal` - This EXE provides Win32 hosting for the TerminalApp. It will set up XAML islands, and is responsible for drawing the window, either as a standard window or with content in the titlebar (non-client area). * `/src/cascadia/CascadiaPackage` - This is a project for packaging the Windows Terminal and its dependencies into an .appx/.msix for deploying to the machine. - * `/src/cascadia/PublicTerminalCore` - This is a DLL wrapper for the TerminalCore and Renderer, similar to `TermControl`, which exposes some exported functions that so the Terminal can be used from C#. * `/src/cascadia/WpfTerminalControl` - A DLL implementing a WPF version of the Terminal Control. * `/src/host` – The meat of the windows console host. This includes buffer, input, output, windowing, server management, clipboard, and most interactions with the console host window that aren’t stated anywhere else. We’re trying to pull things out that are reusable into other libraries, but it’s a work in progress * `/src/host/lib` – Builds the reusable LIB copy of the host diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 6366468e68..9dc3a8afd5 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -24,6 +24,13 @@ "pattern": "^(-?\\d+)?(,\\s?(-?\\d+)?)?$", "type": "string" }, + "CSSLengthPercentage": { + "pattern": "^[+-]?\\d+(?:\\.\\d+)?(?:%|ch|pt|px)?$", + "type": [ + "string", + "null" + ] + }, "DynamicProfileSource": { "enum": [ "Windows.Terminal.Wsl", @@ -78,11 +85,44 @@ } ] }, + "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": { "description": "The name of a color scheme to use when unfocused.", - "type": "string" + "oneOf": [ + { + "$ref": "#/$defs/SchemePair" + }, + { + "type": "string" + } + ] }, "foreground": { "$ref": "#/$defs/Color", @@ -229,6 +269,26 @@ "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" + }, + "SchemePair": { + "description": "Contains both a light and dark color scheme for the Terminal to use, depending on the theme of the application.", + "properties": { + "light": { + "default": "Campbell", + "description": "Name of the scheme to use when the app is using light theme", + "type": "string" + }, + "dark": { + "default": "Campbell", + "description": "Name of the scheme to use when the app is using dark theme", + "type": "string" } }, "type": "object" @@ -292,6 +352,14 @@ } }, "additionalProperties": false + }, + "cellWidth": { + "$ref": "#/$defs/CSSLengthPercentage", + "description": "Override the width of the terminal's cells. The override works similar to CSS' letter-spacing. It defaults to the natural glyph advance width of the primary font rounded to the nearest pixel." + }, + "cellHeight": { + "$ref": "#/$defs/CSSLengthPercentage", + "description": "Override the height of the terminal's cells. The override works similar to CSS' line-height. Defaults to the sum of the natural glyph ascend, descend and line-gap of the primary font rounded to the nearest pixel. The default is usually quite close to setting this to 1.2." } }, "type": "object" @@ -310,8 +378,12 @@ }, "ShortcutActionName": { "enum": [ + "addMark", "adjustFontSize", + "adjustOpacity", + "clearAllMarks", "clearBuffer", + "clearMark", "closeOtherPanes", "closeOtherTabs", "closePane", @@ -322,6 +394,7 @@ "copy", "duplicateTab", "expandSelectionToWord", + "experimental.colorSelection", "exportBuffer", "find", "findMatch", @@ -329,62 +402,60 @@ "globalSummon", "identifyWindow", "identifyWindows", + "markMode", "moveFocus", "movePane", - "swapPane", - "markMode", "moveTab", "multipleActions", "newTab", "newWindow", "nextTab", + "openAbout", "openNewTabDropdown", "openSettings", + "openSystemMenu", "openTabColorPicker", + "openTabRenamer", "openWindowRenamer", "paste", "prevTab", - "renameTab", - "openSystemMenu", - "openTabRenamer", "quakeMode", + "quit", + "renameTab", + "renameWindow", "resetFontSize", "resizePane", - "renameWindow", + "restoreLastClosed", "scrollDown", "scrollDownPage", + "scrollToBottom", + "scrollToMark", + "scrollToTop", "scrollUp", "scrollUpPage", - "scrollToBottom", - "scrollToTop", + "searchWeb", + "selectAll", "sendInput", "setColorScheme", + "setFocusMode", + "setFullScreen", + "setMaximized", "setTabColor", + "showSuggestions", "splitPane", + "swapPane", + "switchSelectionEndpoint", "switchToTab", "tabSearch", "toggleAlwaysOnTop", "toggleBlockSelection", "toggleFocusMode", - "selectAll", - "setFocusMode", - "switchSelectionEndpoint", "toggleFullscreen", - "setFullScreen", - "setMaximized", "togglePaneZoom", - "toggleSplitOrientation", "toggleReadOnlyMode", "toggleShaderEffects", + "toggleSplitOrientation", "wt", - "quit", - "adjustOpacity", - "restoreLastClosed", - "addMark", - "scrollToMark", - "clearMark", - "clearAllMarks", - "experimental.colorSelection", "unbound" ], "type": "string" @@ -539,6 +610,160 @@ }, "type": "object" }, + "NewTabMenuEntryType": { + "enum": [ + "source", + "profile", + "folder", + "separator", + "remainingProfiles", + "matchProfiles" + ] + }, + "NewTabMenuEntry": { + "properties": { + "type": { + "description": "The type of menu entry", + "$ref": "#/$defs/NewTabMenuEntryType" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "FolderEntryInlining": { + "enum": [ + "never", + "auto" + ] + }, + "FolderEntry": { + "description": "A folder entry in the new tab dropdown", + "allOf": [ + { + "$ref": "#/$defs/NewTabMenuEntry" + }, + { + "properties": { + "type": { + "type": "string", + "const": "folder" + }, + "name": { + "type": "string", + "default": "", + "description": "The name of the folder to show in the menu" + }, + "icon": { + "$ref": "#/$defs/Icon" + }, + "entries": { + "type": "array", + "description": "The entries to put inside this folder", + "minItems": 1, + "items": { + "$ref": "#/$defs/NewTabMenuEntry" + } + }, + "inline": { + "description": "When set to auto and the folder only has a single entry, the entry will show directly and no folder will be rendered", + "default": "never", + "$ref": "#/$defs/FolderEntryInlining" + }, + "allowEmpty": { + "description": "Whether to render a folder without entries, or to hide it", + "default": false, + "type": "boolean" + } + } + } + ] + }, + "SeparatorEntry": { + "description": "A separator in the new tab dropdown", + "allOf": [ + { + "$ref": "#/$defs/NewTabMenuEntry" + }, + { + "properties": { + "type": { + "type": "string", + "const": "separator" + } + } + } + ] + }, + "ProfileEntry": { + "description": "A profile in the new tab dropdown", + "allOf": [ + { + "$ref": "#/$defs/NewTabMenuEntry" + }, + { + "properties": { + "type": { + "type": "string", + "const": "profile" + }, + "profile": { + "type": "string", + "default": "", + "description": "The name or GUID of the profile to show in this entry" + } + } + } + ] + }, + "RemainingProfilesEntry": { + "description": "The set of profiles that are not yet explicitly included in another entry, such as the profile or source entries. This entry can be used at most one time!", + "allOf": [ + { + "$ref": "#/$defs/NewTabMenuEntry" + }, + { + "properties": { + "type": { + "type": "string", + "const": "remainingProfiles" + } + } + } + ] + }, + "MatchProfilesEntry": { + "description": "A set of profiles all matching the given name, source, or command line, to show in the new tab dropdown", + "allOf": [ + { + "$ref": "#/$defs/NewTabMenuEntry" + }, + { + "properties": { + "type": { + "type": "string", + "const": "matchProfiles" + }, + "name": { + "type": "string", + "default": "", + "description": "The name of the profiles to match" + }, + "source": { + "type": "string", + "default": "", + "description": "The source of the profiles to match" + }, + "commandline": { + "type": "string", + "default": "", + "description": "The command line of the profiles to match" + } + } + } + ] + }, "SwitchToAdjacentTabArgs": { "oneOf": [ { @@ -607,6 +832,11 @@ "default": false, "description": "If true, the copied content will be copied as a single line (even if there are hard line breaks present in the text). If false, newlines persist from the selected text." }, + "dismissSelection": { + "type": "boolean", + "default": true, + "description": "If false, the copied content will be copied and the selection will not be dismissed. If true, the selection will be dismissed." + }, "copyFormatting": { "default": null, "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied. Not setting this value inherits the behavior of the `copyFormatting` global setting.", @@ -1518,6 +1748,29 @@ } ] }, + "SearchWebAction": { + "description": "Search the web for selected text", + "allOf": [ + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "properties": { + "action": { + "type": "string", + "const": "searchWeb" + }, + "queryUrl": { + "type": "string", + "description": "URL of the web page to launch, %s is replaced with the selected text" + } + } + } + ], + "required": [ + "queryUrl" + ] + }, "AdjustOpacityAction": { "description": "Changes the opacity of the active Terminal window. If `relative` is specified, then this action will increase/decrease relative to the current opacity.", "allOf": [ @@ -1546,6 +1799,183 @@ } ] }, + "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", + "hover", + "never", + "activeOnly" + ], + "type": "string" + }, + "ThemeColor": { + "description": "A special kind of color for use in themes. Can be an #rrggbb color, #rrggbbaa color, or a special value. 'accent' is evaluated as the user's selected Accent color in the OS, and 'terminalBackground' will be evaluated as the background color of the active terminal pane.", + "oneOf": [ + { + "pattern": "^#[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{2})?)?$", + "type": "string", + "format": "color", + "default": "#000000ff" + }, + { + "const": "accent", + "type": "string" + }, + { + "const": "terminalBackground", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "TabTheme": { + "additionalProperties": false, + "description": "A set of properties for customizing the appearance of the tabs", + "properties": { + "background": { + "description": "The color of a tab when it is the active tab", + "$ref": "#/$defs/ThemeColor" + }, + "unfocusedBackground": { + "description": "The color of a tab when it is not the active tab", + "$ref": "#/$defs/ThemeColor" + }, + "showCloseButton": { + "description": "Controls the visibility of the close button on the tab", + "$ref": "#/$defs/ShowCloseButton" + } + } + }, + "TabRowTheme": { + "additionalProperties": false, + "description": "A set of properties for customizing the appearance of the tab row", + "properties": { + "background": { + "description": "The color of the tab row when the window is the foreground window.", + "$ref": "#/$defs/ThemeColor" + }, + "unfocusedBackground": { + "description": "The color of the tab row when the window is inactive", + "$ref": "#/$defs/ThemeColor" + } + } + }, + "WindowTheme": { + "additionalProperties": false, + "description": "A set of properties for customizing the appearance of the window itself", + "properties": { + "applicationTheme": { + "description": "Which UI theme the Terminal should use for controls", + "enum": [ "light", "dark", "system" ], + "type": "string" + }, + "useMica": { + "description": "True if the Terminal should use a Mica backdrop for the window. This will apply underneath all controls (including the terminal panes and the titlebar)", + "type": "boolean", + "default": false + }, + "experimental.rainbowFrame": { + "description": "When enabled, the frame of the window will cycle through all the colors. Enabling this will override the `frame` and `unfocusedFrame` settings.", + "type": "boolean", + "default": false + }, + "frame": { + "description": "The color of the window frame when the window is inactive. This only works on Windows 11", + "$ref": "#/$defs/ThemeColor" + }, + "unfocusedFrame": { + "description": "The color of the window frame when the window is inactive. This only works on Windows 11", + "$ref": "#/$defs/ThemeColor" + } + } + }, + "Theme": { + "additionalProperties": false, + "description": "A set of properties for customizing the appearance of the window. This controls things like the titlebar, the tabs, the application theme.", + "properties": { + "name": { + "type": "string", + "description": "The name of the theme. This will be displayed in the settings UI.", + "not": { + "enum": [ "light", "dark", "system" ] + } + }, + "tab": { + "$ref": "#/$defs/TabTheme" + }, + "tabRow": { + "$ref": "#/$defs/TabRowTheme" + }, + "window": { + "$ref": "#/$defs/WindowTheme" + } + } + }, + "ThemePair": { + "additionalProperties": false, + "description": "A pair of Theme names, to allow the Terminal to switch theme based on the OS theme", + "properties": { + "light": { + "type": "string", + "description": "The name of the theme to use when the OS is in Light theme", + "default": "light" + }, + "dark": { + "type": "string", + "description": "The name of the theme to use when the OS is in Dark theme", + "default": "dark" + } + } + }, + "NewTabMenu": { + "description": "Defines the order and structure of the 'new tab' menu. It can consist of e.g. profiles, folders, and separators.", + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/FolderEntry" + }, + { + "$ref": "#/$defs/SeparatorEntry" + }, + { + "$ref": "#/$defs/ProfileEntry" + }, + { + "$ref": "#/$defs/MatchProfilesEntry" + }, + { + "$ref": "#/$defs/RemainingProfilesEntry" + } + ] + } + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -1660,6 +2090,12 @@ { "$ref": "#/$defs/ColorSelectionAction" }, + { + "$ref": "#/$defs/SearchWebAction" + }, + { + "$ref": "#/$defs/ShowSuggestionsAction" + }, { "type": "null" } @@ -1742,6 +2178,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.", @@ -1762,6 +2203,11 @@ "description": "When set to true, the terminal will focus the pane on mouse hover.", "type": "boolean" }, + "compatibility.isolatedMode": { + "default": false, + "description": "When set to true, Terminal windows will not be able to interact with each other (including global hotkeys, tab drag/drop, running commandlines in existing windows, etc.). This is a compatibility escape hatch for users who are running into certain windowing issues.", + "type": "boolean" + }, "copyFormatting": { "default": true, "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied.", @@ -1924,20 +2370,32 @@ }, "type": "array" }, + "newTabMenu": { + "$ref": "#/$defs/NewTabMenu" + }, "language": { "default": "", "description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.", "type": "string" }, "theme": { - "default": "system", - "description": "Sets the theme of the application. The special value \"system\" refers to the active Windows system theme.", - "enum": [ - "light", - "dark", - "system" - ], - "type": "string" + "default": "dark", + "description": "Sets the theme of the application. This value should be the name of one of the themes defined in `themes`. The Terminal also includes the themes `dark`, `light`, and `system`.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ThemePair" + } + ] + }, + "themes": { + "description": "The list of available themes", + "items": { + "$ref": "#/$defs/Theme" + }, + "type": "array" }, "showTabsInTitlebar": { "default": true, @@ -2070,6 +2528,11 @@ "null" ] }, + "compatibility.reloadEnvironmentVariables": { + "default": true, + "description": "When set to true, when opening a new tab or pane it will get reloaded environment variables.", + "type": "boolean" + }, "unfocusedAppearance": { "$ref": "#/$defs/AppearanceConfig", "description": "Sets the appearance of the terminal when it is unfocused.", @@ -2170,7 +2633,14 @@ "colorScheme": { "default": "Campbell", "description": "Name of the terminal color scheme to use. Color schemes are defined under \"schemes\".", - "type": "string" + "oneOf": [ + { + "$ref": "#/$defs/SchemePair" + }, + { + "type": "string" + } + ] }, "commandline": { "description": "Executable used in the profile.", @@ -2215,6 +2685,13 @@ "default": false, "description": "When true, this profile should always open in an elevated context. If the window isn't running as an Administrator, then a new elevated window will be created." }, + "environment": { + "description": "Key-value pairs representing environment variables to set. Environment variable names are not case sensitive. You can reference existing environment variable names by enclosing them in literal percent characters (e.g. %PATH%).", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "experimental.autoMarkPrompts": { "default": false, "description": "When set to true, prompts will automatically be marked.", @@ -2396,7 +2873,10 @@ }, "startingDirectory": { "description": "The directory the shell starts in when it is loaded.", - "type": "string" + "type": [ + "string", + "null" + ] }, "suppressApplicationTitle": { "description": "When set to true, tabTitle overrides the default title of the tab and any title change messages from the application will be suppressed. When set to false, tabTitle behaves as normal.", diff --git a/doc/color_nudging.html b/doc/color_nudging.html new file mode 100644 index 0000000000..cfbbc4bd11 --- /dev/null +++ b/doc/color_nudging.html @@ -0,0 +1,235 @@ + + + + + Perceptual Color Nudging + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + +
InputWCAG21:
APCA:
ΔE2000
(ConEmu)
WCAG21:
APCA:
ΔEOKWCAG21:
APCA:
+
+
+

Input

+
+
+
+
+
+
+
+
+
+

ΔE2000 (ConEmu)

+
+
+
+
+
+
+
+
+
+

ΔEOK

+
+
+
+
+
+
+
+
+ + + + diff --git a/doc/roadmap-2022.md b/doc/roadmap-2022.md index eab75a0e9b..5c4d5bf7e7 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 0000000000..c27faa340e --- /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 diff --git a/doc/specs/#11000 - Marks/Shell-Integration-Marks.md b/doc/specs/#11000 - Marks/Shell-Integration-Marks.md new file mode 100644 index 0000000000..1d81157ea9 --- /dev/null +++ b/doc/specs/#11000 - Marks/Shell-Integration-Marks.md @@ -0,0 +1,499 @@ +--- +author: Mike Griese +created on: 2022-03-28 +last updated: 2023-07-19 +issue id: 11000, 1527, 6232 +--- + +##### [Original issue: [#1527]] [experimental PR [#12948]] [remaining marks [#14341]] + +# Windows Terminal - Shell Integration (Marks) + +## Abstract + +_"Shell integration" refers to a broad category of ways by which a commandline +shell can drive richer integration with the terminal. This spec in particular is +most concerned with "marks" and other semantic markup of the buffer._ + +Marks are a new buffer-side feature that allow the commandline application or +user to add a bit of metadata to a range of text. This can be used for marking a +region of text as a prompt, marking a command as succeeded or failed, quickly +marking errors in the output. These marks can then be exposed to the user as +pips on the scrollbar, or as icons in the margins. Additionally, the user can +quickly scroll between different marks, to allow easy navigation between +important information in the buffer. + +Marks in the Windows Terminal are a combination of functionality from a variety +of different terminal emulators. "Marks" attempts to unify these different, but +related pieces of functionality. + +## Background + +There's a large amount of prior art on this subject. I've attempted to collect +as much as possible in the ["Relevant external docs"](#Relevant-external-docs) +section below. "Marks" have been used in different scenarios by different +emulators for different purposes. The common thread running between them of +marking a region of text in the buffer with a special meaning. + +* iTerm2, ConEmu, FinalTerm et.al. support emitting a VT sequence to indicate + that a line is a "prompt" line. This is often used for quick navigation + between these prompts. +* FinalTerm (and xterm.js) also support marking up more than just the prompt. + They go so far as to differentiate the start/end of the prompt, the start of + the commandline input, and the start/end of the command output. + `FTCS_COMMAND_FINISHED` is even designed to include metadata indicating + whether a command succeeded or failed. +* Additionally, Terminal.app allows users to "bookmark" lines via the UI. That + allows users to quickly come back to something they feel is important. +* Consider also editors like Visual Studio. VS also uses little marks on the + scrollbar to indicate where other matches are for whatever the given search + term is. + +### "Elevator" pitch + +The Terminal provides a way for command line shells to semantically mark parts +of the command-line output. By marking up parts of the output, the Terminal can +provide richer experiences. The Terminal will know where each command starts and +stops, what the actual command was and what the output of that command is. This +allows the terminal to expose quick actions for: + +* Quickly navigating the history by scrolling between commands +* Re-running a previous command in the history +* Copying all the output of a single command +* A visual indicator to separate out one command-line from the next, for quicker + mental parsing of the output of the command-line. +* Collapsing the output of a command, as to reduce noise +* Visual indicators that highlight commands that succeeded or failed. +* Jumping to previously used directories + +### User Stories + +This is a bit of a unusual section, as this feature was already partially +implemented when this spec was written. + +Story | Size | Description +--|-----------|-- +A | ✅ Done | The user can use mark each prompt and have a mark displayed on the scrollbar +B | ✅ Done | The user can perform an action to scroll between marks +C | ✅ Done | The user can manually add marks to the buffer +D | ✅ Done | The shell can emit different marks to differentiate between prompt, command, and output +E | ✅ Done | Clearing the buffer clears marks +F | 🐣 Crawl | Marks stay in the same place you'd expect after resizing the buffer. +G | ✅ Done | Users can perform an action to select the previous command's output +H | 🚶 Walk | The find dialog can display marks on the scrollbar indicating the position of search matches +I | 🏃‍♂️ Run | The terminal can display icons in the gutter, with quick actions for that command (re-run, copy output, etc) +J | 🏃‍♂️ Run | The terminal can display a faint horizontal separator between commands in the buffer. +K | 🚀 Sprint | The terminal can "collapse" content between two marks. +L | 🚀 Sprint | The terminal can display a sticky header above the control which displays the most recent command +M | 🚀 Sprint | The user can open a dialog to manually bookmark a line with a custom comment + +## Solution Design + +### Supported VT sequences + +* [x] iTerm2's OSC `SetMark` (in [#12948]) +* [x] FinalTerm prompt markup sequences + - [x] **FTCS_PROMPT** was added in [#13163] + - [x] The rest in [#14341] +* [ ] additionally, VsCode's FinalTerm prompt markup variant, `OSC 663` +* [ ] [ConEmu's + `OSC9;12`](https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC) +* [ ] Any custom OSC we may want to author ourselves. + +The FinalTerm prompt sequences are probably the most complicated version of all +these, so it's important to give these a special callout. Almost all the other +VT sequences are roughly equivalent to **FTCS_PROMPT**. The xterm.js / VsCode +version has additional cases, that they ironically added to work around conpty +not understanding these sequences originally. + +#### FinalTerm sequences + +The relevant FinalTerm sequences for marking up the prompt are as follows. + + +![image](FTCS-diagram.png) + +* **FTCS_PROMPT**: `OSC 133 ; A ST` + - The start of a prompt. Internally, this sets a marker in the buffer + indicating we started a prompt at the current cursor position, and that + marker will be used when we get a **FTCS_COMMAND_START** +* **FTCS_COMMAND_START**: `OSC 133 ; B ST` + - The start of a commandline (READ: the end of the prompt). When it follows a + **FTCS_PROMPT**, it creates a mark in the buffer from the location of the + **FTCS_PROMPT** to the current cursor position, with the category of + `prompt` +* **FTCS_COMMAND_EXECUTED**: `OSC 133 ; C ST` + - The start of the command output / the end of the commandline. +* **FTCS_COMMAND_FINISHED**: `OSC 133 ; D ; [Ps] ST` + - the end of a command. + +Same deal for the **FTCS_COMMAND_EXECUTED**/**FTCS_COMMAND_FINISHED** ones. +**FTCS_COMMAND_EXECUTED** does nothing until we get a **FTCS_COMMAND_FINISHED**, +and the `[Ps]` parameter determines the category. + - `[Ps] == 0`: success + - anything else: error + +This whole sequence will get turned into a single mark. + +When we get the **FTCS_COMMAND_FINISHED**, set the category of the prompt mark +that preceded it, so that the `prompt` becomes an `error` or a `success`. + + +### Buffer implementation + +In the initial PR ([#12948]), marks were stored simply as a `vector`, +where a mark had a start and end point. These wouldn't reflow on resize, and +didn't support all of the FTCS sequences. + +There's ultimately three types of region here we need to mark: +* The prompt (starting from A) +* the command (starting from B) +* the output (starting from C) + +That intuitively feels a bit like a text attribute almost. Additionally, the +prompt should be connected to its subsequent command and output, s.t. we can +* Select command output +* re-run command + +easily. Supposedly, we could do this by iterating through the whole buffer to +find the previous/next {whatever}[[1](#footnote-1)]. Additionally, the prompt +needs to be able to contain the status / category, and a `133;D` needs to be +able to change the category of the previous prompt/command. + +If we instead do a single mark for each command, from `133;A` to `133;A`, and +have sub-points for elements within the command +* `133;A` starts a mark on the current line, at the current position. +* `133;B` sets the end of the mark to the current position. +* `133;C` updates the mark's `commandStart` to the current end, then sets the + end of the mark to the current position. +* `133;D` updates the mark's `outputStart` to the current end, then sets the end + of the mark to the current position. It also updates the category of the mark, + if needed. + +Each command then only shows up as a single mark on the scrollbar. Jumping +between commands is easy, `scrollToMark` operates on `mark.start`, which is +where the prompt started. "Bookmarks", i.e. things started by the user wouldn't +have `commandStart` or `outputStart` in them. Getting the text of the command, +of the output is easy - it's just the text between sub-points. + +Reflow still sucks though - we'd need to basically iterate over all the marks as +we're reflowing, to make sure we put them into the right place in the new +buffer. This is annoying and tedious, but shouldn't realistically be a +performance problem. + +#### Cmd.exe considerations + +`cmd.exe` is generally a pretty bad shell, and doesn't have a lot of the same +hooks that other shells do, that might allow for us to emit the +**FTCS_COMMAND_EXECUTED** sequence. However, cmd.exe also doesn't allow +multiline prompts, so we can be relatively certain that when the user presses +enter, that's the end of the prompt. We will treat the +`autoMarkPrompts` setting (which auto-marks enter) as the _end of the +prompt_. That would at least allow cmd.exe to emit a {command finished}{prompt +start}{prompt...}{command start} in the prompt, and have us add the command +executed. It is not perfect (we wouldn't be able to get error information), but +it does work well enough. + +```cmd +PROMPT $e]133;D$e\$e]133;A$e\$e]9;9;$P$e\%PROMPT%$e]133;B$e\ +``` + +## Settings proposals + +The below are the proposed additions to the settings for supporting marks and +interacting with them. Some of these have already been added as experimental +settings - these would be promoted to no longer be experimental. + +Many of the sub-points on these settings are definitely "Future Consideration" +level settings. For example, the `scrollToMark` `"highlight"` property. That one +is certainly not something we need to ship with. + +### Actions + +In addition to driving marks via the output, we will also want to support adding +marks manually. These can be thought of like "bookmarks" - a user indicated +region that means something to the user. + +* [ ] `addMark`: add a mark to the buffer. If there's a selection, place the + mark covering at the selection. Otherwise, place the mark on the cursor row. + - [x] `color`: a color for the scrollbar mark. (in [#12948]) + - [ ] `category`: one of `{"prompt", "error", "warning", "success", "info"}` +* [ ] `scrollToMark` + - [x] `direction`: `["first", "previous", "next", "last"]` (in [#12948]) + - [ ] `category`: `flags({categories}...)`, default `"all"`. Only scroll to + one of the categories specified (e.g. only scroll to the previous error, + only the previous prompt, or just any mark) + - [ ] [#13449] - `center` or some other setting that controls how the mark is scrolled in. + - Maybe `top` (current) /`center` (as proposed) /`nearestEdge` (when + scrolling down, put the mark at the bottom line of viewport , up -> top + line)? + - [ ] [#13455] - `highlight`: `bool`, default true. Display a temporary + highlight around the mark when scrolling to it. ("Highlight" != "select") + - If the mark has prompt/command/output sections, only select the prompt and command. + - If the mark has zero width (i.e. the user just wanted to bookmark a line), + then highlight the entire row. +* [x] `clearMark`: Remove any marks in the selected region (or at the cursor + position) (in [#12948]) +* [x] `clearAllMarks`: Remove all the marks from the buffer. (in [#12948]) + + +#### Selecting commands & output + +_Inspired by a long weekend of manually copying .csv output from the Terminal to +a spreadsheet, only to discover that we rejected [#4588] some years ago._ + +* [x] `selectCommand(direction=[prev, next])`: Starting at the active selection + anchor, (or the cursor if there's no selection), select the command that + starts before/after this point (exclusive). Probably shouldn't wrap around + the buffer. + * Since this will create a selection including the start of the command, + performing this again will select the _next_ command (in whatever + direction). +* [x] `selectOutput(direction=[prev, next])`: same as above, but with command outputs. + +A convenient workflow might be a `multipleActions([selectOutput(prev), +copy()])`, to quickly select the previous commands output. + +### Per-profile settings + +* [x] `autoMarkPrompts`: `bool`, default `false`. (in [#12948]) +* [ ] `showFindMatchesOnScrollbar`: `bool`, default `true`. +* [ ] `showMarksOnScrollbar`: `bool` or `flags({categories}...)` + * As an example: `"showMarksOnScrollbar": ["error", "success"]`). + * Controls if marks should be displayed on the scrollbar. + * If `true`/`"all"`, then all marks are displayed. + * If `false`/`"none"`, then no marks are displayed. + * If a set of categories are provided, only display marks from those categories. + * [x] the bool version is (in [#12948]) + * [ ] The `flags({categories}...)` version is not done yet. +* [ ] `showGutterIcons`, for displaying gutter icons. + +## UX Design + +An example of what colored marks look like: + +![Select the entire output of a command](https://user-images.githubusercontent.com/18356694/207696859-a227abe2-ccd4-4b81-8a2c-8a22219cd0dd.gif) + +This gif demos both prompt marks and marks for search results: + +![](https://user-images.githubusercontent.com/18356694/191330278-3f6bc207-1bd5-4ebd-bb0e-1f84b0170f49.gif) + + +### Gutter icons + +![](vscode-shell-integration-gutter-mark.png) +_An example of what the icons in the VsCode gutter look like_ + +### Multiple marks on the same line + +When it comes to displaying marks on the scrollbar, or in the margins, the +relative priority of these marks matters. Marks are given the following +priority, with errors being the highest priority. +* Error +* Warning +* Success +* Prompt +* Info (default) + +## Work needed to get marks to v1 + +* [x] Clearing the screen leaves marks behind + * [x] Make sure `ED2` works to clear/move marks + * [x] Same with `ED3` + * [x] Same with `cls` / `Clear-Host` + * [x] Clear Buffer action too. +* [X] Circling doesn't update scrollbar + * I think this was fixed in [#14341], or in [#14045] +* [ ] Resizing / reflowing marks +* [x] marks should be stored in the `TextBuffer` + +## Future Considerations +* adding a timestamp for when a line was marked? +* adding a comment to the mark. How do we display that comment? a TeachingTip on + the scrollbar maybe (actually that's a cool idea) +* adding a shape to the mark? Terminal.app marks prompt lines with brackets in + the margins +* Marks are currently just displayed as "last one in wins", they should have a + real sort order +* Should the height of a mark on the scrollbar be dependent on font size & + buffer height? I think I've got it set to like, a static 2dip, but maybe it + should represent the actual height of the row (down to a min 1dip) +* [#13455] - highlight a mark when scrolled to with the `scrollToMark` action. + This is left as a future consideration to figure out what kind of UI we want + here. Do we want to highlight + - the prompt? + - the whole row of the prompt? + - the prompt and the command? + - The whole prompt & command & output? +* an `addBookmark` action: This one's basically just `addMark`, but opens a prompt + (like the window renamer) to add some text as a comment. Automatically + populated with the selected text (if there was some). + - A dedicated transient pane for displaying non-terminal content might be + useful for such a thing. + - This might just make more sense as a parameter to `addMark`. +* Other ideas for `addMark` parameters: + - `icon`: This would require us to better figure out how we display gutter + icons. This would probably be like, a _shape_ rather than an arbitrary + image. + - `note`: a note to stick on the mark, as a comment. Might be more valuable + with something like `addBookmark`. + +### Gutter icons + +VsCode implements a set of gutter icons to the left of the buffer lines, to +provide a UI element for exposing some quick actions to perform, powered by +shell integration. + +Gutter icons don't need to implement app-level actions at all. They _should_ be +part of the control. At least, part of the UWP `TermControl`. These are some +basic "actions" we could add to that menu. Since these are all attached to a +mark anyways, we already know what mark the user interacted with, and where the +start/end already is. +* Copy command +* Copy output +* Re-run command +* Save as task +* Explain this (for errors) + +To allow comments in marks (ala "bookmarks"), we can use +the gutter flyout to display the comment, and have the tooltip display that +comment. + +This is being left as a future consideration for now. We need to really sit and +consider what the UX is like for this. +* Do we just stick the gutter icons in the margin/padding? +* Have it be a separate space in the "buffer" + - If it's in the buffer itself, we can render it with the renderer, which by + all accounts, we probably should. + +### Edge cases where these might not work as expected + +Much of the benefits of shell integration come from literal buffer text parsing. +This can lead to some rough edge cases, such as: + +* the user presses Ctrl+VEscape to input an ESC character + and the shell displays it as `^[` +* the user presses Ctrl+VCtrl+J to input an LF character + and the shell displays it as a line break +* the user presses Enter within a quoted string and the shell + displays a continuation prompt +* the user types a command including an exclamation point, and the shell invokes + history expansion and echoes the result of expansion before it runs the + command +* The user has a prompt with a right-aligned status, ala + ![](https://user-images.githubusercontent.com/189190/254475719-5007df07-6cc3-42e8-baf7-2572579eb2b9.png) + +In these cases, the effects of shell integration will likely not work as +intended. There are various possible solutions that are being explored. We might +want to in the future also use [VsCode's extension to the FTCS sequences] to +enable the shell to tell the terminal the literal resulting commandline. + +There's been [other proposals] to extend shell integration features as well. + +### Rejected ideas + +There was originally some discussion as to whether this is a design that should +be unified with generic pattern matchers. Something like the URL detection, +which identifies part of the buffer and then "marks" it. Prototypes for both of +these features are going in very different directions, however. Likely best to +leave them separate. + +## Resources + +### Other related issues + +Not necessarily marks related, but could happily leverage this functionality. + +* [#5916] and [#12366], which are likely to be combined into a single thread + - Imagine a trigger that automatically detects `error:.*` and then marks the line +* [#9583] + - Imagine selecting some text, colorizing & marking it all at once + - `addMark(selection:false)`, above, was inspired by this. +* [#7561] (and broadly, [#3920]) + - Search results should maybe show up here on the scrollbar too. +* [#13455] +* [#13449] +* [#4588] +* [#14754] - A "sticky header" for the `TermControl` that could display the + previous command at the top of the buffer. +* [#2226] - a scrollable "minimap" in te scrollbar, as opposed to marks + +### Relevant external docs +* **GREAT** summary of the state of the ecosystem: https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/28 +* https://iterm2.com/documentation-escape-codes.html + - `OSC 1337 ; SetMark ST` under "Set Mark" + - under "Shell Integration/FinalTerm +* https://support.apple.com/en-ca/guide/terminal/trml135fbc26/mac + - discusses auto-marked lines on `enter`/`^C`/`^D` + - allows bookmarking lines with selection + - bookmarks can have a name (maybe not super important) +* [How to Use Marks in OS X’s Terminal for Easier Navigation](https://www.howtogeek.com/256548/how-to-use-marks-in-os-xs-terminal-for-easier-navigation/) +* [Terminal: The ‘\[‘ Marks the Spot](https://scriptingosx.com/2017/03/terminal-the-marks-the-spot/) +* Thread with VsCode (xterm.js) implementation notes: https://github.com/microsoft/terminal/issues/1527#issuecomment-1076455642 +* [xterm.js prompt markup sequences](https://github.com/microsoft/vscode/blob/39cc1e1c42b2e53e83b1846c2857ca194848cc1d/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts#L50-L52) +* [VsCode command tracking release notes](https://code.visualstudio.com/updates/v1_22#_command-tracking), also [Terminal shell integration](https://code.visualstudio.com/updates/v1_65#_terminal-shell-integration) +* ConEMU: + Sequence | Description + -- | -- + ESC ] 9 ; 12 ST | Let ConEmu treat current cursor position as prompt start. Useful with PS1. + +* https://iterm2.com/documentation-one-page.html#documentation-triggers.html" + +### Footnotes + +[1]: Intuitively, this feels prohibitively expensive, +but you'd be mistaken. + +An average device right now (I mean something that was alright about 5 years +ago, like an 8700k with regular DDR4) does about 4GB/s of random, un-cached +memory access. While low-end devices are probably a bit slower, I think 4GB/s is +a good estimate regardless. That's because non-random memory access is way way +faster than that at around 20GB/s (DDR4 2400 - what most laptops had for the +last decade). + +Assuming a 120 column * 32k line buffer (our current maximum), the buffer would +be about 21MB large. Going through the entire buffer linearly at 20GB/s would +take just 1ms (including all text and metadata). If we assume that each row has +a mark, that marks are 36 byte large and assuming the worst case of random +access, we can go through all 32k within about 0.3ms. + + + +_(Thanks lhecker for these notes)_ + + +[#1527]: https://github.com/microsoft/terminal/issues/1527 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#1527]: https://github.com/microsoft/terminal/issues/1527 +[#6232]: https://github.com/microsoft/terminal/issues/6232 +[#2226]: https://github.com/microsoft/terminal/issues/2226 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#13163]: https://github.com/microsoft/terminal/issues/13163 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#13455]: https://github.com/microsoft/terminal/issues/13455 +[#13449]: https://github.com/microsoft/terminal/issues/13449 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#4588]: https://github.com/microsoft/terminal/issues/4588 +[#5804]: https://github.com/microsoft/terminal/issues/5804 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#12948]: https://github.com/microsoft/terminal/issues/12948 +[#13455]: https://github.com/microsoft/terminal/issues/13455 +[#5916]: https://github.com/microsoft/terminal/issues/5916 +[#12366]: https://github.com/microsoft/terminal/issues/12366 +[#9583]: https://github.com/microsoft/terminal/issues/9583 +[#7561]: https://github.com/microsoft/terminal/issues/7561 +[#3920]: https://github.com/microsoft/terminal/issues/3920 +[#13455]: https://github.com/microsoft/terminal/issues/13455 +[#13449]: https://github.com/microsoft/terminal/issues/13449 +[#4588]: https://github.com/microsoft/terminal/issues/4588 +[#14341]: https://github.com/microsoft/terminal/issues/14341 +[#14045]: https://github.com/microsoft/terminal/issues/14045 +[#14754]: https://github.com/microsoft/terminal/issues/14754 +[#14341]: https://github.com/microsoft/terminal/issues/14341 + +[VsCode's extension to the FTCS sequences]: https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st + +[other proposals]: https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/6#f6de1e5703f5806d0821d92b0274e895c4b6d850 diff --git a/doc/specs/#11000 - Marks/ftcs-diagram.png b/doc/specs/#11000 - Marks/ftcs-diagram.png new file mode 100644 index 0000000000..f36112e0ea Binary files /dev/null and b/doc/specs/#11000 - Marks/ftcs-diagram.png differ diff --git a/doc/specs/#11000 - Marks/vscode-shell-integration-gutter-mark.png b/doc/specs/#11000 - Marks/vscode-shell-integration-gutter-mark.png new file mode 100644 index 0000000000..d73a5c7b14 Binary files /dev/null and b/doc/specs/#11000 - Marks/vscode-shell-integration-gutter-mark.png differ diff --git a/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md b/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md index ee0f9ee79a..3d2b6532cd 100644 --- a/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md +++ b/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md @@ -1,7 +1,7 @@ --- author: Mike Griese @zadjii-msft created on: 2020-5-13 -last updated: 2020-08-04 +last updated: 2022-11-18 issue id: 1571 --- @@ -76,6 +76,34 @@ There are five `type`s of objects in this menu: - The `"entries"` property specifies a list of menu entries that will appear nested under this entry. This can contain other `"type":"folder"` groups as well! + - The `"inline"` property accepts two values + - `auto`: When the folder only has one entry in it, don't actually create a + nested layer to then menu. Just place the single entry in the layer that + folder would occupy. (Useful for dynamic profile sources with only a + single entry). + - `never`: (**default**) Always create a nested entry, even for a single + sub-item. + - The `allowEmpty` property will force this entry to show up in the menu, even + if it doesn't have any profiles in it. This defaults to `false`, meaning + that folders without any entries in them will just be ignored when + generating the menu. This will be more useful with the `matchProfile` entry, + below. + + When this is true, and the folder is empty, we should add a + placeholder `` entry to the menu, to indicate that no profiles were + in that folder. + - _This setting is probably pretty niche, and not a requirement_. More of a + theoretical suggestion than anything. + - In the case of no entries for this folder, we should make sure to also + reflect the `inline` property: + - `allowEmpty:true`, `inline:auto`: just ignore the entry at all. Don't + add a placeholder to the parent list. + - `allowEmpty:true`, `inline:never`: Add a nested entry, with an + `` placeholder. + - `allowEmpty:false`, `inline:auto`: just ignore the entry at all. Don't + add a placeholder to the parent list. + - `allowEmpty:false`, `inline:never`: just ignore the entry at all. Don't + add a placeholder to the parent list. * `"type":"action"`: This represents a menu entry that should execute a specific `ShortcutAction`. - the `id` property will specify the global action ID (see [#6899], [#7175]) @@ -97,6 +125,16 @@ There are five `type`s of objects in this menu: enabling all other profiles to also be accessible. - The "name" of these entries will simply be the name of the profile - The "icon" of these entries will simply be the profile's icon + - This won't include any profiles that have been included via `matchProfile` + entries (below) +* `"type": "matchProfile"`: Expands to all the profiles that match a given + string. This lets the user easily specify a whole collection of profiles for a + folder, without needing to add them all manually. + - `"name"`, `"commandline"` or `"source"`: These three properties are used to + filter the list of profiles, based on the matching property in the profile + itself. The value is a string to compare with the corresponding property in + the profile. A full string comparison is done - not a regex or partial + string match. The "default" new tab menu could be imagined as the following blob of json: @@ -108,6 +146,42 @@ The "default" new tab menu could be imagined as the following blob of json: } ``` +Alternatively, we could consider something like the following. This would place +CMD, PowerShell, and all PowerShell cores in the root at the top, followed by +nested entries for each subsequent dynamic profile generator. + +```jsonc +{ + "newTabMenu": [ + { "type":"profile", "profile": "cmd" }, + { "type":"profile", "profile": "Windows PowerShell" }, + { "type": "matchProfile", "source": "Microsoft.Terminal.PowerShellCore" } + { + "type": "folder", + "name": "WSL", + "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" } ] + }, + { + "type": "folder", + "name": "Visual Studio", + "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.VisualStudio" } ] + }, + // ... etc for other profile generators + { "type": "remainingProfiles" } + ] +} +``` + +I might only recommend that for `userDefaults.json`, which is the json files +used as a template for a user's new settings file. This would prevent us from +moving the user's cheese too much, if they're already using the Terminal and +happy with their list as is. Especially consider someone who's default profile +is a WSL distro, which would now need two clicks to get to. + +> _note_: We will also want to support the same `{ "key": "SomeResourceString"}` +> syntax used by the Command Palette commands +> for specifying localizable names, if we chose to pursue this route. + ### Other considerations Also considered during the investigation for this feature was re-using the list @@ -154,6 +228,42 @@ The design chosen in this spec more cleanly separates the responsibilities of the list of profiles and the contents of the new tab menu. This way, each object can be defined independent of the structure of the other. +Regarding implementation of `matchProfile` entries: In order to build the menu, +we'll evaluate the entries in the following order: + +* all explicit `profile` entries +* then all `matchProfile` entries, using profiles not already specified +* then expand out `remainingProfiles` with anything not found above. + +As an example: + +```jsonc +{ + "newTabMenu": [ + { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" } + { + "type": "folder", + "name": "WSLs", + "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" } ] + }, + { "type": "remainingProfiles" } + ] +} +``` + +For profiles { "Profile A", "Profile B (WSL)", "Profile C (WSL)" }, This would +expand to: + +``` +New Tab Button ▽ +├─ Profile A +├─ Profile B (WSL) +├─ Profile C (WSL) +└─ WSLs + └─ Profile B (WSL) + └─ Profile C (WSL) +``` + ## UI/UX Design See the above _figure 1_. @@ -289,7 +399,39 @@ And assuming the user has bound: - Close Tab: `{ "action": "closeTab", "index": "${selectedTab.index}" }` - Close Other Tabs: `{ "action": "closeTabs", "otherThan": "${selectedTab.index}" }` - Close Tabs to the Right: `{ "action": "closeTabs", "after": "${selectedTab.index}" }` +* We may want to consider regex, tag-based, or some other type of matching for + `matchProfile` entries in the future. We originally considered using regex for + `matchProfile` by default, but decided instead on full string matches to leave + room for regex matching in the future. Should we chose to pursue something + like that, we should use a settings structure like: + ```json + "type": "profileMatch", + "source": { "type": "regex", "value": ".*wsl.*" } + ``` +* We may want to expand `matchProfile` to match on other properties too. (`title`?) +* We may want to consider adding support for capture groups, e.g. + ```json + { + "type": "profileMatch", + "name": { "type": "regex", "value": "^ssh: (.*)" } + } + ``` + for matching to all your `ssh: ` profiles, but populate the name in the entry + with that first capture group. So, ["ssh: foo", "ssh: bar"] would just expand + to a "foo" and "bar" entry. + +## Updates + +_February 2022_: Doc updated in response to some discussion in [#11326] and +[#7774]. In those PRs, it became clear that there needs to be a simple way of +collecting up a whole group of profiles automatically for sorting in these +menus. Although discussion centered on how hard it would be for extensions to +provide that customization themselves, the `match` statement was added as a way +to allow the user to easily filter those profiles themselves. + +This was something we had originally considered as a "future consideration", but +ultimately deemed it to be out of scope for the initial spec review. [#2046]: https://github.com/microsoft/terminal/issues/2046 @@ -298,3 +440,5 @@ And assuming the user has bound: [#3337]: https://github.com/microsoft/terminal/issues/3337 [#6899]: https://github.com/microsoft/terminal/issues/6899 [#7175]: https://github.com/microsoft/terminal/issues/7175 +[#11326]: https://github.com/microsoft/terminal/issues/11326 +[#7774]: https://github.com/microsoft/terminal/issues/7774 diff --git a/doc/specs/#492 - Default Terminal/spec.md b/doc/specs/#492 - Default Terminal/spec.md index 9f6afb6899..c85982c64f 100644 --- a/doc/specs/#492 - Default Terminal/spec.md +++ b/doc/specs/#492 - Default Terminal/spec.md @@ -201,7 +201,7 @@ Concerns: ### Accessibility -Accessibility applications are the most likely to resort to a method of spelunking the process tree or window handles to attempt to find content to read out. Presuming they have hardcoded rules for console-type applications, these algorithms could be surprised by the substitution of another terminal environment. +Accessibility applications are the most likely to resort to a method of spelunking the process tree or window handles to attempt to find content to read out. Presuming they have hard-coded rules for console-type applications, these algorithms could be surprised by the substitution of another terminal environment. The major players here that I am considering are NVDA, JAWS, and Narrator. As far as I am aware, all of these applications attempt to drive their interactivity through UI Automation where possible. And we have worked with all of these applications in the past in improving their support for both `conhost.exe` and the Windows Terminal product. I have relatively high confidence that we will be able to work with them again to help update these assistive products to understand the new UI delegation, if necessary. diff --git a/doc/specs/#4993 - Keyboard Selection/Keyboard-Selection.md b/doc/specs/#4993 - Keyboard Selection/Keyboard-Selection.md index 0d9116d44e..c74618c567 100644 --- a/doc/specs/#4993 - Keyboard Selection/Keyboard-Selection.md +++ b/doc/specs/#4993 - Keyboard Selection/Keyboard-Selection.md @@ -76,7 +76,7 @@ These are in accordance with ConHost's keyboard selection model. This idea was abandoned due to several reasons: 1. Keyboard selection should be a standard way to interact with a terminal across all consumers (i.e. WPF control, etc.) -2. There isn't really another set of key bindings that makes sense for this. We already hardcoded ESC as a way to clear the selection. This is just an extension of that. +2. There isn't really another set of key bindings that makes sense for this. We already hard-coded ESC as a way to clear the selection. This is just an extension of that. 3. Adding 12 conditionally effective key bindings takes the spot of 12 potential non-conditional key bindings. It would be nice if a different key binding could be set when the selection is not active, but that makes the settings design much more complicated. 4. 12 new items in the command palette is also pretty excessive. 5. If proven wrong when this is in WT Preview, we can revisit this and make them customizable then. It's better to add the ability to customize it later than take it away. diff --git a/doc/specs/#754 - Cascading Default Settings.md b/doc/specs/#754 - Cascading Default Settings.md index 67cebe58c1..73fd1928dc 100644 --- a/doc/specs/#754 - Cascading Default Settings.md +++ b/doc/specs/#754 - Cascading Default Settings.md @@ -115,7 +115,7 @@ greater detail below: ### Default Settings -We'll have a static version of the "Default" file **hardcoded within the +We'll have a static version of the "Default" file **hard-coded within the application package**. This `defaults.json` file will live within the application's package, which will prevent users from being able to edit it. @@ -128,19 +128,19 @@ won't actually be generated, but because it's shipped with our app, it'll be overridden each time the app is updated. "Auto-generated" should be good enough to indicate to users that it should not be modified. -Because the `defaults.json` file is hardcoded within our application, we can use +Because the `defaults.json` file is hard-coded within our application, we can use its text directly, without loading the file from disk. This should help save some startup time, as we'll only need to load the user settings from disk. When we make changes to the default settings, or we make changes to the settings -schema, we should make sure that we update the hardcoded `defaults.json` with +schema, we should make sure that we update the hard-coded `defaults.json` with the new values. That way, the `defaults.json` file will always have the complete set of settings in it. ### Layering settings When we load the settings, we'll do it in three stages. First, we'll deserialize -the default settings that we've hardcoded. We'll then generate any profiles that +the default settings that we've hard-coded. We'll then generate any profiles that might come from dynamic profile sources. Then, we'll intelligently layer the user's setting upon those we've already loaded. If a user wants to make changes to some objects, like the default profiles, we'll need to make sure to load from diff --git a/doc/specs/#885 - Terminal Settings Model/#885 - Terminal Settings Model.md b/doc/specs/#885 - Terminal Settings Model/#885 - Terminal Settings Model.md index ad36121840..5426c9716d 100644 --- a/doc/specs/#885 - Terminal Settings Model/#885 - Terminal Settings Model.md +++ b/doc/specs/#885 - Terminal Settings Model/#885 - Terminal Settings Model.md @@ -158,7 +158,7 @@ For `settings.json`, `_globals` will only hold the values set in `settings.json` This process becomes a bit more complex for `Profile` because it can fallback in the following order: 1. `settings.json` profile 2. `settings.json` `profiles.defaults` -3. (if a dynamic profile) the hardcoded value in the dynamic profile generator +3. (if a dynamic profile) the hard-coded value in the dynamic profile generator 4. `defaults.json` profile `CascadiaSettings` must do the following... @@ -280,7 +280,7 @@ TerminalApp will construct and reference a `CascadiaSettings settings` as follow and layers the settings.json data on top of it. - check for errors/warnings, and handle them appropriately -This will be different from the current model which has the settings.json path hardcoded, and is simplified +This will be different from the current model which has the settings.json path hard-coded, and is simplified to a `LoadAll()` call wrapped in error handlers. **NOTE:** This model allows us to layer even more settings files on top of the existing Terminal Settings diff --git a/doc/specs/drafts/#2634 - Broadcast Input/#2634 - Broadcast Input.md b/doc/specs/drafts/#2634 - Broadcast Input/#2634 - Broadcast Input.md new file mode 100644 index 0000000000..76cc91f508 --- /dev/null +++ b/doc/specs/drafts/#2634 - Broadcast Input/#2634 - Broadcast Input.md @@ -0,0 +1,410 @@ +--- +author: Mike Griese @zadjii-msft +created on: 2021-03-03 +last updated: 2022-11-04 +issue id: #2634 +--- + +# Broadcast Input + +## Abstract + +"Broadcast Input" is a feature present on other terminals which allows the user +to send the same input to multiple tabs or panes at the same time. This can make +it simpler for the user to run the same command in multiple directories or +servers at the same time. + +With a viable prototype in [#9222], it's important that we have a well-defined +plan for how we want this feature to be exposed before merging that PR. This +spec is intended to be a lighter-than-usual spec to build consensus on the +design of how the actions should be expressed. + +## Background + +### Inspiration + +This spec is heavily inspired by the [iTerm2 implementation]. @carlos-zamora did +a great job of breaking down how iTerm2 works in [this comment]. + +SecureCRT also implements a similar feature using a "chat window" that can send +the input in the chat window to all tabs. This seemed like a less ergonomic +solution, so it was not seriously considered. + +Additionally, Terminator (on \*nix) allows for a similar feature through the use +of "groups". From [@zljubisic]: + +> In Linux terminator you can define groups, and than put each pane in one of +> defined groups. Afterwards, you can choose broadcasting to all panes, only +> certain group or no broadcast at all. + +This also seemed like a less powerful version of broadcast input than the +iterm2-like version, so it was also not further investigated. + +### User Stories + +iTerm2 supports the following actions: + +* **Story A:** _Send input to current session only_: The default setting. +* **Story B:** _Broadcast to all panes in all tabs_: Anything you type on the + keyboard goes to all sessions in this window. +* **Story C:** _Broadcast to all panes in current tab_: Anything you type on the + keyboard goes to all sessions in this tab. +* **Story D:** _Toggle broadcast input to current session_: Toggles whether this + session receives broadcasted keystrokes within this window. + + +## Solution Design + +### Proposal 1: iTerm2-like Modal Input Broadcast + +iTerm2 implements broadcast input as a type of "modal" system. The user is in +one of the following modes: + +* Broadcast to all panes in all tabs +* Broadcast to all panes in the current tab +* Broadcast to some set of panes within the current tab +* Don't broadcast input at all (the default behavior) + +These modes are vaguely per-tab state. There's a global "broadcast to all tabs & +panes" property. Then, each tab also has a pair of values: +* Should input be sent to all panes in this tab? +* If not, which panes should input be sent to? + +It's not possible to send input to one pane in tab A, then another pane in tab +B, without enabling the global "broadcast to everyone" mode. + +This seems to break down into the following actions: + +```json +{ "action": "toggleBroadcastInput", "scope": "window" }, +{ "action": "toggleBroadcastInput", "scope": "tab" }, +{ "action": "toggleBroadcastInput", "scope": "pane" }, +{ "action": "disableBroadcastInput" }, +``` + +Which would be accompanied by the following internal properties: +* A window (`TerminalPage`-level) property for `broadcastToAllPanesAndTabs` +* A per-tab property for `broadcastToAllPanes` +* A per-tab set of panes to broadcast to + +The scopes would work as follows: + +* `"scope": "window"`: Toggle the window's "broadcast to all tabs and panes" + setting. +* `"scope": "tab"`: Toggle the tab's "broadcast to all panes in this tab" + setting. + - This does not modify the set of panes that the user is broadcasting to in + the tab, merely toggles the tab's setting. If the user has a set of panes + they're broadcasting to in this tab, then toggles this setting on and off, + we'll return to broadcasting to that set. +* `"scope": "pane"`: Add this pane to the set of panes being broadcasted to in + this tab. + - **TODO!: FOR DISCUSSION**: Should this disable the tab's + "broadcastToAllPanes" setting? Or should it leave that alone? +* `"disableBroadcastInput"`: Set the global setting to false, the tab's setting + to false, and clear the set of panes being broadcasted to for this tab. + - **TODO!** This could also just be `"action": "toggleBroadcastInput", + "scope": "none"` + +#### Pros +* This is exactly how iTerm2 does it, so there's prior art. +* If you're not globally broadcasting, then you're only ever broadcasting to + some (sub)set of the panes in the current tab. So global broadcast mode is + the only time a user would need to worry about input being to be sent to + an inactive tab. +* You can have a set of panes to broadcast to in the first tab, then a + _separate_ set to broadcast to in a second tab. Broadcasting in one tab + does not affect the other. + +#### Cons +* I frankly think the `tab`/`pane` interaction can be a little weird. Like for + this scenario: + - enable broadcast input for tab 1 + - switch to tab 2 + - enable broadcast input for a pane in tab 2 + + There's valid confusion to be had between the following two behaviors: + 1. input goes to all of tab 1 _and_ the pane in tab 2 + 2. input only goes to the pane in tab 2 +* You can't broadcast to a subset of panes in inactive tabs, in addition to + the active tab. All panes you want to broadcast to must be in the active + tab. +* Does creating a new split in a pane that's being broadcast to add that pane to + the broadcast set? + +#### What would this mean for PR #9222? + +The prototype PR [#9222] basically just implemented `{ "action": +"toggleBroadcastInput", "scope": "tab" }`. We could make `tab` the default +`scope` if no other one is specified, and then the PR would need basically no +modifications. Future PRs could add args to the `toggleBroadcastInput` action, +without breaking users who bind a key to that action now. + +### Proposal 2: Broadcast Set + +This was the design I had originally came up with before investigating iTerm2 +much closer. This design involves a "broadcast set" of panes. All the panes in +the broadcast set would also get the `KeySent` and `CharSent` events, in +addition to the active pane. (The active pane may be a part of the broadcast +set). If a pane is read-only in the broadcast set, then it won't handle those +broadcasted events (obviously). + +As far as actions, we're looking at something like: + +* **A** Only send input to the active pane + * Remove all the panes from the broadcast set +* **B** send input to all panes in all tabs + * If all the panes are in the broadcast set, remove them all. Otherwise, add + all panes in all tabs to the broadcast set. +* **C** send input to all panes in the current tab + * If all the panes in the current tab are in the broadcast set, remove them + from the broadcast set. Otherwise, add all the panes from this tab to the + broadcast set. +* **D** toggle sending input to the current pane + * If this pane is in the broadcast set, remove it. Otherwise add it. +This seems to break down into the following actions: + +```json +{ "action": "disableBroadcastInput" }, +{ "action": "toggleBroadcastInput", "scope": "window" }, +{ "action": "toggleBroadcastInput", "scope": "tab" }, +{ "action": "toggleBroadcastInput", "scope": "pane" }, +``` + +Which would be accompanied by the following internal properties: +* A window (`TerminalPage`-level) set of panes to broadcast to. + +#### Pros: +* Mentally, you're either adding panes to the set of panes to broadcast to, or + removing them. +* You can broadcast to panes in multiple tabs, without broadcasting to _all_ + panes in all tabs. + +#### Cons: +* is _slightly_ different from iTerm2. +* Does creating a new split in a pane that's being broadcast to add that pane to + the broadcast set? +* You can't have a set of panes to broadcast to in the one tab, and a different + set in another tab. As an example: + 1. in tab 1, you add panes A and B to the broadcast set. Typing in either one + goes to both A and B. + 2. in tab 1, switch to pane C. Now input goes to A, B and C. + 3. in tab 1, switch to pane D. Now input goes to A, B and D. + 4. switch to tab 2, pane E. Now input goes to A, B and E. + + You can't have like, a set with A & B (in 1), then E & F (in 2). So if someone + wants to type to both panes in 1, then both panes in 2, then both panes in 1, + they need to keep toggling which panes are in the broadcast set. + +#### What would this mean for PR #9222? + +Similar to Proposal 1, we'd use `tab` as the default value for `scope`. In the +future, when we add support for the other scopes, we'd change how the +broadcasting works, to use a set of panes to broadcast to, instead of just the +tab-level property. + +### Proposal 3: It's iTerm2, but slightly different + +While typing this up, I thought maybe it might make more sense if we took the +iTerm2 version, and changed it slightly: + +* `"scope": "tab"`: If all the panes are in the broadcast set for this tab, then + remove them all. Otherwise, add all the panes in this tab to this tab's + broadcast set. +* `"scope": "pane"`: If this pane is in the broadcast set for a tab, then remove + it. Otherwise, add it. + +With this, we get rid of the tab-level setting for "broadcast to all the panes +in this tab", and rely only on the broadcast set for that tab. + +#### Pros: +* All the pros from proposal A +* Does away with the seemingly weird toggling between "all the panes in a tab" + and "some of the panes in a tab" that's possible with proposal A + +#### Cons: +* You can't broadcast to a subset of panes in inactive tabs, in addition to + the active tab. All panes you want to broadcast to must be in the active + tab. +* is _slightly_ different from iTerm2. Just _slightly_. +* Does creating a new split in a pane that's being broadcast to add that pane to + the broadcast set? + +#### What would this mean for PR #9222? + +Same as with proposal A, we wouldn't change anything in the current PR. A future +PR that would add the other scope's to that action would need to change how the +broadcasting within a tab works, to use a set of panes to broadcast to, instead +of just the tab-level property. + +## Conclusion + +I'm proposing these settings for broader discussion. I'm not really sure which I +like most at this point. 1 & 3 have the advantage of being most similar to the +prior art, but 2 is more easily extendable to "groups" (see [Future +Considerations](#Future-Considerations)). + +**TODO!**: Make a decision. + +_**Fortunately**_: All these proposals actually use the same set of actions. So +it doesn't _really_ matter which we pick right now. We can unblock [#9222] as +the implementation of the `"tab"` scope, and address other scopes in the future. +We should still decide long-term which of these we'd like, but the actions seem +universal. + +## UI/UX Design + +This is supposed to be a quick & dirty spec, so I'm LARGELY skipping this. + +As far as indicators go, we'll throw something like: + +![NetworkTower Segoe UI Icon](broadcast-segoe-icon.png) + +in the tab when a pane is being broadcasted to. If all tabs are being +broadcasted to, then they'll all have that icon. If a tab is inactive, and any +pane in that tab is being broadcast to, then show the icon in the tab. + +It probably makes the most sense to have pane titlebars ([#4998]) also display +that icon. + +In the original PR, it was suggested to use some variant of the [accent color] +to on the borders of panes that are currently receiving broadcasted input. We're +already using the accent color on the borders of the active pane. +`SystemAccentColorLight*`/`SystemAccentColorDark*` would provide a way of using +a similar hue with different lightness/saturation. This would be a decent visual +indicator that they're _not_ the active pane, but they are going to receive +input. Something a bit like: + +![A sample of using the border to indicate the broadcasted-to panes](broadcast-input-borders.gif) + +This should obviously be able to be overridden in the user's theme, similar to +the pane border colors. + +iTerm2 also supports displaying "stripes" in the background of all the panes +that are being broadcast too. That's certainly another way of indicating this +feature to the user. I'm not sure how we'd layer it with the background image +though. **I recommend we ignore this for now, and leave this as a follow-up**. + +### Tab context menu items + +For reference, refer to the following from iTerm2: +![image](https://user-images.githubusercontent.com/2578976/64075757-fa971980-ccee-11e9-9e44-47aaf3bca76c.png) + +We don't have a menu bar like on MacOS, but we do have a tab context menu. We +could add these items as a nested entry under each tab. If we wanted to do this, +we should also make sure to dynamically change the icon of the MenuItem to +reflect the current broadcast state. + +## Potential Issues + + + + + + + +
Compatibility + +[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"? + +
+ +[comment]: # If there are any other potential issues, make sure to include them here. + +## Implementation plan + +* [ ] Resurrect [#9222], and use that to implement `"scope": "tab"`. This is + implemented the same, regardless of which proposal we chose. +* [ ] Add a tab context menu entry for toggling broadcast input, with a dynamic + icon based on the current state. +* [ ] Implement `"scope": "window"`. Again, this is implemented the same regardless + of which proposal we pursue. +* [ ] Decide between the two proposals here. +* [ ] Implement `"scope": "pane"`. + +Doing the first element here is probably the most important one for most users, +and can be done regardless of the proposal chosen here. As such, we could even +suggest the default value of `scope` be `tab`. If we did that, then we wouldn't +need to do any args at all in the initial version. + +## Future Considerations + +Let's look to iTerm2, who's supported this feature for years, for some +inspiration of future things we should be worried about. If their users have +asked for these features, then it's inevitable that our users will too 😉 + +* [iterm2#6709] - Broadcast Input to multiple windows + - This is pretty straightforward. Would require coordination with the Monarch + though, and I'm worried about the perf hit of tossing every keystroke across + the process boundary. + - I suppose this would be `{ "action": "toggleBroadcastInput", "scope": + "global" }` +* [iterm2#6451], [iterm2#5563] - "Broadcast commands" + - iTerm2 has an action that lets the user manually clear the terminal-side + buffer. (This is tracked on the Windows Terminal as [#1882]). It might make + sense for there to be a mode where some _actions_ are also broadcast to + panes, not just key strokes. But which actions would those be? Moving the + selection anchors? Copy doesn't really make sense. Paste _does_ though. + Maybe the open find dialog / next&prev search match actions? + - This probably would require it's own spec. +* [iterm2#6007] - Different stripe color for different broadcast modes + - Have one color to indicate when broadcasting in `global` scope, another in + `tab` scope, a third in `pane` scope. + - This might mesh well with theming ([#3327]), for properties like + `pane.broadcastBorderColor.globalScope`, + `pane.broadcastBorderColor.paneScope`. Don't love those names, but you get + the idea. +* **[iterm2#5639]: Broadcast groups**, [iterm2#3372] - Broadcast Input to + multiple but not all tabs + - This is probably the most interesting request. I think this one identifies a + major shortcoming of the above proposals. With proposal 2, there's only ever + one top-level broadcast group. With proposals 1 & 3, there's per-tab + broadcast groups. In neither proposal can you have multiple concurrent + side-by-side broadcast groups. + - Groups should probably work across tabs. This would suggest that Proposal 2 + is closer to how groups would work. Instead of there being one top-level + set, there would be multiple. **I'm not sure how proposals 1&3 would + seamlessly transition into also supporting groups**. + - The major trick here is: how do we differentiate these different groups to + the user? If we used the broadcast icon with a number, maybe in the corner + of the tab? Like [📡: 1]? Can a pane be in multiple broadcast sets at the + same time? + - The natural arg idea would be `{ "action": "toggleBroadcastInput", "scope": + "tab", "group": 1 }` to say "add all panes in the tab to broadcast group 1", + or "remove all panes in the tab from broadcast group 1". If panes are in + another group, they'd be moved to the specified group. If all panes are in + that group, then remove them all. + - The UI for this would certainly get complex fast. + - This also matches the Terminator-style broadcasting to groups. +* Re: stripes in the background of the tab. We could expose a pane's current + broadcast state to the pixel shader, and a user could use a custom pixel + shader to add stripes behind the text in the shader code. That's one possible + solution. + +## Resources + +[comment]: # Be sure to add links to references, resources, footnotes, etc. + + +### Footnotes + +
[1]: + + +[#1882]: https://github.com/microsoft/terminal/issues/1882 +[#2634]: https://github.com/microsoft/terminal/issues/2634 +[#4998]: https://github.com/microsoft/terminal/issues/4998 +[#3327]: https://github.com/microsoft/terminal/issues/3327 +[#9222]: https://github.com/microsoft/terminal/pull/9222 +[this comment]: https://github.com/microsoft/terminal/issues/2634#issuecomment-789116413 +[iTerm2 implementation]: https://iterm2.com/documentation-one-page.html#documentation-menu-items.html +[@zljubisic]: https://github.com/microsoft/terminal/pull/9222#issuecomment-789143189 +[accent color]: https://docs.microsoft.com/en-us/windows/uwp/design/style/color#accent-color-palette + + +[iterm2#6709]: https://gitlab.com/gnachman/iterm2/-/issues/6709 +[iterm2#6451]: https://gitlab.com/gnachman/iterm2/-/issues/6451 +[iterm2#6007]: https://gitlab.com/gnachman/iterm2/-/issues/6007 +[iterm2#5639]: https://gitlab.com/gnachman/iterm2/-/issues/5639 +[iterm2#5563]: https://gitlab.com/gnachman/iterm2/-/issues/5563 +[iterm2#3372]: https://gitlab.com/gnachman/iterm2/-/issues/3372 diff --git a/doc/specs/drafts/#2634 - Broadcast Input/broadcast-input-borders.gif b/doc/specs/drafts/#2634 - Broadcast Input/broadcast-input-borders.gif new file mode 100644 index 0000000000..fb4078b31c Binary files /dev/null and b/doc/specs/drafts/#2634 - Broadcast Input/broadcast-input-borders.gif differ diff --git a/doc/specs/drafts/#2634 - Broadcast Input/broadcast-segoe-icon.png b/doc/specs/drafts/#2634 - Broadcast Input/broadcast-segoe-icon.png new file mode 100644 index 0000000000..049ea8df24 Binary files /dev/null and b/doc/specs/drafts/#2634 - Broadcast Input/broadcast-segoe-icon.png differ diff --git a/doc/specs/drafts/#642 - Buffer Exporting and Logging/#642 - Buffer Exporting and Logging.md b/doc/specs/drafts/#642 - Buffer Exporting and Logging/#642 - Buffer Exporting and Logging.md new file mode 100644 index 0000000000..5e66e762c8 --- /dev/null +++ b/doc/specs/drafts/#642 - Buffer Exporting and Logging/#642 - Buffer Exporting and Logging.md @@ -0,0 +1,380 @@ +--- +author: Mike Griese @zadjii-msft +created on: 2021-08-31 +last updated: 2021-08-31 +issue id: #642 +--- + +# Buffer Exporting and Logging + +## Abstract + +A common user need is the ability to export the history of a terminal session to +a file, for later inspection or validation. This is something that could be +triggered manually. Many terminal emulators provide the ability to automatically +log the output of a session to a file, so the history is always captured. This +spec will address improvements to the Windows Terminal to enable these kinds of +exporting and logging scenarios. + +## Background + +### Inspiration + +Below are screenshots from the settings pages of three different terminal +emulators with similar features - PuTTY, SecureCRT, and ConEmu: + +![PuTTY settings](PuTTY-logging-settings.png) + +_figure 1: PuTTY settings_ + +![SecureCRT settings](SecureCRT-logging-settings.png) + +_figure 2: SecureCRT settings_ + +![ConEmu settings](ConEmu-logging-settings.png) + +_figure 3: ConEmu settings_ + +These applications all offer some settings in common. Primarily, the important +feature is the ability to specify a path to a log file which contains some +special string formatting. This allows the user to log to different files based +on the time & date of the session, or based on the session name. + +### User Stories + +* **Story A**: The user is able to use a context menu entry on the tab to export + the contents of the buffer to a file, which they are prompted for. + - This is explicitly what was requested in [#642] +* **Story B**: The user can bind an action to export the contents of the buffer + to a file, which they are prompted for. + - Very similar to **A**, but via the command palette or a keybinding. +* **Story C**: The user can export to an explicit file via an action + - similar to **B**, but allowing for declaring the path to a file rather than + prompting at runtime. +* **Story D**: The user can choose to append to a file when exporting, rather + than overwriting. +* **Story E**: The user can specify a format string in the path to the file to + export to, which the Terminal will automatically replace with variables like + the time, date, and profile name. +* **Story F**: When opening a specific profile, the user can automatically log + to a file +* **Story G**: The user can execute an action to start or stop logging to a + given file. + + +## Solution Design + +I'm proposing the following actions and profile settings + +* New Action: `exportBuffer()`. + - Export the contents of the buffer to a file. + - `path` (string, defaults to `""`): When empty, prompt the user for a name of + a file to export to, using a file picker. This path accepts special + formatting strings that will be substituted with certain variables + (discussed [below](#path-formatting)). + - `append` (boolean, defaults to `false`): When `false`, the file's contents + will be overwritten. When `true`, the buffer contents will be appended to + the end of the file. +* New Profile Settings object: `logSettings` + - This is an object that describes a set of behavior for logging a profile. + - `path`: Same as the `path` in the `ExportBufferArgs` above + - `append`: Same as the `append` in the `ExportBufferArgs` above + - `captureAllOutput`: (boolean, defaults to `false`) When true, don't log only + printable characters, also log non-printable escape characters written to + the Terminal. + - `captureInput`: (boolean, defaults to `false`) Additionally log input to the + Terminal to the file. Input will be formatted as the traditional VT + sequences, rather than the full `win32-input` encoding. + - `newFileEveryDay`: (boolean, defaults to `false`) This requires the `day` to + be an element of the path format string. When logging with this setting, + opens a new file at midnight and starts writing that one. + +* New Profile setting: `logAutomatically` (boolean, default `false`). When true, + terminals with this profile will begin logging automatically. +* New Action: `toggleLogging()`. + - Start or stop logging to the configured file. If the terminal is already + logging with different settings than in this action, then stop logging + regardless (don't just start logging to the new file) + - This action accepts all the same args the profile's `logSettings` object. + - If _any_ args are provided, use those args. If _none_ are provided, then use + the logging settings present in the profile (if there are any). + - If there's not path provided (either in the args to the action or in the + profile), prompt the user to pick a file to log to. + +### Examples +```json +{ + "actions": [ + { "keys": "f1", "command": "exportBuffer" }, + { "keys": "f2", "command": { "action": "exportBuffer", "path": "c:\\logs\\${year}-${month}-${date}\\{profile}.txt" } }, + + { "keys": "f3", "command": "toggleLogging" }, + { "keys": "f4", "command": { "action": "toggleLogging", "path": "c:\\logs\\${profile}.log", "append": true } }, + ], + "profiles": [ + { + "name": "foo", + "logging": { + "path": "c:\\foo.txt", + "append": true + }, + "automaticallyLog": false + }, + { + "name": "bar", + "logging": { + "path": "c:\\logs\\${date}\\bar.txt", + "append": false + }, + "automaticallyLog": true + } + ] +} +``` + +Revisiting our original stories: + +* **Story A**: This is already implemented in [#11062] +* **Story B**: This is the action bound to f1. +* **Story C**: This is the action bound to f2. +* **Story D**: This is the `append` property in the actions, profile settings. +* **Story E**: An example of this is in the action bound to f2, + f4, and in the profile "bar"'s logging settings. +* **Story F**: The profile "bar" is configured to automatically log when opened. +* **Story G**: This is the action bound to f4. + +In addition, +* When opening the profile "foo", it will not automatically log to a file. + - Pressing f3 will begin logging to `c:\foo.txt` + - Pressing f4 will begin logging to `c:\logs\foo.log` + +### Path formatting + +[TODO!]: # TODO! + +For discussion: What syntax do we want? +* PuTTY uses `&Y`, `&M`, `&D`, `&T`, `&H`, `&P` for year, month, day, time, host + and port respectively. +* SecureCRT uses: + - `%H` – hostname + - `%S` – session name + - `%Y` – four-digit year + - `%M` – two-digit month + - `%D` – two-digit day of the month + - `%h` – two-digit hour + - `%m` – two-digit minute + - `%s` – two-digit seconds + - `%t` – three-digit milliseconds + - `%%` – percent (%) + - `%envvar%` – environment variable (for instance `%USERNAME%`) + +We have some precedent for formatting with `${braces}`, a la the iterable +command in the Command Palette (e.g `${profile.name}`). Additionally, [#9287] +implements support for environment variables in the Terminal with the +`${env:VARIABLE}` syntax. + +What variables do we want exposed, and how do we want users to be able to format +them? + +This doc was initially authored assuming we'd go with a `${braces}` syntax, like: + - `${profile}` – profile name + - `${year}` – four-digit year + - `${month}` – two-digit month + - `${day}` – two-digit day of the month + - `${hour}` – two-digit hour + - `${minute}` – two-digit minute + - `${second}` – two-digit second + - `${ms}` – three-digit milliseconds + - `${env:variable}` – environment variable (for instance `${env:USERPROFILE}`) + (inspired by [#9287]) + +### Exporting vs Logging +As far as specific implementation details goes, exporting is the easier work to +do. [#11062] already wires up the `TerminalApp` to retrieve the buffer contents +from the `TermControl`, so writing them at request is easy. + +Logging is harder. We don't want the `TermControl` telling the `TerminalApp` +layer about every piece of output logged. Especially in the post-[#5000] world +where that's a cross-process hop. Instead, we'll want the `ControlCore` / +`ControlInteractivity` to do _logging_ themselves. + +### Logging Mechanics + +#### When do we log? + +[TODO!]: # TODO! + +When do we decide to actually log? Take for example typing in a `pwsh` or +`bash` prompt. Imagine the user types +what, then hits +BkspBksp, such that the prompt is just `wh`. What should +the log contain? `what^h ^h^h ^h`[[1]](#footnote-1)? `wh`? + +My worry with logging the backspaces is that conpty is sometimes a bit noisier +than it needs to be with using `^H` as a cursor positioning sequence. Should we +only log lines when the cursor newlines or otherwise moves from the line it is +currently on? + +I'll need to look at what PuTTY emits for the "Printable output" option. + +#### What happens when we _start_ logging? + +If the user has a terminal that did not start with logging enabled, but then +started logging with `toggleLogging`, what should we log? All future output? Or +should we log the current buffer contents as well? + +I'm inclined to lean towards simply "all future output", and ignore any current +buffer content. If the user rally wants to log the current buffer contents _and_ +start logging, they can use a `multipleActions` action ([#11045]) to +`exportBuffer` to a file, then `toggleLogging` to that same file with +`"append":true`. + +## Potential Issues + + + + + + + + + + + +
Compatibility + +Since this functionality is entirely new, nothing here should negatively affect +existing functionality. + +
Performance, Power, and Efficiency + +When logging, it's expected there will be a measurable performance hit. We can +try to mitigate this by only writing to the file on a background thread, +separate from the connection or rendering thread. Since auto-logging will only +take place in the content process, we're not worried about the file writing +occurring on the UI thread. + +
+ +Also frequently requested is the ability to log timestamps of when commands are +executed. I don't think that this is a valuable feature for the Terminal to +implement ourselves. Windows Terminal is fundamentally just a _terminal +emulator_, it doesn't really know what's going on behind the scenes with +whatever client application (`cmd`, `powershell`, `bash`, `vim`) that is +connected to it. WT doesn't know when the user is typing in commands to the +shell, or if the user is just typing in text in `emacs` or something. There's no +way for the terminal to know that. It's _typically_ the client application's +responsibility to save it's own command history. `bash` and `powershell` both do +a pretty good job of saving this to another file to restore across sessions, +while `cmd.exe` doesn't. + +Windows is a messy world and this model gets a little tricky here. `cmd.exe` +isn't actually managing it's own command history _at all_. `conhost` is doing +that work on behalf of the client applications. Some long time ago someone +thought it would be a good idea to have the `readline` functionality baked +directly into the console host. Whether that was a good idea or not remains to +be seen - it's certainly made things like `python.exe`'s REPL easier to +implement, since they don't need to maintain their own history buffer, but it +makes it hard to de-tangle behavior like this from the console itself. + +I'm not sure how it would be possible to add a keybinding to the Windows +Terminal that would be able to save the console's _command_ history. Especially +considering the Terminal might _not_ be connected to a console host session at +all. If the Windows Terminal were directly running a `wsl` instance (something +that's not possible today, but something we've considered adding in the future), +then there wouldn't be a `conhost` in the process tree at all, and now +requesting the command history from the console wouldn't work _mysteriously_. + +Furthermore, shells can always be configured to emit timestamps in their prompts +themselves. Since the Terminal has no knowledge of when a command is actually +entered, but the _shell_ does, it makes the most sense to configure the user's +_shell_ to emit that information. The Terminal will then dutifully log that +output along with everything else. + +## Implementation Plan + +Below is a rough outline of how I'd go about implementing these features. Each +lop-level checkbox could be its own PR, following from [#11062]. + +### Buffer exporting + +* [ ] Add an `exportBuffer()` action that opens the file picker +* [ ] Add a string `path` parameter to `exportBuffer()` that allows the user to + press a key and immediately export the buffer to a whole path + - default to `""`, which indicates "open the file picker" +* [ ] add a boolean `append` (default to `false`) parameter to `exportBuffer`. + When true, export to the file given by appending, not overwriting the file +* [ ] Enable string formatting in the `path` parameter. + - What format do we want? `yyyy-mm-dd`? `%Y-%m-%D`? `&Y-&m-&D`? `${year}-${month}-${day}`? + - What are all the variables we want? + - Year, month, day, hour, minute - those are easy + - `WT_SESSION`, for a uuid for each session maybe? + - Profile name perhaps? Commandline? +* [ ] more... + +### Automatic logging + +* [ ] `toggleLogging()` Action for start/stop logging, with `path`, `append` + properties (like `exportBuffer()`) + - `ToggleLoggingArgs` contains a single member `LoggingSettings`, which + contains `path` and `append` properties. This will make sense below. +* [ ] add `LoggingSettings` property for "log all output" (default would just be + "log printable output") +* [ ] add `LoggingSettings` property for "log input" (Though, we'd probably want + to log it as normal VT encoded, not as `win32-input` encoded) +* [ ] Per-profile setting for `logSettings`, which can contain an entire + `LoggingSettings` (like the `ToggleLoggingArgs`). When `toggleLogging` with no + args, try to use the profile's `loggingSettings` instead. +* [ ] Per-profile setting for `automaticallyLog`, which would log by default + when the profile is opened +* [ ] `LoggingSettings` property for "New file every day", which only works when + the `{day}` is in the path string. When auto-logging with this setting, opens + a new file at midnight and starts writing that one. + + +### Future Considerations + +* When logging begins, the Terminal could display a toast for "Logging to + {filename}", and a similar one for "Stopped logging to {filename}". +* There's no good way of displaying a UI element to indicate that a pane is + currently logging to a file. I don't believe PuTTY displays any sort of + indicator. SecureCRT only displays a checkbox within the context menus of the + application itself. + ![SecureCRT context menu](SecureCRT-context-menu.png) + + Maybe when logging to a file, we could replace the "Export Text" context menu + entry with "Stop Logging" +* We could maybe add a setting to disable logging from the alt buffer. This + might help make this setting more valuable for users who are using full-screen + applications like `vim`. Since those applications redraw the entire viewport + contents frequently, the log might be unnecessarily noisy. Disabling logging + while in the alt buffer would show that the user opened vim, and then they did + some things after vim exited. +* Logging all output will be VERY helpful to us in the future for trying to + recreate bugs on our end that users can repro but we can't! + +## Resources + +PuTTY Logging documentation: https://tartarus.org/~simon/putty-snapshots/htmldoc/Chapter4.html#config-logfilename +ConEmu Logging documentation: https://conemu.github.io/en/AnsiLogFiles.html + +### Footnotes + +
[1]: Remember that `^H` is non-destructive, so the +sequence `what^h ^h^h ^h` is can be read as: + * print "what" + * move the cursor back one + * print a space (overwriting 't') + * move the cursor back one (now it's on the space where 't' was) + * move the cursor back one + * print a space (overwriting 'a') + * move the cursor back one (now it's on the space where 'a' was) + +[#642]: https://github.com/microsoft/terminal/issues/642 +[#5000]: https://github.com/microsoft/terminal/issues/5000 +[#9287]: https://github.com/microsoft/terminal/pull/9287 +[#11045]: https://github.com/microsoft/terminal/pull/11045 +[#11062]: https://github.com/microsoft/terminal/pull/11062 diff --git a/doc/specs/drafts/#642 - Buffer Exporting and Logging/ConEmu-logging-settings.png b/doc/specs/drafts/#642 - Buffer Exporting and Logging/ConEmu-logging-settings.png new file mode 100644 index 0000000000..016dfa1fb2 Binary files /dev/null and b/doc/specs/drafts/#642 - Buffer Exporting and Logging/ConEmu-logging-settings.png differ diff --git a/doc/specs/drafts/#642 - Buffer Exporting and Logging/PuTTY-logging-settings.png b/doc/specs/drafts/#642 - Buffer Exporting and Logging/PuTTY-logging-settings.png new file mode 100644 index 0000000000..3432170404 Binary files /dev/null and b/doc/specs/drafts/#642 - Buffer Exporting and Logging/PuTTY-logging-settings.png differ diff --git a/doc/specs/drafts/#642 - Buffer Exporting and Logging/SecureCRT-logging-settings.png b/doc/specs/drafts/#642 - Buffer Exporting and Logging/SecureCRT-logging-settings.png new file mode 100644 index 0000000000..6d058bea37 Binary files /dev/null and b/doc/specs/drafts/#642 - Buffer Exporting and Logging/SecureCRT-logging-settings.png differ diff --git a/doc/specs/drafts/#642 - Buffer Exporting and Logging/securecrt-context-menu.png b/doc/specs/drafts/#642 - Buffer Exporting and Logging/securecrt-context-menu.png new file mode 100644 index 0000000000..673bac124c Binary files /dev/null and b/doc/specs/drafts/#642 - Buffer Exporting and Logging/securecrt-context-menu.png differ diff --git a/doc/specs/drafts/#997 Non-Terminal-Panes.md b/doc/specs/drafts/#997 Non-Terminal-Panes.md index 6fb539cf33..279f451cbc 100644 --- a/doc/specs/drafts/#997 Non-Terminal-Panes.md +++ b/doc/specs/drafts/#997 Non-Terminal-Panes.md @@ -60,7 +60,7 @@ it could host any arbitrary content. ### Security -I don't forsee this implementation by itself raising security concerns. This +I don't foresee this implementation by itself raising security concerns. This feature is mostly concerned with adding support for arbitrary controls, not actually implementing some arbitrary controls. diff --git a/doc/specs/portable-mode-spec.md b/doc/specs/portable-mode-spec.md new file mode 100644 index 0000000000..c75f6e9f7d --- /dev/null +++ b/doc/specs/portable-mode-spec.md @@ -0,0 +1,90 @@ +--- +author: Dustin L. Howett @DHowett +created on: 2023-03-22 +last updated: 2023-03-22 +issue id: none +--- + +# Windows Terminal "Portable" Mode + +## Abstract + +Since we are planning on officially supporting unpackaged execution, I propose a special mode where Terminal stores its +settings in a `settings` folder next to `WindowsTerminal.exe`. + +## Inspiration + +- [PortableApps](https://portableapps.com) +- "Embeddable" Python, which relies on the deployment of a specific file to the Python root + +## Solution Design + +- _If running without package identity,_ `CascadiaSettings` will look for the presence of a file called `.portable` next + to `Microsoft.Terminal.Settings.Model.dll`. +- If that file is present, it will change the settings and state paths to be rooted in a subfolder named `settings` next + to `Microsoft.Terminal.Settings.Model.dll`. + +Right now, _the only thing_ that makes Terminal not work in a "portable" manner is that it saves settings to +`%LOCALAPPDATA%`. + +## UI/UX Design + +_No UI/UX impact is expected._ + +## Capabilities + +- Distributors could ship a self-contained and preconfigured Terminal installation. +- Users could archive fully-working preconfigured versions of Terminal. +- Developers (such as those on the team) could easily test multiple versions of Terminal without worrying about global + settings pollution. + +### Accessibility + +_No change is expected._ + +### Security + +_No change is expected._ + +### Reliability + +More code always bears a risk. + +### Compatibility + +This is a net new feature, and it does not break any existing features. A distributor (or a user) can opt in (or out) by +adding (or removing) the `.portable` file. + +The following features may be impacted. + +- **Dynamic Profiles** and **Fragment Extensions** + - _No impact expected._ Dynamic profiles will still be generated. If a portable installation is moved to a machine without the dynamic profile source, that profile will disappear. +- `firstWindowPreference` and `state.json` + - _No impact expected._ + - State is stored next to settings, even for portable installations. + - If a dynamic profile was saved in `state` and has been removed, Terminal will proceed as in non-portable mode. +- Moving an install from Windows 10 to Windows 11 and back + - _No impact expected._ +- "Machine-specific" settings, like those about rendering and repainting + - _No impact expected._ + - Terminal does not distinguish settings that are specific to a machine. These settings will move along with the portable install. +- The shell extension + - _No impact expected._ + - The shell extension will not be registered with Windows. + - If we choose to register the shell extension, it is already prepared for running a version of WT from the same directory. Registering the portable shell extension will make it launch portable Terminal. + +### Performance, Power, and Efficiency + +_No change is expected._ + +## Potential Issues + +- User confusion around where settings are stored. + +## Future considerations + +- In the future, perhaps `.portable` could itself contain a directory path into which we would store settings. +- We could consider adding an indicator in the Settings UI. +- Because we are using the module path of the Settings Model DLL, a future unpackaged version of the shell extension + that supports profile loading would read the right settings file (assuming it used the settings model.) +- If we choose to store the shell extension cache in the registry, we would need to avoid doing so in portable mode. diff --git a/doc/terminal-a11y-2023.md b/doc/terminal-a11y-2023.md new file mode 100644 index 0000000000..3686120319 --- /dev/null +++ b/doc/terminal-a11y-2023.md @@ -0,0 +1,217 @@ +# Windows Terminal Accessibility +## Abstract +The release of Windows Terminal has served as a way to reinvigorate the command-line ecosystem on Windows by providing a modern experience that is consistently updated. This experience has caused faster innovation in the Windows command-line ecosystem that can be seen across various areas like expanded virtual terminal sequence support and enhanced accessibility. Command-line apps can now leverage these innovations to create a better experience for the end-user. + +Since accessibility is a very broad area, this document is intended to present recent innovations in the accessibility space for the command-line ecosystem. Furthermore, it will propose additional improvements that can be made alongside key stakeholders that could benefit from such changes. + +## Windows Command-line Ecosystem +### First-party terminals +For many years, Console Host (Conhost) was the only first-party terminal on Windows. In 2019, Windows Terminal was released to the world as an open source first-party terminal. Windows Terminal was distributed through the Microsoft Store and received regular updates throughout the year, much more frequently than Conhost. In October 2022, Windows Terminal was enabled as the default terminal on Windows. + +A significant amount of code is shared between Conhost and Windows Terminal to create the terminal area. To enable an accessible experience for this area, a shared UI Automation provider was introduced in 2019[^1], enabling accessibility tools to navigate and read contents from the terminal area. In 2020, Windows Terminal was updated to dispatch UIA events signaling when the cursor position, text output, or selection changed; this left the work of identifying what changed in the output to the attached screen reader application[^2]. In 2022, Windows Terminal was updated to dispatch UIA notifications with a payload of what text was written to the screen[^3]. + +### Internal Partners +There are many first-party command-line applications on Windows. The following are a few examples of those that are regularly updated: +- [**GitHub CLI**](https://cli.github.com/): a tool that can be used to query and interact with GitHub repos (open source) +- [**Winget**](https://github.com/microsoft/winget-cli): a tool to install applications and other packages +- [**PSReadLine**](https://github.com/PowerShell/PSReadLine): a PowerShell module that enhances the input line experience +- [**Windows Subsystem for Linux (WSL)**](https://learn.microsoft.com/en-us/windows/wsl/): a tool to manage and run GNU/Linux environments without a traditional virtual machine +- [**PowerShell**](https://github.com/PowerShell/PowerShell): a cross-platform command-line shell (open source) + +Additionally, CMD is a Windows-specific command-line shell. Though it is no longer being updated, it is still widely used. + +### External Command-line Applications +There are many third-party command-line applications on Windows that are regularly updated. +The following examples take over the entire viewport: +- [**Vim**](https://www.vim.org/): a modal-based text editor with an extensive plugin system +- **Emacs**: a shortcut-oriented text editor with an extensive plugin system +- [**Emacspeak**](https://emacspeak.sourceforge.net/): an Emacs subsystem that leverages Emacs' context-specific information to produce speech output +- **Midnight Commander**: a file manager with an extensive text user interface +- **Far Manager**: a file manager for Windows with an exclusive text user interface +- **tmux**: a terminal multiplexer +The following examples don't take over the entire viewport: +- [**Oh My Posh**](https://ohmyposh.dev/): a tool to customize shell prompts +- **git**: a tool for version control +The following examples operate as command-line shells: +- [**Bash**](https://www.gnu.org/software/bash/) is the default shell for most Linux distributions +- [**Fish shell**](https://fishshell.com/) provides a rich shell experience with features like autosuggestion support and VGA colors +- [**Z shell**](https://zsh.sourceforge.io/) is an extended Bourne shell + +## Accessibility tools and features +### Screen Readers +Windows is predominantly dominated by three major screen readers: Narrator, JAWS, and NVDA. +- **Narrator**: a free first-party screen reader that ships with Windows. +- **JAWS**: a third-party screen reader that requires an annual license to use. +- **NVDA**: a free third-party screen reader that is open source. +It's important to note that most users generally use NVDA or JAWS[^4], so it's very important to consider the experience under both of those screen readers. + +### Miscellaneous User Tools +Windows has many built-in features that enable a more accessible experiences. The following is a list of Windows accessibility settings and tools that may have an impact on apps: +- **Text size setting**: dictates the default size of text throughout all of Windows +- **Always show scrollbars setting**: forces the scrollbars to always be expanded and shown. Generally, this setting is automatically respected when using the Windows UI library. +- **Transparency effects setting**: allows transparency effects within the Windows shell +- **Animation effects setting**: allows animation effects +- **Text cursor indicator**: displays a colorful UI over the text cursor to make it stand out more and easier to find. Note, this is powered by UI Automation selection changed events because an empty selection is the cursor's position. +- **Magnifier**: zooms the monitor to provide a closer look at the current mouse and cursor position. Note, this is powered by UI Automation selection changed events because an empty selection is the cursor's position. +- **Color filters**: manipulates displayed colors to address color-blindness +- **High contrast themes**: ensures all displayed content has a high contrast ratio. Generally, this setting is automatically respected when using the Windows UI library. However, non-WinUI surfaces (like the terminal area) need to do additional work to respect this. +- **Voice access**: enables users to interact with their PC using their voice. This is powered by UI Automation to identify what controls can be interacted with and how. + +### Verification Tools +[Accessibility Insights](https://accessibilityinsights.io/) is a modern tool that can be used to test and understand accessibility issues on Windows. It comes with a built-in color contrast analyzer, a UI Automation event listener, and a UI Automation tree explorer. Additionally, it can be used to explore and interact with different control patterns using the same API screen readers and other accessibility tools rely on. Accessibility Insights is also capable of running automated tests in CI and locally to detect common and simple issues. + +## Proposed Accessibility Improvements + +### Respect Text Size OS setting +Windows Terminal has a profile setting for the font size (`"font"."size": `) which is then used when instantiating new terminal sessions. There should be a way to make the terminal font size respect the text size setting in Windows found in the Settings App > Accessibility > Text Size. + +A possible solution is to scale the session's text size at runtime based on the OS setting. Furthermore, the Windows Terminal's settings UI should warn the user when they are attempting to modify the font size profile setting. + +This is tracked by [Windows accessibility "text size" parameter ignored by terminal · Issue #13600](https://github.com/microsoft/terminal/issues/13600) + +### Respect High Contrast Mode +When high contrast mode is enabled, any WinUI surfaces are automatically changed to respect the newly desired contrast mode. However, the terminal area does not. In 2021, the Delta E algorithm was added to Windows Terminal as a tool to improve color contrast in the terminal area if desired[^5]. In 2022, it was exposed via an `adjustIndistinguishableColors` profile setting with a variety of possible configurations[^6]. + +There are several possible solutions to this issue, which all should be exposed to the user. Such solutions include the following: +- Enabling `adjustIndistinguishableColors` automatically when high contrast mode is enabled + - This leverages the Delta E algorithm to force an adequate contrast ratio. The algorithm would need to be expanded to expose the threshold. +- Reducing the colors used to match the Windows Contrast Theme colors + - Edge uses this heuristic and it has the added benefit that Windows Terminal is respecting the user's high contrast theme set in the OS. +- Implementing the `DECSTGLT` escape sequence which enables switching between different color modes (one of which is monochrome) +Additionally, we should automatically make the terminal fully opaque and ignore the acrylic or traditional transparency setting. + +This is tracked by [[Epic] Improved High Contrast support · Issue #12999](https://github.com/microsoft/terminal/issues/12999) + +### Enhanced UI Automation movement mechanisms +UI Automation has an `ITextRangeProvider` interface that can be used to represent a span of text. These text ranges can then be manipulated to explore the text area. Such manipulations include moving either (or both) endpoint(s) to encompass a text unit or move by unit. UI Automation supports the following text units: +- Character +- Format +- Word +- Line +- Paragraph +- Page +- Document +Windows Terminal and Conhost implement this `ITextRangeProvider` interface. However, this implementation doesn't support all of the available text units. Though this is standard across other implementations, this provides an opportunity for growth. + +#### UI Automation: Movement by page +Movement by page is a relatively abstract concept in terminals. Arguably, the viewport could be leveraged to be considered a "page", which would provide users with a quick way to navigate the buffer. + +This is tracked by [UIA: support movement by page · Issue #13756](https://github.com/microsoft/terminal/issues/13756) + +#### UI Automation: Movement by format +The terminal area supports various text decorations including but not limited to underlined, bold, and italic text. These text attributes were exposed via the `ITextRangeProvider` interface in [PR #10366](https://github.com/microsoft/terminal/pull/10336). UI automation has support for navigating through contiguous spans adhering to a text attribute, which could be useful to expose to UIA clients. + +This is tracked by [UIA Formatted Text Navigation · Issue #3567](https://github.com/microsoft/terminal/issues/3567). + +#### Movement by prompt +[PR #12948](https://github.com/microsoft/terminal/pull/12948) added support for scrollbar marks in Windows Terminal. As a part of this, an `experimental.autoMarkPrompts` profile setting registers scrollbar marks with each prompt when enabled. This is a fantastic way to navigate through the scroll history quickly and easily. + +Though UI automation doesn't have a text unit for prompts, we could define a "paragraph" as the space between two prompts. + +This issue is tracked by [megathread: Scrollbar Marks · Issue #11000](https://github.com/microsoft/terminal/issues/11000). + +### Mark Mode support for degenerate range +[PR #13053](https://github.com/microsoft/terminal/pull/13053) added support for mark mode in Windows Terminal. Mark mode allows users to create and modify selections by exclusively using the keyboard. However, screen reader users have reported it as a strange experience because it always has a cell of text selected; this results in the screen reader reading "x selected, y unselected" as opposed to the expected "x" when moving the cursor around. + +Unfortunately, the changes required to fix this are very extensive because selections are stored as two inclusive terminal coordinates, which makes it impossible to represent an empty selection. + +This is tracked by [A11y: windows terminal emits selection/deselection events in mark mode when navigating with arrow keys · Issue #13447](https://github.com/microsoft/terminal/issues/13447). + +### Search improvements +The search dialog can be used to perform a runtime query of the text buffer. However, in its current form, it does not count the total number of matches like other developer tools (i.e. Visual Studio Code). This work item tracks two things: +1. Support for counting the total number of matches +2. Displaying the number of results in the search box and announcing it via a UIA notification to the screen reader +Additional search features may fall into this including but not limited to: +- Highlight the search results in the scroll bar +- Successful searches should read the line where text was found (similar to VS Code experience) + +It seems that UI Automation has no guidance for a consistent, good search experience on Windows. Noting that VS Code and Microsoft Word have two different search experiences, it may be valuable to create documentation and guidance for other developer tools on Windows to follow, thus helping to create a more consistent search experience. + +This is tracked by [Search should display the number of results it finds · Issue #6319](https://github.com/microsoft/terminal/issues/6319) and [[JAWS] Search results read "results found" instead of something useful · Issue #14153](https://github.com/microsoft/terminal/issues/14153). + +### Shell suggestions +Shells provide different forms of autocompletion. However, that autocompletion isn't necessarily an accessible experience. For example, CMD's standard autocompletion experience rewrites the "word". The screen reader rereads the new "word" properly, however, the user must cycle between the options until the desired one is shown. As another example, + +PowerShell supports menu completion, where possible completions are displayed as a table of text and relevant help text is displayed. However, screen readers struggle with this because the entire menu is redrawn every time, making it harder to understand what exactly is "selected" (as the concept of selection in this instance is a shell-side concept represented by visual manipulation). + +A possible solution is to introduce a new VT sequence to have the shell provide a payload. This payload can contain things like the suggested completion as well as associated help text. Such data can then be leveraged by Windows Terminal to create UI elements. Doing so leverages WinUI's accessible design. By designing a new VT sequence, other terminal emulators and CLI apps can opt-in to this functionality too. + +This is tracked by [Enhance shell autocompletion with a cool new user interface and shell completion protocol · Issue #3121](https://github.com/microsoft/terminal/issues/3121). + +### Scripts Panel +Common command-line experiences revolve around inputting a command into the shell and executing it. Expert command-line users can remember complex commands off the top of their heads. Windows Terminal could help users by storing complex commands. Furthermore, these commands should be able to be easily shared between users. + +From an accessibility standpoint, this can be very useful for users with mobility issues, as it is particularly difficult to write complex commands quickly. + +This is tracked by [Feature Request - Scripts Panel · Issue #1595](https://github.com/microsoft/terminal/issues/1595). + +### Broadcast Input +Windows Terminal supports running multiple sessions at once across various panes, tabs, and windows. The broadcast input feature allows users to send input to multiple sessions at once. + +From an accessibility standpoint, this can be very useful for users with mobility issues, as it is time consuming to write commands to multiple sessions quickly, particularly if those commands are complex. + +This is tracked by [Support broadcast input? · Issue #2634](https://github.com/microsoft/terminal/issues/2634). + +### UI Automation Notification Related Improvements +In 2022, Windows Terminal added UI Automation notifications that contained a payload of text output[^3]. This was done for various reasons. For one, Narrator was not reading new output by the terminal, and would require changes on their end to accomplish this. Another reason is that NVDA is unable to handle too many text changed events, which Conhost and Windows Terminal are both culprits of since they dispatch an event when text is written to the terminal output. + +UIA notifications have provided many compatibility benefits since screen readers automatically read notifications they receive. Additionally, this has provided the possibility for major performance enhancements as screen readers may no longer be required to diff the text buffer and figure out what has changed. NVDA has prototyped listening to notifications and ignoring text changed events entirely[^7]. However, it reveals underlying challenges with this new model such as how to handle passwords. The proposals listed in this section are intended to have Windows Terminal achieve improved performance and accessibility quality. + +#### VT Screen Reader Control +Some command-line applications are simply too difficult to create a consistent accessible experience. Applications that draw decorative content, for example, may have that content read by a screen reader. + +In 2019, Daniel Imms wrote a spec proposing a VT sequence that can partially control the attached screen reader[^8]. This VT sequence consists of three main formats: +1. Stop announcing incoming data to the screen reader. The screen reader will resume announcing incoming data if any key is pressed. +2. Resume announcing incoming data to the screen reader. +3. Announce the associated string payload immediately. +Additionally, all three formats include a string payload that will be announced immediately by the screen reader (as is done with the third format). + +JAWS and Narrator both immediately read UIA notifications as that is how Windows Terminal presents newly output text. As described earlier, NVDA currently has notifications disabled, but is prototyping moving towards a world where they can drop text diffing entirely in favor of this. + +With Windows Terminal now dispatching UIA notifications, it would be relatively trivial for Windows Terminal to extract the string payload from the relevant VT sequences and present it in a new UIA notification. Additionally, an internal flag would be enabled to suppress UIA notifications for text output until the flag is flipped via a key press or the relevant VT sequence is received. + +This is tracked by [Unable to use applications that hook the arrow keys using Windows Console Host. · Issue #13666](https://github.com/microsoft/terminal/issues/13666) + +#### Screen Reader Backpressure Control Proposal +NVDA has reported issues where Windows Terminal sends too may text changed events at once, causing NVDA to hang. Several considerations have been made in this area to address this issue: +- **Batch notifications**: unhelpful because Terminal doesn't know how much text is intended to be output +- **Diff text before sending a notification**: same problem as above +- **Provide an API to throttle notifications**: causes a security risk for denial of service attacks +Surprisingly, UI Automation doesn't have a built-in way to handle backpressure. However, the appropriate fix here seems to be either on the UI Automation side to fix this for all UIA clients, or on the NVDA side to improve event handling. + +This is tracked by [UIA: reduce number of extraneous text change events in conhost · Issue #10822](https://github.com/microsoft/terminal/issues/10822) + +## Proposed Priorities +The following table assigns priorities to the aforementioned work items. These priorities are solely based on accessibility impact to improve the Windows command-line ecosystem. + +| Priority | Work Item | Reasoning | +|----------|-----------|-----------| +| 1 | VT Screen Reader Control | Several partners (both CLI apps and UIA clients) would benefit from this feature and have expressed interest in adopting it. | +| 1 | Shell suggestions | Several partners (both CLI apps and UIA clients) would benefit from this feature and have expressed interest in adopting it. | +| 1 | Search improvements | Search is very difficult to use in its current implementation. | +| 1 | Mark Mode support for degenerate range | Experience is barely usable and very unfriendly for non-sighted users. This is the only non-mouse method to select text. | +| 2 | Respect High Contrast Mode | Clear workarounds exist, but the golden path scenario fails where a user enables high contrast mode and content does not respect it. | +| 2 | Text Size OS Setting | Clear workarounds exist, but the golden path scenario fails where a user enables an OS setting and content does not respect it. | +| 2 | Broadcast Input | Users with mobility issues would greatly benefit from this. | +| 3 | Scripts Panel | Send-input commands currently exist. This would just expose it better. | +| 3 | UIA: Move to prompt | Additional UIA text unit support is good. | +| 3 | UIA: Move by page | Additional UIA text unit support is good. | +| 3 | UIA: Move by format | Additional UIA text unit support is good. | + +Generally, the reasoning behind these priorities can be broken down as follows: +- Priority 1 bucket: + - These work items provide access to features within the command-line ecosystem that are practically unusable for users with accessibility needs. +- Priority 2 bucket: + - Active workarounds exist for these issues, but they could be cumbersome. +- Priority 3 bucket: + - Purely innovative ideas that provide an exceptional experience for users. + +## References +[^1]: [Accessibility: Set-up UIA Tree by carlos-zamora · Pull Request #1691](https://github.com/microsoft/terminal/pull/1691) +[^2]: [Fire UIA Events for Output and Cursor Movement by carlos-zamora · Pull Request #4826](https://github.com/microsoft/terminal/pull/4826) +[^3]: [Use UIA notifications for text output by carlos-zamora · Pull Request #12358](https://github.com/microsoft/terminal/pull/12358) +[^4]: [WebAIM: Screen Reader User Survey #8 Results](https://webaim.org/projects/screenreadersurvey8/#:~:text=NVDA%20is%20now%20the%20most%20commonly%20used%20screen,respondents%20use%20more%20than%20one%20desktop%2Flaptop%20screen%20reader.) +[^5]: [Implement the Delta E algorithm to improve color perception by PankajBhojwani · Pull Request #11095](https://github.com/microsoft/terminal/pull/11095) +[^6]: [Change AdjustIndistinguishableColors to an enum setting instead of a boolean setting by PankajBhojwani · Pull Request #13512](https://github.com/microsoft/terminal/pull/13512) +[^7]: [Prototype for Windows Terminal: Use notifications instead of monitoring for new text by leonardder · Pull Request #14047 · nvaccess/nvda (github.com)](https://github.com/nvaccess/nvda/pull/14047) +[^8]: [Control Screen Reader from Applications (#18) · Issues · terminal-wg / specifications · GitLab](https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/18) \ No newline at end of file diff --git a/oss/stb/LICENSE b/oss/stb/LICENSE new file mode 100644 index 0000000000..21635cc163 --- /dev/null +++ b/oss/stb/LICENSE @@ -0,0 +1,37 @@ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/oss/stb/MAINTAINER_README.md b/oss/stb/MAINTAINER_README.md new file mode 100644 index 0000000000..2f23f5346c --- /dev/null +++ b/oss/stb/MAINTAINER_README.md @@ -0,0 +1,4 @@ +### Notes for Future Maintainers + +Search for files prefixed with `stb_` in this project. +At the time of writing, the only file being used is `stb_rect_pack.h`. diff --git a/oss/stb/cgmanifest.json b/oss/stb/cgmanifest.json new file mode 100644 index 0000000000..69fa8d1e26 --- /dev/null +++ b/oss/stb/cgmanifest.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/component-detection-manifest.json", + "Registrations": [ + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/nothings/stb", + "commitHash": "5736b15f7ea0ffb08dd38af21067c314d6a3aae9" + } + } + } + ], + "Version": 1 +} diff --git a/oss/stb/stb_rect_pack.h b/oss/stb/stb_rect_pack.h new file mode 100644 index 0000000000..6a633ce666 --- /dev/null +++ b/oss/stb/stb_rect_pack.h @@ -0,0 +1,623 @@ +// stb_rect_pack.h - v1.01 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Before #including, +// +// #define STB_RECT_PACK_IMPLEMENTATION +// +// in the file that you want to have the implementation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// github:IntellectualKitty +// +// Bugfixes / warning fixes +// Jeremy Jaussaud +// Fabian Giesen +// +// Version history: +// +// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section +// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles +// 0.99 (2019-02-07) warning fixes +// 0.11 (2017-03-03) return packing success/fail result +// 0.10 (2016-10-25) remove cast-away-const to avoid warnings +// 0.09 (2016-08-27) fix compiler warnings +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// See end of file for license information. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +typedef int stbrp_coord; + +#define STBRP__MAXVAL 0x7fffffff +// Mostly for internal use, but this is the maximum supported coordinate value. + +STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#define STBRP__CDECL __cdecl +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#define STBRP__CDECL +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; + context->extra[1].y = (1<<30); + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + // if it can't possibly fit, bail immediately + if (width > c->width || height > c->height) { + fr.prev_link = NULL; + fr.x = fr.y = 0; + return fr; + } + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height <= c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int STBRP__CDECL rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int STBRP__CDECL rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/res/terminal/Terminal_Can.svg b/res/terminal/Terminal_Can.svg new file mode 100644 index 0000000000..5e7f491784 --- /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 0000000000..c08c3747a5 --- /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 0000000000..2beb03d98e Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-100.png differ 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 0000000000..6119d303b4 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Can/LargeTile.scale-100_contrast-white.png b/res/terminal/images-Can/LargeTile.scale-100_contrast-white.png new file mode 100644 index 0000000000..6156e8ed8e Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-100_contrast-white.png differ 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 0000000000..9859633fe6 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-125.png differ 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 0000000000..253879e1b3 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-125_contrast-black.png differ 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 0000000000..f4007a261c Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-125_contrast-white.png differ 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 0000000000..34bc2a28da Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-150.png differ 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 0000000000..c1850b2622 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-150_contrast-black.png differ 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 0000000000..27eb42bf6d Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-150_contrast-white.png differ 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 0000000000..d23621d3c7 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-200.png differ 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 0000000000..5244c12001 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-200_contrast-black.png differ 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 0000000000..671be6ca5b Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Can/LargeTile.scale-400.png b/res/terminal/images-Can/LargeTile.scale-400.png new file mode 100644 index 0000000000..e492964ee1 Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-400.png differ diff --git a/res/terminal/images-Can/LargeTile.scale-400_contrast-black.png b/res/terminal/images-Can/LargeTile.scale-400_contrast-black.png new file mode 100644 index 0000000000..754cba078e Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Can/LargeTile.scale-400_contrast-white.png b/res/terminal/images-Can/LargeTile.scale-400_contrast-white.png new file mode 100644 index 0000000000..7898871c3c Binary files /dev/null and b/res/terminal/images-Can/LargeTile.scale-400_contrast-white.png differ 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 0000000000..c9003ee241 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-100.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-black.png b/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-black.png new file mode 100644 index 0000000000..ce51243c62 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-white.png b/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-white.png new file mode 100644 index 0000000000..be8642d7f3 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-100_contrast-white.png differ 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 0000000000..c82284ab1c Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-125.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-black.png b/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-black.png new file mode 100644 index 0000000000..f7094e3374 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-black.png differ 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 0000000000..e1b34489ea Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-125_contrast-white.png differ 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 0000000000..b32469a3c4 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-150.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-black.png b/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-black.png new file mode 100644 index 0000000000..62f84ba9aa Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-black.png differ 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 0000000000..a6d842bbde Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-150_contrast-white.png differ 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 0000000000..9dee409a6e Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-200.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-black.png b/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-black.png new file mode 100644 index 0000000000..8427e96d13 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-black.png differ 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 0000000000..c646e57cfb Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-400.png b/res/terminal/images-Can/LockScreenLogo.scale-400.png new file mode 100644 index 0000000000..4d563ccfa3 Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-400.png differ 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 0000000000..217ea2d31a Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-white.png b/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-white.png new file mode 100644 index 0000000000..cc9222195a Binary files /dev/null and b/res/terminal/images-Can/LockScreenLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Can/SmallTile.scale-100.png b/res/terminal/images-Can/SmallTile.scale-100.png new file mode 100644 index 0000000000..818f007147 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-100.png differ 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 0000000000..ac8380a456 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-100_contrast-black.png differ 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 0000000000..bf658c223a Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-100_contrast-white.png differ 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 0000000000..e045842365 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-125.png differ 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 0000000000..f511c39342 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-125_contrast-black.png differ 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 0000000000..f99d62e07f Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-125_contrast-white.png differ 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 0000000000..6549d5a635 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-150.png differ diff --git a/res/terminal/images-Can/SmallTile.scale-150_contrast-black.png b/res/terminal/images-Can/SmallTile.scale-150_contrast-black.png new file mode 100644 index 0000000000..eaaa162a8b Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-150_contrast-black.png differ 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 0000000000..49732d785a Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Can/SmallTile.scale-200.png b/res/terminal/images-Can/SmallTile.scale-200.png new file mode 100644 index 0000000000..db1f628116 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-200.png differ diff --git a/res/terminal/images-Can/SmallTile.scale-200_contrast-black.png b/res/terminal/images-Can/SmallTile.scale-200_contrast-black.png new file mode 100644 index 0000000000..97cb534f49 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-200_contrast-black.png differ 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 0000000000..87b9c9a50a Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-200_contrast-white.png differ 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 0000000000..3d97a501f2 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-400.png differ 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 0000000000..3815ab97e2 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-400_contrast-black.png differ 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 0000000000..2549ef9df3 Binary files /dev/null and b/res/terminal/images-Can/SmallTile.scale-400_contrast-white.png differ 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 0000000000..ea1b1f0678 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-100.png differ 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 0000000000..8a1e60299d Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-100_contrast-black.png differ 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 0000000000..71167720c7 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-100_contrast-white.png differ 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 0000000000..0424633169 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-125.png differ 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 0000000000..9c1add7299 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Can/SplashScreen.scale-125_contrast-white.png b/res/terminal/images-Can/SplashScreen.scale-125_contrast-white.png new file mode 100644 index 0000000000..a40233da92 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-125_contrast-white.png differ 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 0000000000..27d820c280 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-150.png differ 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 0000000000..91f66af5e5 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-150_contrast-black.png differ 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 0000000000..7afbaf5bd7 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-150_contrast-white.png differ 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 0000000000..9361fe9985 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-200.png differ diff --git a/res/terminal/images-Can/SplashScreen.scale-200_contrast-black.png b/res/terminal/images-Can/SplashScreen.scale-200_contrast-black.png new file mode 100644 index 0000000000..1a7521a58e Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-200_contrast-black.png differ 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 0000000000..5fa1be7a03 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-200_contrast-white.png differ 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 0000000000..623e949335 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-400.png differ diff --git a/res/terminal/images-Can/SplashScreen.scale-400_contrast-black.png b/res/terminal/images-Can/SplashScreen.scale-400_contrast-black.png new file mode 100644 index 0000000000..4207263708 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-400_contrast-black.png differ 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 0000000000..f6d81ec112 Binary files /dev/null and b/res/terminal/images-Can/SplashScreen.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-100.png b/res/terminal/images-Can/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..def5906c90 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-100.png differ 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 0000000000..44cc29a026 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-black.png differ 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 0000000000..999d17a9f8 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-125.png b/res/terminal/images-Can/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..ee24fa1985 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-125.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-black.png b/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-black.png new file mode 100644 index 0000000000..73de9ee6cc Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-black.png differ 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 0000000000..1e831a8259 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-125_contrast-white.png differ 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 0000000000..eeabf03075 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-150.png differ 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 0000000000..b3c2860ac3 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-white.png b/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-white.png new file mode 100644 index 0000000000..9d764e5abf Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-200.png b/res/terminal/images-Can/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..5a7a399b6e Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-200.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-black.png b/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-black.png new file mode 100644 index 0000000000..1e2490cb94 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-white.png b/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-white.png new file mode 100644 index 0000000000..17cfde292b Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-200_contrast-white.png differ 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 0000000000..93140af5ac Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-400.png differ 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 0000000000..543f5bc817 Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-black.png differ 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 0000000000..a77e7292eb Binary files /dev/null and b/res/terminal/images-Can/Square150x150Logo.scale-400_contrast-white.png differ 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 0000000000..15abb73cd3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-100.png differ 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 0000000000..a50b0a4ece Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-white.png new file mode 100644 index 0000000000..f7d646aa1b Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-100_contrast-white.png differ 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 0000000000..dafd4c9258 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-125.png differ 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 0000000000..de7ae9e6e8 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-black.png differ 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 0000000000..a07b08d631 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-125_contrast-white.png differ 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 0000000000..c40a92a47a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-150.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-black.png new file mode 100644 index 0000000000..d6cc7bf6b4 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-black.png differ 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 0000000000..5a07009093 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-150_contrast-white.png differ 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 0000000000..58dd848bd2 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-200.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-black.png new file mode 100644 index 0000000000..cb40a508e3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-black.png differ 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 0000000000..c36c591574 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-200_contrast-white.png differ 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 0000000000..b060cedd53 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-400.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-black.png new file mode 100644 index 0000000000..0202fc44b1 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-white.png new file mode 100644 index 0000000000..df1f969230 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.scale-400_contrast-white.png differ 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 0000000000..4613842aae Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated.png new file mode 100644 index 0000000000..4613842aae Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..3640ec3fa3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png differ 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 0000000000..df9dfc917f Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png differ 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 0000000000..3640ec3fa3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-black.png differ 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 0000000000..df9dfc917f Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-16_contrast-white.png differ 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 0000000000..517a25e0c9 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20.png differ 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 0000000000..517a25e0c9 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated.png differ 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 0000000000..5ea768ed4a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png differ 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 0000000000..1bc5a22917 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png differ 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 0000000000..5ea768ed4a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-black.png differ 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 0000000000..1bc5a22917 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-20_contrast-white.png differ 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 0000000000..c9003ee241 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..c9003ee241 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..ce51243c62 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png new file mode 100644 index 0000000000..be8642d7f3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png differ 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 0000000000..ce51243c62 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-white.png new file mode 100644 index 0000000000..be8642d7f3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-24_contrast-white.png differ 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 0000000000..9c8208e4a3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256.png differ 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 0000000000..9c8208e4a3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated.png differ 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 0000000000..1ecd842894 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png new file mode 100644 index 0000000000..b98386d4de Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-black.png new file mode 100644 index 0000000000..1ecd842894 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-white.png new file mode 100644 index 0000000000..b98386d4de Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-256_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30.png new file mode 100644 index 0000000000..c82284ab1c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated.png new file mode 100644 index 0000000000..c82284ab1c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..f7094e3374 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png differ 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 0000000000..e1b34489ea Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png differ 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 0000000000..f7094e3374 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-black.png differ 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 0000000000..e1b34489ea Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-30_contrast-white.png differ 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 0000000000..2ea6ca6dcb Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32.png differ 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 0000000000..2ea6ca6dcb Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated.png differ 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 0000000000..7363adf4cf Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png differ 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 0000000000..b4aa9036f3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-black.png new file mode 100644 index 0000000000..7363adf4cf Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-black.png differ 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 0000000000..b4aa9036f3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-32_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36.png new file mode 100644 index 0000000000..b32469a3c4 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated.png new file mode 100644 index 0000000000..b32469a3c4 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..62f84ba9aa Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png differ 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 0000000000..a6d842bbde Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png differ 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 0000000000..62f84ba9aa Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-black.png differ 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 0000000000..a6d842bbde Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-36_contrast-white.png differ 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 0000000000..a18d3f74d5 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40.png differ 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 0000000000..a18d3f74d5 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated.png differ 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 0000000000..93ef2fac9c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png differ 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 0000000000..3eb56d4b7e Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-black.png new file mode 100644 index 0000000000..93ef2fac9c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-black.png differ 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 0000000000..3eb56d4b7e Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-40_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..9dee409a6e Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated.png new file mode 100644 index 0000000000..9dee409a6e Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..8427e96d13 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png differ 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 0000000000..c646e57cfb Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-black.png new file mode 100644 index 0000000000..8427e96d13 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-black.png differ 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 0000000000..c646e57cfb Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-48_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60.png new file mode 100644 index 0000000000..708b820f23 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated.png new file mode 100644 index 0000000000..708b820f23 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..d742b73489 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png differ 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 0000000000..d83ab159db Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png differ 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 0000000000..d742b73489 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-black.png differ 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 0000000000..d83ab159db Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-60_contrast-white.png differ 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 0000000000..ce8b45704f Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64.png differ 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 0000000000..ce8b45704f Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated.png differ 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 0000000000..4d97ed69bd Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png new file mode 100644 index 0000000000..5cf6bb424a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png differ 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 0000000000..4d97ed69bd Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-white.png new file mode 100644 index 0000000000..5cf6bb424a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-64_contrast-white.png differ 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 0000000000..11c86c39b9 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72.png differ 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 0000000000..11c86c39b9 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated.png differ 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 0000000000..bb27466d5a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png differ 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 0000000000..ce7023b41c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-black.png new file mode 100644 index 0000000000..bb27466d5a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-black.png differ 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 0000000000..ce7023b41c Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-72_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80.png new file mode 100644 index 0000000000..48294fb888 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated.png new file mode 100644 index 0000000000..48294fb888 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png new file mode 100644 index 0000000000..e636f00d75 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png new file mode 100644 index 0000000000..7fd1b95e7a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png differ 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 0000000000..e636f00d75 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-white.png new file mode 100644 index 0000000000..7fd1b95e7a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-80_contrast-white.png differ 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 0000000000..4d563ccfa3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96.png differ 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 0000000000..4d563ccfa3 Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated.png differ 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 0000000000..217ea2d31a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png new file mode 100644 index 0000000000..cc9222195a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-black.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-black.png new file mode 100644 index 0000000000..217ea2d31a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-black.png differ diff --git a/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-white.png b/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-white.png new file mode 100644 index 0000000000..cc9222195a Binary files /dev/null and b/res/terminal/images-Can/Square44x44Logo.targetsize-96_contrast-white.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-100.png b/res/terminal/images-Can/StoreLogo.scale-100.png new file mode 100644 index 0000000000..6c9ab5a9a1 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-100.png differ 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 0000000000..433e4ff611 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-100_contrast-black.png differ 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 0000000000..830999c88d Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-125.png b/res/terminal/images-Can/StoreLogo.scale-125.png new file mode 100644 index 0000000000..231fe2d0d2 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-125.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-125_contrast-black.png b/res/terminal/images-Can/StoreLogo.scale-125_contrast-black.png new file mode 100644 index 0000000000..011fbab34a Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-125_contrast-black.png differ 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 0000000000..75836cb6eb Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-150.png b/res/terminal/images-Can/StoreLogo.scale-150.png new file mode 100644 index 0000000000..3ccbacbe49 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-150.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-150_contrast-black.png b/res/terminal/images-Can/StoreLogo.scale-150_contrast-black.png new file mode 100644 index 0000000000..9f780332c7 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-150_contrast-black.png differ 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 0000000000..303c49854e Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-150_contrast-white.png differ 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 0000000000..aabec52a43 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-200.png differ 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 0000000000..6d215086ff Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-200_contrast-white.png b/res/terminal/images-Can/StoreLogo.scale-200_contrast-white.png new file mode 100644 index 0000000000..e85980232d Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-200_contrast-white.png differ 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 0000000000..f95132214d Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-400.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-400_contrast-black.png b/res/terminal/images-Can/StoreLogo.scale-400_contrast-black.png new file mode 100644 index 0000000000..9618452c43 Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Can/StoreLogo.scale-400_contrast-white.png b/res/terminal/images-Can/StoreLogo.scale-400_contrast-white.png new file mode 100644 index 0000000000..b1baf36f9c Binary files /dev/null and b/res/terminal/images-Can/StoreLogo.scale-400_contrast-white.png differ 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 0000000000..d0ab0c03aa Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-100.png differ 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 0000000000..7b30c8a322 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-black.png differ 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 0000000000..1a9bd3f3a6 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-100_contrast-white.png differ 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 0000000000..ba080ed8f8 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-125.png differ 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 0000000000..e55030a8aa Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-white.png b/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-white.png new file mode 100644 index 0000000000..226d684a00 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-125_contrast-white.png differ 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 0000000000..b9ace2768a Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-150.png differ 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 0000000000..0b14eacfa2 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-white.png b/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-white.png new file mode 100644 index 0000000000..6c8ca0d3cb Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-150_contrast-white.png differ 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 0000000000..ea1b1f0678 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-200.png differ 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 0000000000..8a1e60299d Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-black.png differ 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 0000000000..71167720c7 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-200_contrast-white.png differ 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 0000000000..9361fe9985 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-400.png differ diff --git a/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-black.png b/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-black.png new file mode 100644 index 0000000000..1a7521a58e Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-black.png differ 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 0000000000..5fa1be7a03 Binary files /dev/null and b/res/terminal/images-Can/Wide310x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Can/terminal.ico b/res/terminal/images-Can/terminal.ico new file mode 100644 index 0000000000..4449fc7252 Binary files /dev/null and b/res/terminal/images-Can/terminal.ico differ 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 0000000000..3b2e743758 Binary files /dev/null and b/res/terminal/images-Can/terminal_contrast-black.ico differ 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 0000000000..b791799c20 Binary files /dev/null and b/res/terminal/images-Can/terminal_contrast-white.ico differ diff --git a/samples/PixelShaders/Outlines.hlsl b/samples/PixelShaders/Outlines.hlsl index 789f5dc2d2..ecee3adcca 100644 --- a/samples/PixelShaders/Outlines.hlsl +++ b/samples/PixelShaders/Outlines.hlsl @@ -6,14 +6,14 @@ SamplerState samplerState; // Terminal settings such as the resolution of the texture cbuffer PixelShaderSettings { - // The number of seconds since the pixel shader was enabled - float Time; - // UI Scale - float Scale; - // Resolution of the shaderTexture - float2 Resolution; - // Background color as rgba - float4 Background; + // The number of seconds since the pixel shader was enabled + float Time; + // UI Scale + float Scale; + // Resolution of the shaderTexture + float2 Resolution; + // Background color as rgba + float4 Background; }; // A pixel shader is a program that given a texture coordinate (tex) produces a color. @@ -29,38 +29,19 @@ float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET // effect, read the colors offset on the left, right, top, bottom of this // fragment, as well as on the corners of this fragment. // - // You could get away with fewer samples, but the resulting outlines will be - // blurrier. - - //left, right, top, bottom: - float4 leftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, 0.0)/Resolution.y); - float4 rightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, 0.0)/Resolution.y); - float4 topColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 0.0, 1.0)/Resolution.y); - float4 bottomColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 0.0, -1.0)/Resolution.y); - - // Corners - float4 topLeftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, 1.0)/Resolution.y); - float4 topRightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, 1.0)/Resolution.y); - float4 bottomLeftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, -1.0)/Resolution.y); - float4 bottomRightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, -1.0)/Resolution.y); - - // Now, if any of those adjacent cells has text in it, then the *color vec4 // will have a non-zero .w (which is used for alpha). Use that alpha value // to add some black to the current fragment. // // This will result in only coloring fragments adjacent to text, but leaving // background images (for example) untouched. - float3 outlineColor = float3(0, 0, 0); - float4 result = color; - result = result + float4(outlineColor, leftColor.w); - result = result + float4(outlineColor, rightColor.w); - result = result + float4(outlineColor, topColor.w); - result = result + float4(outlineColor, bottomColor.w); - result = result + float4(outlineColor, topLeftColor.w); - result = result + float4(outlineColor, topRightColor.w); - result = result + float4(outlineColor, bottomLeftColor.w); - result = result + float4(outlineColor, bottomRightColor.w); - return result; + for (int dy = -2; dy <= 2; dy += 2) { + for (int dx = -2; dx <= 2; dx += 2) { + float4 neighbor = shaderTexture.Sample(samplerState, tex, int2(dx, dy)); + color.a += neighbor.a; + } + } + + return color; } diff --git a/scratch/ScratchIslandApp/SampleApp/App.cpp b/scratch/ScratchIslandApp/SampleApp/App.cpp index 5a2ae523f6..2b1df560e6 100644 --- a/scratch/ScratchIslandApp/SampleApp/App.cpp +++ b/scratch/ScratchIslandApp/SampleApp/App.cpp @@ -12,19 +12,12 @@ using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::UI::Xaml::Navigation; +namespace xaml = ::winrt::Windows::UI::Xaml; + namespace winrt::SampleApp::implementation { App::App() { - // This is the same trick that Initialize() is about to use to figure out whether we're coming - // from a UWP context or from a Win32 context - // See https://github.com/windows-toolkit/Microsoft.Toolkit.Win32/blob/52611c57d89554f357f281d0c79036426a7d9257/Microsoft.Toolkit.Win32.UI.XamlApplication/XamlApplication.cpp#L42 - const auto dispatcherQueue = ::winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); - if (dispatcherQueue) - { - _isUwp = true; - } - Initialize(); // Disable XAML's automatic backplating of text when in High Contrast @@ -33,6 +26,44 @@ namespace winrt::SampleApp::implementation HighContrastAdjustment(::winrt::Windows::UI::Xaml::ApplicationHighContrastAdjustment::None); } + void App::Initialize() + { + const auto dispatcherQueue = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + if (!dispatcherQueue) + { + _windowsXamlManager = xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); + } + else + { + _isUwp = true; + } + } + + void App::Close() + { + if (_bIsClosed) + { + return; + } + + _bIsClosed = true; + + if (_windowsXamlManager) + { + _windowsXamlManager.Close(); + } + _windowsXamlManager = nullptr; + + Exit(); + { + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } + } + } + SampleAppLogic App::Logic() { static SampleAppLogic logic; diff --git a/scratch/ScratchIslandApp/SampleApp/App.h b/scratch/ScratchIslandApp/SampleApp/App.h index fb6235d753..9debfd86a2 100644 --- a/scratch/ScratchIslandApp/SampleApp/App.h +++ b/scratch/ScratchIslandApp/SampleApp/App.h @@ -12,12 +12,22 @@ namespace winrt::SampleApp::implementation { public: App(); + void Initialize(); + void Close(); void OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs const&); + bool IsDisposed() const + { + return _bIsClosed; + } + SampleApp::SampleAppLogic Logic(); private: bool _isUwp = false; + winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager _windowsXamlManager = nullptr; + winrt::Windows::Foundation::Collections::IVector _providers = winrt::single_threaded_vector(); + bool _bIsClosed = false; }; } diff --git a/scratch/ScratchIslandApp/SampleApp/App.idl b/scratch/ScratchIslandApp/SampleApp/App.idl index e51a73332f..9a8e1affed 100644 --- a/scratch/ScratchIslandApp/SampleApp/App.idl +++ b/scratch/ScratchIslandApp/SampleApp/App.idl @@ -7,10 +7,12 @@ namespace SampleApp { // ADD ARBITRARY APP LOGIC TO SampleAppLogic.idl, NOT HERE. // This is for XAML platform setup only. - [default_interface] runtimeclass App : Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication + [default_interface] runtimeclass App : Windows.UI.Xaml.Application, Windows.Foundation.IClosable { App(); SampleAppLogic Logic { get; }; + + Boolean IsDisposed { get; }; } } diff --git a/scratch/ScratchIslandApp/SampleApp/App.xaml b/scratch/ScratchIslandApp/SampleApp/App.xaml index c6a2328f41..42a5ee1888 100644 --- a/scratch/ScratchIslandApp/SampleApp/App.xaml +++ b/scratch/ScratchIslandApp/SampleApp/App.xaml @@ -2,20 +2,19 @@ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. See LICENSE in the project root for license information. --> - + - + @@ -67,5 +66,5 @@ - - + + diff --git a/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj index 5ed5342232..478fbeac20 100644 --- a/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj +++ b/scratch/ScratchIslandApp/SampleApp/SampleAppLib.vcxproj @@ -20,7 +20,6 @@ true - true @@ -128,6 +127,16 @@ false false + + + true + false + $(TargetPlatformMoniker) + $(TargetPlatformDisplayName) + CppWinRTImplicitlyExpandTargetPlatform + True + + @@ -158,7 +167,7 @@ diff --git a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj index f1a5c8968b..adc69c1f05 100644 --- a/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj +++ b/scratch/ScratchIslandApp/SampleApp/dll/SampleApp.vcxproj @@ -13,7 +13,6 @@ true - true @@ -45,7 +44,7 @@ - diff --git a/scratch/ScratchIslandApp/SampleApp/packages.config b/scratch/ScratchIslandApp/SampleApp/packages.config index 5df52911da..857f429dac 100644 --- a/scratch/ScratchIslandApp/SampleApp/packages.config +++ b/scratch/ScratchIslandApp/SampleApp/packages.config @@ -2,5 +2,5 @@ - + diff --git a/scratch/ScratchIslandApp/SampleApp/pch.h b/scratch/ScratchIslandApp/SampleApp/pch.h index ba01e45e45..dbb486ab75 100644 --- a/scratch/ScratchIslandApp/SampleApp/pch.h +++ b/scratch/ScratchIslandApp/SampleApp/pch.h @@ -46,7 +46,6 @@ #include "winrt/Windows.UI.Xaml.Markup.h" #include "winrt/Windows.UI.ViewManagement.h" -#include #include #include #include diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj index 30a36c91fe..fa14a3b19a 100644 --- a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj @@ -17,8 +17,6 @@ true - true - true true @@ -90,7 +88,7 @@ Particularly tricky is Microsoft.Terminal.Core.winmd. That winmd doesn't have its own DLL (it doesn't have any activatable classes, only structs and interfaces). However, it too is necessary for Terminal.Control to be able to - marshall the Core types across the boundary. + marshal the Core types across the boundary. --> $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd @@ -197,26 +195,6 @@ - - - - <_TerminalConnectionDlls Include="$(OpenConsoleCommonOutDir)\TerminalConnection\*.dll" /> - - - $(ProjectName) - BuiltProjectOutputGroup - %(Filename)%(Extension) - - - - <_WindowsTerminalExe Include="$(OpenConsoleCommonOutDir)\WindowsTerminal\*.exe" /> diff --git a/scratch/ScratchIslandApp/WindowExe/packages.config b/scratch/ScratchIslandApp/WindowExe/packages.config index 595507a802..3543aaf75b 100644 --- a/scratch/ScratchIslandApp/WindowExe/packages.config +++ b/scratch/ScratchIslandApp/WindowExe/packages.config @@ -1,7 +1,6 @@ - + - diff --git a/src/buffer/out/AttrRow.cpp b/src/buffer/out/AttrRow.cpp deleted file mode 100644 index 25def986e1..0000000000 --- a/src/buffer/out/AttrRow.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "AttrRow.hpp" - -// Routine Description: -// - constructor -// Arguments: -// - cchRowWidth - the length of the default text attribute -// - attr - the default text attribute -// Return Value: -// - constructed object -ATTR_ROW::ATTR_ROW(const til::CoordType width, const TextAttribute attr) : - _data(gsl::narrow_cast(width), attr) {} - -// Routine Description: -// - Sets all properties of the ATTR_ROW to default values -// Arguments: -// - attr - The default text attributes to use on text in this row. -void ATTR_ROW::Reset(const TextAttribute attr) -{ - _data.replace(0, _data.size(), attr); -} - -// Routine Description: -// - Takes an existing row of attributes, and changes the length so that it fills the NewWidth. -// If the new size is bigger, then the last attr is extended to fill the NewWidth. -// If the new size is smaller, the runs are cut off to fit. -// Arguments: -// - oldWidth - The original width of the row. -// - newWidth - The new width of the row. -// Return Value: -// - , throws exceptions on failures. -void ATTR_ROW::Resize(const til::CoordType newWidth) -{ - _data.resize_trailing_extent(gsl::narrow(newWidth)); -} - -// Routine Description: -// - returns a copy of the TextAttribute at the specified column -// Arguments: -// - column - the column to get the attribute for -// Return Value: -// - the text attribute at column -// Note: -// - will throw on error -TextAttribute ATTR_ROW::GetAttrByColumn(const til::CoordType column) const -{ - return _data.at(gsl::narrow(column)); -} - -// Routine Description: -// - Finds the hyperlink IDs present in this row and returns them -// Return value: -// - The hyperlink IDs present in this row -std::vector ATTR_ROW::GetHyperlinks() const -{ - std::vector ids; - for (const auto& run : _data.runs()) - { - if (run.value.IsHyperlink()) - { - ids.emplace_back(run.value.GetHyperlinkId()); - } - } - return ids; -} - -// Routine Description: -// - Sets the attributes (colors) of all character positions from the given position through the end of the row. -// Arguments: -// - iStart - Starting index position within the row -// - attr - Attribute (color) to fill remaining characters with -// Return Value: -// - -bool ATTR_ROW::SetAttrToEnd(const til::CoordType beginIndex, const TextAttribute attr) -{ - _data.replace(gsl::narrow(beginIndex), _data.size(), attr); - return true; -} - -// Method Description: -// - Replaces all runs in the row with the given toBeReplacedAttr with the new -// attribute replaceWith. -// Arguments: -// - toBeReplacedAttr - the attribute to replace in this row. -// - replaceWith - the new value for the matching runs' attributes. -// Return Value: -// - -void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith) -{ - _data.replace_values(toBeReplacedAttr, replaceWith); -} - -// Routine Description: -// - Takes an attribute, and merges it into this row from beginIndex (inclusive) to endIndex (exclusive). -// - For example, if the current row was [{4, BLUE}], the merge arguments were -// { beginIndex = 1, endIndex = 3, newAttr = RED }, then the row would modified to be -// [{ 1, BLUE}, {2, RED}, {1, BLUE}]. -// Arguments: -// - beginIndex, endIndex: The [beginIndex, endIndex) range that's to be replaced with newAttr. -// - newAttr: The attribute to merge into this row. -// Return Value: -// - -void ATTR_ROW::Replace(const til::CoordType beginIndex, const til::CoordType endIndex, const TextAttribute& newAttr) -{ - _data.replace(gsl::narrow(beginIndex), gsl::narrow(endIndex), newAttr); -} - -ATTR_ROW::const_iterator ATTR_ROW::begin() const noexcept -{ - return _data.begin(); -} - -ATTR_ROW::const_iterator ATTR_ROW::end() const noexcept -{ - return _data.end(); -} - -ATTR_ROW::const_iterator ATTR_ROW::cbegin() const noexcept -{ - return _data.cbegin(); -} - -ATTR_ROW::const_iterator ATTR_ROW::cend() const noexcept -{ - return _data.cend(); -} - -bool operator==(const ATTR_ROW& a, const ATTR_ROW& b) noexcept -{ - return a._data == b._data; -} diff --git a/src/buffer/out/AttrRow.hpp b/src/buffer/out/AttrRow.hpp deleted file mode 100644 index 5a6de23c4d..0000000000 --- a/src/buffer/out/AttrRow.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- AttrRow.hpp - -Abstract: -- contains data structure for the attributes of one row of screen buffer - -Author(s): -- Michael Niksa (miniksa) 10-Apr-2014 -- Paul Campbell (paulcam) 10-Apr-2014 - -Revision History: -- From components of output.h/.c - by Therese Stowell (ThereseS) 1990-1991 -- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) ---*/ - -#pragma once - -#include "til/rle.h" -#include "TextAttribute.hpp" - -class ATTR_ROW final -{ - using rle_vector = til::small_rle; - -public: - using const_iterator = rle_vector::const_iterator; - - ATTR_ROW(til::CoordType width, TextAttribute attr); - - ~ATTR_ROW() = default; - - ATTR_ROW(const ATTR_ROW&) = default; - ATTR_ROW& operator=(const ATTR_ROW&) = default; - ATTR_ROW(ATTR_ROW&&) - noexcept = default; - ATTR_ROW& operator=(ATTR_ROW&&) noexcept = default; - - TextAttribute GetAttrByColumn(til::CoordType column) const; - std::vector GetHyperlinks() const; - - bool SetAttrToEnd(til::CoordType beginIndex, TextAttribute attr); - void ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith); - void Resize(til::CoordType newWidth); - void Replace(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr); - - const_iterator begin() const noexcept; - const_iterator end() const noexcept; - - const_iterator cbegin() const noexcept; - const_iterator cend() const noexcept; - - friend bool operator==(const ATTR_ROW& a, const ATTR_ROW& b) noexcept; - friend class ROW; - -private: - void Reset(const TextAttribute attr); - - rle_vector _data; - -#ifdef UNIT_TESTING - friend class CommonState; -#endif -}; diff --git a/src/buffer/out/CharRow.cpp b/src/buffer/out/CharRow.cpp deleted file mode 100644 index 704adacf21..0000000000 --- a/src/buffer/out/CharRow.cpp +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "CharRow.hpp" -#include "unicode.hpp" -#include "Row.hpp" - -// Routine Description: -// - constructor -// Arguments: -// - rowWidth - the size (in wchar_t) of the char and attribute rows -// - pParent - the parent ROW -// Return Value: -// - instantiated object -// Note: will through if unable to allocate char/attribute buffers -#pragma warning(push) -#pragma warning(disable : 26447) // small_vector's constructor says it can throw but it should not given how we use it. This suppresses this error for the AuditMode build. -CharRow::CharRow(til::CoordType rowWidth, ROW* const pParent) noexcept : - _data(rowWidth, value_type()), - _pParent{ FAIL_FAST_IF_NULL(pParent) } -{ -} -#pragma warning(pop) - -// Routine Description: -// - gets the size of the row, in glyph cells -// Arguments: -// - -// Return Value: -// - the size of the row -til::CoordType CharRow::size() const noexcept -{ - return gsl::narrow_cast(_data.size()); -} - -// Routine Description: -// - Sets all properties of the CharRowBase to default values -// Arguments: -// - sRowWidth - The width of the row. -// Return Value: -// - -void CharRow::Reset() noexcept -{ - for (auto& cell : _data) - { - cell.Reset(); - } -} - -// Routine Description: -// - resizes the width of the CharRowBase -// Arguments: -// - newSize - the new width of the character and attributes rows -// Return Value: -// - S_OK on success, otherwise relevant error code -[[nodiscard]] HRESULT CharRow::Resize(const til::CoordType newSize) noexcept -{ - try - { - const value_type insertVals; - _data.resize(newSize, insertVals); - } - CATCH_RETURN(); - - return S_OK; -} - -typename CharRow::iterator CharRow::begin() noexcept -{ - return _data.begin(); -} - -typename CharRow::const_iterator CharRow::cbegin() const noexcept -{ - return _data.cbegin(); -} - -typename CharRow::iterator CharRow::end() noexcept -{ - return _data.end(); -} - -typename CharRow::const_iterator CharRow::cend() const noexcept -{ - return _data.cend(); -} - -// Routine Description: -// - Inspects the current internal string to find the left edge of it -// Arguments: -// - -// Return Value: -// - The calculated left boundary of the internal string. -til::CoordType CharRow::MeasureLeft() const noexcept -{ - auto it = _data.cbegin(); - while (it != _data.cend() && it->IsSpace()) - { - ++it; - } - return gsl::narrow_cast(it - _data.cbegin()); -} - -// Routine Description: -// - Inspects the current internal string to find the right edge of it -// Arguments: -// - -// Return Value: -// - The calculated right boundary of the internal string. -til::CoordType CharRow::MeasureRight() const -{ - auto it = _data.crbegin(); - while (it != _data.crend() && it->IsSpace()) - { - ++it; - } - return gsl::narrow_cast(_data.crend() - it); -} - -void CharRow::ClearCell(const til::CoordType column) -{ - _data.at(column).Reset(); -} - -// Routine Description: -// - Tells you whether or not this row contains any valid text. -// Arguments: -// - -// Return Value: -// - True if there is valid text in this row. False otherwise. -bool CharRow::ContainsText() const noexcept -{ - for (const auto& cell : _data) - { - if (!cell.IsSpace()) - { - return true; - } - } - return false; -} - -// Routine Description: -// - gets the attribute at the specified column -// Arguments: -// - column - the column to get the attribute for -// Return Value: -// - the attribute -// Note: will throw exception if column is out of bounds -const DbcsAttribute& CharRow::DbcsAttrAt(const til::CoordType column) const -{ - return _data.at(column).DbcsAttr(); -} - -// Routine Description: -// - gets the attribute at the specified column -// Arguments: -// - column - the column to get the attribute for -// Return Value: -// - the attribute -// Note: will throw exception if column is out of bounds -DbcsAttribute& CharRow::DbcsAttrAt(const til::CoordType column) -{ - return _data.at(column).DbcsAttr(); -} - -// Routine Description: -// - resets text data at column -// Arguments: -// - column - column index to clear text data from -// Return Value: -// - -// Note: will throw exception if column is out of bounds -void CharRow::ClearGlyph(const til::CoordType column) -{ - _data.at(column).EraseChars(); -} - -// Routine Description: -// - returns text data at column as a const reference. -// Arguments: -// - column - column to get text data for -// Return Value: -// - text data at column -// - Note: will throw exception if column is out of bounds -const CharRow::reference CharRow::GlyphAt(const til::CoordType column) const -{ - THROW_HR_IF(E_INVALIDARG, column < 0 || column >= gsl::narrow_cast(_data.size())); - return { const_cast(*this), column }; -} - -// Routine Description: -// - returns text data at column as a reference. -// Arguments: -// - column - column to get text data for -// Return Value: -// - text data at column -// - Note: will throw exception if column is out of bounds -CharRow::reference CharRow::GlyphAt(const til::CoordType column) -{ - THROW_HR_IF(E_INVALIDARG, column < 0 || column >= gsl::narrow_cast(_data.size())); - return { *this, column }; -} - -std::wstring CharRow::GetText() const -{ - std::wstring wstr; - wstr.reserve(_data.size()); - - for (til::CoordType i = 0; i < gsl::narrow_cast(_data.size()); ++i) - { - const auto glyph = GlyphAt(i); - if (!DbcsAttrAt(i).IsTrailing()) - { - for (const auto wch : glyph) - { - wstr.push_back(wch); - } - } - } - return wstr; -} - -// Method Description: -// - get delimiter class for a position in the char row -// - used for double click selection and uia word navigation -// Arguments: -// - column: column to get text data for -// - wordDelimiters: the delimiters defined as a part of the DelimiterClass::DelimiterChar -// Return Value: -// - the delimiter class for the given char -const DelimiterClass CharRow::DelimiterClassAt(const til::CoordType column, const std::wstring_view wordDelimiters) const -{ - THROW_HR_IF(E_INVALIDARG, column < 0 || column >= gsl::narrow_cast(_data.size())); - - const auto glyph = *GlyphAt(column).begin(); - if (glyph <= UNICODE_SPACE) - { - return DelimiterClass::ControlChar; - } - else if (wordDelimiters.find(glyph) != std::wstring_view::npos) - { - return DelimiterClass::DelimiterChar; - } - else - { - return DelimiterClass::RegularChar; - } -} - -UnicodeStorage& CharRow::GetUnicodeStorage() noexcept -{ - return _pParent->GetUnicodeStorage(); -} - -const UnicodeStorage& CharRow::GetUnicodeStorage() const noexcept -{ - return _pParent->GetUnicodeStorage(); -} - -// Routine Description: -// - calculates the key used by the given column of the char row to store glyph data in UnicodeStorage -// Arguments: -// - column - the column to generate the key for -// Return Value: -// - the til::point key for data access from UnicodeStorage for the column -til::point CharRow::GetStorageKey(const til::CoordType column) const noexcept -{ - return { column, _pParent->GetId() }; -} - -// Routine Description: -// - Updates the pointer to the parent row (which might change if we shuffle the rows around) -// Arguments: -// - pParent - Pointer to the parent row -void CharRow::UpdateParent(ROW* const pParent) -{ - _pParent = FAIL_FAST_IF_NULL(pParent); -} diff --git a/src/buffer/out/CharRow.hpp b/src/buffer/out/CharRow.hpp deleted file mode 100644 index 321986c80b..0000000000 --- a/src/buffer/out/CharRow.hpp +++ /dev/null @@ -1,117 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CharRow.hpp - -Abstract: -- contains data structure for UCS2 encoded character data of a row - -Author(s): -- Michael Niksa (miniksa) 10-Apr-2014 -- Paul Campbell (paulcam) 10-Apr-2014 - -Revision History: -- From components of output.h/.c - by Therese Stowell (ThereseS) 1990-1991 -- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) ---*/ - -#pragma once - -#include - -#include "DbcsAttribute.hpp" -#include "CharRowCellReference.hpp" -#include "CharRowCell.hpp" -#include "UnicodeStorage.hpp" - -class ROW; - -enum class DelimiterClass -{ - ControlChar, - DelimiterChar, - RegularChar -}; - -// the characters of one row of screen buffer -// we keep the following values so that we don't write -// more pixels to the screen than we have to: -// left is initialized to screenbuffer width. right is -// initialized to zero. -// -// [ foo.bar 12-12-61 ] -// ^ ^ ^ ^ -// | | | | -// Chars Left Right end of Chars buffer -class CharRow final -{ -public: - using glyph_type = wchar_t; - using value_type = CharRowCell; - using iterator = til::small_vector::iterator; - using const_iterator = til::small_vector::const_iterator; - using const_reverse_iterator = til::small_vector::const_reverse_iterator; - using reference = CharRowCellReference; - - CharRow(til::CoordType rowWidth, ROW* const pParent) noexcept; - - til::CoordType size() const noexcept; - [[nodiscard]] HRESULT Resize(const til::CoordType newSize) noexcept; - til::CoordType MeasureLeft() const noexcept; - til::CoordType MeasureRight() const; - bool ContainsText() const noexcept; - const DbcsAttribute& DbcsAttrAt(const til::CoordType column) const; - DbcsAttribute& DbcsAttrAt(const til::CoordType column); - void ClearGlyph(const til::CoordType column); - - const DelimiterClass DelimiterClassAt(const til::CoordType column, const std::wstring_view wordDelimiters) const; - - // working with glyphs - const reference GlyphAt(const til::CoordType column) const; - reference GlyphAt(const til::CoordType column); - - // iterators - iterator begin() noexcept; - const_iterator cbegin() const noexcept; - const_iterator begin() const noexcept { return cbegin(); } - - iterator end() noexcept; - const_iterator cend() const noexcept; - const_iterator end() const noexcept { return cend(); } - - UnicodeStorage& GetUnicodeStorage() noexcept; - const UnicodeStorage& GetUnicodeStorage() const noexcept; - til::point GetStorageKey(const til::CoordType column) const noexcept; - - void UpdateParent(ROW* const pParent); - - friend CharRowCellReference; - friend class ROW; - -private: - void Reset() noexcept; - void ClearCell(const til::CoordType column); - std::wstring GetText() const; - -protected: - // storage for glyph data and dbcs attributes - til::small_vector _data; - - // ROW that this CharRow belongs to - ROW* _pParent; -}; - -template -void OverwriteColumns(InputIt1 startChars, InputIt1 endChars, InputIt2 startAttrs, CharRow::iterator outIt) -{ - std::transform(startChars, - endChars, - startAttrs, - outIt, - [](const wchar_t wch, const DbcsAttribute attr) { - return CharRow::value_type{ wch, attr }; - }); -} diff --git a/src/buffer/out/CharRowCell.cpp b/src/buffer/out/CharRowCell.cpp deleted file mode 100644 index 2ada05d3c5..0000000000 --- a/src/buffer/out/CharRowCell.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -#include "precomp.h" - -#include "CharRowCell.hpp" -#include "unicode.hpp" - -// default glyph value, used for resetting the character data portion of a cell -static constexpr wchar_t DefaultValue = UNICODE_SPACE; - -// Routine Description: -// - "erases" the glyph. really sets it back to the default "empty" value -void CharRowCell::EraseChars() noexcept -{ - if (_attr.IsGlyphStored()) - { - _attr.SetGlyphStored(false); - } - _wch = DefaultValue; -} - -// Routine Description: -// - resets this object back to the defaults it would have from the default constructor -void CharRowCell::Reset() noexcept -{ - _attr.Reset(); - _wch = DefaultValue; -} - -// Routine Description: -// - checks if cell contains a space glyph -// Return Value: -// - true if cell contains a space glyph, false otherwise -bool CharRowCell::IsSpace() const noexcept -{ - return !_attr.IsGlyphStored() && _wch == UNICODE_SPACE; -} - -// Routine Description: -// - Access the DbcsAttribute for the cell -// Return Value: -// - ref to the cells' DbcsAttribute -DbcsAttribute& CharRowCell::DbcsAttr() noexcept -{ - return _attr; -} - -// Routine Description: -// - Access the DbcsAttribute for the cell -// Return Value: -// - ref to the cells' DbcsAttribute -const DbcsAttribute& CharRowCell::DbcsAttr() const noexcept -{ - return _attr; -} - -// Routine Description: -// - Access the cell's wchar field. this does not access any char data through UnicodeStorage. -// Return Value: -// - the cell's wchar field -wchar_t& CharRowCell::Char() noexcept -{ - return _wch; -} - -// Routine Description: -// - Access the cell's wchar field. this does not access any char data through UnicodeStorage. -// Return Value: -// - the cell's wchar field -const wchar_t& CharRowCell::Char() const noexcept -{ - return _wch; -} diff --git a/src/buffer/out/CharRowCell.hpp b/src/buffer/out/CharRowCell.hpp deleted file mode 100644 index 2e68a51efb..0000000000 --- a/src/buffer/out/CharRowCell.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CharRowCell.hpp - -Abstract: -- data structure for one cell of a char row. contains the char data for one - coordinate position in the output buffer (leading/trailing information and - the char itself. - -Author(s): -- Austin Diviness (AustDi) 02-May-2018 ---*/ - -#pragma once - -#include "DbcsAttribute.hpp" -#include "unicode.hpp" - -#if (defined(_M_IX86) || defined(_M_AMD64)) -// currently CharRowCell's fields use 3 bytes of memory, leaving the 4th byte in unused. this leads -// to a rather large amount of useless memory allocated. so instead, pack CharRowCell by bytes instead of words. -#pragma pack(push, 1) -#endif - -class CharRowCell final -{ -public: - CharRowCell() noexcept = default; - CharRowCell(const wchar_t wch, const DbcsAttribute attr) noexcept - : - _wch(wch), - _attr(attr) - { - } - - void EraseChars() noexcept; - void Reset() noexcept; - - bool IsSpace() const noexcept; - - DbcsAttribute& DbcsAttr() noexcept; - const DbcsAttribute& DbcsAttr() const noexcept; - - wchar_t& Char() noexcept; - const wchar_t& Char() const noexcept; - - friend constexpr bool operator==(const CharRowCell& a, const CharRowCell& b) noexcept; - -private: - wchar_t _wch{ UNICODE_SPACE }; - DbcsAttribute _attr{}; -}; - -#if (defined(_M_IX86) || defined(_M_AMD64)) -#pragma pack(pop) -#endif - -constexpr bool operator==(const CharRowCell& a, const CharRowCell& b) noexcept -{ - return (a._wch == b._wch && - a._attr == b._attr); -} diff --git a/src/buffer/out/CharRowCellReference.cpp b/src/buffer/out/CharRowCellReference.cpp deleted file mode 100644 index ff5e5da8f1..0000000000 --- a/src/buffer/out/CharRowCellReference.cpp +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "UnicodeStorage.hpp" -#include "CharRow.hpp" - -// Routine Description: -// - assignment operator. will store extended glyph data in a separate storage location -// Arguments: -// - chars - the glyph data to store -void CharRowCellReference::operator=(const std::wstring_view chars) -{ - THROW_HR_IF(E_INVALIDARG, chars.empty()); - if (chars.size() == 1) - { - _cellData().Char() = chars.front(); - _cellData().DbcsAttr().SetGlyphStored(false); - } - else - { - auto& storage = _parent.GetUnicodeStorage(); - const auto key = _parent.GetStorageKey(_index); - storage.StoreGlyph(key, { chars.cbegin(), chars.cend() }); - _cellData().DbcsAttr().SetGlyphStored(true); - } -} - -// Routine Description: -// - implicit conversion to vector operator. -// Return Value: -// - std::vector of the glyph data in the referenced cell -CharRowCellReference::operator std::wstring_view() const -{ - return _glyphData(); -} - -// Routine Description: -// - The CharRowCell this object "references" -// Return Value: -// - ref to the CharRowCell -CharRowCell& CharRowCellReference::_cellData() -{ - return _parent._data.at(_index); -} - -// Routine Description: -// - The CharRowCell this object "references" -// Return Value: -// - ref to the CharRowCell -const CharRowCell& CharRowCellReference::_cellData() const -{ - return _parent._data.at(_index); -} - -// Routine Description: -// - the glyph data of the referenced cell -// Return Value: -// - the glyph data -std::wstring_view CharRowCellReference::_glyphData() const -{ - if (_cellData().DbcsAttr().IsGlyphStored()) - { - const auto& text = _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)); - - return { text.data(), text.size() }; - } - else - { - return { &_cellData().Char(), 1 }; - } -} - -// Routine Description: -// - gets read-only iterator to the beginning of the glyph data -// Return Value: -// - iterator of the glyph data -CharRowCellReference::const_iterator CharRowCellReference::begin() const -{ - if (_cellData().DbcsAttr().IsGlyphStored()) - { - return _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)).data(); - } - else - { - return &_cellData().Char(); - } -} - -// Routine Description: -// - get read-only iterator to the end of the glyph data -// Return Value: -// - end iterator of the glyph data -#pragma warning(push) -#pragma warning(disable : 26481) -// TODO GH 2672: eliminate using pointers raw as begin/end markers in this class -CharRowCellReference::const_iterator CharRowCellReference::end() const -{ - if (_cellData().DbcsAttr().IsGlyphStored()) - { - const auto& chars = _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)); - return chars.data() + chars.size(); - } - else - { - return &_cellData().Char() + 1; - } -} -#pragma warning(pop) - -bool operator==(const CharRowCellReference& ref, const std::vector& glyph) -{ - const auto& dbcsAttr = ref._cellData().DbcsAttr(); - if (glyph.size() == 1 && dbcsAttr.IsGlyphStored()) - { - return false; - } - else if (glyph.size() > 1 && !dbcsAttr.IsGlyphStored()) - { - return false; - } - else if (glyph.size() == 1 && !dbcsAttr.IsGlyphStored()) - { - return ref._cellData().Char() == glyph.front(); - } - else - { - const auto& chars = ref._parent.GetUnicodeStorage().GetText(ref._parent.GetStorageKey(ref._index)); - return chars == glyph; - } -} - -bool operator==(const std::vector& glyph, const CharRowCellReference& ref) -{ - return ref == glyph; -} diff --git a/src/buffer/out/CharRowCellReference.hpp b/src/buffer/out/CharRowCellReference.hpp deleted file mode 100644 index ecbae5809e..0000000000 --- a/src/buffer/out/CharRowCellReference.hpp +++ /dev/null @@ -1,63 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CharRowCellReference.hpp - -Abstract: -- reference class for the glyph data of a char row cell - -Author(s): -- Austin Diviness (AustDi) 02-May-2018 ---*/ - -#pragma once - -#include "DbcsAttribute.hpp" -#include "CharRowCell.hpp" -#include - -class CharRow; - -class CharRowCellReference final -{ -public: - using const_iterator = const wchar_t*; - - CharRowCellReference(CharRow& parent, const til::CoordType index) noexcept : - _parent{ parent }, - _index{ index } - { - } - - ~CharRowCellReference() = default; - CharRowCellReference(const CharRowCellReference&) noexcept = default; - CharRowCellReference(CharRowCellReference&&) noexcept = default; - - void operator=(const CharRowCellReference&) = delete; - void operator=(CharRowCellReference&&) = delete; - - void operator=(const std::wstring_view chars); - operator std::wstring_view() const; - - const_iterator begin() const; - const_iterator end() const; - - friend bool operator==(const CharRowCellReference& ref, const std::vector& glyph); - friend bool operator==(const std::vector& glyph, const CharRowCellReference& ref); - -private: - // what char row the object belongs to - CharRow& _parent; - // the index of the cell in the parent char row - til::CoordType _index; - - CharRowCell& _cellData(); - const CharRowCell& _cellData() const; - - std::wstring_view _glyphData() const; -}; - -bool operator==(const CharRowCellReference& ref, const std::vector& glyph); -bool operator==(const std::vector& glyph, const CharRowCellReference& ref); diff --git a/src/buffer/out/DbcsAttribute.hpp b/src/buffer/out/DbcsAttribute.hpp index 442505052c..4e5ca88be5 100644 --- a/src/buffer/out/DbcsAttribute.hpp +++ b/src/buffer/out/DbcsAttribute.hpp @@ -16,129 +16,22 @@ Revision History: #pragma once -class DbcsAttribute final +enum class DbcsAttribute : uint8_t { -public: - enum class Attribute : BYTE - { - Single = 0x00, - Leading = 0x01, - Trailing = 0x02 - }; - - DbcsAttribute() noexcept : - _attribute{ Attribute::Single }, - _glyphStored{ false } - { - } - - DbcsAttribute(const Attribute attribute) noexcept : - _attribute{ attribute }, - _glyphStored{ false } - { - } - - constexpr bool IsSingle() const noexcept - { - return _attribute == Attribute::Single; - } - - constexpr bool IsLeading() const noexcept - { - return _attribute == Attribute::Leading; - } - - constexpr bool IsTrailing() const noexcept - { - return _attribute == Attribute::Trailing; - } - - constexpr bool IsDbcs() const noexcept - { - return IsLeading() || IsTrailing(); - } - - constexpr bool IsGlyphStored() const noexcept - { - return _glyphStored; - } - - void SetGlyphStored(const bool stored) noexcept - { - _glyphStored = stored; - } - - void SetSingle() noexcept - { - _attribute = Attribute::Single; - } - - void SetLeading() noexcept - { - _attribute = Attribute::Leading; - } - - void SetTrailing() noexcept - { - _attribute = Attribute::Trailing; - } - - void Reset() noexcept - { - SetSingle(); - SetGlyphStored(false); - } - - WORD GeneratePublicApiAttributeFormat() const noexcept - { - WORD publicAttribute = 0; - if (IsLeading()) - { - WI_SetFlag(publicAttribute, COMMON_LVB_LEADING_BYTE); - } - if (IsTrailing()) - { - WI_SetFlag(publicAttribute, COMMON_LVB_TRAILING_BYTE); - } - return publicAttribute; - } - - static DbcsAttribute FromPublicApiAttributeFormat(WORD publicAttribute) - { - // it's not valid to be both a leading and trailing byte - if (WI_AreAllFlagsSet(publicAttribute, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE)) - { - THROW_HR(E_INVALIDARG); - } - - DbcsAttribute attr; - if (WI_IsFlagSet(publicAttribute, COMMON_LVB_LEADING_BYTE)) - { - attr.SetLeading(); - } - else if (WI_IsFlagSet(publicAttribute, COMMON_LVB_TRAILING_BYTE)) - { - attr.SetTrailing(); - } - return attr; - } - - friend constexpr bool operator==(const DbcsAttribute& a, const DbcsAttribute& b) noexcept; - -private: - Attribute _attribute : 2; - bool _glyphStored : 1; - -#ifdef UNIT_TESTING - friend class TextBufferTests; -#endif + Single, + Leading, + Trailing, }; -constexpr bool operator==(const DbcsAttribute& a, const DbcsAttribute& b) noexcept +constexpr WORD GeneratePublicApiAttributeFormat(DbcsAttribute attribute) noexcept { - return a._attribute == b._attribute; + switch (attribute) + { + case DbcsAttribute::Leading: + return COMMON_LVB_LEADING_BYTE; + case DbcsAttribute::Trailing: + return COMMON_LVB_TRAILING_BYTE; + default: + return 0; + } } - -static_assert(sizeof(DbcsAttribute) == sizeof(BYTE), "DbcsAttribute should be one byte big. if this changes then it needs " - "either an implicit conversion to a BYTE or an update to all places " - "that assume it's a byte big"); diff --git a/src/buffer/out/LineRendition.hpp b/src/buffer/out/LineRendition.hpp index 7be4a7b59b..954bd38509 100644 --- a/src/buffer/out/LineRendition.hpp +++ b/src/buffer/out/LineRendition.hpp @@ -13,7 +13,7 @@ Abstract: #pragma once -enum class LineRendition +enum class LineRendition : uint8_t { SingleWidth, DoubleWidth, @@ -25,19 +25,19 @@ constexpr til::inclusive_rect ScreenToBufferLine(const til::inclusive_rect& line { // Use shift right to quickly divide the Left and Right by 2 for double width lines. const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1; - return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom }; + return { line.left >> scale, line.top, line.right >> scale, line.bottom }; } constexpr til::point ScreenToBufferLine(const til::point& line, const LineRendition lineRendition) { // Use shift right to quickly divide the Left and Right by 2 for double width lines. const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1; - return { line.X >> scale, line.Y }; + return { line.x >> scale, line.y }; } constexpr til::inclusive_rect BufferToScreenLine(const til::inclusive_rect& line, const LineRendition lineRendition) { // Use shift left to quickly multiply the Left and Right by 2 for double width lines. const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1; - return { line.Left << scale, line.Top, (line.Right << scale) + scale, line.Bottom }; + return { line.left << scale, line.top, (line.right << scale) + scale, line.bottom }; } diff --git a/src/buffer/out/OutputCell.hpp b/src/buffer/out/OutputCell.hpp index da9482e5d9..5dd5126705 100644 --- a/src/buffer/out/OutputCell.hpp +++ b/src/buffer/out/OutputCell.hpp @@ -80,7 +80,7 @@ private: // basic_string contains a small storage internally so we don't need // to worry about heap allocation for short strings. std::wstring _text; - DbcsAttribute _dbcsAttribute; + DbcsAttribute _dbcsAttribute = DbcsAttribute::Single; TextAttribute _textAttribute; TextAttributeBehavior _behavior; diff --git a/src/buffer/out/OutputCellIterator.cpp b/src/buffer/out/OutputCellIterator.cpp index 1b585cd28c..a623350f03 100644 --- a/src/buffer/out/OutputCellIterator.cpp +++ b/src/buffer/out/OutputCellIterator.cpp @@ -5,12 +5,13 @@ #include "OutputCellIterator.hpp" +#include + #include "../../types/inc/convert.hpp" -#include "../../types/inc/Utf16Parser.hpp" #include "../../types/inc/GlyphWidth.hpp" #include "../../inc/conattrs.hpp" -static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; +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. @@ -81,7 +82,7 @@ OutputCellIterator::OutputCellIterator(const CHAR_INFO& charInfo, const size_t f // - This is an iterator over a range of text only. No color data will be modified as the text is inserted. // Arguments: // - utf16Text - UTF-16 text range -OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text) : +OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text) noexcept : _mode(Mode::LooseTextOnly), _currentView(s_GenerateView(utf16Text)), _run(utf16Text), @@ -97,7 +98,7 @@ OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text) : // Arguments: // - utf16Text - UTF-16 text range // - attribute - Color to apply over the entire range -OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute& attribute, const size_t fillLimit) : +OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute& attribute, const size_t fillLimit) noexcept : _mode(Mode::Loose), _currentView(s_GenerateView(utf16Text, attribute)), _run(utf16Text), @@ -112,7 +113,7 @@ OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text, const // - This is an iterator over legacy colors only. The text is not modified. // Arguments: // - legacyAttrs - One legacy color item per cell -OutputCellIterator::OutputCellIterator(const gsl::span legacyAttrs) noexcept : +OutputCellIterator::OutputCellIterator(const std::span legacyAttrs) noexcept : _mode(Mode::LegacyAttr), _currentView(s_GenerateViewLegacyAttr(til::at(legacyAttrs, 0))), _run(legacyAttrs), @@ -127,7 +128,7 @@ OutputCellIterator::OutputCellIterator(const gsl::span legacyAttrs) // - This is an iterator over legacy cell data. We will use the unicode text and the legacy color attribute. // Arguments: // - charInfos - Multiple cell with unicode text and legacy color data. -OutputCellIterator::OutputCellIterator(const gsl::span charInfos) noexcept : +OutputCellIterator::OutputCellIterator(const std::span charInfos) noexcept : _mode(Mode::CharInfo), _currentView(s_GenerateView(til::at(charInfos, 0))), _run(charInfos), @@ -142,7 +143,7 @@ OutputCellIterator::OutputCellIterator(const gsl::span charInfo // - This is an iterator over existing OutputCells with full text and color data. // Arguments: // - cells - Multiple cells in a run -OutputCellIterator::OutputCellIterator(const gsl::span cells) : +OutputCellIterator::OutputCellIterator(const std::span cells) : _mode(Mode::Cell), _currentView(s_GenerateView(til::at(cells, 0))), _run(cells), @@ -180,15 +181,15 @@ OutputCellIterator::operator bool() const noexcept } case Mode::Cell: { - return _pos < std::get>(_run).size(); + return _pos < std::get>(_run).size(); } case Mode::CharInfo: { - return _pos < std::get>(_run).size(); + return _pos < std::get>(_run).size(); } case Mode::LegacyAttr: { - return _pos < std::get>(_run).size(); + return _pos < std::get>(_run).size(); } default: FAIL_FAST_HR(E_NOTIMPL); @@ -197,6 +198,11 @@ OutputCellIterator::operator bool() const noexcept CATCH_FAIL_FAST(); } +size_t OutputCellIterator::Position() const noexcept +{ + return _pos; +} + // Routine Description: // - Advances the iterator one position over the underlying data source. // Return Value: @@ -240,13 +246,10 @@ OutputCellIterator& OutputCellIterator::operator++() { if (!_TryMoveTrailing()) { - if (_currentView.DbcsAttr().IsTrailing()) + if (_currentView.DbcsAttr() == DbcsAttribute::Trailing) { - auto dbcsAttr = _currentView.DbcsAttr(); - dbcsAttr.SetLeading(); - _currentView = OutputCellView(_currentView.Chars(), - dbcsAttr, + DbcsAttribute::Leading, _currentView.TextAttr(), _currentView.TextAttrBehavior()); } @@ -265,7 +268,7 @@ OutputCellIterator& OutputCellIterator::operator++() _pos++; if (operator bool()) { - _currentView = s_GenerateView(til::at(std::get>(_run), _pos)); + _currentView = s_GenerateView(til::at(std::get>(_run), _pos)); } break; } @@ -275,7 +278,7 @@ OutputCellIterator& OutputCellIterator::operator++() _pos++; if (operator bool()) { - _currentView = s_GenerateView(til::at(std::get>(_run), _pos)); + _currentView = s_GenerateView(til::at(std::get>(_run), _pos)); } break; } @@ -285,7 +288,7 @@ OutputCellIterator& OutputCellIterator::operator++() _pos++; if (operator bool()) { - _currentView = s_GenerateViewLegacyAttr(til::at(std::get>(_run), _pos)); + _currentView = s_GenerateViewLegacyAttr(til::at(std::get>(_run), _pos)); } break; } @@ -336,13 +339,10 @@ const OutputCellView* OutputCellIterator::operator->() const noexcept // - False if this wasn't applicable and the caller should update the view. bool OutputCellIterator::_TryMoveTrailing() noexcept { - if (_currentView.DbcsAttr().IsLeading()) + if (_currentView.DbcsAttr() == DbcsAttribute::Leading) { - auto dbcsAttr = _currentView.DbcsAttr(); - dbcsAttr.SetTrailing(); - _currentView = OutputCellView(_currentView.Chars(), - dbcsAttr, + DbcsAttribute::Trailing, _currentView.TextAttr(), _currentView.TextAttrBehavior()); return true; @@ -362,7 +362,7 @@ bool OutputCellIterator::_TryMoveTrailing() noexcept // - view - View representing characters corresponding to a single glyph // Return Value: // - Object representing the view into this cell -OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view) +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view) noexcept { return s_GenerateView(view, InvalidTextAttribute, TextAttributeBehavior::Current); } @@ -377,8 +377,7 @@ OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view) // - attr - Color attributes to apply to the text // Return Value: // - Object representing the view into this cell -OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, - const TextAttribute attr) +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, const TextAttribute attr) noexcept { return s_GenerateView(view, attr, TextAttributeBehavior::Stored); } @@ -394,17 +393,10 @@ OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, // - behavior - Behavior of the given text attribute (used when writing) // Return Value: // - Object representing the view into this cell -OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, - const TextAttribute attr, - const TextAttributeBehavior behavior) +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, const TextAttribute attr, const TextAttributeBehavior behavior) noexcept { - const auto glyph = Utf16Parser::ParseNext(view); - DbcsAttribute dbcsAttr; - if (IsGlyphFullWidth(glyph)) - { - dbcsAttr.SetLeading(); - } - + const auto glyph = til::utf16_next(view); + const auto dbcsAttr = IsGlyphFullWidth(glyph) ? DbcsAttribute::Leading : DbcsAttribute::Single; return OutputCellView(glyph, dbcsAttr, attr, behavior); } @@ -420,13 +412,7 @@ OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, OutputCellView OutputCellIterator::s_GenerateView(const wchar_t& wch) noexcept { const auto glyph = std::wstring_view(&wch, 1); - - DbcsAttribute dbcsAttr; - if (IsGlyphFullWidth(wch)) - { - dbcsAttr.SetLeading(); - } - + const auto dbcsAttr = IsGlyphFullWidth(wch) ? DbcsAttribute::Leading : DbcsAttribute::Single; return OutputCellView(glyph, dbcsAttr, InvalidTextAttribute, TextAttributeBehavior::Current); } @@ -457,13 +443,7 @@ OutputCellView OutputCellIterator::s_GenerateView(const TextAttribute& attr) noe OutputCellView OutputCellIterator::s_GenerateView(const wchar_t& wch, const TextAttribute& attr) noexcept { const auto glyph = std::wstring_view(&wch, 1); - - DbcsAttribute dbcsAttr; - if (IsGlyphFullWidth(wch)) - { - dbcsAttr.SetLeading(); - } - + const auto dbcsAttr = IsGlyphFullWidth(wch) ? DbcsAttribute::Leading : DbcsAttribute::Single; return OutputCellView(glyph, dbcsAttr, attr, TextAttributeBehavior::Stored); } @@ -498,14 +478,14 @@ OutputCellView OutputCellIterator::s_GenerateView(const CHAR_INFO& charInfo) noe { const auto glyph = std::wstring_view(&charInfo.Char.UnicodeChar, 1); - DbcsAttribute dbcsAttr; + DbcsAttribute dbcsAttr = DbcsAttribute::Single; if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_LEADING_BYTE)) { - dbcsAttr.SetLeading(); + dbcsAttr = DbcsAttribute::Leading; } else if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_TRAILING_BYTE)) { - dbcsAttr.SetTrailing(); + dbcsAttr = DbcsAttribute::Trailing; } const TextAttribute textAttr(charInfo.Attributes); diff --git a/src/buffer/out/OutputCellIterator.hpp b/src/buffer/out/OutputCellIterator.hpp index 89447a5102..0199b7c4aa 100644 --- a/src/buffer/out/OutputCellIterator.hpp +++ b/src/buffer/out/OutputCellIterator.hpp @@ -37,17 +37,18 @@ public: OutputCellIterator(const TextAttribute& attr, const size_t fillLimit = 0) noexcept; OutputCellIterator(const wchar_t& wch, const TextAttribute& attr, const size_t fillLimit = 0) noexcept; OutputCellIterator(const CHAR_INFO& charInfo, const size_t fillLimit = 0) noexcept; - OutputCellIterator(const std::wstring_view utf16Text); - OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute& attribute, const size_t fillLimit = 0); - OutputCellIterator(const gsl::span legacyAttributes) noexcept; - OutputCellIterator(const gsl::span charInfos) noexcept; - OutputCellIterator(const gsl::span cells); + OutputCellIterator(const std::wstring_view utf16Text) noexcept; + OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute& attribute, const size_t fillLimit = 0) noexcept; + OutputCellIterator(const std::span legacyAttributes) noexcept; + OutputCellIterator(const std::span charInfos) noexcept; + OutputCellIterator(const std::span cells); ~OutputCellIterator() = default; OutputCellIterator& operator=(const OutputCellIterator& it) = default; operator bool() const noexcept; + size_t Position() const noexcept; til::CoordType GetCellDistance(OutputCellIterator other) const noexcept; til::CoordType GetInputDistance(OutputCellIterator other) const noexcept; friend til::CoordType operator-(OutputCellIterator one, OutputCellIterator two) = delete; @@ -86,13 +87,13 @@ private: }; Mode _mode; - gsl::span _legacyAttrs; + std::span _legacyAttrs; std::variant< std::wstring_view, - gsl::span, - gsl::span, - gsl::span, + std::span, + std::span, + std::span, std::monostate> _run; @@ -100,15 +101,9 @@ private: bool _TryMoveTrailing() noexcept; - static OutputCellView s_GenerateView(const std::wstring_view view); - - static OutputCellView s_GenerateView(const std::wstring_view view, - const TextAttribute attr); - - static OutputCellView s_GenerateView(const std::wstring_view view, - const TextAttribute attr, - const TextAttributeBehavior behavior); - + static OutputCellView s_GenerateView(const std::wstring_view view) noexcept; + static OutputCellView s_GenerateView(const std::wstring_view view, const TextAttribute attr) noexcept; + static OutputCellView s_GenerateView(const std::wstring_view view, const TextAttribute attr, const TextAttributeBehavior behavior) noexcept; static OutputCellView s_GenerateView(const wchar_t& wch) noexcept; static OutputCellView s_GenerateViewLegacyAttr(const WORD& legacyAttr) noexcept; static OutputCellView s_GenerateView(const TextAttribute& attr) noexcept; diff --git a/src/buffer/out/OutputCellRect.cpp b/src/buffer/out/OutputCellRect.cpp index 6b789298d3..18ea859568 100644 --- a/src/buffer/out/OutputCellRect.cpp +++ b/src/buffer/out/OutputCellRect.cpp @@ -34,9 +34,9 @@ OutputCellRect::OutputCellRect(const til::CoordType rows, const til::CoordType c // - row - The Y position or row index in the buffer. // Return Value: // - Read/write span of OutputCells -gsl::span OutputCellRect::GetRow(const til::CoordType row) +std::span OutputCellRect::GetRow(const til::CoordType row) { - return gsl::span(_FindRowOffset(row), _cols); + return std::span(_FindRowOffset(row), _cols); } // Routine Description: @@ -47,7 +47,7 @@ gsl::span OutputCellRect::GetRow(const til::CoordType row) // - Read-only iterator of OutputCells OutputCellIterator OutputCellRect::GetRowIter(const til::CoordType row) const { - const gsl::span view(_FindRowOffset(row), _cols); + const std::span view(_FindRowOffset(row), _cols); return OutputCellIterator(view); } diff --git a/src/buffer/out/OutputCellRect.hpp b/src/buffer/out/OutputCellRect.hpp index 4e5d38cf81..fd3fc08012 100644 --- a/src/buffer/out/OutputCellRect.hpp +++ b/src/buffer/out/OutputCellRect.hpp @@ -32,7 +32,7 @@ public: OutputCellRect() noexcept; OutputCellRect(const til::CoordType rows, const til::CoordType cols); - gsl::span GetRow(const til::CoordType row); + std::span GetRow(const til::CoordType row); OutputCellIterator GetRowIter(const til::CoordType row) const; til::CoordType Height() const noexcept; diff --git a/src/buffer/out/OutputCellView.cpp b/src/buffer/out/OutputCellView.cpp index 5b454fe396..0b47a23bf2 100644 --- a/src/buffer/out/OutputCellView.cpp +++ b/src/buffer/out/OutputCellView.cpp @@ -40,20 +40,7 @@ OutputCellView::OutputCellView(const std::wstring_view view, // - Count of column cells on the screen til::CoordType OutputCellView::Columns() const noexcept { - if (DbcsAttr().IsSingle()) - { - return 1; - } - else if (DbcsAttr().IsLeading()) - { - return 2; - } - else if (DbcsAttr().IsTrailing()) - { - return 1; - } - - return 1; + return DbcsAttr() == DbcsAttribute::Leading ? 2 : 1; } // Routine Description: diff --git a/src/buffer/out/OutputCellView.hpp b/src/buffer/out/OutputCellView.hpp index 65d0ffac07..e32d866bbb 100644 --- a/src/buffer/out/OutputCellView.hpp +++ b/src/buffer/out/OutputCellView.hpp @@ -56,7 +56,7 @@ public: private: std::wstring_view _view; - DbcsAttribute _dbcsAttr; + DbcsAttribute _dbcsAttr = DbcsAttribute::Single; TextAttribute _textAttr; TextAttributeBehavior _behavior; }; diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 362dffe912..4036b991d8 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -3,29 +3,227 @@ #include "precomp.h" #include "Row.hpp" -#include "CharRow.hpp" + +#include +#include + #include "textBuffer.hpp" -#include "../types/inc/convert.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) +{ + for (; count; --count, ++dest, ++val) + { + *dest = val; + } + return dest; +} + +// ROW::ReplaceCharacters needs to calculate `val + count` after +// calling iota_n() and this function achieves both things at once. +template +constexpr OutIt iota_n_mut(OutIt dest, Diff count, T& val) +{ + for (; count; --count, ++dest, ++val) + { + *dest = val; + } + return dest; +} + +// Same as std::fill, but purpose-built for very small `last - first` +// where a trivial loop outperforms vectorization. +template +constexpr FwdIt fill_small(FwdIt first, FwdIt last, const T val) +{ + for (; first != last; ++first) + { + *first = val; + } + return first; +} + +// Same as std::fill_n, but purpose-built for very small `count` +// where a trivial loop outperforms vectorization. +template +constexpr OutIt fill_n_small(OutIt dest, Diff count, const T val) +{ + for (; count; --count, ++dest) + { + *dest = val; + } + return dest; +} + +// Same as std::copy_n, but purpose-built for very short `count` +// where a trivial loop outperforms vectorization. +template +constexpr OutIt copy_n_small(InIt first, Diff count, OutIt dest) +{ + for (; count; --count, ++dest, ++first) + { + *dest = *first; + } + 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; +} + +// If given a pointer inside the ROW's text buffer, this function will return the corresponding column. +// This function in particular returns the glyph's first column. +til::CoordType CharToColumnMapper::GetLeadingColumnAt(const wchar_t* str) noexcept +{ + return GetLeadingColumnAt(str - _chars); +} + +// If given a pointer inside the ROW's text buffer, this function will return the corresponding column. +// This function in particular returns the glyph's last column (this matters for wide glyphs). +til::CoordType CharToColumnMapper::GetTrailingColumnAt(const wchar_t* str) noexcept +{ + return GetTrailingColumnAt(str - _chars); +} // Routine Description: // - constructor // Arguments: -// - rowId - the row index in the text buffer // - rowWidth - the width of the row, cell elements // - fillAttribute - the default text attribute -// - pParent - the text buffer that this row belongs to // Return Value: // - constructed object -ROW::ROW(const til::CoordType rowId, const til::CoordType rowWidth, const TextAttribute fillAttribute, TextBuffer* const pParent) : - _id{ rowId }, - _rowWidth{ rowWidth }, - _charRow{ rowWidth, this }, - _attrRow{ rowWidth, fillAttribute }, - _lineRendition{ LineRendition::SingleWidth }, - _wrapForced{ false }, - _doubleBytePadded{ false }, - _pParent{ pParent } +ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute) : + _charsBuffer{ charsBuffer }, + _chars{ charsBuffer, rowWidth }, + _charOffsets{ charOffsetsBuffer, ::base::strict_cast(rowWidth) + 1u }, + _attr{ rowWidth, fillAttribute }, + _columnCount{ rowWidth } { + _init(); +} + +void ROW::SetWrapForced(const bool wrap) noexcept +{ + _wrapForced = wrap; +} + +bool ROW::WasWrapForced() const noexcept +{ + return _wrapForced; +} + +void ROW::SetDoubleBytePadded(const bool doubleBytePadded) noexcept +{ + _doubleBytePadded = doubleBytePadded; +} + +bool ROW::WasDoubleBytePadded() const noexcept +{ + return _doubleBytePadded; +} + +void ROW::SetLineRendition(const LineRendition lineRendition) noexcept +{ + _lineRendition = lineRendition; +} + +LineRendition ROW::GetLineRendition() const noexcept +{ + return _lineRendition; +} + +// 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 +{ + if (_lineRendition == LineRendition::SingleWidth) [[likely]] + { + return _columnCount - _doubleBytePadded; + } + return (_columnCount - (_doubleBytePadded << 1)) >> 1; } // Routine Description: @@ -34,42 +232,176 @@ ROW::ROW(const til::CoordType rowId, const til::CoordType rowWidth, const TextAt // - Attr - The default attribute (color) to fill // Return Value: // - -bool ROW::Reset(const TextAttribute Attr) +void ROW::Reset(const TextAttribute& attr) noexcept { + _charsHeap.reset(); + _chars = { _charsBuffer, _columnCount }; + // Constructing and then moving objects into place isn't free. + // Modifying the existing object is _much_ faster. + *_attr.runs().unsafe_shrink_to_size(1) = til::rle_pair{ attr, _columnCount }; _lineRendition = LineRendition::SingleWidth; _wrapForced = false; _doubleBytePadded = false; - _charRow.Reset(); - try - { - _attrRow.Reset(Attr); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - return false; - } - return true; + _init(); } -// Routine Description: -// - resizes ROW to new width -// Arguments: -// - width - the new width, in cells -// Return Value: -// - S_OK if successful, otherwise relevant error -[[nodiscard]] HRESULT ROW::Resize(const til::CoordType width) +void ROW::_init() noexcept { - RETURN_IF_FAILED(_charRow.Resize(width)); - try +#pragma warning(push) +#pragma warning(disable : 26462) // The value pointed to by '...' is assigned only once, mark it as a pointer to const (con.4). +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + + // Fills _charsBuffer with whitespace and correspondingly _charOffsets + // with successive numbers from 0 to _columnCount+1. +#if defined(TIL_SSE_INTRINSICS) + alignas(__m256i) static constexpr uint16_t whitespaceData[]{ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }; + alignas(__m256i) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + alignas(__m256i) static constexpr uint16_t increment16Data[]{ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; + alignas(__m128i) static constexpr uint16_t increment8Data[]{ 8, 8, 8, 8, 8, 8, 8, 8 }; + + // The AVX loop operates on 32 bytes at a minimum. Since _charsBuffer/_charOffsets uses 2 byte large + // wchar_t/uint16_t respectively, this translates to 16-element writes, which equals a _columnCount of 15, + // because it doesn't include the past-the-end char-offset as described in the _charOffsets member comment. + if (__isa_available >= __ISA_AVAILABLE_AVX2 && _columnCount >= 15) { - _attrRow.Resize(width); + auto chars = _charsBuffer; + auto charOffsets = _charOffsets.data(); + + // The backing buffer for both chars and charOffsets is guaranteed to be 16-byte aligned, + // but AVX operations are 32-byte large. As such, when we write out the last chunk, we + // have to align it to the ends of the 2 buffers. This results in a potential overlap of + // 16 bytes between the last write in the main loop below and the final write afterwards. + // + // An example: + // If you have a terminal between 16 and 23 columns the buffer has a size of 48 bytes. + // The main loop below will iterate once, as it writes out bytes 0-31 and then exits. + // The final write afterwards cannot write bytes 32-63 because that would write + // out of bounds. Instead it writes bytes 16-47, overwriting 16 overlapping bytes. + // This is better than branching and switching to SSE2, because both things are slow. + // + // Since we want to exit the main loop with at least 1 write left to do as the final write, + // we need to subtract 1 alignment from the buffer length (= 16 bytes). Since _columnCount is + // in wchar_t's we subtract -8. The same applies to the ~7 here vs ~15. If you squint slightly + // you'll see how this is effectively the inverse of what CalculateCharsBufferStride does. + const auto tailColumnOffset = gsl::narrow_cast((_columnCount - 8u) & ~7); + const auto charsEndLoop = chars + tailColumnOffset; + const auto charOffsetsEndLoop = charOffsets + tailColumnOffset; + + const auto whitespace = _mm256_load_si256(reinterpret_cast(&whitespaceData[0])); + auto offsetsLoop = _mm256_load_si256(reinterpret_cast(&offsetsData[0])); + const auto offsets = _mm256_add_epi16(offsetsLoop, _mm256_set1_epi16(tailColumnOffset)); + + if (chars < charsEndLoop) + { + const auto increment = _mm256_load_si256(reinterpret_cast(&increment16Data[0])); + + do + { + _mm256_storeu_si256(reinterpret_cast<__m256i*>(chars), whitespace); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsets), offsetsLoop); + offsetsLoop = _mm256_add_epi16(offsetsLoop, increment); + chars += 16; + charOffsets += 16; + } while (chars < charsEndLoop); + } + + _mm256_storeu_si256(reinterpret_cast<__m256i*>(charsEndLoop), whitespace); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsetsEndLoop), offsets); } - CATCH_RETURN(); + else + { + auto chars = _charsBuffer; + auto charOffsets = _charOffsets.data(); + const auto charsEnd = chars + _columnCount; - _rowWidth = width; + const auto whitespace = _mm_load_si128(reinterpret_cast(&whitespaceData[0])); + const auto increment = _mm_load_si128(reinterpret_cast(&increment8Data[0])); + auto offsets = _mm_load_si128(reinterpret_cast(&offsetsData[0])); - return S_OK; + do + { + _mm_storeu_si128(reinterpret_cast<__m128i*>(chars), whitespace); + _mm_storeu_si128(reinterpret_cast<__m128i*>(charOffsets), offsets); + offsets = _mm_add_epi16(offsets, increment); + chars += 8; + charOffsets += 8; + // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large. + // --> The while loop uses <= to emit at least 1 more write. + } while (chars <= charsEnd); + } +#elif defined(TIL_ARM_NEON_INTRINSICS) + alignas(uint16x8_t) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7 }; + + auto chars = _charsBuffer; + auto charOffsets = _charOffsets.data(); + const auto charsEnd = chars + _columnCount; + + const auto whitespace = vdupq_n_u16(L' '); + const auto increment = vdupq_n_u16(8); + auto offsets = vld1q_u16(&offsetsData[0]); + + do + { + vst1q_u16(chars, whitespace); + vst1q_u16(charOffsets, offsets); + offsets = vaddq_u16(offsets, increment); + chars += 8; + charOffsets += 8; + // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large. + // --> The while loop uses <= to emit at least 1 more write. + } while (chars <= charsEnd); +#else +#error "Vectorizing this function improves overall performance by up to 40%. Don't remove this warning, just add the vectorized code." + std::fill_n(_charsBuffer, _columnCount, UNICODE_SPACE); + std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 }); +#endif + +#pragma warning(push) +} + +void ROW::TransferAttributes(const til::small_rle& attr, til::CoordType newWidth) +{ + _attr = attr; + _attr.resize_trailing_extent(gsl::narrow(newWidth)); +} + +void ROW::CopyFrom(const ROW& source) +{ + _lineRendition = source._lineRendition; + _wrapForced = source._wrapForced; + + RowCopyTextFromState state{ + .source = source, + .sourceColumnLimit = source.GetReadableColumnCount(), + }; + CopyTextFrom(state); + + TransferAttributes(source.Attributes(), _columnCount); +} + +// Returns the previous possible cursor position, preceding the given column. +// Returns 0 if column is less than or equal to 0. +til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept +{ + return _adjustBackward(_clampedColumn(column - 1)); +} + +// Returns the next possible cursor position, following the given column. +// Returns the row width if column is beyond the width of the row. +til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept +{ + return _adjustForward(_clampedColumnInclusive(column + 1)); +} + +// Returns the starting column of the glyph at the given column. +// In other words, if you have 3 wide glyphs +// AA BB CC +// 01 23 45 <-- column +// then `AdjustToGlyphStart(3)` returns 2. +til::CoordType ROW::AdjustToGlyphStart(til::CoordType column) const noexcept +{ + return _adjustBackward(_clampedColumn(column)); } // Routine Description: @@ -78,20 +410,10 @@ bool ROW::Reset(const TextAttribute Attr) // - column - 0-indexed column index // Return Value: // - -void ROW::ClearColumn(const til::CoordType column) +void ROW::ClearCell(const til::CoordType column) { - THROW_HR_IF(E_INVALIDARG, column >= _charRow.size()); - _charRow.ClearCell(column); -} - -UnicodeStorage& ROW::GetUnicodeStorage() noexcept -{ - return _pParent->GetUnicodeStorage(); -} - -const UnicodeStorage& ROW::GetUnicodeStorage() const noexcept -{ - return _pParent->GetUnicodeStorage(); + static constexpr std::wstring_view space{ L" " }; + ReplaceCharacters(column, 1, space); } // Routine Description: @@ -103,17 +425,17 @@ const UnicodeStorage& ROW::GetUnicodeStorage() const noexcept // - limitRight - right inclusive column ID for the last write in this row. (optional, will just write to the end of row if nullopt) // Return Value: // - iterator to first cell that was not written to this row. -OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType index, const std::optional wrap, std::optional limitRight) +OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType columnBegin, const std::optional wrap, std::optional limitRight) { - THROW_HR_IF(E_INVALIDARG, index >= _charRow.size()); - THROW_HR_IF(E_INVALIDARG, limitRight.value_or(0) >= _charRow.size()); + THROW_HR_IF(E_INVALIDARG, columnBegin >= size()); + THROW_HR_IF(E_INVALIDARG, limitRight.value_or(0) >= size()); // If we're given a right-side column limit, use it. Otherwise, the write limit is the final column index available in the char row. - const auto finalColumnInRow = limitRight.value_or(_charRow.size() - 1); + const auto finalColumnInRow = limitRight.value_or(size() - 1); auto currentColor = it->TextAttr(); uint16_t colorUses = 0; - auto colorStarts = gsl::narrow_cast(index); + auto colorStarts = gsl::narrow_cast(columnBegin); auto currentIndex = colorStarts; while (it && currentIndex <= finalColumnInRow) @@ -131,7 +453,7 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType i { // Otherwise, commit this color into the run and save off the new one. // Now commit the new color runs into the attr row. - _attrRow.Replace(colorStarts, currentIndex, currentColor); + _attr.replace(colorStarts, currentIndex, currentColor); currentColor = it->TextAttr(); colorUses = 1; colorStarts = currentIndex; @@ -141,31 +463,51 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType i // Fill the text if the behavior isn't set to saying there's only a color stored in this iterator. if (it->TextAttrBehavior() != TextAttributeBehavior::StoredOnly) { + const auto fillingFirstColumn = currentIndex == 0; const auto fillingLastColumn = currentIndex == finalColumnInRow; + const auto attr = it->DbcsAttr(); + const auto& chars = it->Chars(); - // TODO: MSFT: 19452170 - We need to ensure when writing any trailing byte that the one to the left - // is a matching leading byte. Likewise, if we're writing a leading byte, we need to make sure we still have space in this loop - // for the trailing byte coming up before writing it. - - // If we're trying to fill the first cell with a trailing byte, pad it out instead by clearing it. - // Don't increment iterator. We'll advance the index and try again with this value on the next round through the loop. - if (currentIndex == 0 && it->DbcsAttr().IsTrailing()) + switch (attr) { - _charRow.ClearCell(currentIndex); - } - // If we're trying to fill the last cell with a leading byte, pad it out instead by clearing it. - // Don't increment iterator. We'll exit because we couldn't write a lead at the end of a line. - else if (fillingLastColumn && it->DbcsAttr().IsLeading()) - { - _charRow.ClearCell(currentIndex); - SetDoubleBytePadded(true); - } - // Otherwise, copy the data given and increment the iterator. - else - { - _charRow.DbcsAttrAt(currentIndex) = it->DbcsAttr(); - _charRow.GlyphAt(currentIndex) = it->Chars(); + case DbcsAttribute::Leading: + if (fillingLastColumn) + { + // The wide char doesn't fit. Pad with whitespace. + // Don't increment the iterator. Instead we'll return from this function and the + // caller can call WriteCells() again on the next row with the same iterator position. + ClearCell(currentIndex); + SetDoubleBytePadded(true); + } + else + { + ReplaceCharacters(currentIndex, 2, chars); + ++it; + } + break; + case DbcsAttribute::Trailing: + if (fillingFirstColumn) + { + // The wide char doesn't fit. Pad with whitespace. + // Ignore the character. There's no correct alternative way to handle this situation. + ClearCell(currentIndex); + } + else if (it.Position() == 0) + { + // A common way to back up and restore the buffer is via `ReadConsoleOutputW` and + // `WriteConsoleOutputW` respectively. But the area might bisect/intersect/clip wide characters and + // only backup either their leading or trailing half. In general, in the rest of conhost, we're + // throwing away the trailing half of all `CHAR_INFO`s (during text rendering, as well as during + // `ReadConsoleOutputW`), so to make this code behave the same and prevent surprises, we need to + // make sure to only look at the trailer if it's the first `CHAR_INFO` the user is trying to write. + ReplaceCharacters(currentIndex - 1, 2, chars); + } ++it; + break; + default: + ReplaceCharacters(currentIndex, 1, chars); + ++it; + break; } // If we're asked to (un)set the wrap status and we just filled the last column with some text... @@ -191,8 +533,642 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType i // Now commit the final color into the attr row if (colorUses) { - _attrRow.Replace(colorStarts, currentIndex, currentColor); + _attr.replace(colorStarts, currentIndex, currentColor); } return it; } + +void ROW::SetAttrToEnd(const til::CoordType columnBegin, const TextAttribute attr) +{ + _attr.replace(_clampedColumnInclusive(columnBegin), _attr.size(), attr); +} + +void ROW::ReplaceAttributes(const til::CoordType beginIndex, const til::CoordType endIndex, const TextAttribute& newAttr) +{ + _attr.replace(_clampedColumnInclusive(beginIndex), _clampedColumnInclusive(endIndex), newAttr); +} + +[[msvc::forceinline]] ROW::WriteHelper::WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept : + row{ row }, + chars{ chars } +{ + colBeg = row._clampedColumnInclusive(columnBegin); + colLimit = row._clampedColumnInclusive(columnLimit); + chBegDirty = row._uncheckedCharOffset(colBeg); + colBegDirty = row._adjustBackward(colBeg); + leadingSpaces = colBeg - colBegDirty; + chBeg = chBegDirty + leadingSpaces; + colEnd = colBeg; + colEndDirty = 0; + charsConsumed = 0; +} + +[[msvc::forceinline]] bool ROW::WriteHelper::IsValid() const noexcept +{ + return colBeg < colLimit && !chars.empty(); +} + +void ROW::ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars) +try +{ + WriteHelper h{ *this, columnBegin, _columnCount, chars }; + if (!h.IsValid()) + { + return; + } + h.ReplaceCharacters(width); + h.Finish(); +} +catch (...) +{ + // Due to this function writing _charOffsets first, then calling _resizeChars (which may throw) and only then finally + // filling in _chars, we might end up in a situation were _charOffsets contains offsets outside of the _chars array. + // --> Restore this row to a known "okay"-state. + Reset(TextAttribute{}); + throw; +} + +[[msvc::forceinline]] void ROW::WriteHelper::ReplaceCharacters(til::CoordType width) noexcept +{ + const auto colEndNew = gsl::narrow_cast(colEnd + width); + if (colEndNew > colLimit) + { + colEndDirty = colLimit; + } + else + { + til::at(row._charOffsets, colEnd++) = chBeg; + for (; colEnd < colEndNew; ++colEnd) + { + til::at(row._charOffsets, colEnd) = gsl::narrow_cast(chBeg | CharOffsetsTrailer); + } + + colEndDirty = colEnd; + charsConsumed = chars.size(); + } +} + +void ROW::ReplaceText(RowWriteState& state) +try +{ + WriteHelper h{ *this, state.columnBegin, state.columnLimit, state.text }; + if (!h.IsValid()) + { + state.columnEnd = h.colBeg; + state.columnBeginDirty = h.colBeg; + state.columnEndDirty = h.colBeg; + return; + } + h.ReplaceText(); + h.Finish(); + + state.text = state.text.substr(h.charsConsumed); + // Here's why we set `state.columnEnd` to `colLimit` if there's remaining text: + // Callers should be able to use `state.columnEnd` as the next cursor position, as well as the parameter for a + // follow-up call to ReplaceAttributes(). But if we fail to insert a wide glyph into the last column of a row, + // that last cell (which now contains padding whitespace) should get the same attributes as the rest of the + // string so that the row looks consistent. This requires us to return `colLimit` instead of `colLimit - 1`. + // Additionally, this has the benefit that callers can detect line wrapping by checking `columnEnd >= columnLimit`. + state.columnEnd = state.text.empty() ? h.colEnd : h.colLimit; + state.columnBeginDirty = h.colBegDirty; + state.columnEndDirty = h.colEndDirty; +} +catch (...) +{ + Reset(TextAttribute{}); + throw; +} + +[[msvc::forceinline]] void ROW::WriteHelper::ReplaceText() noexcept +{ + // This function starts with a fast-pass for ASCII. ASCII is still predominant in technical areas. + // + // We can infer the "end" from the amount of columns we're given (colLimit - colBeg), + // because ASCII is always 1 column wide per character. + auto it = chars.begin(); + const auto end = it + std::min(chars.size(), colLimit - colBeg); + size_t ch = chBeg; + + while (it != end) + { + if (*it >= 0x80) [[unlikely]] + { + _replaceTextUnicode(ch, it); + return; + } + + til::at(row._charOffsets, colEnd) = gsl::narrow_cast(ch); + ++colEnd; + ++ch; + ++it; + } + + colEndDirty = colEnd; + charsConsumed = ch - chBeg; +} + +[[msvc::forceinline]] void ROW::WriteHelper::_replaceTextUnicode(size_t ch, std::wstring_view::const_iterator it) noexcept +{ + const auto end = chars.end(); + + while (it != end) + { + unsigned int width = 1; + auto ptr = &*it; + const auto wch = *ptr; + size_t advance = 1; + + ++it; + + // Even in our slow-path we can avoid calling IsGlyphFullWidth if the current character is ASCII. + // It also allows us to skip the surrogate pair decoding at the same time. + if (wch >= 0x80) + { + if (til::is_surrogate(wch)) + { + if (it != end && til::is_leading_surrogate(wch) && til::is_trailing_surrogate(*it)) + { + advance = 2; + ++it; + } + else + { + ptr = &UNICODE_REPLACEMENT; + } + } + + width = IsGlyphFullWidth({ ptr, advance }) + 1u; + } + + const auto colEndNew = gsl::narrow_cast(colEnd + width); + if (colEndNew > colLimit) + { + colEndDirty = colLimit; + charsConsumed = ch - chBeg; + return; + } + + // Fill our char-offset buffer with 1 entry containing the mapping from the + // current column (colEnd) to the start of the glyph in the string (ch)... + til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch); + // ...followed by 0-N entries containing an indication that the + // columns are just a wide-glyph extension of the preceding one. + while (colEnd < colEndNew) + { + til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch | CharOffsetsTrailer); + } + + ch += advance; + } + + colEndDirty = colEnd; + charsConsumed = ch - chBeg; +} + +void ROW::CopyTextFrom(RowCopyTextFromState& state) +try +{ + auto& source = state.source; + const auto sourceColBeg = source._clampedColumnInclusive(state.sourceColumnBegin); + const auto sourceColLimit = source._clampedColumnInclusive(state.sourceColumnLimit); + std::span charOffsets; + std::wstring_view chars; + + if (sourceColBeg < sourceColLimit) + { + charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast(sourceColLimit) - sourceColBeg + 1); + const auto beg = size_t{ charOffsets.front() } & CharOffsetsMask; + const auto end = size_t{ charOffsets.back() } & CharOffsetsMask; + // We _are_ using span. But C++ decided that string_view and span aren't convertible. + // _chars is a std::span for performance and because it refers to raw, shared memory. +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + chars = { source._chars.data() + beg, end - beg }; + } + + WriteHelper h{ *this, state.columnBegin, state.columnLimit, chars }; + + if (!h.IsValid() || + // If we were to copy text from ourselves, we'd overwrite + // our _charOffsets and break Finish() which reads from it. + this == &state.source || + // Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd + // element is the length of the first glyph) and begins/ends with a non-trailer offset. We don't really + // need to test for the end offset, since `WriteHelper::WriteWithOffsets` already takes care of that. + charOffsets.size() < 2 || WI_IsFlagSet(charOffsets.front(), CharOffsetsTrailer)) + { + state.columnEnd = h.colBeg; + state.columnBeginDirty = h.colBeg; + state.columnEndDirty = h.colBeg; + state.sourceColumnEnd = source._columnCount; + return; + } + + h.CopyTextFrom(charOffsets); + h.Finish(); + + // state.columnEnd is computed identical to ROW::ReplaceText. Check it out for more information. + state.columnEnd = h.charsConsumed == chars.size() ? h.colEnd : h.colLimit; + state.columnBeginDirty = h.colBegDirty; + state.columnEndDirty = h.colEndDirty; + state.sourceColumnEnd = sourceColBeg + h.colEnd - h.colBeg; +} +catch (...) +{ + Reset(TextAttribute{}); + throw; +} + +[[msvc::forceinline]] void ROW::WriteHelper::CopyTextFrom(const std::span& charOffsets) noexcept +{ + // Since our `charOffsets` input is already in columns (just like the `ROW::_charOffsets`), + // we can directly look up the end char-offset, but... + const auto colEndDirtyInput = std::min(gsl::narrow_cast(colLimit - colBeg), gsl::narrow(charOffsets.size() - 1)); + + // ...since the colLimit might intersect with a wide glyph in `charOffset`, we need to adjust our input-colEnd. + auto colEndInput = colEndDirtyInput; + for (; WI_IsFlagSet(til::at(charOffsets, colEndInput), CharOffsetsTrailer); --colEndInput) + { + } + + const auto baseOffset = til::at(charOffsets, 0); + const auto endOffset = til::at(charOffsets, colEndInput); + const auto inToOutOffset = gsl::narrow_cast(chBeg - baseOffset); +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + const auto dst = row._charOffsets.data() + colEnd; + + _copyOffsets(dst, charOffsets.data(), colEndInput, inToOutOffset); + + colEnd += colEndInput; + colEndDirty = gsl::narrow_cast(colBeg + colEndDirtyInput); + charsConsumed = endOffset - baseOffset; +} + +#pragma warning(push) +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +[[msvc::forceinline]] void ROW::WriteHelper::_copyOffsets(uint16_t* __restrict dst, const uint16_t* __restrict src, uint16_t size, uint16_t offset) noexcept +{ + __assume(src != nullptr); + __assume(dst != nullptr); + + // All tested compilers (including MSVC) will neatly unroll and vectorize + // this loop, which is why it's written in this particular way. + for (const auto end = src + size; src != end; ++src, ++dst) + { + const uint16_t ch = *src; + const uint16_t off = ch & CharOffsetsMask; + const uint16_t trailer = ch & CharOffsetsTrailer; + const uint16_t newOff = off + offset; + *dst = newOff | trailer; + } +} +#pragma warning(pop) + +[[msvc::forceinline]] void ROW::WriteHelper::Finish() +{ + colEndDirty = row._adjustForward(colEndDirty); + + const uint16_t trailingSpaces = colEndDirty - colEnd; + const auto chEndDirtyOld = row._uncheckedCharOffset(colEndDirty); + const auto chEndDirty = chBegDirty + charsConsumed + leadingSpaces + trailingSpaces; + + if (chEndDirty != chEndDirtyOld) + { + row._resizeChars(colEndDirty, chBegDirty, chEndDirty, chEndDirtyOld); + } + + { + // std::copy_n compiles to memmove. We can do better. It also gets rid of an extra branch, + // because std::copy_n avoids calling memmove if the count is 0. It's never 0 for us. + const auto itBeg = row._chars.begin() + chBeg; + memcpy(&*itBeg, chars.data(), charsConsumed * sizeof(wchar_t)); + + if (leadingSpaces) + { + fill_n_small(row._chars.begin() + chBegDirty, leadingSpaces, L' '); + iota_n(row._charOffsets.begin() + colBegDirty, leadingSpaces, chBegDirty); + } + if (trailingSpaces) + { + fill_n_small(itBeg + charsConsumed, trailingSpaces, L' '); + iota_n(row._charOffsets.begin() + colEnd, trailingSpaces, gsl::narrow_cast(chBeg + charsConsumed)); + } + } + + // This updates `_doubleBytePadded` whenever we write the last column in the row. `_doubleBytePadded` tells our text + // reflow algorithm whether it should ignore the last column. This is important when writing wide characters into + // the terminal: If the last wide character in a row only fits partially, we should render whitespace, but + // during text reflow pretend as if no whitespace exists. After all, the user didn't write any whitespace there. + // + // The way this is written, it'll set `_doubleBytePadded` to `true` no matter whether a wide character didn't fit, + // or if the last 2 columns contain a wide character and a narrow character got written into the left half of it. + // In both cases `trailingSpaces` is 1 and fills the last column and `_doubleBytePadded` will be `true`. + if (colEndDirty == row._columnCount) + { + row.SetDoubleBytePadded(colEnd < row._columnCount); + } +} + +// This function represents the slow path of ReplaceCharacters(), +// as it reallocates the backing buffer and shifts the char offsets. +// The parameters are difficult to explain, but their names are identical to +// local variables in ReplaceCharacters() which I've attempted to document there. +void ROW::_resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDirty, uint16_t chEndDirtyOld) +{ + const auto diff = chEndDirty - chEndDirtyOld; + const auto currentLength = _charSize(); + const auto newLength = currentLength + diff; + + if (newLength <= _chars.size()) + { + std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, _chars.begin() + chEndDirty); + } + else + { + const auto minCapacity = std::min(UINT16_MAX, _chars.size() + (_chars.size() >> 1)); + const auto newCapacity = gsl::narrow(std::max(newLength, minCapacity)); + + auto charsHeap = std::make_unique_for_overwrite(newCapacity); + const std::span chars{ charsHeap.get(), newCapacity }; + + std::copy_n(_chars.begin(), chBegDirty, chars.begin()); + std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, chars.begin() + chEndDirty); + + _charsHeap = std::move(charsHeap); + _chars = chars; + } + + auto it = _charOffsets.begin() + colEndDirty; + const auto end = _charOffsets.end(); + for (; it != end; ++it) + { + *it = gsl::narrow_cast(*it + diff); + } +} + +til::small_rle& ROW::Attributes() noexcept +{ + return _attr; +} + +const til::small_rle& ROW::Attributes() const noexcept +{ + return _attr; +} + +TextAttribute ROW::GetAttrByColumn(const til::CoordType column) const +{ + return _attr.at(_clampedUint16(column)); +} + +std::vector ROW::GetHyperlinks() const +{ + std::vector ids; + for (const auto& run : _attr.runs()) + { + if (run.value.IsHyperlink()) + { + ids.emplace_back(run.value.GetHyperlinkId()); + } + } + return ids; +} + +uint16_t ROW::size() const noexcept +{ + return _columnCount; +} + +til::CoordType ROW::MeasureLeft() const noexcept +{ + const auto text = GetText(); + const auto beg = text.begin(); + const auto end = text.end(); + auto it = beg; + + for (; it != end; ++it) + { + if (*it != L' ') + { + break; + } + } + + return gsl::narrow_cast(it - beg); +} + +til::CoordType ROW::MeasureRight() const noexcept +{ + if (_wrapForced) + { + auto width = _columnCount; + if (_doubleBytePadded) + { + width--; + } + return width; + } + + const auto text = GetText(); + const auto beg = text.begin(); + const auto end = text.end(); + auto it = end; + + for (; it != beg; --it) + { + // it[-1] is safe as `it` is always greater than `beg` (loop invariant). + if (til::at(it, -1) != L' ') + { + break; + } + } + + // We're supposed to return the measurement in cells and not characters + // and therefore simply calculating `it - beg` would be wrong. + // + // An example: The row is 10 cells wide and `it` points to the second character. + // `it - beg` would return 1, but it's possible it's actually 1 wide glyph and 8 whitespace. + return gsl::narrow_cast(_columnCount - (end - it)); +} + +bool ROW::ContainsText() const noexcept +{ + const auto text = GetText(); + const auto beg = text.begin(); + const auto end = text.end(); + auto it = beg; + + for (; it != end; ++it) + { + if (*it != L' ') + { + return true; + } + } + + return false; +} + +std::wstring_view ROW::GlyphAt(til::CoordType column) const noexcept +{ + auto col = _clampedColumn(column); + + // Safety: col is [0, _columnCount). + const auto beg = _uncheckedCharOffset(col); + // Safety: col cannot be incremented past _columnCount, because the last + // _charOffset at index _columnCount will never get the CharOffsetsTrailer flag. + while (_uncheckedIsTrailer(++col)) + { + } + // Safety: col is now (0, _columnCount]. + const auto end = _uncheckedCharOffset(col); + + return { _chars.begin() + beg, _chars.begin() + end }; +} + +DbcsAttribute ROW::DbcsAttrAt(til::CoordType column) const noexcept +{ + const auto col = _clampedColumn(column); + + auto attr = DbcsAttribute::Single; + // Safety: col is [0, _columnCount). + if (_uncheckedIsTrailer(col)) + { + attr = DbcsAttribute::Trailing; + } + // Safety: col+1 is [1, _columnCount]. + else if (_uncheckedIsTrailer(::base::strict_cast(col) + 1u)) + { + attr = DbcsAttribute::Leading; + } + + return { attr }; +} + +std::wstring_view ROW::GetText() const noexcept +{ + 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 = 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); + // Safety: col is [0, _columnCount). + const auto glyph = _uncheckedChar(_uncheckedCharOffset(col)); + + if (glyph <= L' ') + { + return DelimiterClass::ControlChar; + } + else if (wordDelimiters.find(glyph) != std::wstring_view::npos) + { + return DelimiterClass::DelimiterChar; + } + else + { + return DelimiterClass::RegularChar; + } +} + +template +constexpr uint16_t ROW::_clampedUint16(T v) noexcept +{ + return static_cast(clamp(v, 0, 65535)); +} + +template +constexpr uint16_t ROW::_clampedColumn(T v) const noexcept +{ + return static_cast(clamp(v, 0, _columnCount - 1)); +} + +template +constexpr uint16_t ROW::_clampedColumnInclusive(T v) const noexcept +{ + return static_cast(clamp(v, 0, _columnCount)); +} + +uint16_t ROW::_charSize() const noexcept +{ + // Safety: _charOffsets is an array of `_columnCount + 1` entries. + return _charOffsets[_columnCount]; +} + +// Safety: off must be [0, _charSize()]. +template +wchar_t ROW::_uncheckedChar(T off) const noexcept +{ + return _chars[off]; +} + +// Safety: col must be [0, _columnCount]. +template +uint16_t ROW::_uncheckedCharOffset(T col) const noexcept +{ + assert(col < _charOffsets.size()); + return _charOffsets[col] & CharOffsetsMask; +} + +// Safety: col must be [0, _columnCount]. +template +bool ROW::_uncheckedIsTrailer(T col) const noexcept +{ + assert(col < _charOffsets.size()); + 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 5c2040078d..29f7fe18b8 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -1,69 +1,169 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- Row.hpp - -Abstract: -- data structure for information associated with one row of screen buffer - -Author(s): -- Michael Niksa (miniksa) 10-Apr-2014 -- Paul Campbell (paulcam) 10-Apr-2014 - -Revision History: -- From components of output.h/.c - by Therese Stowell (ThereseS) 1990-1991 -- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once -#include "AttrRow.hpp" +#include + #include "LineRendition.hpp" #include "OutputCell.hpp" #include "OutputCellIterator.hpp" -#include "CharRow.hpp" -#include "UnicodeStorage.hpp" +class ROW; class TextBuffer; +enum class DelimiterClass +{ + ControlChar, + DelimiterChar, + RegularChar +}; + +struct RowWriteState +{ + // The text you want to write into the given ROW. When ReplaceText() returns, + // this is updated to remove all text from the beginning that was successfully written. + std::wstring_view text; // IN/OUT + // The column at which to start writing. + til::CoordType columnBegin = 0; // IN + // The first column which should not be written to anymore. + til::CoordType columnLimit = til::CoordTypeMax; // IN + + // The column 1 past the last glyph that was successfully written into the row. If you need to call + // ReplaceAttributes() to colorize the written range, etc., this is the columnEnd parameter you want. + // If you want to continue writing where you left off, this is also the next columnBegin parameter. + til::CoordType columnEnd = 0; // OUT + // The first column that got modified by this write operation. In case that the first glyph we write overwrites + // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg. + til::CoordType columnBeginDirty = 0; // OUT + // This is 1 past the last column that was modified and will be 1 past columnEnd if we overwrote + // the leading half of a wide glyph and had to fill the trailing half with whitespace. + til::CoordType columnEndDirty = 0; // OUT +}; + +struct RowCopyTextFromState +{ + // The row to copy text from. + const ROW& source; // IN + // The column at which to start writing. + til::CoordType columnBegin = 0; // IN + // The first column which should not be written to anymore. + til::CoordType columnLimit = til::CoordTypeMax; // IN + // The column at which to start reading from source. + til::CoordType sourceColumnBegin = 0; // IN + // The first column which should not be read from anymore. + til::CoordType sourceColumnLimit = til::CoordTypeMax; // IN + + til::CoordType columnEnd = 0; // OUT + // The first column that got modified by this write operation. In case that the first glyph we write overwrites + // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg. + til::CoordType columnBeginDirty = 0; // OUT + // This is 1 past the last column that was modified and will be 1 past columnEnd if we overwrote + // the leading half of a wide glyph and had to fill the trailing half with whitespace. + til::CoordType columnEndDirty = 0; // OUT + // This is 1 past the last column that was read from. + til::CoordType sourceColumnEnd = 0; // OUT +}; + +// 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: - ROW(const til::CoordType rowId, const til::CoordType rowWidth, const TextAttribute fillAttribute, TextBuffer* const pParent); + // The implicit agreement between ROW and TextBuffer is that the `charsBuffer` and `charOffsetsBuffer` + // arrays have a minimum alignment of 16 Bytes and a size of `rowWidth+1`. The former is used to + // implement Reset() efficiently via SIMD and the latter is used to store the past-the-end offset + // into the `charsBuffer`. Even though the `charsBuffer` could be only `rowWidth` large we need them + // to be the same size so that the SIMD code can process both arrays in the same loop simultaneously. + // This wastes up to 5.8% memory but increases overall scrolling performance by around 40%. + // These methods exists to make this agreement explicit and serve as a reminder. + // + // TextBuffer calculates the distance in bytes between two ROWs (_bufferRowStride) as the sum of these values. + // As such it's important that we return sizes with a minimum alignment of alignof(ROW). + static constexpr size_t CalculateRowSize() noexcept + { + return (sizeof(ROW) + 15) & ~15; + } + static constexpr size_t CalculateCharsBufferSize(size_t columns) noexcept + { + return (columns * sizeof(wchar_t) + 16) & ~15; + } + static constexpr size_t CalculateCharOffsetsBufferSize(size_t columns) noexcept + { + return (columns * sizeof(uint16_t) + 16) & ~15; + } - til::CoordType size() const noexcept { return _rowWidth; } + ROW() = default; + ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute); - void SetWrapForced(const bool wrap) noexcept { _wrapForced = wrap; } - bool WasWrapForced() const noexcept { return _wrapForced; } + ROW(const ROW& other) = delete; + ROW& operator=(const ROW& other) = delete; - void SetDoubleBytePadded(const bool doubleBytePadded) noexcept { _doubleBytePadded = doubleBytePadded; } - bool WasDoubleBytePadded() const noexcept { return _doubleBytePadded; } + ROW(ROW&& other) = default; + ROW& operator=(ROW&& other) = default; - const CharRow& GetCharRow() const noexcept { return _charRow; } - CharRow& GetCharRow() noexcept { return _charRow; } + void SetWrapForced(const bool wrap) noexcept; + bool WasWrapForced() const noexcept; + void SetDoubleBytePadded(const bool doubleBytePadded) noexcept; + bool WasDoubleBytePadded() const noexcept; + void SetLineRendition(const LineRendition lineRendition) noexcept; + LineRendition GetLineRendition() const noexcept; + til::CoordType GetReadableColumnCount() const noexcept; - const ATTR_ROW& GetAttrRow() const noexcept { return _attrRow; } - ATTR_ROW& GetAttrRow() noexcept { return _attrRow; } + void Reset(const TextAttribute& attr) noexcept; + void TransferAttributes(const til::small_rle& attr, til::CoordType newWidth); + void CopyFrom(const ROW& source); - LineRendition GetLineRendition() const noexcept { return _lineRendition; } - void SetLineRendition(const LineRendition lineRendition) noexcept { _lineRendition = lineRendition; } + til::CoordType NavigateToPrevious(til::CoordType column) const noexcept; + til::CoordType NavigateToNext(til::CoordType column) const noexcept; + til::CoordType AdjustToGlyphStart(til::CoordType column) const noexcept; - til::CoordType GetId() const noexcept { return _id; } - void SetId(const til::CoordType id) noexcept { _id = id; } + void ClearCell(til::CoordType column); + OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt); + void SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr); + void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr); + void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars); + void ReplaceText(RowWriteState& state); + void CopyTextFrom(RowCopyTextFromState& state); - bool Reset(const TextAttribute Attr); - [[nodiscard]] HRESULT Resize(const til::CoordType width); + til::small_rle& Attributes() noexcept; + const til::small_rle& Attributes() const noexcept; + TextAttribute GetAttrByColumn(til::CoordType column) const; + std::vector GetHyperlinks() const; + uint16_t size() const noexcept; + til::CoordType MeasureLeft() const noexcept; + til::CoordType MeasureRight() const noexcept; + bool ContainsText() const noexcept; + std::wstring_view GlyphAt(til::CoordType column) const noexcept; + 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; - void ClearColumn(const til::CoordType column); - std::wstring GetText() const { return _charRow.GetText(); } - - UnicodeStorage& GetUnicodeStorage() noexcept; - const UnicodeStorage& GetUnicodeStorage() const noexcept; - - OutputCellIterator WriteCells(OutputCellIterator it, const til::CoordType index, const std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt); + auto AttrBegin() const noexcept { return _attr.begin(); } + auto AttrEnd() const noexcept { return _attr.end(); } #ifdef UNIT_TESTING friend constexpr bool operator==(const ROW& a, const ROW& b) noexcept; @@ -71,23 +171,138 @@ public: #endif private: - CharRow _charRow; - ATTR_ROW _attrRow; - LineRendition _lineRendition; - til::CoordType _id; - til::CoordType _rowWidth; + // WriteHelper exists because other forms of abstracting this functionality away (like templates with lambdas) + // where only very poorly optimized by MSVC as it failed to inline the templates. + struct WriteHelper + { + explicit WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept; + bool IsValid() const noexcept; + void ReplaceCharacters(til::CoordType width) noexcept; + void ReplaceText() noexcept; + void _replaceTextUnicode(size_t ch, std::wstring_view::const_iterator it) noexcept; + void CopyTextFrom(const std::span& charOffsets) noexcept; + static void _copyOffsets(uint16_t* dst, const uint16_t* src, uint16_t size, uint16_t offset) noexcept; + void Finish(); + + // Parent pointer. + ROW& row; + // The text given by the caller. + const std::wstring_view& chars; + + // This is the same as the columnBegin parameter for ReplaceText(), etc., + // but clamped to a valid range via _clampedColumnInclusive. + uint16_t colBeg; + // This is the same as the columnLimit parameter for ReplaceText(), etc., + // but clamped to a valid range via _clampedColumnInclusive. + uint16_t colLimit; + + // The column 1 past the last glyph that was successfully written into the row. If you need to call + // ReplaceAttributes() to colorize the written range, etc., this is the columnEnd parameter you want. + // If you want to continue writing where you left off, this is also the next columnBegin parameter. + uint16_t colEnd; + // The first column that got modified by this write operation. In case that the first glyph we write overwrites + // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg. + uint16_t colBegDirty; + // Similar to dirtyBeg, this is 1 past the last column that was modified and will be 1 past colEnd if + // we overwrote the leading half of a wide glyph and had to fill the trailing half with whitespace. + uint16_t colEndDirty; + // The offset in ROW::chars at which we start writing the contents of WriteHelper::chars. + uint16_t chBeg; + // The offset at which we start writing leadingSpaces-many whitespaces. + uint16_t chBegDirty; + // The same as `colBeg - colBegDirty`. This is the amount of whitespace + // we write at chBegDirty, before the actual WriteHelper::chars content. + uint16_t leadingSpaces; + // The amount of characters copied from WriteHelper::chars. + size_t charsConsumed; + }; + + // To simplify the detection of wide glyphs, we don't just store the simple character offset as described + // for _charOffsets. Instead we use the most significant bit to indicate whether any column is the + // trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer. + static constexpr uint16_t CharOffsetsTrailer = 0x8000; + static constexpr uint16_t CharOffsetsMask = 0x7fff; + + template + static constexpr uint16_t _clampedUint16(T v) noexcept; + template + constexpr uint16_t _clampedColumn(T v) const noexcept; + template + constexpr uint16_t _clampedColumnInclusive(T v) const noexcept; + + uint16_t _charSize() 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). + // * _chars and _charsHeap are redundant + // If _charsHeap is stored in _chars, we can still infer that + // _chars was allocated on the heap if _chars != _charsBuffer. + // * _chars doesn't need a size_t size() + // The size may never exceed an uint16_t anyways. + // * _charOffsets doesn't need a size() at all + // The length is already stored in _columns. + + // Most text uses only a single wchar_t per codepoint / grapheme cluster. + // That's why TextBuffer allocates a large blob of virtual memory which we can use as + // a simplified chars buffer, without having to allocate any additional heap memory. + // _charsBuffer fits _columnCount characters at most. + wchar_t* _charsBuffer = nullptr; + // ...but if this ROW needs to store more than _columnCount characters + // then it will allocate a larger string on the heap and store it here. + // The capacity of this string on the heap is stored in _chars.size(). + std::unique_ptr _charsHeap; + // _chars either refers to our _charsBuffer or _charsHeap, defaulting to the former. + // _chars.size() is NOT the length of the string, but rather its capacity. + // _charOffsets[_columnCount] stores the length. + std::span _chars; + // _charOffsets accelerates indexing into the above _chars string given a terminal column, + // by storing the character index/offset at which a column's text in _chars starts. + // It stores 1 more item than this row is wide, allowing it to store the + // past-the-end offset, which is thus equal to the length of the string. + // + // For instance given a 4 column ROW containing "abcd" it would store 01234, + // because each of "abcd" are 1 column wide and 1 wchar_t per column. + // Given "a\u732Bd" it would store 01123, because "\u732B" is a wide glyph + // and "11" indicates that both column 1 and 2 start at &_chars[1] (= wide glyph). + // The fact that the next offset is 2 tells us that the glyph is 1 wchar_t long. + // Given "a\uD83D\uDE00d" ("\uD83D\uDE00" is an Emoji) it would store 01134, + // because while it's 2 cells wide as indicated by 2 offsets that are identical (11), + // the next offset is 3, which indicates that the glyph is 3-1 = 2 wchar_t long. + // + // In other words, _charOffsets tells us both the width in chars and width in columns. + // See CharOffsetsTrailer for more information. + std::span _charOffsets; + // _attr is a run-length-encoded vector of TextAttribute with a decompressed + // length equal to _columnCount (= 1 TextAttribute per column). + til::small_rle _attr; + // The width of the row in visual columns. + uint16_t _columnCount = 0; + // Stores double-width/height (DECSWL/DECDWL/DECDHL) attributes. + LineRendition _lineRendition = LineRendition::SingleWidth; // Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line - bool _wrapForced; + bool _wrapForced = false; // Occurs when the user runs out of text to support a double byte character and we're forced to the next line - bool _doubleBytePadded; - TextBuffer* _pParent; // non ownership pointer + bool _doubleBytePadded = false; }; #ifdef UNIT_TESTING constexpr bool operator==(const ROW& a, const ROW& b) noexcept { // comparison is only used in the tests; this should suffice. - return (a._pParent == b._pParent && - a._id == b._id); + return a._charsBuffer == b._charsBuffer; } #endif diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 55e3b15a5e..d5b0e5e4a5 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 +// 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::Underlined, isUnderlined); -} - -void TextAttribute::SetDoublyUnderlined(bool isDoublyUnderlined) 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 2a61e8902d..4fe8c9e637 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 @@ public: _attrs{ CharacterAttributes::Normal }, _foreground{}, _background{}, - _hyperlinkId{ 0 } + _hyperlinkId{ 0 }, + _underlineColor{} { } @@ -42,16 +54,28 @@ public: _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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: void SetDefaultForeground() noexcept; void SetDefaultBackground() noexcept; + void SetDefaultUnderlineColor() noexcept; void SetDefaultRenditionAttributes() noexcept; bool BackgroundIsDefault() const noexcept; @@ -147,8 +173,8 @@ public: // 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 @@ private: 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/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp index 7440cc03d3..2e2e6f9fa0 100644 --- a/src/buffer/out/TextColor.cpp +++ b/src/buffer/out/TextColor.cpp @@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept bool TextColor::IsLegacy() const noexcept { - return IsIndex16() || (IsIndex256() && _index < 16); + return (IsIndex16() || IsIndex256()) && _index < 16; } bool TextColor::IsIndex16() const noexcept @@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept return _meta == ColorType::IsDefault; } +bool TextColor::IsDefaultOrLegacy() const noexcept +{ + return _meta != ColorType::IsRgb && _index < 16; +} + bool TextColor::IsRgb() const noexcept { return _meta == ColorType::IsRgb; diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h index b975028760..0b71078fbf 100644 --- a/src/buffer/out/TextColor.h +++ b/src/buffer/out/TextColor.h @@ -37,12 +37,14 @@ Revision History: #include "WexTestClass.h" #endif +// The enum values being in this particular order allows the compiler to do some useful optimizations, +// like simplifying `IsIndex16() || IsIndex256()` into a simple range check without branching. enum class ColorType : BYTE { - IsIndex256 = 0x0, - IsIndex16 = 0x1, - IsDefault = 0x2, - IsRgb = 0x3 + IsDefault, + IsIndex16, + IsIndex256, + IsRgb }; enum class ColorAlias : size_t @@ -121,6 +123,7 @@ public: bool IsIndex16() const noexcept; bool IsIndex256() const noexcept; bool IsDefault() const noexcept; + bool IsDefaultOrLegacy() const noexcept; bool IsRgb() const noexcept; void SetColor(const COLORREF rgbColor) noexcept; diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp new file mode 100644 index 0000000000..7dec8d2dc0 --- /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 0000000000..c8c325143e --- /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/UnicodeStorage.cpp b/src/buffer/out/UnicodeStorage.cpp deleted file mode 100644 index b7c63bfb41..0000000000 --- a/src/buffer/out/UnicodeStorage.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "UnicodeStorage.hpp" - -UnicodeStorage::UnicodeStorage() noexcept : - _map{} -{ -} - -// Routine Description: -// - fetches the text associated with key -// Arguments: -// - key - the key into the storage -// Return Value: -// - the glyph data associated with key -// Note: will throw exception if key is not stored yet -const UnicodeStorage::mapped_type& UnicodeStorage::GetText(const key_type key) const -{ - return _map.at(key); -} - -// Routine Description: -// - stores glyph data associated with key. -// Arguments: -// - key - the key into the storage -// - glyph - the glyph data to store -void UnicodeStorage::StoreGlyph(const key_type key, const mapped_type& glyph) -{ - _map.insert_or_assign(key, glyph); -} - -// Routine Description: -// - erases key and its associated data from the storage -// Arguments: -// - key - the key to remove -void UnicodeStorage::Erase(const key_type key) noexcept -{ - _map.erase(key); -} - -// Routine Description: -// - Remaps all of the stored items to new coordinate positions -// based on a bulk rearrangement of row IDs and potential row width resize. -// Arguments: -// - rowMap - A map of the old row IDs to the new row IDs. -// - width - The width of the new row. Remove any items that are beyond the row width. -// - Use nullopt if we're not resizing the width of the row, just renumbering the rows. -void UnicodeStorage::Remap(const std::unordered_map& rowMap, const std::optional width) -{ - // Make a temporary map to hold all the new row positioning - std::unordered_map newMap; - - // Walk through every stored item. - for (const auto& pair : _map) - { - // Extract the old coordinate position - const auto oldCoord = pair.first; - - // Only try to short-circuit based on width if we were told it changed - // by being given a new width value. - if (width.has_value()) - { - // Get the column ID - const auto oldColId = oldCoord.X; - - // If the column index is at/beyond the row width, don't bother copying it to the new map. - if (oldColId >= width.value()) - { - continue; - } - } - - // Get the row ID from the position as that's what we need to remap - const auto oldRowId = oldCoord.Y; - - // Use the mapping given to convert the old row ID to the new row ID - const auto mapIter = rowMap.find(oldRowId); - - // If there's no mapping to a new row, don't bother copying it to the new map. The row is gone. - if (mapIter == rowMap.end()) - { - continue; - } - - const auto newRowId = mapIter->second; - - // Generate a new coordinate with the same X as the old one, but a new Y value. - const auto newCoord = til::point{ oldCoord.X, newRowId }; - - // Put the adjusted coordinate into the map with the original value. - newMap.emplace(newCoord, pair.second); - } - - // Swap into the stored map, free the temporary when we exit. - _map.swap(newMap); -} diff --git a/src/buffer/out/UnicodeStorage.hpp b/src/buffer/out/UnicodeStorage.hpp deleted file mode 100644 index 609338dca5..0000000000 --- a/src/buffer/out/UnicodeStorage.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- UnicodeStorage.hpp - -Abstract: -- dynamic storage location for glyphs that can't normally fit in the output buffer - -Author(s): -- Austin Diviness (AustDi) 02-May-2018 ---*/ - -#pragma once - -#include -#include - -#include - -// std::unordered_map needs help to know how to hash a til::point -namespace std -{ - template<> - struct hash - { - // Routine Description: - // - hashes a coord. coord will be hashed by storing the x and y values consecutively in the lower - // bits of a size_t. - // Arguments: - // - coord - the coord to hash - // Return Value: - // - the hashed coord - size_t operator()(const til::point coord) const noexcept - { - return til::hash(coord); - } - }; -} - -class UnicodeStorage final -{ -public: - using key_type = typename til::point; - using mapped_type = typename std::vector; - - UnicodeStorage() noexcept; - - const mapped_type& GetText(const key_type key) const; - - void StoreGlyph(const key_type key, const mapped_type& glyph); - - void Erase(const key_type key) noexcept; - - void Remap(const std::unordered_map& rowMap, const std::optional width); - -private: - std::unordered_map _map; - -#ifdef UNIT_TESTING - friend class UnicodeStorageTests; - friend class TextBufferTests; -#endif -}; diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 7dcc239540..9084a60003 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -101,6 +101,13 @@ void Cursor::SetIsOn(const bool fIsOn) noexcept void Cursor::SetBlinkingAllowed(const bool fBlinkingAllowed) noexcept { _fBlinkingAllowed = fBlinkingAllowed; + // GH#2642 - From what we've gathered from other terminals, when blinking is + // disabled, the cursor should remain On always, and have the visibility + // controlled by the IsVisible property. So when you do a printf "\e[?12l" + // to disable blinking, the cursor stays stuck On. At this point, only the + // cursor visibility property controls whether the user can see it or not. + // (Yes, the cursor can be On and NOT Visible) + _fIsOn = true; _RedrawCursorAlways(); } @@ -199,7 +206,7 @@ void Cursor::SetPosition(const til::point cPosition) noexcept void Cursor::SetXPosition(const til::CoordType NewX) noexcept { _RedrawCursor(); - _cPosition.X = NewX; + _cPosition.x = NewX; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -207,7 +214,7 @@ void Cursor::SetXPosition(const til::CoordType NewX) noexcept void Cursor::SetYPosition(const til::CoordType NewY) noexcept { _RedrawCursor(); - _cPosition.Y = NewY; + _cPosition.y = NewY; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -215,7 +222,7 @@ void Cursor::SetYPosition(const til::CoordType NewY) noexcept void Cursor::IncrementXPosition(const til::CoordType DeltaX) noexcept { _RedrawCursor(); - _cPosition.X += DeltaX; + _cPosition.x += DeltaX; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -223,7 +230,7 @@ void Cursor::IncrementXPosition(const til::CoordType DeltaX) noexcept void Cursor::IncrementYPosition(const til::CoordType DeltaY) noexcept { _RedrawCursor(); - _cPosition.Y += DeltaY; + _cPosition.y += DeltaY; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -231,7 +238,7 @@ void Cursor::IncrementYPosition(const til::CoordType DeltaY) noexcept void Cursor::DecrementXPosition(const til::CoordType DeltaX) noexcept { _RedrawCursor(); - _cPosition.X -= DeltaX; + _cPosition.x -= DeltaX; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -239,7 +246,7 @@ void Cursor::DecrementXPosition(const til::CoordType DeltaX) noexcept void Cursor::DecrementYPosition(const til::CoordType DeltaY) noexcept { _RedrawCursor(); - _cPosition.Y -= DeltaY; + _cPosition.y -= DeltaY; _RedrawCursor(); ResetDelayEOLWrap(); } @@ -279,9 +286,9 @@ void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept _cursorType = OtherCursor._cursorType; } -void Cursor::DelayEOLWrap(const til::point coordDelayedAt) noexcept +void Cursor::DelayEOLWrap() noexcept { - _coordDelayedAt = coordDelayedAt; + _coordDelayedAt = _cPosition; _fDelayedEolWrap = true; } diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index bbd908eb3e..3c377e61b2 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -76,7 +76,7 @@ public: void CopyProperties(const Cursor& OtherCursor) noexcept; - void DelayEOLWrap(const til::point coordDelayedAt) noexcept; + void DelayEOLWrap() noexcept; void ResetDelayEOLWrap() noexcept; til::point GetDelayedAtPosition() const noexcept; bool IsDelayedEOLWrap() const noexcept; diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index f55f751d98..20385eff09 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -11,7 +11,6 @@ - @@ -24,16 +23,12 @@ - - - Create - + - @@ -49,11 +44,8 @@ - - - - + diff --git a/src/buffer/out/precomp.h b/src/buffer/out/precomp.h index 3ae7439f78..2ecc2c00d1 100644 --- a/src/buffer/out/precomp.h +++ b/src/buffer/out/precomp.h @@ -1,41 +1,7 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- precomp.h - -Abstract: -- Contains external headers to include in the precompile phase of console build process. -- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). ---*/ - -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once -// clang-format off +#include -// This includes support libraries from the CRT, STL, WIL, and GSL -#include "LibraryIncludes.h" - -#pragma warning(push) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers -#define NOMCX -#define NOHELP -#define NOCOMM -#endif - -// Windows Header Files: -#include -#include - -// private dependencies -#include "../inc/unicode.hpp" -#pragma warning(pop) - -// clang-format on +#include diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 6bbcdc1ae6..9707e8fdea 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -2,344 +2,140 @@ // Licensed under the MIT license. #include "precomp.h" - #include "search.h" -#include "CharRow.hpp" #include "textBuffer.hpp" -#include "../types/inc/Utf16Parser.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") -// - uiaData - The IUiaData 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(IUiaData& uiaData, - const std::wstring_view str, - const Direction direction, - const Sensitivity sensitivity) : - _direction(direction), - _sensitivity(sensitivity), - _needle(s_CreateNeedleFromString(str)), - _uiaData(uiaData), - _coordAnchor(s_GetInitialAnchor(uiaData, direction)) +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: -// - 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") -// - uiaData - The IUiaData 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(IUiaData& uiaData, - 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), - _uiaData(uiaData) -{ - _coordNext = _coordAnchor; -} - -// Routine Description -// - Locates the next instance of the search term within the screen buffer. -// Arguments: -// - - Uses internal state from constructor -// Return Value: -// - True if we found another item. False if we've reached the end of the buffer. -// - NOTE: You can FindNext() again after False to go around the buffer again. -bool Search::FindNext() -{ - if (_reachedEnd) + if (_needle == needle && + _caseInsensitive == caseInsensitive && + _lastMutationId == lastMutationId) { - _reachedEnd = false; + _step = reverse ? -1 : 1; return false; } - do + _renderData = &renderData; + _needle = needle; + _caseInsensitive = caseInsensitive; + _lastMutationId = lastMutationId; + + _results = textBuffer.SearchText(needle, caseInsensitive); + _index = reverse ? gsl::narrow_cast(_results.size()) - 1 : 0; + _step = reverse ? -1 : 1; + + return true; +} + +void Search::MoveToCurrentSelection() +{ + if (_renderData->IsSelectionActive()) { - if (_FindNeedleInHaystackAt(_coordNext, _coordSelStart, _coordSelEnd)) - { - _UpdateNextPosition(); - _reachedEnd = _coordNext == _coordAnchor; - return true; - } - else - { - _UpdateNextPosition(); - } + MoveToPoint(_renderData->GetTextBuffer().ScreenToBufferPosition(_renderData->GetSelectionAnchor())); + } +} - } while (_coordNext != _coordAnchor); +void Search::MoveToPoint(const til::point anchor) noexcept +{ + if (_results.empty()) + { + return; + } - return false; + const auto count = gsl::narrow_cast(_results.size()); + ptrdiff_t index = 0; + + if (_step < 0) + { + for (index = count - 1; index >= 0 && til::at(_results, index).start > anchor; --index) + { + } + } + else + { + for (index = 0; index < count && til::at(_results, index).start < anchor; ++index) + { + } + } + + _index = (index + count) % count; +} + +void Search::MovePastPoint(const til::point anchor) noexcept +{ + if (_results.empty()) + { + return; + } + + const auto count = gsl::narrow_cast(_results.size()); + ptrdiff_t index = 0; + + if (_step < 0) + { + for (index = count - 1; index >= 0 && til::at(_results, index).start >= anchor; --index) + { + } + } + else + { + for (index = 0; index < count && til::at(_results, index).start <= anchor; ++index) + { + } + } + + _index = (index + count) % count; +} + +void Search::FindNext() noexcept +{ + if (const auto count{ gsl::narrow_cast(_results.size()) }) + { + _index = (_index + _step + count) % count; + } +} + +const til::point_span* Search::GetCurrent() const noexcept +{ + const auto index = gsl::narrow_cast(_index); + if (index < _results.size()) + { + return &til::at(_results, index); + } + return nullptr; } // Routine Description: // - Takes the found word and selects it in the screen buffer -void Search::Select() const + +bool Search::SelectCurrent() const { - // Convert buffer selection offsets into the equivalent screen coordinates - // required by SelectNewRegion, taking line renditions into account. - const auto& textBuffer = _uiaData.GetTextBuffer(); - const auto selStart = textBuffer.BufferToScreenPosition(_coordSelStart); - const auto selEnd = textBuffer.BufferToScreenPosition(_coordSelEnd); - _uiaData.SelectNewRegion(selStart, selEnd); + if (const auto s = GetCurrent()) + { + // 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 false; } -// 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 +const std::vector& Search::Results() const noexcept { - // Only select if we've found something. - if (_coordSelEnd >= _coordSelStart) - { - _uiaData.ColorSelection(_coordSelStart, _coordSelEnd, attr); - } + return _results; } -// 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 +ptrdiff_t Search::CurrentMatch() const noexcept { - return { _coordSelStart, _coordSelEnd }; -} - -// Routine Description: -// - Finds the anchor position where we will start searches from. -// - This position will represent the "wrap around" point in the buffer or where -// we reach the end of our search. -// - If the screen buffer given already has a selection in it, it will be used to determine the anchor. -// - Otherwise, we will choose one of the ends of the screen buffer depending on direction. -// Arguments: -// - uiaData - The reference to the IUiaData interface type object -// - direction - The intended direction of the search -// Return Value: -// - Coordinate to start the search from. -til::point Search::s_GetInitialAnchor(const IUiaData& uiaData, const Direction direction) -{ - const auto& textBuffer = uiaData.GetTextBuffer(); - const auto textBufferEndPosition = uiaData.GetTextBufferEndPosition(); - if (uiaData.IsSelectionActive()) - { - // 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(uiaData.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; - } - 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; - - for (const auto& needleCell : _needle) - { - // Haystack is the buffer. Needle is the string we were given. - const auto hayIter = _uiaData.GetTextBuffer().GetTextDataAt(bufferPos); - const auto hayChars = *hayIter; - const auto needleChars = std::wstring_view(needleCell.data(), needleCell.size()); - - // If we didn't match at any point of the needle, return false. - if (!_CompareChars(hayChars, needleChars)) - { - return false; - } - - _IncrementCoord(bufferPos); - } - - _DecrementCoord(bufferPos); - - // If we made it the whole way through the needle, then it was in the haystack. - // Fill out the span that we found the result at and return true. - start = pos; - end = bufferPos; - - return true; -} - -// Routine Description: -// - Provides an abstraction for comparing two spans of text. -// - Internally handles case sensitivity based on object construction. -// Arguments: -// - one - String view representing the first string of text -// - two - String view representing the second string of text -// Return Value: -// - True if they are the same. False otherwise. -bool Search::_CompareChars(const std::wstring_view one, const std::wstring_view two) const noexcept -{ - if (one.size() != two.size()) - { - return false; - } - - for (size_t i = 0; i < one.size(); i++) - { - if (_ApplySensitivity(one.at(i)) != _ApplySensitivity(two.at(i))) - { - return false; - } - } - - return true; -} - -// Routine Description: -// - Provides an abstraction for conditionally applying case sensitivity -// based on object construction -// Arguments: -// - wch - Character to adjust if necessary -// Return Value: -// - Adjusted value (or not). -wchar_t Search::_ApplySensitivity(const wchar_t wch) const noexcept -{ - if (_sensitivity == Sensitivity::CaseInsensitive) - { - return ::towlower(wch); - } - else - { - return wch; - } -} - -// Routine Description: -// - Helper to increment a coordinate in respect to the associated screen buffer -// Arguments -// - coord - Updated by function to increment one position (will wrap X and Y direction) -void Search::_IncrementCoord(til::point& coord) const noexcept -{ - _uiaData.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 -{ - _uiaData.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 - { - 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 = _uiaData.GetTextBufferEndPosition(); - - if (_coordNext.Y > bufferEndPosition.Y || - (_coordNext.Y == bufferEndPosition.Y && _coordNext.X > bufferEndPosition.X)) - { - if (_direction == Direction::Forward) - { - _coordNext = {}; - } - else - { - _coordNext = bufferEndPosition; - } - } -} - -// 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) -{ - const auto charData = Utf16Parser::Parse(wstr); - std::vector> cells; - for (const auto chars : charData) - { - if (IsGlyphFullWidth(std::wstring_view{ chars.data(), chars.size() })) - { - cells.emplace_back(chars); - } - cells.emplace_back(chars); - } - return cells; + return _index; } diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index c5581f2bf8..c2d035e3e9 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -17,71 +17,35 @@ Revision History: #pragma once -#include -#include "TextAttribute.hpp" #include "textBuffer.hpp" -#include "../types/IUiaData.h" - -// This used to be in find.h. -#define SEARCH_STRING_LENGTH (80) +#include "../renderer/inc/IRenderData.hpp" class Search final { public: - enum class Direction - { - Forward, - Backward - }; + Search() = default; - enum class Sensitivity - { - CaseInsensitive, - CaseSensitive - }; + bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive); - Search(Microsoft::Console::Types::IUiaData& uiaData, - const std::wstring_view str, - const Direction dir, - const Sensitivity sensitivity); + void MoveToCurrentSelection(); + void MoveToPoint(til::point anchor) noexcept; + void MovePastPoint(til::point anchor) noexcept; + void FindNext() noexcept; - Search(Microsoft::Console::Types::IUiaData& uiaData, - const std::wstring_view str, - const Direction dir, - const Sensitivity sensitivity, - const til::point anchor); + const til::point_span* GetCurrent() const noexcept; + bool SelectCurrent() const; - bool FindNext(); - void Select() const; - void Color(const TextAttribute attr) const; - - std::pair GetFoundLocation() const noexcept; + const std::vector& Results() const noexcept; + ptrdiff_t CurrentMatch() const noexcept; 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(); + // _renderData is a pointer so that Search() is constexpr default constructable. + Microsoft::Console::Render::IRenderData* _renderData = nullptr; + std::wstring _needle; + bool _caseInsensitive = false; + uint64_t _lastMutationId = 0; - void _IncrementCoord(til::point& coord) const noexcept; - void _DecrementCoord(til::point& coord) const noexcept; - - static til::point s_GetInitialAnchor(const Microsoft::Console::Types::IUiaData& uiaData, 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::Types::IUiaData& _uiaData; - -#ifdef UNIT_TESTING - friend class SearchTests; -#endif + 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 b78f575b24..6611cc0aa5 100644 --- a/src/buffer/out/sources.inc +++ b/src/buffer/out/sources.inc @@ -29,7 +29,6 @@ PRECOMPILED_CXX = 1 PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ - ..\AttrRow.cpp \ ..\cursor.cpp \ ..\OutputCell.cpp \ ..\OutputCellIterator.cpp \ @@ -41,11 +40,8 @@ SOURCES= \ ..\textBuffer.cpp \ ..\textBufferCellIterator.cpp \ ..\textBufferTextIterator.cpp \ - ..\CharRow.cpp \ - ..\CharRowCell.cpp \ - ..\CharRowCellReference.cpp \ - ..\UnicodeStorage.cpp \ - ..\search.cpp \ + ..\search.cpp \ + ..\UTextAdapter.cpp \ INCLUDES= \ $(INCLUDES); \ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index ce7a8ed980..26c8152ea8 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -4,21 +4,35 @@ #include "precomp.h" #include "textBuffer.hpp" -#include "CharRow.hpp" -#include "../renderer/base/renderer.hpp" -#include "../types/inc/utils.hpp" -#include "../types/inc/convert.hpp" -#include "../../types/inc/Utf16Parser.hpp" +#include +#include + +#include "UTextAdapter.h" #include "../../types/inc/GlyphWidth.hpp" - -#pragma hdrstop +#include "../renderer/base/renderer.hpp" +#include "../types/inc/convert.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: @@ -30,32 +44,208 @@ using PointTree = interval_tree::IntervalTree; // Return Value: // - constructed object // Note: may throw exception -TextBuffer::TextBuffer(const til::size screenBufferSize, +TextBuffer::TextBuffer(til::size screenBufferSize, const TextAttribute defaultAttributes, const UINT cursorSize, const bool isActiveBuffer, Microsoft::Console::Render::Renderer& renderer) : - _firstRow{ 0 }, - _currentAttributes{ defaultAttributes }, - _cursor{ cursorSize, *this }, - _storage{}, - _unicodeStorage{}, - _isActiveBuffer{ isActiveBuffer }, _renderer{ renderer }, - _size{}, - _currentHyperlinkId{ 1 }, - _currentPatternId{ 0 } + _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 } { - // initialize ROWs - _storage.reserve(gsl::narrow(screenBufferSize.Y)); - for (til::CoordType i = 0; i < screenBufferSize.Y; ++i) + // Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text. + screenBufferSize.width = std::max(screenBufferSize.width, 1); + screenBufferSize.height = std::max(screenBufferSize.height, 1); + _reserve(screenBufferSize, defaultAttributes); +} + +TextBuffer::~TextBuffer() +{ + if (_buffer) { - _storage.emplace_back(i, screenBufferSize.X, _currentAttributes, this); + _destroy(); + } +} + +// I put these functions in a block at the start of the class, because they're the most +// fundamental aspect of TextBuffer: It implements the basic gap buffer text storage. +// It's also fairly tricky code. +#pragma region buffer management +#pragma warning(push) +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + +// MEM_RESERVEs memory sufficient to store height-many ROW structs, +// as well as their ROW::_chars and ROW::_charOffsets buffers. +// +// We use explicit virtual memory allocations to not taint the general purpose allocator +// with our huge allocation, as well as to be able to reduce the private working set of +// the application by only committing what we actually need. This reduces conhost's +// memory usage from ~7MB down to just ~2MB at startup in the general case. +void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes) +{ + const auto w = gsl::narrow(screenBufferSize.width); + const auto h = gsl::narrow(screenBufferSize.height); + + constexpr auto rowSize = ROW::CalculateRowSize(); + const auto charsBufferSize = ROW::CalculateCharsBufferSize(w); + const auto charOffsetsBufferSize = ROW::CalculateCharOffsetsBufferSize(w); + const auto rowStride = rowSize + charsBufferSize + charOffsetsBufferSize; + assert(rowStride % alignof(ROW) == 0); + + // 65535*65535 cells would result in a allocSize of 8GiB. + // --> Use uint64_t so that we can safely do our calculations even on x86. + // We allocate 1 additional row, which will be used for GetScratchpadRow(). + const auto rowCount = ::base::strict_cast(h) + 1; + const auto allocSize = gsl::narrow(rowCount * rowStride); + + // NOTE: Modifications to this block of code might have to be mirrored over to ResizeTraditional(). + // It constructs a temporary TextBuffer and then extracts the members below, overwriting itself. + _buffer = wil::unique_virtualalloc_ptr{ + static_cast(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE))) + }; + _bufferEnd = _buffer.get() + allocSize; + _commitWatermark = _buffer.get(); + _initialAttributes = defaultAttributes; + _bufferRowStride = rowStride; + _bufferOffsetChars = rowSize; + _bufferOffsetCharOffsets = rowSize + charsBufferSize; + _width = w; + _height = h; +} + +// MEM_COMMITs the memory and constructs all ROWs up to and including the given row pointer. +// It's expected that the caller verifies the parameter. It goes hand in hand with _getRowByOffsetDirect(). +// +// Declaring this function as noinline allows _getRowByOffsetDirect() to be inlined, +// which improves overall TextBuffer performance by ~6%. And all it cost is this annotation. +// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.) +__declspec(noinline) void TextBuffer::_commit(const std::byte* row) +{ + const auto rowEnd = row + _bufferRowStride; + const auto remaining = gsl::narrow_cast(_bufferEnd - _commitWatermark); + const auto minimum = gsl::narrow_cast(rowEnd - _commitWatermark); + const auto ideal = minimum + _bufferRowStride * _commitReadAheadRowCount; + const auto size = std::min(remaining, ideal); + + THROW_LAST_ERROR_IF_NULL(VirtualAlloc(_commitWatermark, size, MEM_COMMIT, PAGE_READWRITE)); + + _construct(_commitWatermark + size); +} + +// Destructs and MEM_DECOMMITs all previously constructed ROWs. +// You can use this (or rather the Reset() method) to fully clear the TextBuffer. +void TextBuffer::_decommit() noexcept +{ + _destroy(); + VirtualFree(_buffer.get(), 0, MEM_DECOMMIT); + _commitWatermark = _buffer.get(); +} + +// Constructs ROWs up to (excluding) the ROW pointed to by `until`. +void TextBuffer::_construct(const std::byte* until) noexcept +{ + for (; _commitWatermark < until; _commitWatermark += _bufferRowStride) + { + const auto row = reinterpret_cast(_commitWatermark); + const auto chars = reinterpret_cast(_commitWatermark + _bufferOffsetChars); + const auto indices = reinterpret_cast(_commitWatermark + _bufferOffsetCharOffsets); + std::construct_at(row, chars, indices, _width, _initialAttributes); + } +} + +// Destroys all previously constructed ROWs. +// Be careful! This doesn't reset any of the members, in particular the _commitWatermark. +void TextBuffer::_destroy() const noexcept +{ + for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride) + { + std::destroy_at(reinterpret_cast(it)); + } +} + +// This function is "direct" because it trusts the caller to properly wrap the "offset" +// parameter modulo the _height of the buffer, etc. But keep in mind that a offset=0 +// is the GetScratchpadRow() and not the GetRowByOffset(0). That one is offset=1. +ROW& TextBuffer::_getRowByOffsetDirect(size_t offset) +{ + const auto row = _buffer.get() + _bufferRowStride * offset; + THROW_HR_IF(E_UNEXPECTED, row < _buffer.get() || row >= _bufferEnd); + + if (row >= _commitWatermark) + { + _commit(row); } - _UpdateSize(); + 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. +til::CoordType TextBuffer::_estimateOffsetOfLastCommittedRow() const noexcept +{ + const auto lastRowOffset = (_commitWatermark - _buffer.get()) / _bufferRowStride; + // This subtracts 2 from the offset to account for the: + // * scratchpad row at offset 0, whereas regular rows start at offset 1. + // * fact that _commitWatermark points _past_ the last committed row, + // but we want to return an index pointing at the last row. + return std::max(0, gsl::narrow_cast(lastRowOffset - 2)); +} + +// Retrieves a row from the buffer by its offset from the first row of the text buffer +// (what corresponds to the top row of the screen buffer). +const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const +{ + 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::GetMutableRowByOffset(const til::CoordType index) +{ + _lastMutationId++; + return _getRow(index); +} + +// Returns a row filled with whitespace and the current attributes, for you to freely use. +ROW& TextBuffer::GetScratchpadRow() +{ + return GetScratchpadRow(_currentAttributes); +} + +// Returns a row filled with whitespace and the given attributes, for you to freely use. +ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes) +{ + auto& r = _getRowByOffsetDirect(0); + r.Reset(attributes); + return r; +} + +#pragma warning(pop) +#pragma endregion + // Routine Description: // - Copies properties from another text buffer into this one. // - This is primarily to copy properties that would otherwise not be specified during CreateInstance @@ -76,35 +266,28 @@ void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) noexcept // - Total number of rows in the buffer til::CoordType TextBuffer::TotalRowCount() const noexcept { - return gsl::narrow_cast(_storage.size()); + return _height; } -// Routine Description: -// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to -// the top row of the screen buffer) +// Method Description: +// - Gets the number of glyphs in the buffer between two points. +// - IMPORTANT: Make sure that start is before end, or this will never return! // Arguments: -// - Number of rows down from the first row of the buffer. +// - start - The starting point of the range to get the glyph count for. +// - end - The ending point of the range to get the glyph count for. // Return Value: -// - const reference to the requested row. Asserts if out of bounds. -const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const noexcept +// - The number of glyphs in the buffer between the two points. +size_t TextBuffer::GetCellDistance(const til::point from, const til::point to) const { - // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. - const auto offsetIndex = gsl::narrow_cast(_firstRow + index) % _storage.size(); - return til::at(_storage, offsetIndex); -} - -// Routine Description: -// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to -// the top row of the screen buffer) -// Arguments: -// - Number of rows down from the first row of the buffer. -// Return Value: -// - reference to the requested row. Asserts if out of bounds. -ROW& TextBuffer::GetRowByOffset(const til::CoordType index) noexcept -{ - // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. - const auto offsetIndex = gsl::narrow_cast(_firstRow + index) % _storage.size(); - return til::at(_storage, offsetIndex); + auto startCell = GetCellDataAt(from); + const auto endCell = GetCellDataAt(to); + auto delta = 0; + while (startCell != endCell) + { + ++startCell; + ++delta; + } + return delta; } // Routine Description: @@ -151,10 +334,10 @@ TextBufferTextIterator TextBuffer::GetTextLineDataAt(const til::point at) const TextBufferCellIterator TextBuffer::GetCellLineDataAt(const til::point at) const { til::inclusive_rect limit; - limit.Top = at.Y; - limit.Bottom = at.Y; - limit.Left = 0; - limit.Right = GetSize().RightInclusive(); + limit.top = at.y; + limit.bottom = at.y; + limit.left = 0; + limit.right = GetSize().RightInclusive(); return TextBufferCellIterator(*this, at, Viewport::FromInclusive(limit)); } @@ -185,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; - try - { - prevDbcsAttr = prevRow.GetCharRow().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.IsSingle() && dbcsAttribute.IsTrailing()) - { - // N, T failing case (uncorrectable) - fValidSequence = false; - } - else if (prevDbcsAttr.IsLeading()) - { - if (dbcsAttribute.IsSingle() || dbcsAttribute.IsLeading()) - { - // L, N and L, L failing cases (correctable) - fValidSequence = false; - fCorrectableByErase = true; - } - } - else if (prevDbcsAttr.IsTrailing() && dbcsAttribute.IsTrailing()) - { - // T, T failing case (uncorrectable) - fValidSequence = false; - } - - // If it's correctable by erase, erase the previous character - if (fCorrectableByErase) - { - // Erase previous character into an N type. - try - { - prevRow.ClearColumn(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 @@ -280,32 +378,165 @@ bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribut //Return Value: // - true if we successfully prepared the buffer and moved the cursor // - false otherwise (out of memory) -bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) +void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) { - // This function corrects most errors. If this is false, we had an uncorrectable one which - // older versions of conhost simply let pass by unflinching. - LOG_HR_IF(E_NOT_VALID_STATE, !(_AssertValidDoubleByteSequence(dbcsAttribute))); // Shouldn't be uncorrectable sequences unless something is very wrong. - - auto fSuccess = true; // Now compensate if we don't have enough space for the upcoming double byte sequence // We only need to compensate for leading bytes - if (dbcsAttribute.IsLeading()) + if (dbcsAttribute == DbcsAttribute::Leading) { const auto cursorPosition = GetCursor().GetPosition(); - const auto lineWidth = GetLineWidth(cursorPosition.Y); + const auto lineWidth = GetLineWidth(cursorPosition.y); // If we're about to lead on the last column in the row, we need to add a padding space - if (cursorPosition.X == lineWidth - 1) + 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 - fSuccess = IncrementCursor(); + IncrementCursor(); + } + } +} + +// 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 +{ + 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); +} + +// 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) +{ + auto& r = GetMutableRowByOffset(row); + r.ReplaceText(state); + r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes); + TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 })); +} + +// Fills an area of the buffer with a given fill character(s) and attributes. +void TextBuffer::FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes) +{ + if (!rect || fill.empty()) + { + return; + } + + auto& scratchpad = GetScratchpadRow(attributes); + + // The scratchpad row gets reset to whitespace by default, so there's no need to + // initialize it again. Filling with whitespace is the most common operation by far. + if (fill != L" ") + { + RowWriteState state{ + .columnLimit = rect.right, + .columnEnd = rect.left, + }; + + // Fill the scratchpad row with consecutive copies of "fill" up to the amount we need. + // + // We don't just create a single string with N copies of "fill" and write that at once, + // because that might join neighboring combining marks unintentionally. + // + // Building the buffer this way is very wasteful and slow, but it's still 3x + // faster than what we had before and no one complained about that either. + // It's seldom used code and probably not worth optimizing for. + while (state.columnEnd < rect.right) + { + state.columnBegin = state.columnEnd; + state.text = fill; + scratchpad.ReplaceText(state); + } + } + + // Fill the given rows with copies of the scratchpad row. That's a little + // slower when filling just a single row, but will be much faster for >1 rows. + { + RowCopyTextFromState state{ + .source = scratchpad, + .columnBegin = rect.left, + .columnLimit = rect.right, + .sourceColumnBegin = rect.left, + }; + + for (auto y = rect.top; y < rect.bottom; ++y) + { + auto& r = GetMutableRowByOffset(y); + r.CopyTextFrom(state); + r.ReplaceAttributes(rect.left, rect.right, attributes); + TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, y, state.columnEndDirty, y + 1 })); } } - return fSuccess; } // Routine Description: @@ -353,8 +584,8 @@ OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt, it = WriteLine(it, lineTarget, wrap); // Move to the next line down. - lineTarget.X = 0; - ++lineTarget.Y; + lineTarget.x = 0; + ++lineTarget.y; } return it; @@ -381,8 +612,8 @@ OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, } // Get the row and write the cells - auto& row = GetRowByOffset(target.Y); - const auto newIt = row.WriteCells(givenIt, target.X, wrap, limitRight); + 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. const auto written = newIt.GetCellDistance(givenIt); @@ -401,45 +632,37 @@ OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, //Return Value: // - true if we successfully inserted the character // - false otherwise (out of memory) -bool TextBuffer::InsertCharacter(const std::wstring_view chars, +void TextBuffer::InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr) { // Ensure consistent buffer state for double byte characters based on the character type we're about to insert - auto fSuccess = _PrepareForDoubleByteSequence(dbcsAttribute); + _PrepareForDoubleByteSequence(dbcsAttribute); - if (fSuccess) + // Get the current cursor position + const auto iRow = GetCursor().GetPosition().y; // row stored as logical position, not array position + const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal. + + // Get the row associated with the given logical position + auto& Row = GetMutableRowByOffset(iRow); + + // Store character and double byte data + switch (dbcsAttribute) { - // Get the current cursor position - const auto iRow = GetCursor().GetPosition().Y; // row stored as logical position, not array position - const auto iCol = GetCursor().GetPosition().X; // column logical and array positions are equal. - - // Get the row associated with the given logical position - auto& Row = GetRowByOffset(iRow); - - // Store character and double byte data - auto& charRow = Row.GetCharRow(); - - try - { - charRow.GlyphAt(iCol) = chars; - charRow.DbcsAttrAt(iCol) = dbcsAttribute; - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - return false; - } - - // Store color data - fSuccess = Row.GetAttrRow().SetAttrToEnd(iCol, attr); - if (fSuccess) - { - // Advance the cursor - fSuccess = IncrementCursor(); - } + case DbcsAttribute::Leading: + Row.ReplaceCharacters(iCol, 2, chars); + break; + case DbcsAttribute::Trailing: + Row.ReplaceCharacters(iCol - 1, 2, chars); + break; + default: + Row.ReplaceCharacters(iCol, 1, chars); + break; } - return fSuccess; + + // Store color data + Row.SetAttrToEnd(iCol, attr); + IncrementCursor(); } //Routine Description: @@ -451,9 +674,9 @@ bool TextBuffer::InsertCharacter(const std::wstring_view chars, //Return Value: // - true if we successfully inserted the character // - false otherwise (out of memory) -bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr) +void TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr) { - return InsertCharacter({ &wch, 1 }, dbcsAttribute, attr); + InsertCharacter({ &wch, 1 }, dbcsAttribute, attr); } //Routine Description: @@ -463,7 +686,7 @@ bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttr // - - Always sets to wrap //Return Value: // - -void TextBuffer::_SetWrapOnCurrentRow() noexcept +void TextBuffer::_SetWrapOnCurrentRow() { _AdjustWrapOnCurrentRow(true); } @@ -475,13 +698,13 @@ void TextBuffer::_SetWrapOnCurrentRow() noexcept // - fSet - True if this row has a wrap. False otherwise. //Return Value: // - -void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) noexcept +void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) { // The vertical position of the cursor represents the current row we're manipulating. - const auto uiCurrentRowOffset = GetCursor().GetPosition().Y; + const auto uiCurrentRowOffset = GetCursor().GetPosition().y; // Set the wrap status as appropriate - GetRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet); + GetMutableRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet); } //Routine Description: @@ -492,27 +715,25 @@ void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) noexcept //Return Value: // - true if we successfully moved the cursor. // - false otherwise (out of memory) -bool TextBuffer::IncrementCursor() +void TextBuffer::IncrementCursor() { // Cursor position is stored as logical array indices (starts at 0) for the window // Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79. // So subtract 1 from buffer size in each direction to find the index of the final column in the buffer - const auto iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().Y) - 1; + const auto iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().y) - 1; // Move the cursor one position to the right GetCursor().IncrementXPosition(1); - auto fSuccess = true; // If we've passed the final valid column... - if (GetCursor().GetPosition().X > iFinalColumnIndex) + if (GetCursor().GetPosition().x > iFinalColumnIndex) { // Then mark that we've been forced to wrap _SetWrapOnCurrentRow(); // Then move the cursor to a new line - fSuccess = NewlineCursor(); + NewlineCursor(); } - return fSuccess; } //Routine Description: @@ -521,9 +742,8 @@ bool TextBuffer::IncrementCursor() // - //Return Value: // - true if we successfully moved the cursor. -bool TextBuffer::NewlineCursor() +void TextBuffer::NewlineCursor() { - auto fSuccess = false; const auto iFinalRowIndex = GetSize().BottomInclusive(); // Reset the cursor position to 0 and move down one line @@ -531,28 +751,23 @@ bool TextBuffer::NewlineCursor() GetCursor().IncrementYPosition(1); // If we've passed the final valid row... - if (GetCursor().GetPosition().Y > iFinalRowIndex) + if (GetCursor().GetPosition().y > iFinalRowIndex) { // Stay on the final logical/offset row of the buffer. GetCursor().SetYPosition(iFinalRowIndex); // Instead increment the circular buffer to move us into the "oldest" row of the backing buffer - fSuccess = IncrementCircularBuffer(); + IncrementCircularBuffer(); } - else - { - fSuccess = true; - } - return fSuccess; } //Routine Description: // - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable. //Arguments: -// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row. +// - fillAttributes - the attributes with which the recycled row will be initialized. //Return Value: // - true if we successfully incremented the buffer. -bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) +void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes) { // FirstRow is at any given point in time the array index in the circular buffer that corresponds // to the logical position 0 in the window (cursor coordinates and all other coordinates). @@ -565,15 +780,7 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) _PruneHyperlinks(); // Second, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. - auto fillAttributes = _currentAttributes; - if (inVtMode) - { - // The VT standard requires that the new row is initialized with - // the current background color, but with no meta attributes set. - fillAttributes.SetStandardErase(); - } - const auto fSuccess = _storage.at(_firstRow).Reset(fillAttributes); - if (fSuccess) + 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) @@ -585,7 +792,6 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) _firstRow = 0; } } - return fSuccess; } //Routine Description: @@ -600,34 +806,34 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) // - The viewport //Return value: // - Coordinate position (relative to the text buffer) -til::point TextBuffer::GetLastNonSpaceCharacter(std::optional viewOptional) const +til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) const { - const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize(); + const auto viewport = viewOptional ? *viewOptional : GetSize(); til::point coordEndOfText; // Search the given viewport by starting at the bottom. - coordEndOfText.Y = viewport.BottomInclusive(); + coordEndOfText.y = std::min(viewport.BottomInclusive(), _estimateOffsetOfLastCommittedRow()); - const auto& currRow = GetRowByOffset(coordEndOfText.Y); + const auto& currRow = GetRowByOffset(coordEndOfText.y); // The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character) - coordEndOfText.X = currRow.GetCharRow().MeasureRight() - 1; + coordEndOfText.x = currRow.MeasureRight() - 1; // If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text. const auto viewportTop = viewport.Top(); - auto fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop); // this row is empty, and we're not at the top + auto fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop); // this row is empty, and we're not at the top while (fDoBackUp) { - coordEndOfText.Y--; - const auto& backupRow = GetRowByOffset(coordEndOfText.Y); + coordEndOfText.y--; + const auto& backupRow = GetRowByOffset(coordEndOfText.y); // We need to back up to the previous row if this line is empty, AND there are more rows - coordEndOfText.X = backupRow.GetCharRow().MeasureRight() - 1; - fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop); + coordEndOfText.x = backupRow.MeasureRight() - 1; + fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop); } // don't allow negative results - coordEndOfText.Y = std::max(coordEndOfText.Y, 0); - coordEndOfText.X = std::max(coordEndOfText.X, 0); + coordEndOfText.y = std::max(coordEndOfText.y, 0); + coordEndOfText.x = std::max(coordEndOfText.x, 0); return coordEndOfText; } @@ -639,25 +845,25 @@ til::point TextBuffer::GetLastNonSpaceCharacter(std::optional 0) + if (coordPosition.x > 0) { - coordPosition.X--; + coordPosition.x--; } else { // Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous) - if (coordPosition.Y > 0) + if (coordPosition.y > 0) { // move the cursor up one line - coordPosition.Y--; + coordPosition.y--; // and to the right edge - coordPosition.X = GetLineWidth(coordPosition.Y) - 1; + coordPosition.x = GetLineWidth(coordPosition.y) - 1; } } @@ -671,12 +877,7 @@ const til::CoordType TextBuffer::GetFirstRowIndex() const noexcept const Viewport TextBuffer::GetSize() const noexcept { - return _size; -} - -void TextBuffer::_UpdateSize() -{ - _size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow(_storage.size()) }); + return Viewport::FromDimensions({ _width, _height }); } void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept @@ -684,27 +885,21 @@ void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept _firstRow = FirstRowIndex; } -void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType size, const til::CoordType delta) +void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size, const til::CoordType delta) { - // If we don't have to move anything, leave early. if (delta == 0) { return; } - // OK. We're about to play games by moving rows around within the deque to - // scroll a massive region in a faster way than copying things. - // To make this easier, first correct the circular buffer to have the first row be 0 again. - if (_firstRow != 0) - { - // Rotate the buffer to put the first row at the front. - std::rotate(_storage.begin(), _storage.begin() + _firstRow, _storage.end()); + // Since the for() loop uses !=, we must ensure that size is positive. + // A negative size doesn't make any sense anyways. + size = std::max(0, size); - // The first row is now at the top. - _firstRow = 0; - } + til::CoordType y = 0; + til::CoordType end = 0; + til::CoordType step = 0; - // Rotate just the subsection specified if (delta < 0) { // The layout is like this: @@ -714,33 +909,20 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType // | 0 begin // | 1 // | 2 - // | 3 A. begin + firstRow + delta (because delta is negative) + // | 3 A. firstRow + delta (because delta is negative) // | 4 - // | 5 B. begin + firstRow + // | 5 B. firstRow // | 6 // | 7 - // | 8 C. begin + firstRow + size + // | 8 C. firstRow + size // | 9 // | 10 // | 11 // - end // We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it. - // So the final layout will be - // --- (storage) ---- - // | 0 begin - // | 1 - // | 2 - // | 5 - // | 6 - // | 7 - // | 3 - // | 4 - // | 8 - // | 9 - // | 10 - // | 11 - // - end - std::rotate(_storage.begin() + firstRow + delta, _storage.begin() + firstRow, _storage.begin() + firstRow + size); + y = firstRow; + end = firstRow + size; + step = 1; } else { @@ -753,36 +935,24 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType // | 2 // | 3 // | 4 - // | 5 A. begin + firstRow + // | 5 A. firstRow // | 6 // | 7 - // | 8 B. begin + firstRow + size + // | 8 B. firstRow + size // | 9 - // | 10 C. begin + firstRow + size + delta + // | 10 C. firstRow + size + delta // | 11 // - end // We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it. - // So the final layout will be - // --- (storage) ---- - // | 0 begin - // | 1 - // | 2 - // | 3 - // | 4 - // | 8 - // | 9 - // | 5 - // | 6 - // | 7 - // | 10 - // | 11 - // - end - std::rotate(_storage.begin() + firstRow, _storage.begin() + firstRow + size, _storage.begin() + firstRow + size + delta); + y = firstRow + size - 1; + end = firstRow - 1; + step = -1; } - // Renumber the IDs now that we've rearranged where the rows sit within the buffer. - // Refreshing should also delegate to the UnicodeStorage to re-key all the stored unicode sequences (where applicable). - _RefreshRowIDs(std::nullopt); + for (; y != end; y += step) + { + GetMutableRowByOffset(y + delta).CopyFrom(GetRowByOffset(y)); + } } Cursor& TextBuffer::GetCursor() noexcept @@ -795,7 +965,12 @@ const Cursor& TextBuffer::GetCursor() const noexcept return _cursor; } -[[nodiscard]] TextAttribute TextBuffer::GetCurrentAttributes() const noexcept +uint64_t TextBuffer::GetLastMutationId() const noexcept +{ + return _lastMutationId; +} + +const TextAttribute& TextBuffer::GetCurrentAttributes() const noexcept { return _currentAttributes; } @@ -805,11 +980,16 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no _currentAttributes = currentAttributes; } -void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition) +void TextBuffer::SetWrapForced(const til::CoordType y, bool 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); + const auto rowIndex = cursorPosition.y; + auto& row = GetMutableRowByOffset(rowIndex); if (row.GetLineRendition() != lineRendition) { row.SetLineRendition(lineRendition); @@ -819,11 +999,9 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition) if (lineRendition != LineRendition::SingleWidth) { const auto fillChar = L' '; - auto fillAttrs = GetCurrentAttributes(); - fillAttrs.SetStandardErase(); const auto fillOffset = GetLineWidth(rowIndex); const auto fillLength = gsl::narrow(GetSize().Width() - fillOffset); - const OutputCellIterator fillData{ fillChar, fillAttrs, fillLength }; + const OutputCellIterator fillData{ fillChar, fillAttributes, fillLength }; row.WriteCells(fillData, fillOffset, false); // We also need to make sure the cursor is clamped within the new width. GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition)); @@ -832,62 +1010,58 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition) } } -void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept +void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) { for (auto row = startRow; row < endRow; row++) { - GetRowByOffset(row).SetLineRendition(LineRendition::SingleWidth); + GetMutableRowByOffset(row).SetLineRendition(LineRendition::SingleWidth); } } -LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const noexcept +LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const { return GetRowByOffset(row).GetLineRendition(); } -bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const noexcept +bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const { return GetLineRendition(row) != LineRendition::SingleWidth; } -til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const noexcept +til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const { // Use shift right to quickly divide the width by 2 for double width lines. const auto scale = IsDoubleWidthLine(row) ? 1 : 0; return GetSize().Width() >> scale; } -til::point TextBuffer::ClampPositionWithinLine(const til::point position) const noexcept +til::point TextBuffer::ClampPositionWithinLine(const til::point position) const { - const auto rightmostColumn = GetLineWidth(position.Y) - 1; - return { std::min(position.X, rightmostColumn), position.Y }; + const auto rightmostColumn = GetLineWidth(position.y) - 1; + return { std::min(position.x, rightmostColumn), position.y }; } -til::point TextBuffer::ScreenToBufferPosition(const til::point position) const noexcept +til::point TextBuffer::ScreenToBufferPosition(const til::point position) const { // Use shift right to quickly divide the X pos by 2 for double width lines. - const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0; - return { position.X >> scale, position.Y }; + const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0; + return { position.x >> scale, position.y }; } -til::point TextBuffer::BufferToScreenPosition(const til::point position) const noexcept +til::point TextBuffer::BufferToScreenPosition(const til::point position) const { // Use shift left to quickly multiply the X pos by 2 for double width lines. - const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0; - return { position.X << scale, position.Y }; + const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0; + return { position.x << scale, position.y }; } // Routine Description: // - Resets the text contents of this buffer with the default character // and the default current color attributes -void TextBuffer::Reset() +void TextBuffer::Reset() noexcept { - const auto attr = GetCurrentAttributes(); - - for (auto& row : _storage) - { - row.Reset(attr); - } + _decommit(); + _initialAttributes = _currentAttributes; } // Routine Description: @@ -896,64 +1070,40 @@ void TextBuffer::Reset() // - newSize - new size of screen. // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. -[[nodiscard]] NTSTATUS TextBuffer::ResizeTraditional(const til::size newSize) noexcept +void TextBuffer::ResizeTraditional(til::size newSize) { - RETURN_HR_IF(E_INVALIDARG, newSize.X < 0 || newSize.Y < 0); + // Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text. + newSize.width = std::max(newSize.width, 1); + newSize.height = std::max(newSize.height, 1); - try + TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; + const auto cursorRow = GetCursor().GetPosition().y; + const auto copyableRows = std::min(_height, newSize.height); + til::CoordType srcRow = 0; + til::CoordType dstRow = 0; + + if (cursorRow >= newSize.height) { - const auto currentSize = GetSize().Dimensions(); - const auto attributes = GetCurrentAttributes(); - - til::CoordType TopRow = 0; // new top row of the screen buffer - if (newSize.Y <= GetCursor().GetPosition().Y) - { - TopRow = GetCursor().GetPosition().Y - newSize.Y + 1; - } - const auto TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.Y; - - // rotate rows until the top row is at index 0 - for (auto i = 0; i < TopRowIndex; i++) - { - _storage.emplace_back(std::move(_storage.front())); - _storage.erase(_storage.begin()); - } - - _SetFirstRowIndex(0); - - // realloc in the Y direction - // remove rows if we're shrinking - while (_storage.size() > static_cast(newSize.Y)) - { - _storage.pop_back(); - } - // add rows if we're growing - while (_storage.size() < static_cast(newSize.Y)) - { - _storage.emplace_back(gsl::narrow_cast(_storage.size()), newSize.X, attributes, this); - } - - // Now that we've tampered with the row placement, refresh all the row IDs. - // Also take advantage of the row ID refresh loop to resize the rows in the X dimension - // and cleanup the UnicodeStorage characters that might fall outside the resized buffer. - _RefreshRowIDs(newSize.X); - - // Update the cached size value - _UpdateSize(); + srcRow = cursorRow - newSize.height + 1; } - CATCH_RETURN(); - return S_OK; -} + for (; dstRow < copyableRows; ++dstRow, ++srcRow) + { + newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); + } -const UnicodeStorage& TextBuffer::GetUnicodeStorage() const noexcept -{ - return _unicodeStorage; -} + // NOTE: Keep this in sync with _reserve(). + _buffer = std::move(newBuffer._buffer); + _bufferEnd = newBuffer._bufferEnd; + _commitWatermark = newBuffer._commitWatermark; + _initialAttributes = newBuffer._initialAttributes; + _bufferRowStride = newBuffer._bufferRowStride; + _bufferOffsetChars = newBuffer._bufferOffsetChars; + _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets; + _width = newBuffer._width; + _height = newBuffer._height; -UnicodeStorage& TextBuffer::GetUnicodeStorage() noexcept -{ - return _unicodeStorage; + _SetFirstRowIndex(0); } void TextBuffer::SetAsActiveBuffer(const bool isActiveBuffer) noexcept @@ -1019,76 +1169,6 @@ void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText) } } -// Routine Description: -// - Method to help refresh all the Row IDs after manipulating the row -// by shuffling pointers around. -// - This will also update parent pointers that are stored in depth within the buffer -// (e.g. it will update CharRow parents pointing at Rows that might have been moved around) -// - Optionally takes a new row width if we're resizing to perform a resize operation and cleanup -// any high unicode (UnicodeStorage) runs while we're already looping through the rows. -// Arguments: -// - newRowWidth - Optional new value for the row width. -void TextBuffer::_RefreshRowIDs(std::optional newRowWidth) -{ - std::unordered_map rowMap; - til::CoordType i = 0; - til::CoordType newWidth = newRowWidth.value_or(_storage.at(0).size()); - for (auto& it : _storage) - { - // Build a map so we can update Unicode Storage - rowMap.emplace(it.GetId(), i); - - // Update the IDs - it.SetId(i++); - - // Also update the char row parent pointers as they can get shuffled up in the rotates. - it.GetCharRow().UpdateParent(&it); - - // Realloc in the X direction - // BODGY: We unpack the optional early and resize here unconditionally - // due to a codegen issue in LKG14. We used to check the optional in - // every iteration of the loop, but that resulted in the optimizer - // emitting a copy of the loop, used when the optional was empty, that - // never exited. Oops. - THROW_IF_FAILED(it.Resize(newWidth)); - } - - // Give the new mapping to Unicode Storage - _unicodeStorage.Remap(rowMap, newRowWidth); -} - -// Routine Description: -// - Retrieves the first row from the underlying buffer. -// Arguments: -// - -// Return Value: -// - reference to the first row. -ROW& TextBuffer::_GetFirstRow() noexcept -{ - return GetRowByOffset(0); -} - -// Routine Description: -// - Retrieves the row that comes before the given row. -// - Does not wrap around the screen buffer. -// Arguments: -// - The current row. -// Return Value: -// - reference to the previous row -// Note: -// - will throw exception if called with the first row of the text buffer -ROW& TextBuffer::_GetPrevRowNoWrap(const ROW& Row) -{ - auto prevRowIndex = Row.GetId() - 1; - if (prevRowIndex < 0) - { - prevRowIndex = TotalRowCount() - 1; - } - - THROW_HR_IF(E_FAIL, Row.GetId() == _firstRow); - return _storage.at(prevRowIndex); -} - // Method Description: // - get delimiter class for buffer cell position // - used for double click selection and uia word navigation @@ -1099,7 +1179,7 @@ ROW& TextBuffer::_GetPrevRowNoWrap(const ROW& Row) // - the delimiter class for the given char DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const { - return GetRowByOffset(pos.Y).GetCharRow().DelimiterClassAt(pos.X, wordDelimiters); + return GetRowByOffset(pos.y).DelimiterClassAt(pos.x, wordDelimiters); } // Method Description: @@ -1216,7 +1296,7 @@ til::point TextBuffer::_GetWordStartForSelection(const til::point target, const const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters); // expand left until we hit the left boundary or a different delimiter class - while (result.X > bufferSize.Left() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)) + while (result.x > bufferSize.Left() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)) { bufferSize.DecrementInBounds(result); } @@ -1334,7 +1414,7 @@ til::point TextBuffer::_GetWordEndForSelection(const til::point target, const st const auto bufferSize = GetSize(); // can't expand right - if (target.X == bufferSize.RightInclusive()) + if (target.x == bufferSize.RightInclusive()) { return target; } @@ -1343,7 +1423,7 @@ til::point TextBuffer::_GetWordEndForSelection(const til::point target, const st const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters); // expand right until we hit the right boundary or a different delimiter class - while (result.X < bufferSize.RightInclusive() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)) + while (result.x < bufferSize.RightInclusive() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)) { bufferSize.IncrementInBounds(result); } @@ -1364,7 +1444,7 @@ void TextBuffer::_PruneHyperlinks() // If the buffer does not contain the same reference, we can remove that hyperlink from our map // This way, obsolete hyperlink references are cleared from our hyperlink map instead of hanging around // Get all the hyperlink references in the row we're erasing - const auto hyperlinks = _storage.at(_firstRow).GetAttrRow().GetHyperlinks(); + const auto hyperlinks = GetRowByOffset(0).GetHyperlinks(); if (!hyperlinks.empty()) { @@ -1380,7 +1460,7 @@ void TextBuffer::_PruneHyperlinks() // to see if those references are anywhere else for (til::CoordType i = 1; i < total; ++i) { - const auto nextRowRefs = GetRowByOffset(i).GetAttrRow().GetHyperlinks(); + const auto nextRowRefs = GetRowByOffset(i).GetHyperlinks(); for (auto id : nextRowRefs) { if (firstRowRefs.find(id) != firstRowRefs.end()) @@ -1474,7 +1554,7 @@ til::point TextBuffer::GetGlyphStart(const til::point pos, std::optionalDbcsAttr().IsTrailing()) + if (resultPos != limit && GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Trailing) { bufferSize.DecrementInBounds(resultPos, true); } @@ -1501,7 +1581,7 @@ til::point TextBuffer::GetGlyphEnd(const til::point pos, bool accessibilityMode, resultPos = limit; } - if (resultPos != limit && GetCellDataAt(resultPos)->DbcsAttr().IsLeading()) + if (resultPos != limit && GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Leading) { bufferSize.IncrementInBounds(resultPos, true); } @@ -1549,7 +1629,7 @@ bool TextBuffer::MoveToNextGlyph(til::point& pos, bool allowExclusiveEnd, std::o const bool success{ ++iter }; // Move again if we're on a wide glyph - if (success && iter->DbcsAttr().IsTrailing()) + if (success && iter->DbcsAttr() == DbcsAttribute::Trailing) { ++iter; } @@ -1581,7 +1661,7 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos, std::optional // try to move. If we can't, we're done. const auto success = bufferSize.DecrementInBounds(resultPos, true); - if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsLeading()) + if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Leading) { bufferSize.DecrementInBounds(resultPos, true); } @@ -1617,25 +1697,25 @@ const std::vector TextBuffer::GetTextRects(til::point start std::make_tuple(start, end) : std::make_tuple(end, start); - const auto textRectSize = 1 + lowerCoord.Y - higherCoord.Y; + const auto textRectSize = 1 + lowerCoord.y - higherCoord.y; textRects.reserve(textRectSize); - for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++) + for (auto row = higherCoord.y; row <= lowerCoord.y; row++) { til::inclusive_rect textRow; - textRow.Top = row; - textRow.Bottom = row; + textRow.top = row; + textRow.bottom = row; - if (blockSelection || higherCoord.Y == lowerCoord.Y) + if (blockSelection || higherCoord.y == lowerCoord.y) { // set the left and right margin to the left-/right-most respectively - textRow.Left = std::min(higherCoord.X, lowerCoord.X); - textRow.Right = std::max(higherCoord.X, lowerCoord.X); + textRow.left = std::min(higherCoord.x, lowerCoord.x); + textRow.right = std::max(higherCoord.x, lowerCoord.x); } else { - textRow.Left = (row == higherCoord.Y) ? higherCoord.X : bufferSize.Left(); - textRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferSize.RightInclusive(); + textRow.left = (row == higherCoord.y) ? higherCoord.x : bufferSize.Left(); + textRow.right = (row == lowerCoord.y) ? lowerCoord.x : bufferSize.RightInclusive(); } // If we were passed screen coordinates, convert the given range into @@ -1676,8 +1756,8 @@ std::vector TextBuffer::GetTextSpans(til::point start, til::poi for (auto rect : rects) { - const til::point first = { rect.Left, rect.Top }; - const til::point second = { rect.Right, rect.Bottom }; + const til::point first = { rect.left, rect.top }; + const til::point second = { rect.right, rect.bottom }; textSpans.emplace_back(first, second); } } @@ -1698,16 +1778,16 @@ std::vector TextBuffer::GetTextSpans(til::point start, til::poi // equivalent buffer offsets, taking line rendition into account. if (!bufferCoordinates) { - higherCoord = ScreenToBufferLine(higherCoord, GetLineRendition(higherCoord.Y)); - lowerCoord = ScreenToBufferLine(lowerCoord, GetLineRendition(lowerCoord.Y)); + higherCoord = ScreenToBufferLine(higherCoord, GetLineRendition(higherCoord.y)); + lowerCoord = ScreenToBufferLine(lowerCoord, GetLineRendition(lowerCoord.y)); } - til::inclusive_rect asRect = { higherCoord.X, higherCoord.Y, lowerCoord.X, lowerCoord.Y }; + til::inclusive_rect asRect = { higherCoord.x, higherCoord.y, lowerCoord.x, lowerCoord.y }; _ExpandTextRow(asRect); - higherCoord.X = asRect.Left; - higherCoord.Y = asRect.Top; - lowerCoord.X = asRect.Right; - lowerCoord.Y = asRect.Bottom; + higherCoord.x = asRect.left; + higherCoord.y = asRect.top; + lowerCoord.x = asRect.right; + lowerCoord.y = asRect.bottom; textSpans.emplace_back(higherCoord, lowerCoord); } @@ -1727,10 +1807,10 @@ void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const const auto bufferSize = GetSize(); // expand left side of rect - til::point targetPoint{ textRow.Left, textRow.Top }; - if (GetCellDataAt(targetPoint)->DbcsAttr().IsTrailing()) + til::point targetPoint{ textRow.left, textRow.top }; + if (GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Trailing) { - if (targetPoint.X == bufferSize.Left()) + if (targetPoint.x == bufferSize.Left()) { bufferSize.IncrementInBounds(targetPoint); } @@ -1738,14 +1818,14 @@ void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const { bufferSize.DecrementInBounds(targetPoint); } - textRow.Left = targetPoint.X; + textRow.left = targetPoint.x; } // expand right side of rect - targetPoint = { textRow.Right, textRow.Bottom }; - if (GetCellDataAt(targetPoint)->DbcsAttr().IsLeading()) + targetPoint = { textRow.right, textRow.bottom }; + if (GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Leading) { - if (targetPoint.X == bufferSize.RightInclusive()) + if (targetPoint.x == bufferSize.RightInclusive()) { bufferSize.DecrementInBounds(targetPoint); } @@ -1753,7 +1833,7 @@ void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const { bufferSize.IncrementInBounds(targetPoint); } - textRow.Right = targetPoint.X; + textRow.right = targetPoint.x; } } @@ -1788,7 +1868,7 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF, // for each row in the selection for (size_t i = 0; i < rows; i++) { - const auto iRow = selectionRects.at(i).Top; + const auto iRow = selectionRects.at(i).top; const auto highlight = Viewport::FromInclusive(selectionRects.at(i)); @@ -1813,7 +1893,7 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF, { const auto& cell = *it; - if (!cell.DbcsAttr().IsTrailing()) + if (cell.DbcsAttr() != DbcsAttribute::Trailing) { const auto chars = cell.Chars(); selectionText.append(chars); @@ -1913,7 +1993,7 @@ std::wstring TextBuffer::GetPlainText(const til::point& start, const til::point& for (; it && spanLength > 0; ++it, --spanLength) { const auto& cell = *it; - if (!cell.DbcsAttr().IsTrailing()) + if (cell.DbcsAttr() != DbcsAttribute::Trailing) { const auto chars = cell.Chars(); text.append(chars); @@ -2326,223 +2406,181 @@ void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::w // the new buffer. The rows's new value is placed back into this parameter. // Return Value: // - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT. -HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo) +void TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Viewport* lastCharacterViewport, PositionInformation* positionInfo) { const auto& oldCursor = oldBuffer.GetCursor(); auto& newCursor = newBuffer.GetCursor(); - // We need to save the old cursor position so that we can - // place the new cursor back on the equivalent character in - // the new buffer. - const auto cOldCursorPos = oldCursor.GetPosition(); - const auto cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport); + til::point oldCursorPos = oldCursor.GetPosition(); + til::point newCursorPos; - const auto cOldRowsTotal = cOldLastChar.Y + 1; + // BODGY: We use oldCursorPos in two critical places below: + // * To compute an oldHeight that includes at a minimum the cursor row + // * For REFLOW_JANK_CURSOR_WRAP (see comment below) + // Both of these would break the reflow algorithm, but the latter of the two in particular + // would cause the main copy loop below to deadlock. In other words, these two lines + // protect this function against yet-unknown bugs in other parts of the code base. + oldCursorPos.x = std::clamp(oldCursorPos.x, 0, oldBuffer._width - 1); + oldCursorPos.y = std::clamp(oldCursorPos.y, 0, oldBuffer._height - 1); - til::point cNewCursorPos; - auto fFoundCursorPos = false; - auto foundOldMutable = false; - auto foundOldVisible = false; - auto hr = S_OK; - // Loop through all the rows of the old buffer and reprint them into the new buffer - til::CoordType iOldRow = 0; - for (; iOldRow < cOldRowsTotal; iOldRow++) + const auto lastRowWithText = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport).y; + + auto mutableViewportTop = positionInfo ? positionInfo->mutableViewportTop : til::CoordTypeMax; + auto visibleViewportTop = positionInfo ? positionInfo->visibleViewportTop : til::CoordTypeMax; + + til::CoordType oldY = 0; + til::CoordType newY = 0; + til::CoordType newX = 0; + til::CoordType newWidth = newBuffer.GetSize().Width(); + til::CoordType newYLimit = til::CoordTypeMax; + + const auto oldHeight = std::max(lastRowWithText, oldCursorPos.y) + 1; + const auto newHeight = newBuffer.GetSize().Height(); + const auto newWidthU16 = gsl::narrow_cast(newWidth); + + // Copy oldBuffer into newBuffer until oldBuffer has been fully consumed. + for (; oldY < oldHeight && newY < newYLimit; ++oldY) { - // Fetch the row and its "right" which is the last printable character. - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - const auto cOldColsTotal = oldBuffer.GetLineWidth(iOldRow); - const auto& charRow = row.GetCharRow(); - auto iRight = charRow.MeasureRight(); + const auto& oldRow = oldBuffer.GetRowByOffset(oldY); - // If we're starting a new row, try and preserve the line rendition - // from the row in the original buffer. - const auto newBufferPos = newBuffer.GetCursor().GetPosition(); - if (newBufferPos.X == 0) + // A pair of double height rows should optimally wrap as a union (i.e. after wrapping there should be 4 lines). + // But for this initial implementation I chose the alternative approach: Just truncate them. + if (oldRow.GetLineRendition() != LineRendition::SingleWidth) { - auto& newRow = newBuffer.GetRowByOffset(newBufferPos.Y); - newRow.SetLineRendition(row.GetLineRendition()); - } - - // There is a special case here. If the row has a "wrap" - // flag on it, but the right isn't equal to the width (one - // index past the final valid index in the row) then there - // were a bunch trailing of spaces in the row. - // (But the measuring functions for each row Left/Right do - // not count spaces as "displayable" so they're not - // included.) - // As such, adjust the "right" to be the width of the row - // to capture all these spaces - if (row.WasWrapForced()) - { - iRight = cOldColsTotal; - - // And a combined special case. - // If we wrapped off the end of the row by adding a - // piece of padding because of a double byte LEADING - // character, then remove one from the "right" to - // leave this padding out of the copy process. - if (row.WasDoubleBytePadded()) + // Since rows with a non-standard line rendition should be truncated it's important + // that we pretend as if the previous row ended in a newline, even if it didn't. + // This is what this if does: It newlines. + if (newX) { - iRight--; - } - } - - // Loop through every character in the current row (up to - // the "right" boundary, which is one past the final valid - // character) - til::CoordType iOldCol = 0; - const auto copyRight = iRight; - for (; iOldCol < copyRight; iOldCol++) - { - if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) - { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; + newX = 0; + newY++; } - try - { - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto glyph = row.GetCharRow().GlyphAt(iOldCol); - const auto dbcsAttr = row.GetCharRow().DbcsAttrAt(iOldCol); - const auto textAttr = row.GetAttrRow().GetAttrByColumn(iOldCol); + auto& newRow = newBuffer.GetMutableRowByOffset(newY); - if (!newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr)) - { - hr = E_OUTOFMEMORY; - break; - } + // See the comment marked with "REFLOW_RESET". + if (newY >= newHeight) + { + newRow.Reset(newBuffer._initialAttributes); } - CATCH_RETURN(); + + newRow.CopyFrom(oldRow); + newRow.SetWrapForced(false); + + if (oldY == oldCursorPos.y) + { + newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x), newY }; + } + if (oldY >= mutableViewportTop) + { + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; + } + if (oldY >= visibleViewportTop) + { + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; + } + + newY++; + continue; } - // GH#32: Copy the attributes from the rest of the row into this new buffer. - // From where we are in the old buffer, to the end of the row, copy the - // remaining attributes. - // - if the old buffer is smaller than the new buffer, then just copy - // what we have, as it was. We already copied all _text_ with colors, - // but it's possible for someone to just put some color into the - // buffer to the right of that without any text (as just spaces). The - // buffer looks weird to the user when we resize and it starts losing - // those colors, so we need to copy them over too... as long as there - // is space. The last attr in the row will be extended to the end of - // the row in the new buffer. - // - if the old buffer is WIDER, than we might have wrapped onto a new - // line. Use the cursor's position's Y so that we know where the new - // row is, and start writing at the cursor position. Again, the attr - // in the last column of the old row will be extended to the end of the - // row that the text was flowed onto. - // - if the text in the old buffer didn't actually fill the whole - // line in the new buffer, then we didn't wrap. That's fine. just - // copy attributes from the old row till the end of the new row, and - // move on. - const auto newRowY = newCursor.GetPosition().Y; - auto& newRow = newBuffer.GetRowByOffset(newRowY); - auto newAttrColumn = newCursor.GetPosition().X; - const auto newWidth = newBuffer.GetLineWidth(newRowY); - // Stop when we get to the end of the buffer width, or the new position - // for inserting an attr would be past the right of the new buffer. - for (auto copyAttrCol = iOldCol; - copyAttrCol < cOldColsTotal && newAttrColumn < newWidth; - copyAttrCol++, newAttrColumn++) + // Rows don't store any information for what column the last written character is in. + // We simply truncate all trailing whitespace in this implementation. + auto oldRowLimit = oldRow.MeasureRight(); + if (oldY == oldCursorPos.y) { - try + // REFLOW_JANK_CURSOR_WRAP: + // Pretending as if there's always at least whitespace in front of the cursor has the benefit that + // * the cursor retains its distance from any preceding text. + // * when a client application starts writing on this new, empty line, + // enlarging the buffer unwraps the text onto the preceding line. + oldRowLimit = std::max(oldRowLimit, oldCursorPos.x + 1); + } + + til::CoordType oldX = 0; + + // Copy oldRow into newBuffer until oldRow has been fully consumed. + // We use a do-while loop to ensure that line wrapping occurs and + // that attributes are copied over even for seemingly empty rows. + do + { + // This if condition handles line wrapping. + // Only if we write past the last column we should wrap and as such this if + // condition is in front of the text insertion code instead of behind it. + // A SetWrapForced of false implies an explicit newline, which is the default. + if (newX >= newWidth) { - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto textAttr = row.GetAttrRow().GetAttrByColumn(copyAttrCol); - if (!newRow.GetAttrRow().SetAttrToEnd(newAttrColumn, textAttr)) + newBuffer.GetMutableRowByOffset(newY).SetWrapForced(true); + newX = 0; + newY++; + } + + // REFLOW_RESET: + // If we shrink the buffer vertically, for instance from 100 rows to 90 rows, we will write 10 rows in the + // new buffer twice. We need to reset them before copying text, or otherwise we'll see the previous contents. + // We don't need to be smart about this. Reset() is fast and shrinking doesn't occur often. + if (newY >= newHeight && newX == 0) + { + // We need to ensure not to overwrite the row the cursor is on. + if (newY >= newYLimit) { break; } + newBuffer.GetMutableRowByOffset(newY).Reset(newBuffer._initialAttributes); } - CATCH_LOG(); // Not worth dying over. - } - // If we found the old row that the caller was interested in, set the - // out value of that parameter to the cursor's current Y position (the - // new location of the _end_ of that row in the buffer). - if (positionInfo.has_value()) + auto& newRow = newBuffer.GetMutableRowByOffset(newY); + + RowCopyTextFromState state{ + .source = oldRow, + .columnBegin = newX, + .columnLimit = til::CoordTypeMax, + .sourceColumnBegin = oldX, + .sourceColumnLimit = oldRowLimit, + }; + newRow.CopyTextFrom(state); + + const auto& oldAttr = oldRow.Attributes(); + auto& newAttr = newRow.Attributes(); + const auto attributes = oldAttr.slice(gsl::narrow_cast(oldX), oldAttr.size()); + newAttr.replace(gsl::narrow_cast(newX), newAttr.size(), attributes); + newAttr.resize_trailing_extent(newWidthU16); + + if (oldY == oldCursorPos.y && oldCursorPos.x >= oldX) + { + // In theory AdjustToGlyphStart ensures we don't put the cursor on a trailing wide glyph. + // In practice I don't think that this can possibly happen. Better safe than sorry. + newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x - oldX + newX), newY }; + // If there's so much text past the old cursor position that it doesn't fit into new buffer, + // then the new cursor position will be "lost", because it's overwritten by unrelated text. + // We have two choices how can handle this: + // * If the new cursor is at an y < 0, just put the cursor at (0,0) + // * Stop writing into the new buffer before we overwrite the new cursor position + // This implements the second option. There's no fundamental reason why this is better. + newYLimit = newY + newHeight; + } + if (oldY >= mutableViewportTop) + { + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; + } + if (oldY >= visibleViewportTop) + { + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; + } + + oldX = state.sourceColumnEnd; + newX = state.columnEnd; + } while (oldX < oldRowLimit); + + // If the row had an explicit newline we also need to newline. :) + if (!oldRow.WasWrapForced()) { - if (!foundOldMutable) - { - if (iOldRow >= positionInfo.value().get().mutableViewportTop) - { - positionInfo.value().get().mutableViewportTop = newCursor.GetPosition().Y; - foundOldMutable = true; - } - } - - if (!foundOldVisible) - { - if (iOldRow >= positionInfo.value().get().visibleViewportTop) - { - positionInfo.value().get().visibleViewportTop = newCursor.GetPosition().Y; - foundOldVisible = true; - } - } - } - - if (SUCCEEDED(hr)) - { - // If we didn't have a full row to copy, insert a new - // line into the new buffer. - // Only do so if we were not forced to wrap. If we did - // force a word wrap, then the existing line break was - // only because we ran out of space. - if (iRight < cOldColsTotal && !row.WasWrapForced()) - { - if (!fFoundCursorPos && (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)) - { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; - } - // Only do this if it's not the final line in the buffer. - // On the final line, we want the cursor to sit - // where it is done printing for the cursor - // adjustment to follow. - if (iOldRow < cOldRowsTotal - 1) - { - hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY; - } - else - { - // If we are on the final line of the buffer, we have one more check. - // We got into this code path because we are at the right most column of a row in the old buffer - // that had a hard return (no wrap was forced). - // However, as we're inserting, the old row might have just barely fit into the new buffer and - // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below. - // We need to preserve the memory of the hard return at this point by inserting one additional - // hard newline, otherwise we've lost that information. - // We only do this when the cursor has just barely poured over onto the next line so the hard return - // isn't covered by the soft one. - // e.g. - // The old line was: - // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a. - // The cursor was here ^ - // And the new line will be: - // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end - // | | - // ^ and the cursor is now there. - // If we leave it like this, we've lost the newline information. - // So we insert one more newline so a continued reflow of this buffer by resizing larger will - // continue to look as the original output intended with the newline data. - // After this fix, it looks like this: - // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline) - // | | - // ^ and the cursor is now here. - const auto coordNewCursor = newCursor.GetPosition(); - if (coordNewCursor.X == 0 && coordNewCursor.Y > 0) - { - if (newBuffer.GetRowByOffset(coordNewCursor.Y - 1).WasWrapForced()) - { - hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY; - } - } - } - } + newX = 0; + newY++; } } @@ -2550,99 +2588,39 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, // printable character. This is to fix the `color 2f` scenario, where you // change the buffer colors then resize and everything below the last // printable char gets reset. See GH #12567 - auto newRowY = newCursor.GetPosition().Y + 1; - const auto newHeight = newBuffer.GetSize().Height(); - const auto oldHeight = oldBuffer.GetSize().Height(); - for (; - iOldRow < oldHeight && newRowY < newHeight; - iOldRow++) + const auto initializedRowsEnd = oldBuffer._estimateOffsetOfLastCommittedRow() + 1; + for (; oldY < initializedRowsEnd && newY < newHeight; oldY++, newY++) { - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - - // Optimization: Since all these rows are below the last printable char, - // we can reasonably assume that they are filled with just spaces. - // That's convenient, we can just copy the attr row from the old buffer - // into the new one, and resize the row to match. We'll rely on the - // behavior of ATTR_ROW::Resize to trim down when narrower, or extend - // the last attr when wider. - auto& newRow = newBuffer.GetRowByOffset(newRowY); - const auto newWidth = newBuffer.GetLineWidth(newRowY); - newRow.GetAttrRow() = row.GetAttrRow(); - newRow.GetAttrRow().Resize(newWidth); - - newRowY++; + auto& oldRow = oldBuffer.GetRowByOffset(oldY); + auto& newRow = newBuffer.GetMutableRowByOffset(newY); + auto& newAttr = newRow.Attributes(); + newAttr = oldRow.Attributes(); + newAttr.resize_trailing_extent(newWidthU16); } - if (SUCCEEDED(hr)) + // Since we didn't use IncrementCircularBuffer() we need to compute the proper + // _firstRow offset now, in a way that replicates IncrementCircularBuffer(). + // We need to do the same for newCursorPos.y for basically the same reason. + if (newY > newHeight) { - // Finish copying remaining parameters from the old text buffer to the new one - newBuffer.CopyProperties(oldBuffer); - newBuffer.CopyHyperlinkMaps(oldBuffer); - newBuffer.CopyPatterns(oldBuffer); - - // If we found where to put the cursor while placing characters into the buffer, - // just put the cursor there. Otherwise we have to advance manually. - if (fFoundCursorPos) - { - newCursor.SetPosition(cNewCursorPos); - } - else - { - // Advance the cursor to the same offset as before - // get the number of newlines and spaces between the old end of text and the old cursor, - // then advance that many newlines and chars - auto iNewlines = cOldCursorPos.Y - cOldLastChar.Y; - const auto iIncrements = cOldCursorPos.X - cOldLastChar.X; - const auto cNewLastChar = newBuffer.GetLastNonSpaceCharacter(); - - // If the last row of the new buffer wrapped, there's going to be one less newline needed, - // because the cursor is already on the next line - if (newBuffer.GetRowByOffset(cNewLastChar.Y).WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - else - { - // if this buffer didn't wrap, but the old one DID, then the d(columns) of the - // old buffer will be one more than in this buffer, so new need one LESS. - if (oldBuffer.GetRowByOffset(cOldLastChar.Y).WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - } - - for (auto r = 0; r < iNewlines; r++) - { - if (!newBuffer.NewlineCursor()) - { - hr = E_OUTOFMEMORY; - break; - } - } - if (SUCCEEDED(hr)) - { - for (auto c = 0; c < iIncrements - 1; c++) - { - if (!newBuffer.IncrementCursor()) - { - hr = E_OUTOFMEMORY; - break; - } - } - } - } + newBuffer._firstRow = newY % newHeight; + // _firstRow maps from API coordinates that always start at 0,0 in the top left corner of the + // terminal's scrollback, to the underlying buffer Y coordinate via `(y + _firstRow) % height`. + // Here, we need to un-map the `newCursorPos.y` from the underlying Y coordinate to the API coordinate + // and so we do `(y - _firstRow) % height`, but we add `+ newHeight` to avoid getting negative results. + newCursorPos.y = (newCursorPos.y - newBuffer._firstRow + newHeight) % newHeight; } - if (SUCCEEDED(hr)) - { - // Save old cursor size before we delete it - const auto ulSize = oldCursor.GetSize(); + newBuffer.CopyProperties(oldBuffer); + newBuffer.CopyHyperlinkMaps(oldBuffer); - // Set size back to real size as it will be taking over the rendering duties. - newCursor.SetSize(ulSize); - } + assert(newCursorPos.x >= 0 && newCursorPos.x < newWidth); + assert(newCursorPos.y >= 0 && newCursorPos.y < newHeight); + newCursor.SetSize(oldCursor.GetSize()); + newCursor.SetPosition(newCursorPos); - return hr; + newBuffer._marks = oldBuffer._marks; + newBuffer._trimMarksOutsideBuffer(); } // Method Description: @@ -2751,104 +2729,166 @@ 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) +// 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 { - ++_currentPatternId; - _idsAndPatterns.emplace(std::make_pair(_currentPatternId, regexString)); - return _currentPatternId; + return SearchText(needle, caseInsensitive, 0, til::CoordTypeMax); } -// Method Description: -// - Clears the patterns we know of and resets the pattern ID counter -void TextBuffer::ClearPatternRecognizers() noexcept +// 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 { - _idsAndPatterns.clear(); - _currentPatternId = 0; -} + rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1); -// Method Description: -// - Copies the patterns the other buffer knows about into this one -// Arguments: -// - The other buffer -void TextBuffer::CopyPatterns(const TextBuffer& OtherBuffer) -{ - _idsAndPatterns = OtherBuffer._idsAndPatterns; - _currentPatternId = OtherBuffer._currentPatternId; -} + std::vector results; -// 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 -{ - PointTree::interval_vector intervals; - - std::wstring concatAll; - const auto rowSize = GetRowByOffset(0).size(); - concatAll.reserve(gsl::narrow_cast(rowSize) * gsl::narrow_cast(lastRow - firstRow + 1)); - - // 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) + auto text = ICU::UTextFromTextBuffer(*this, rowBeg, rowEnd); + + uint32_t flags = UREGEX_LITERAL; + WI_SetFlagIf(flags, UREGEX_CASE_INSENSITIVE, caseInsensitive); + + 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)) { - std::wregex regexObj{ idAndPattern.second }; - - // 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(); - - til::CoordType lenUpToThis = 0; - for (auto i = words_begin; i != words_end; ++i) + 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 parsedGlyph : Utf16Parser::Parse(i->prefix().str())) - { - const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() }; - prefixSize += IsGlyphFullWidth(glyph) ? 2 : 1; - } - const auto start = lenUpToThis + prefixSize; - til::CoordType matchSize = 0; - for (const auto parsedGlyph : Utf16Parser::Parse(i->str())) - { - const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() }; - matchSize += IsGlyphFullWidth(glyph) ? 2 : 1; - } - const auto end = start + matchSize; - lenUpToThis = end; + results.emplace_back(ICU::BufferRangeFromMatch(&text, re.get())); + } while (uregex_findNext(re.get(), &status)); + } - const til::point startCoord{ start % rowSize, start / rowSize }; - const til::point endCoord{ end % rowSize, end / rowSize }; + return results; +} - // 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)); +const std::vector& TextBuffer::GetMarks() const noexcept +{ + return _marks; +} + +// Remove all marks between `start` & `end`, inclusive. +void TextBuffer::ClearMarksInRange( + const til::point start, + const til::point end) +{ + auto inRange = [&start, &end](const ScrollMark& m) { + return (m.start >= start && m.start <= end) || + (m.end >= start && m.end <= end); + }; + + _marks.erase(std::remove_if(_marks.begin(), + _marks.end(), + inRange), + _marks.end()); +} +void TextBuffer::ClearAllMarks() noexcept +{ + _marks.clear(); +} + +// Adjust all the marks in the y-direction by `delta`. Positive values move the +// marks down (the positive y direction). Negative values move up. This will +// trim marks that are no longer have a start in the bounds of the buffer +void TextBuffer::ScrollMarks(const int delta) +{ + for (auto& mark : _marks) + { + mark.start.y += delta; + + // If the mark had sub-regions, then move those pointers too + if (mark.commandEnd.has_value()) + { + (*mark.commandEnd).y += delta; + } + if (mark.outputEnd.has_value()) + { + (*mark.outputEnd).y += delta; } } - PointTree result(std::move(intervals)); - return result; + _trimMarksOutsideBuffer(); +} + +// Method Description: +// - Add a mark to our list of marks, and treat it as the active "prompt". For +// the sake of shell integration, we need to know which mark represents the +// current prompt/command/output. Internally, we'll always treat the _last_ +// mark in the list as the current prompt. +// Arguments: +// - m: the mark to add. +void TextBuffer::StartPromptMark(const ScrollMark& m) +{ + _marks.push_back(m); +} +// Method Description: +// - Add a mark to our list of marks. Don't treat this as the active prompt. +// This should be used for marks created by the UI or from other user input. +// By inserting at the start of the list, we can separate out marks that were +// generated by client programs vs ones created by the user. +// Arguments: +// - m: the mark to add. +void TextBuffer::AddMark(const ScrollMark& m) +{ + _marks.insert(_marks.begin(), m); +} + +void TextBuffer::_trimMarksOutsideBuffer() +{ + const til::CoordType height = _height; + std::erase_if(_marks, [height](const auto& m) { + return (m.start.y < 0) || (m.start.y >= height); + }); +} + +std::wstring_view TextBuffer::CurrentCommand() const +{ + if (_marks.size() == 0) + { + return L""; + } + + const auto& curr{ _marks.back() }; + const auto& start{ curr.end }; + const auto& end{ GetCursor().GetPosition() }; + + const auto line = start.y; + const auto& row = GetRowByOffset(line); + return row.GetText(start.x, end.x); +} + +void TextBuffer::SetCurrentPromptEnd(const til::point pos) noexcept +{ + if (_marks.empty()) + { + return; + } + auto& curr{ _marks.back() }; + curr.end = pos; +} +void TextBuffer::SetCurrentCommandEnd(const til::point pos) noexcept +{ + if (_marks.empty()) + { + return; + } + auto& curr{ _marks.back() }; + curr.commandEnd = pos; +} +void TextBuffer::SetCurrentOutputEnd(const til::point pos, ::MarkCategory category) noexcept +{ + if (_marks.empty()) + { + return; + } + auto& curr{ _marks.back() }; + curr.outputEnd = pos; + curr.category = category; } diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 17cfb21b6d..a9d2e1714b 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -54,17 +54,53 @@ filling in the last row, and updating the screen. #include "cursor.h" #include "Row.hpp" #include "TextAttribute.hpp" -#include "UnicodeStorage.hpp" #include "../types/inc/Viewport.hpp" #include "../buffer/out/textBufferCellIterator.hpp" #include "../buffer/out/textBufferTextIterator.hpp" +struct URegularExpression; + namespace Microsoft::Console::Render { class Renderer; } +enum class MarkCategory +{ + Prompt = 0, + Error = 1, + Warning = 2, + Success = 3, + Info = 4 +}; +struct ScrollMark +{ + std::optional color; + til::point start; + til::point end; // exclusive + std::optional commandEnd; + std::optional outputEnd; + + MarkCategory category{ MarkCategory::Info }; + // Other things we may want to think about in the future are listed in + // GH#11000 + + bool HasCommand() const noexcept + { + return commandEnd.has_value() && *commandEnd != end; + } + bool HasOutput() const noexcept + { + return outputEnd.has_value() && *outputEnd != *commandEnd; + } + std::pair GetExtent() const + { + til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) }; + return std::make_pair(til::point{ start }, realEnd); + } +}; + class TextBuffer final { public: @@ -73,14 +109,22 @@ public: const UINT cursorSize, const bool isActiveBuffer, Microsoft::Console::Render::Renderer& renderer); - TextBuffer(const TextBuffer& a) = delete; + + TextBuffer(const TextBuffer&) = delete; + TextBuffer(TextBuffer&&) = delete; + TextBuffer& operator=(const TextBuffer&) = delete; + TextBuffer& operator=(TextBuffer&&) = delete; + + ~TextBuffer(); // Used for duplicating properties to another text buffer void CopyProperties(const TextBuffer& OtherBuffer) noexcept; // row manipulation - const ROW& GetRowByOffset(const til::CoordType index) const noexcept; - ROW& GetRowByOffset(const til::CoordType index) noexcept; + ROW& GetScratchpadRow(); + ROW& GetScratchpadRow(const TextAttribute& attributes); + const ROW& GetRowByOffset(til::CoordType index) const; + ROW& GetMutableRowByOffset(til::CoordType index); TextBufferCellIterator GetCellDataAt(const til::point at) const; TextBufferCellIterator GetCellLineDataAt(const til::point at) const; @@ -89,7 +133,17 @@ public: TextBufferTextIterator GetTextLineDataAt(const til::point at) const; TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const; + size_t GetCellDistance(const til::point from, const til::point to) 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; + + 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); + OutputCellIterator Write(const OutputCellIterator givenIt); OutputCellIterator Write(const OutputCellIterator givenIt, @@ -101,19 +155,20 @@ public: const std::optional setWrap = std::nullopt, const std::optional limitRight = std::nullopt); - bool InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr); - bool InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr); - bool IncrementCursor(); - bool NewlineCursor(); + void InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr); + void InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr); + void IncrementCursor(); + void NewlineCursor(); // Scroll needs access to this to quickly rotate around the buffer. - bool IncrementCircularBuffer(const bool inVtMode = false); + void IncrementCircularBuffer(const TextAttribute& fillAttributes = {}); - til::point GetLastNonSpaceCharacter(std::optional viewOptional = std::nullopt) const; + til::point GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport* viewOptional = nullptr) const; 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; @@ -122,26 +177,24 @@ public: til::CoordType TotalRowCount() const noexcept; - [[nodiscard]] TextAttribute GetCurrentAttributes() const noexcept; + const TextAttribute& GetCurrentAttributes() const noexcept; void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept; - void SetCurrentLineRendition(const LineRendition lineRendition); - void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept; - LineRendition GetLineRendition(const til::CoordType row) const noexcept; - bool IsDoubleWidthLine(const til::CoordType row) const noexcept; + void SetWrapForced(til::CoordType y, bool wrap); + void SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes); + void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow); + LineRendition GetLineRendition(const til::CoordType row) const; + bool IsDoubleWidthLine(const til::CoordType row) const; - til::CoordType GetLineWidth(const til::CoordType row) const noexcept; - til::point ClampPositionWithinLine(const til::point position) const noexcept; - til::point ScreenToBufferPosition(const til::point position) const noexcept; - til::point BufferToScreenPosition(const til::point position) const noexcept; + til::CoordType GetLineWidth(const til::CoordType row) const; + til::point ClampPositionWithinLine(const til::point position) const; + til::point ScreenToBufferPosition(const til::point position) const; + til::point BufferToScreenPosition(const til::point position) const; - void Reset(); + void Reset() noexcept; - [[nodiscard]] HRESULT ResizeTraditional(const til::size newSize) noexcept; - - const UnicodeStorage& GetUnicodeStorage() const noexcept; - UnicodeStorage& GetUnicodeStorage() noexcept; + void ResizeTraditional(const til::size newSize); void SetAsActiveBuffer(const bool isActiveBuffer) noexcept; bool IsActiveBuffer() const noexcept; @@ -209,66 +262,119 @@ public: til::CoordType visibleViewportTop{ 0 }; }; - static HRESULT Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo); + static void Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Microsoft::Console::Types::Viewport* lastCharacterViewport = nullptr, PositionInformation* positionInfo = nullptr); - 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); + void ClearAllMarks() noexcept; + void ScrollMarks(const int delta); + void StartPromptMark(const ScrollMark& m); + void AddMark(const ScrollMark& m); + void SetCurrentPromptEnd(const til::point pos) noexcept; + void SetCurrentCommandEnd(const til::point pos) noexcept; + void SetCurrentOutputEnd(const til::point pos, ::MarkCategory category) noexcept; + std::wstring_view CurrentCommand() const; private: - void _UpdateSize(); - Microsoft::Console::Types::Viewport _size; - std::vector _storage; - Cursor _cursor; - - til::CoordType _firstRow; // indexes top row (not necessarily 0) - - TextAttribute _currentAttributes; - - // storage location for glyphs that can't fit into the buffer normally - UnicodeStorage _unicodeStorage; - - bool _isActiveBuffer; - Microsoft::Console::Render::Renderer& _renderer; - - std::unordered_map _hyperlinkMap; - std::unordered_map _hyperlinkCustomIdMap; - uint16_t _currentHyperlinkId; - - void _RefreshRowIDs(std::optional newRowWidth); + void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes); + void _commit(const std::byte* row); + void _decommit() noexcept; + void _construct(const std::byte* until) noexcept; + void _destroy() const noexcept; + ROW& _getRowByOffsetDirect(size_t offset); + ROW& _getRow(til::CoordType y) const; + til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept; void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept; - - til::point _GetPreviousFromCursor() const noexcept; - - void _SetWrapOnCurrentRow() noexcept; - void _AdjustWrapOnCurrentRow(const bool fSet) noexcept; - + til::point _GetPreviousFromCursor() const; + void _SetWrapOnCurrentRow(); + void _AdjustWrapOnCurrentRow(const bool fSet); // Assist with maintaining proper buffer state for Double Byte character sequences - bool _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute); - bool _AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute); - - ROW& _GetFirstRow() noexcept; - ROW& _GetPrevRowNoWrap(const ROW& row); - + void _PrepareForDoubleByteSequence(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; til::point _GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const; til::point _GetWordEndForAccessibility(const til::point target, const std::wstring_view wordDelimiters, const til::point limit) const; til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const; - void _PruneHyperlinks(); + void _trimMarksOutsideBuffer(); static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text); - std::unordered_map _idsAndPatterns; - size_t _currentPatternId; + Microsoft::Console::Render::Renderer& _renderer; + + std::unordered_map _hyperlinkMap; + std::unordered_map _hyperlinkCustomIdMap; + uint16_t _currentHyperlinkId = 1; + + // This block describes the state of the underlying virtual memory buffer that holds all ROWs, text and attributes. + // Initially memory is only allocated with MEM_RESERVE to reduce the private working set of conhost. + // ROWs are laid out like this in memory: + // ROW <-- sizeof(ROW), stores + // (padding) + // ROW::_charsBuffer <-- _width * sizeof(wchar_t) + // (padding) + // ROW::_charOffsets <-- (_width + 1) * sizeof(uint16_t) + // (padding) + // ... + // Padding may exist for alignment purposes. + // + // The base (start) address of the memory arena. + wil::unique_virtualalloc_ptr _buffer; + // The past-the-end pointer of the memory arena. + std::byte* _bufferEnd = nullptr; + // The range between _buffer (inclusive) and _commitWatermark (exclusive) is the range of + // memory that has already been committed via MEM_COMMIT and contains ready-to-use ROWs. + // + // The problem is that calling VirtualAlloc(MEM_COMMIT) on each ROW one by one is extremely expensive, which forces + // us to commit ROWs in batches and avoid calling it on already committed ROWs. Let's say we commit memory in + // batches of 128 ROWs. One option to know whether a ROW has already been committed is to allocate a vector + // of size `(height + 127) / 128` and mark the corresponding slot as 1 if that 128-sized batch has been committed. + // That way we know not to commit it again. But ROWs aren't accessed randomly. Instead, they're usually accessed + // fairly linearly from row 1 to N. As such we can just commit ROWs up to the point of the highest accessed ROW + // plus some read-ahead of 128 ROWs. This is exactly what _commitWatermark stores: The highest accessed ROW plus + // some read-ahead. It's the amount of memory that has been committed and is ready to use. + // + // _commitWatermark will always be a multiple of _bufferRowStride away from _buffer. + // In other words, _commitWatermark itself will either point exactly onto the next ROW + // that should be committed or be equal to _bufferEnd when all ROWs are committed. + std::byte* _commitWatermark = nullptr; + // This will MEM_COMMIT 128 rows more than we need, to avoid us from having to call VirtualAlloc too often. + // This equates to roughly the following commit chunk sizes at these column counts: + // * 80 columns (the usual minimum) = 60KB chunks, 4.1MB buffer at 9001 rows + // * 120 columns (the most common) = 80KB chunks, 5.6MB buffer at 9001 rows + // * 400 columns (the usual maximum) = 220KB chunks, 15.5MB buffer at 9001 rows + // There's probably a better metric than this. (This comment was written when ROW had both, + // a _chars array containing text and a _charOffsets array contain column-to-text indices.) + static constexpr size_t _commitReadAheadRowCount = 128; + // Before TextBuffer was made to use virtual memory it initialized the entire memory arena with the initial + // attributes right away. To ensure it continues to work the way it used to, this stores these initial attributes. + TextAttribute _initialAttributes; + // ROW ---------------+--+--+ + // (padding) | | v _bufferOffsetChars + // ROW::_charsBuffer | | + // (padding) | v _bufferOffsetCharOffsets + // ROW::_charOffsets | + // (padding) v _bufferRowStride + size_t _bufferRowStride = 0; + size_t _bufferOffsetChars = 0; + size_t _bufferOffsetCharOffsets = 0; + // The width of the buffer in columns. + uint16_t _width = 0; + // The height of the buffer in rows, excluding the scratchpad row. + uint16_t _height = 0; + + TextAttribute _currentAttributes; + til::CoordType _firstRow = 0; // indexes top row (not necessarily 0) + uint64_t _lastMutationId = 0; + + Cursor _cursor; + std::vector _marks; + bool _isActiveBuffer = false; #ifdef UNIT_TESTING friend class TextBufferTests; diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp index a9343d7498..8a177dfec6 100644 --- a/src/buffer/out/textBufferCellIterator.cpp +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -5,7 +5,6 @@ #include "textBufferCellIterator.hpp" -#include "CharRow.hpp" #include "textBuffer.hpp" #include "../types/inc/convert.hpp" #include "../types/inc/viewport.hpp" @@ -37,7 +36,7 @@ TextBufferCellIterator::TextBufferCellIterator(const TextBuffer& buffer, til::po _bounds(limits), _exceeded(false), _view({}, {}, {}, TextAttributeBehavior::Stored), - _attrIter(s_GetRow(buffer, pos)->GetAttrRow().cbegin()) + _attrIter(s_GetRow(buffer, pos)->AttrBegin()) { // Throw if the bounds rectangle is not limited to the inside of the given buffer. THROW_HR_IF(E_INVALIDARG, !buffer.GetSize().IsInBounds(limits)); @@ -45,7 +44,7 @@ TextBufferCellIterator::TextBufferCellIterator(const TextBuffer& buffer, til::po // Throw if the coordinate is not limited to the inside of the given buffer. THROW_HR_IF(E_INVALIDARG, !limits.IsInBounds(pos)); - _attrIter += pos.X; + _attrIter += pos.x; _GenerateView(); } @@ -116,15 +115,15 @@ TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& move // _SetPos() necessitates calling _GenerateView() and thus the construction // of a new OutputCellView(). This has a high performance impact (ICache spill?). // The code below inlines _bounds.IncrementInBounds as well as SetPos. - // In the hot path (_pos.Y doesn't change) we modify the _view directly. + // In the hot path (_pos.y doesn't change) we modify the _view directly. // Hoist these integers which will be used frequently later. const auto boundsRightInclusive = _bounds.RightInclusive(); const auto boundsLeft = _bounds.Left(); const auto boundsBottomInclusive = _bounds.BottomInclusive(); const auto boundsTop = _bounds.Top(); - const auto oldX = _pos.X; - const auto oldY = _pos.Y; + const auto oldX = _pos.x; + const auto oldY = _pos.y; // Under MSVC writing the individual members of a til::point generates worse assembly // compared to having them be local variables. This causes a performance impact. @@ -165,18 +164,17 @@ TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& move _attrIter += diff; _view.UpdateTextAttribute(*_attrIter); - const auto& charRow = _pRow->GetCharRow(); - _view.UpdateText(charRow.GlyphAt(newX)); - _view.UpdateDbcsAttribute(charRow.DbcsAttrAt(newX)); - _pos.X = newX; + _view.UpdateText(_pRow->GlyphAt(newX)); + _view.UpdateDbcsAttribute(_pRow->DbcsAttrAt(newX)); + _pos.x = newX; } else { // cold path (_GenerateView is slow) _pRow = s_GetRow(_buffer, { newX, newY }); - _attrIter = _pRow->GetAttrRow().cbegin() + newX; - _pos.X = newX; - _pos.Y = newY; + _attrIter = _pRow->AttrBegin() + newX; + _pos.x = newX; + _pos.y = newY; _GenerateView(); } @@ -291,16 +289,16 @@ ptrdiff_t TextBufferCellIterator::operator-(const TextBufferCellIterator& it) // - newPos - The new coordinate position. void TextBufferCellIterator::_SetPos(const til::point newPos) { - if (newPos.Y != _pos.Y) + if (newPos.y != _pos.y) { _pRow = s_GetRow(_buffer, newPos); - _attrIter = _pRow->GetAttrRow().cbegin(); - _pos.X = 0; + _attrIter = _pRow->AttrBegin(); + _pos.x = 0; } - if (newPos.X != _pos.X) + if (newPos.x != _pos.x) { - const auto diff = gsl::narrow_cast(newPos.X) - gsl::narrow_cast(_pos.X); + const auto diff = gsl::narrow_cast(newPos.x) - gsl::narrow_cast(_pos.x); _attrIter += diff; } @@ -317,17 +315,17 @@ void TextBufferCellIterator::_SetPos(const til::point newPos) // - pos - Position inside screen buffer bounds to retrieve row // Return Value: // - Pointer to the underlying CharRow structure -const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept +const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos) { - return &buffer.GetRowByOffset(pos.Y); + return &buffer.GetRowByOffset(pos.y); } // Routine Description: // - Updates the internal view. Call after updating row, attribute, or positions. -void TextBufferCellIterator::_GenerateView() +void TextBufferCellIterator::_GenerateView() noexcept { - _view = OutputCellView(_pRow->GetCharRow().GlyphAt(_pos.X), - _pRow->GetCharRow().DbcsAttrAt(_pos.X), + _view = OutputCellView(_pRow->GlyphAt(_pos.x), + _pRow->DbcsAttrAt(_pos.x), *_attrIter, TextAttributeBehavior::Stored); } diff --git a/src/buffer/out/textBufferCellIterator.hpp b/src/buffer/out/textBufferCellIterator.hpp index ffeb037860..30276f2e13 100644 --- a/src/buffer/out/textBufferCellIterator.hpp +++ b/src/buffer/out/textBufferCellIterator.hpp @@ -15,8 +15,7 @@ Author(s): #pragma once -#include "CharRow.hpp" -#include "AttrRow.hpp" +#include "Row.hpp" #include "OutputCellView.hpp" #include "../../types/inc/viewport.hpp" @@ -51,13 +50,13 @@ public: protected: void _SetPos(const til::point newPos); - void _GenerateView(); - static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept; + void _GenerateView() noexcept; + static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos); + til::small_rle::const_iterator _attrIter; OutputCellView _view; const ROW* _pRow; - ATTR_ROW::const_iterator _attrIter; const TextBuffer& _buffer; const Microsoft::Console::Types::Viewport _bounds; bool _exceeded; diff --git a/src/buffer/out/textBufferTextIterator.cpp b/src/buffer/out/textBufferTextIterator.cpp index 9687e31758..36ac370af4 100644 --- a/src/buffer/out/textBufferTextIterator.cpp +++ b/src/buffer/out/textBufferTextIterator.cpp @@ -5,7 +5,6 @@ #include "textBufferTextIterator.hpp" -#include "CharRow.hpp" #include "Row.hpp" #pragma hdrstop diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index 05251647c8..d1fad4c407 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -7,7 +7,6 @@ #include "../textBuffer.hpp" #include "../../renderer/inc/DummyRenderer.hpp" -#include "../../types/inc/Utf16Parser.hpp" #include "../../types/inc/GlyphWidth.hpp" #include @@ -47,8 +46,6 @@ namespace std::vector buffers; }; - static constexpr auto true_due_to_exact_wrap_bug{ true }; - static const TestCase testCases[] = { TestCase{ L"No reflow required", @@ -62,7 +59,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -73,7 +70,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 4, 5 }, @@ -84,7 +81,7 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -100,40 +97,40 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. - { L" ", false }, + { L"F ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 0, 2 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original { - { L"ABCDEF", true_due_to_exact_wrap_bug }, + { L"ABCDEF", false }, { L"$ ", false }, { L" ", false }, { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original { - { L"ABCDEF$", true_due_to_exact_wrap_bug }, // EXACT WRAP BUG: $ should be alone on next line - { L" ", false }, + { L"ABCDEF ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, { L" ", false }, }, - { 6, 0 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -149,7 +146,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 1, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -160,7 +157,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original @@ -171,7 +168,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor on $ + { 1, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original @@ -182,7 +179,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -198,29 +195,29 @@ namespace { L"EFG ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // reduce width by 1 { { L"AB $", true }, - { L" CD", true_due_to_exact_wrap_bug }, - { L" ", false }, + { L" CD", false }, // CD ends with a newline -> .wrap = false { L"EFG ", false }, { L" ", false }, + { L" ", false }, }, - { 6, 0 } // cursor on $ + { 6, 0 }, // cursor on $ }, TestBuffer{ { 8, 5 }, { { L"AB $ ", true }, - { L" CD ", false }, // Goes to false because we hit the end of ..CD - { L"EFG ", false }, // [BUG] EFG moves up due to exact wrap bug above + { L" CD ", false }, + { L"EFG ", false }, { L" ", false }, { L" ", false }, }, - { 6, 0 } // cursor on $ + { 6, 0 }, // cursor on $ }, }, }, @@ -237,19 +234,19 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { //--012345-- { L"カタ ", true }, // KA TA [FORCED SPACER] - { L"カナ$", true_due_to_exact_wrap_bug }, // KA NA + { L"カナ$", false }, // KA NA { L" ", false }, { L" ", false }, { L" ", false }, }, - { 4, 1 } // cursor on $ + { 4, 1 }, // cursor on $ }, TestBuffer{ { 6, 5 }, // grow width back to original @@ -261,7 +258,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 7, 5 }, // grow width wider than original (by one; no visible change!) @@ -273,7 +270,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor on $ + { 2, 1 }, // cursor on $ }, TestBuffer{ { 8, 5 }, // grow width enough to fit second DBCS @@ -285,7 +282,7 @@ namespace { L" ", false }, { L" ", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, }, }, @@ -301,41 +298,29 @@ namespace { L"MNOPQR", false }, { L"STUVWX", false }, }, - { 0, 1 } // cursor on $ + { 0, 1 }, // cursor on $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { - { L"F$ ", false }, - { L"GHIJK", true }, // [BUG] We should see GHIJK\n L\n MNOPQ\n R\n - { L"LMNOP", true }, // The wrapping here is irregular - { L"QRSTU", true }, - { L"VWX ", false }, + { L"$ ", false }, + { L"GHIJK", true }, + { L"L ", false }, + { L"MNOPQ", true }, + { L"R ", false }, }, - { 1, 1 } // [BUG] cursor moves to 1,1 instead of sticking with the $ + { 0, 0 }, }, TestBuffer{ { 6, 5 }, // going back to 6,5, the data lost has been destroyed { - //{ L"F$ ", false }, // [BUG] this line is erroneously destroyed too! - { L"GHIJKL", true }, - { L"MNOPQR", true }, - { L"STUVWX", true }, + { L"$ ", false }, + { L"GHIJKL", false }, + { L"MNOPQR", false }, + { L" ", false }, { L" ", false }, - { L" ", false }, // [BUG] this line is added }, - { 1, 1 }, // [BUG] cursor does not follow [H], it sticks at 1,1 - }, - TestBuffer{ - { 7, 5 }, // a number of errors are carried forward from the previous buffer - { - { L"GHIJKLM", true }, - { L"NOPQRST", true }, - { L"UVWX ", false }, // [BUG] This line loses wrap for some reason - { L" ", false }, - { L" ", false }, - }, - { 0, 1 }, // [BUG] The cursor moves to 0, 1 now, sticking with the [N] from before + { 0, 0 }, }, }, }, @@ -354,18 +339,18 @@ namespace { L" ", false }, { L" ", false }, }, - { 1, 1 } // cursor *after* $ + { 1, 1 }, // cursor *after* $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", false }, // [BUG] EXACT WRAP. $ should be alone on next line. - { L" ", false }, + { L"F ", false }, + { L"$ ", false }, { L" ", false }, { L" ", false }, }, - { 2, 1 } // cursor follows space after $ to next line + { 1, 2 }, // cursor follows space after $ to next line }, }, }, @@ -381,7 +366,7 @@ namespace { L"STUVWX", true }, { L"YZ0 $ ", false }, }, - { 5, 4 } // cursor *after* $ + { 5, 4 }, // cursor *after* $ }, TestBuffer{ { 5, 5 }, // reduce width by 1 @@ -392,7 +377,7 @@ namespace { L"UVWXY", true }, { L"Z0 $ ", false }, }, - { 4, 4 } // cursor follows space after $ to newly introduced bottom line + { 4, 4 }, // cursor follows space after $ to newly introduced bottom line }, }, }, @@ -403,40 +388,36 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 5, 5 }, // reduce width by 1 { { L"ABCDE", true }, - { L"F$ ", true }, // [BUG] This line is marked wrapped, and I do not know why - // v cursor - { L" ", false }, - // ^ cursor + { L"F ", false }, + // The reflow implementation marks a wrapped cursor as a forced row-wrap (= the row is padded with whitespace), so that when + // the buffer is enlarged again, we restore the original cursor position correctly. That's why it says .cursor={5,1} below. + { L"$ ", true }, { L" ", false }, { L" ", false }, }, - { 1, 2 } // cursor stays same linear distance from $ + { 0, 3 }, // $ is now at 0,2 and the cursor used to be 5 columns to the right. -> 0,3 }, TestBuffer{ { 6, 5 }, // grow back to original size { - { L"ABCDEF", true_due_to_exact_wrap_bug }, - // v cursor [BUG] cursor does not retain linear distance from $ + { L"ABCDEF", false }, { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 4, 1 } // cursor stays same linear distance from $ + { 5, 1 }, }, }, }, @@ -447,39 +428,37 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L"BLAH ", false }, { L"BLAH ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 5, 5 }, // reduce width by 1 { - { L"ABCDE", true }, - { L"F$ ", false }, - { L"BLAH ", false }, - { L"BLAH ", true }, // [BUG] this line wraps, no idea why - // v cursor [BUG] cursor erroneously moved to end of all content + { L"F ", false }, + // The reflow implementation pads the row with the cursor with whitespace. + // Search for "REFLOW_JANK_CURSOR_WRAP" to find the corresponding code. + { L"$ ", true }, { L" ", false }, - // ^ cursor + { L"BLAH ", false }, + { L"BLAH ", false }, }, - { 0, 4 } }, + { 0, 2 }, + }, TestBuffer{ { 6, 5 }, // grow back to original size { - { L"ABCDEF", true }, + { L"F ", false }, { L"$ ", false }, { L"BLAH ", false }, - // v cursor [BUG] cursor is pulled up to previous line because it was marked wrapped { L"BLAH ", false }, - // ^ cursor { L" ", false }, }, - { 5, 3 } }, + { 5, 1 }, + }, }, }, TestCase{ @@ -490,27 +469,24 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", false }, { L" ", false }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // The cursor is 5 columns to the right of the $ (last column). }, TestBuffer{ { 2, 5 }, // reduce width aggressively { { L"CD", true }, - { L"EF", true }, + { L"EF", false }, { L"$ ", true }, { L" ", true }, - // v cursor { L" ", false }, - // ^ cursor }, - { 1, 4 } }, + { 1, 4 }, + }, }, }, TestCase{ @@ -520,27 +496,24 @@ namespace { 6, 5 }, { { L"ABCDEF", true }, - // v cursor { L"$ ", true }, - // ^ cursor { L" ", true }, { L" ", true }, { L" ", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively { - { L"EF", true }, - { L"$ ", true }, { L" ", true }, { L" ", true }, - // v cursor [BUG] cursor does not maintain linear distance from $ - { L" ", false }, - // ^ cursor + { L" ", true }, + { L" ", true }, + { L" ", true }, }, - { 1, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -550,14 +523,12 @@ namespace { 6, 5 }, { { L"ABCDEF", true }, - // v cursor { L"$ ", true }, - // ^ cursor { L" ", true }, { L" ", true }, { L" Q", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively @@ -565,12 +536,11 @@ namespace { L" ", true }, { L" ", true }, { L" ", true }, - { L" Q", true }, - // v cursor [BUG] cursor jumps to end of world - { L" ", false }, // POTENTIAL [BUG] a whole new blank line is added for the cursor - // ^ cursor + { L" ", true }, + { L" ", true }, }, - { 1, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -580,27 +550,24 @@ namespace { 6, 5 }, { { L"ABCDEF", false }, - // v cursor { L"$ ", false }, - // ^ cursor { L" ", false }, { L" ", true }, { L" Q", true }, }, - { 5, 1 } // cursor in space far after $ + { 5, 1 }, // cursor in space far after $ }, TestBuffer{ { 2, 5 }, // reduce width aggressively { - { L" ", true }, - { L" ", true }, - { L" ", true }, - { L" Q", true }, - // v cursor [BUG] cursor jumps to different place than fully wrapped case { L" ", false }, - // ^ cursor + { L" ", false }, + { L" ", true }, + { L" ", true }, + { L" ", true }, }, - { 0, 4 } }, + { 1, 0 }, + }, }, }, TestCase{ @@ -615,24 +582,21 @@ namespace { L"$ ", false }, { L" Q", true }, { L" ", true }, - // v cursor { L" ", true }, - // ^ cursor }, - { 5, 4 } // cursor at end of buffer + { 5, 4 }, // cursor at end of buffer }, TestBuffer{ { 2, 5 }, // reduce width aggressively { + { L" ", false }, + { L" ", true }, + { L" ", true }, { L" ", true }, { L" ", true }, - { L" Q", true }, - { L" ", false }, - // v cursor [BUG] cursor loses linear distance from Q; is this important? - { L" ", false }, - // ^ cursor }, - { 0, 4 } }, + { 1, 0 }, + }, }, }, }; @@ -737,34 +701,22 @@ class ReflowTests { auto buffer = std::make_unique(testBuffer.size, TextAttribute{ 0x7 }, 0, false, renderer); - til::CoordType i{}; + til::CoordType y = 0; for (const auto& testRow : testBuffer.rows) { - auto& row{ buffer->GetRowByOffset(i) }; + auto& row{ buffer->GetMutableRowByOffset(y) }; - auto& charRow{ row.GetCharRow() }; row.SetWrapForced(testRow.wrap); - til::CoordType j{}; - for (auto it{ charRow.begin() }; it != charRow.end(); ++it) + til::CoordType x = 0; + for (const auto& ch : testRow.text) { - // Yes, we're about to manually create a buffer. It is unpleasant. - const auto ch{ til::at(testRow.text, j) }; - it->Char() = ch; - if (IsGlyphFullWidth(ch)) - { - it->DbcsAttr().SetLeading(); - it++; - it->Char() = ch; - it->DbcsAttr().SetTrailing(); - } - else - { - it->DbcsAttr().SetSingle(); - } - j++; + const til::CoordType width = IsGlyphFullWidth(ch) ? 2 : 1; + row.ReplaceCharacters(x, width, { &ch, 1 }); + x += width; } - i++; + + y++; } buffer->GetCursor().SetPosition(testBuffer.cursor); @@ -774,7 +726,7 @@ class ReflowTests static std::unique_ptr _textBufferByReflowingTextBuffer(TextBuffer& originalBuffer, const til::size newSize) { auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, renderer); - TextBuffer::Reflow(originalBuffer, *buffer, std::nullopt, std::nullopt); + TextBuffer::Reflow(originalBuffer, *buffer); return buffer; } @@ -783,42 +735,44 @@ class ReflowTests VERIFY_ARE_EQUAL(testBuffer.cursor, buffer.GetCursor().GetPosition()); VERIFY_ARE_EQUAL(testBuffer.size, buffer.GetSize().Dimensions()); - til::CoordType i{}; + til::CoordType y = 0; for (const auto& testRow : testBuffer.rows) { NoThrowString indexString; - const auto& row{ buffer.GetRowByOffset(i) }; + const auto& row{ buffer.GetRowByOffset(y) }; - const auto& charRow{ row.GetCharRow() }; - - indexString.Format(L"[Row %d]", i); + indexString.Format(L"[Row %d]", y); VERIFY_ARE_EQUAL(testRow.wrap, row.WasWrapForced(), indexString); - til::CoordType j{}; - for (auto it{ charRow.begin() }; it != charRow.end(); ++it) + til::CoordType x = 0; + til::CoordType j = 0; + for (const auto& ch : testRow.text) { - indexString.Format(L"[Cell %d, %d; Text line index %d]", it - charRow.begin(), i, j); - // Yes, we're about to manually create a buffer. It is unpleasant. - const auto ch{ til::at(testRow.text, j) }; + indexString.Format(L"[Cell %d, %d; Text line index %d]", x, y, j); + if (IsGlyphFullWidth(ch)) { // Char is full width in test buffer, so // ensure that real buffer is LEAD, TRAIL (ch) - VERIFY_IS_TRUE(it->DbcsAttr().IsLeading(), indexString); - VERIFY_ARE_EQUAL(ch, it->Char(), indexString); + VERIFY_ARE_EQUAL(row.DbcsAttrAt(x), DbcsAttribute::Leading, indexString); + VERIFY_ARE_EQUAL(ch, row.GlyphAt(x).front(), indexString); + ++x; - it++; - VERIFY_IS_TRUE(it->DbcsAttr().IsTrailing(), indexString); + VERIFY_ARE_EQUAL(row.DbcsAttrAt(x), DbcsAttribute::Trailing, indexString); + VERIFY_ARE_EQUAL(ch, row.GlyphAt(x).front(), indexString); + ++x; } else { - VERIFY_IS_TRUE(it->DbcsAttr().IsSingle(), indexString); + VERIFY_ARE_EQUAL(row.DbcsAttrAt(x), DbcsAttribute::Single, indexString); + VERIFY_ARE_EQUAL(ch, row.GlyphAt(x).front(), indexString); + ++x; } - VERIFY_ARE_EQUAL(ch, it->Char(), indexString); j++; } - i++; + + y++; } } @@ -841,7 +795,7 @@ class ReflowTests for (size_t bufferIndex{ 1 }; bufferIndex < testCase.buffers.size(); ++bufferIndex) { const auto& testBuffer{ til::at(testCase.buffers, bufferIndex) }; - Log::Comment(NoThrowString().Format(L"[%zu.%zu] Resizing to %dx%d", i, bufferIndex, testBuffer.size.X, testBuffer.size.Y)); + Log::Comment(NoThrowString().Format(L"[%zu.%zu] Resizing to %dx%d", i, bufferIndex, testBuffer.size.width, testBuffer.size.height)); auto newBuffer{ _textBufferByReflowingTextBuffer(*textBuffer, testBuffer.size) }; diff --git a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj index 3ed68250f9..ad3b48c7cb 100644 --- a/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj +++ b/src/buffer/out/ut_textbuffer/TextBuffer.Unit.Tests.vcxproj @@ -14,8 +14,7 @@ - - + Create @@ -31,7 +30,7 @@ - + @@ -42,4 +41,4 @@ - + \ No newline at end of file diff --git a/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp b/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp deleted file mode 100644 index b2f9cf824a..0000000000 --- a/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "../UnicodeStorage.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; - -class UnicodeStorageTests -{ - TEST_CLASS(UnicodeStorageTests); - - TEST_METHOD(CanOverwriteEmoji) - { - UnicodeStorage storage; - const til::point coord{ 1, 3 }; - const std::vector newMoon{ 0xD83C, 0xDF11 }; - const std::vector fullMoon{ 0xD83C, 0xDF15 }; - - // store initial glyph - storage.StoreGlyph(coord, newMoon); - - // verify it was stored - auto findIt = storage._map.find(coord); - VERIFY_ARE_NOT_EQUAL(findIt, storage._map.end()); - const auto& newMoonGlyph = findIt->second; - VERIFY_ARE_EQUAL(newMoonGlyph.size(), newMoon.size()); - for (size_t i = 0; i < newMoon.size(); ++i) - { - VERIFY_ARE_EQUAL(newMoonGlyph.at(i), newMoon.at(i)); - } - - // overwrite it - storage.StoreGlyph(coord, fullMoon); - - // verify the glyph was overwritten - findIt = storage._map.find(coord); - VERIFY_ARE_NOT_EQUAL(findIt, storage._map.end()); - const auto& fullMoonGlyph = findIt->second; - VERIFY_ARE_EQUAL(fullMoonGlyph.size(), fullMoon.size()); - for (size_t i = 0; i < fullMoon.size(); ++i) - { - VERIFY_ARE_EQUAL(fullMoonGlyph.at(i), fullMoon.at(i)); - } - } -}; diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 25c1b9c5f7..6e43186c76 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 + @@ -153,6 +159,8 @@ + + @@ -161,6 +169,8 @@ + + diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest new file mode 100644 index 0000000000..d72c96e140 --- /dev/null +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -0,0 +1,156 @@ + + + + + + + + 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/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index 59e6667daf..4a0735bb20 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -10,9 +10,11 @@ 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"> + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreNameDev A Lone Developer Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -136,5 +146,6 @@ +
diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 03d484c67d..98fb12b945 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -12,8 +12,10 @@ 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" - IgnorableNamespaces="uap mp rescap uap3"> + xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreNamePre Microsoft Corporation Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -225,6 +235,7 @@ + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 742e74c9d2..c123778d1e 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -12,8 +12,10 @@ 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" - IgnorableNamespaces="uap mp rescap uap3"> + xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreName Microsoft Corporation Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -225,6 +235,7 @@ + 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 e763454442..97efcdddfd 100644 Binary files a/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-100.png and b/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png b/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png index 5e8b8b3e34..c7ee454ce9 100644 Binary files a/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png and b/src/cascadia/CascadiaPackage/ProfileIcons/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw b/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw index eced69ff25..f7d5a7010e 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/ElevateShim/elevate-shim.cpp b/src/cascadia/ElevateShim/elevate-shim.cpp index e9b4d859cf..50449ddcac 100644 --- a/src/cascadia/ElevateShim/elevate-shim.cpp +++ b/src/cascadia/ElevateShim/elevate-shim.cpp @@ -9,7 +9,10 @@ #include #include #include +#include +#include #include +#include // BODGY // @@ -25,18 +28,68 @@ // process can successfully elevate. #pragma warning(suppress : 26461) // we can't change the signature of wWinMain -int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR pCmdLine, int) +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) { - // All of the args passed to us (something like `new-tab -p {guid}`) are in - // pCmdLine + // This will invoke an elevated terminal in two possible ways. See GH#14501 + // In both scenarios, it passes the entire cmdline as-is to the new process. + // + // #1 discover and invoke the app using the GetCurrentApplicationUserModelId + // api using shell:AppsFolder\package!appid + // cmd: shell:AppsFolder\WindowsTerminalDev_8wekyb3d8bbwe!App + // params: new-tab -p {guid} + // + // #2 find and execute WindowsTerminal.exe + // cmd: {same path as this binary}\WindowsTerminal.exe + // params: new-tab -p {guid} - // Get the path to WindowsTerminal.exe, which should live next to us. - std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) }; - // Swap elevate-shim.exe for WindowsTerminal.exe - module.replace_filename(L"WindowsTerminal.exe"); + // see if we're a store app we can invoke with shell:AppsFolder + std::wstring appUserModelId; + const auto result = wil::AdaptFixedSizeToAllocatedResult( + appUserModelId, [&](PWSTR value, size_t valueLength, gsl::not_null valueLengthNeededWithNull) noexcept -> HRESULT { + UINT32 length = gsl::narrow_cast(valueLength); + const LONG rc = GetCurrentApplicationUserModelId(&length, value); + switch (rc) + { + case ERROR_SUCCESS: + *valueLengthNeededWithNull = length; + return S_OK; + + case ERROR_INSUFFICIENT_BUFFER: + *valueLengthNeededWithNull = length; + return S_FALSE; // trigger allocation loop + + case APPMODEL_ERROR_NO_APPLICATION: + return E_FAIL; // we are not running as a store app + + default: + return E_UNEXPECTED; + } + }); + LOG_IF_FAILED(result); + + std::wstring cmd = {}; + if (result == S_OK && appUserModelId.length() > 0) + { + // scenario #1 + cmd = L"shell:AppsFolder\\" + appUserModelId; + } + else + { + // scenario #2 + // Get the path to WindowsTerminal.exe, which should live next to us. + std::filesystem::path module{ + wil::GetModuleFileNameW(nullptr) + }; + // Swap elevate-shim.exe for WindowsTerminal.exe + module.replace_filename(L"WindowsTerminal.exe"); + cmd = module; + } // Go! + // The cmdline argument passed to WinMain is stripping the first argument. + // Using GetCommandLine() instead for lParameters + // disable warnings from SHELLEXECUTEINFOW struct. We can't fix that. #pragma warning(push) #pragma warning(disable : 26476) // Macro uses naked union over variant. @@ -46,8 +99,10 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR pCmdLine, int) seInfo.cbSize = sizeof(seInfo); seInfo.fMask = SEE_MASK_DEFAULT; seInfo.lpVerb = L"runas"; // This asks the shell to elevate the process - seInfo.lpFile = module.c_str(); // This is `...\WindowsTerminal.exe` - seInfo.lpParameters = pCmdLine; // This is `new-tab -p {guid}` + seInfo.lpFile = cmd.c_str(); // This is `shell:AppsFolder\...` or `...\WindowsTerminal.exe` + seInfo.lpParameters = GetCommandLine(); // This is `new-tab -p {guid}` seInfo.nShow = SW_SHOWNORMAL; LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + return 0; } diff --git a/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp b/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp index 721e426761..4789cfa7e9 100644 --- a/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp @@ -79,7 +79,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(til::color(0xFF, 0xFF, 0xFF, 255), til::color{ scheme->CursorColor() }); std::array expectedCampbellTable; - const auto campbellSpan = gsl::make_span(expectedCampbellTable); + const auto campbellSpan = std::span{ expectedCampbellTable }; Utils::InitializeColorTable(campbellSpan); for (size_t i = 0; i < expectedCampbellTable.size(); i++) @@ -217,6 +217,30 @@ namespace SettingsModelLocalTests { "name": "Different reference", "colorScheme": "One Half Dark" + }, + { + "name": "rename neither", + "colorScheme": + { + "dark": "One Half Dark", + "light": "One Half Light" + } + }, + { + "name": "rename only light", + "colorScheme": + { + "dark": "One Half Dark", + "light": "Campbell" + } + }, + { + "name": "rename only dark", + "colorScheme": + { + "dark": "Campbell", + "light": "One Half Light" + } } ] }, @@ -289,6 +313,28 @@ namespace SettingsModelLocalTests "selectionBackground": "#FFFFFF", "white": "#DCDFE4", "yellow": "#E5C07B" + }, + { + "name": "One Half Light", + "foreground": "#383A42", + "background": "#FAFAFA", + "cursorColor": "#4F525D", + "black": "#383A42", + "red": "#E45649", + "green": "#50A14F", + "yellow": "#C18301", + "blue": "#0184BC", + "purple": "#A626A4", + "cyan": "#0997B3", + "white": "#FAFAFA", + "brightBlack": "#4F525D", + "brightRed": "#DF6C75", + "brightGreen": "#98C379", + "brightYellow": "#E4C07A", + "brightBlue": "#61AFEF", + "brightPurple": "#C577DD", + "brightCyan": "#56B5C1", + "brightWhite": "#FFFFFF" } ] })json" }; @@ -296,31 +342,63 @@ namespace SettingsModelLocalTests const auto settings{ winrt::make_self(settingsString) }; const auto newName{ L"Campbell (renamed)" }; + settings->UpdateColorSchemeReferences(L"Campbell", newName); - VERIFY_ARE_EQUAL(newName, settings->ProfileDefaults().DefaultAppearance().ColorSchemeName()); - VERIFY_IS_TRUE(settings->ProfileDefaults().DefaultAppearance().HasColorSchemeName()); + VERIFY_ARE_EQUAL(newName, settings->ProfileDefaults().DefaultAppearance().DarkColorSchemeName()); + VERIFY_ARE_EQUAL(newName, settings->ProfileDefaults().DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(settings->ProfileDefaults().DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_IS_TRUE(settings->ProfileDefaults().DefaultAppearance().HasLightColorSchemeName()); const auto& profiles{ settings->AllProfiles() }; { const auto& prof{ profiles.GetAt(0) }; - VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().ColorSchemeName()); - VERIFY_IS_TRUE(prof.DefaultAppearance().HasColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); } { const auto& prof{ profiles.GetAt(1) }; - VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().ColorSchemeName()); - VERIFY_IS_TRUE(prof.DefaultAppearance().HasColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); } { const auto& prof{ profiles.GetAt(2) }; - VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().ColorSchemeName()); - VERIFY_IS_FALSE(prof.DefaultAppearance().HasColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_IS_FALSE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_FALSE(prof.DefaultAppearance().HasLightColorSchemeName()); } { const auto& prof{ profiles.GetAt(3) }; - VERIFY_ARE_EQUAL(L"One Half Dark", prof.DefaultAppearance().ColorSchemeName()); - VERIFY_IS_TRUE(prof.DefaultAppearance().HasColorSchemeName()); + VERIFY_ARE_EQUAL(L"One Half Dark", prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_ARE_EQUAL(L"One Half Dark", prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); + } + { + const auto& prof{ profiles.GetAt(4) }; + VERIFY_ARE_EQUAL(L"One Half Dark", prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_ARE_EQUAL(L"One Half Light", prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); + } + { + const auto& prof{ profiles.GetAt(5) }; + VERIFY_ARE_EQUAL(L"One Half Dark", prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); + } + { + const auto& prof{ profiles.GetAt(6) }; + VERIFY_ARE_EQUAL(newName, prof.DefaultAppearance().DarkColorSchemeName()); + VERIFY_ARE_EQUAL(L"One Half Light", prof.DefaultAppearance().LightColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasDarkColorSchemeName()); + VERIFY_IS_TRUE(prof.DefaultAppearance().HasLightColorSchemeName()); } } } diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp index 7008feed58..b5139a5263 100644 --- a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -74,6 +74,8 @@ namespace SettingsModelLocalTests TEST_METHOD(TestInheritedCommand); TEST_METHOD(LoadFragmentsWithMultipleUpdates); + TEST_METHOD(MigrateReloadEnvVars); + private: static winrt::com_ptr createSettings(const std::string_view& userJSON) { @@ -744,7 +746,8 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); for (const auto& profile : settings->AllProfiles()) { - VERIFY_ARE_EQUAL(L"Campbell", profile.DefaultAppearance().ColorSchemeName()); + VERIFY_ARE_EQUAL(L"Campbell", profile.DefaultAppearance().DarkColorSchemeName()); + VERIFY_ARE_EQUAL(L"Campbell", profile.DefaultAppearance().LightColorSchemeName()); } } @@ -959,6 +962,10 @@ namespace SettingsModelLocalTests }, { "name": "profile3", + "closeOnExit": "automatic" + }, + { + "name": "profile4", "closeOnExit": null } ] @@ -968,9 +975,10 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(0).CloseOnExit()); VERIFY_ARE_EQUAL(CloseOnExitMode::Always, settings->AllProfiles().GetAt(1).CloseOnExit()); VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->AllProfiles().GetAt(2).CloseOnExit()); + VERIFY_ARE_EQUAL(CloseOnExitMode::Automatic, settings->AllProfiles().GetAt(3).CloseOnExit()); - // Unknown modes parse as "Graceful" - VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(3).CloseOnExit()); + // Unknown modes parse as "Automatic" + VERIFY_ARE_EQUAL(CloseOnExitMode::Automatic, settings->AllProfiles().GetAt(4).CloseOnExit()); } void DeserializationTests::TestCloseOnExitCompatibilityShim() @@ -1524,7 +1532,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); // Because the "parent" command didn't have a name, it couldn't be - // placed into the list of commands. It and it's children are just + // placed into the list of commands. It and its children are just // ignored. VERIFY_ARE_EQUAL(0u, settings->ActionMap().NameMap().Size()); } @@ -2014,4 +2022,40 @@ namespace SettingsModelLocalTests // GH#12520: Fragments should be able to override the name of builtin profiles. VERIFY_ARE_EQUAL(L"NewName", loader.userSettings.profiles[0]->Name()); } + + void DeserializationTests::MigrateReloadEnvVars() + { + static constexpr std::string_view settings1Json{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "compatibility.reloadEnvironmentVariables": false, + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader loader{ settings1Json, DefaultJson }; + loader.MergeInboxIntoUserSettings(); + loader.FinalizeLayering(); + + VERIFY_IS_TRUE(loader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + + const auto settings = winrt::make_self(std::move(loader)); + + Log::Comment(L"Ensure that the profile defaults have the new setting added"); + VERIFY_IS_TRUE(settings->ProfileDefaults().HasReloadEnvironmentVariables()); + VERIFY_IS_FALSE(settings->ProfileDefaults().ReloadEnvironmentVariables()); + } } diff --git a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h index 4e9650c883..e1a235239b 100644 --- a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h +++ b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h @@ -20,7 +20,7 @@ class JsonTestClass public: static Json::Value VerifyParseSucceeded(const std::string_view& content) { - static const std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + static const std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; std::string errs; @@ -31,7 +31,7 @@ public: static std::string toString(const Json::Value& json) { - static const std::unique_ptr writer{ Json::StreamWriterBuilder::StreamWriterBuilder().newStreamWriter() }; + static const std::unique_ptr writer{ Json::StreamWriterBuilder{}.newStreamWriter() }; std::stringstream s; writer->write(json, &s); diff --git a/src/cascadia/LocalTests_SettingsModel/NewTabMenuTests.cpp b/src/cascadia/LocalTests_SettingsModel/NewTabMenuTests.cpp new file mode 100644 index 0000000000..f9e4625e99 --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/NewTabMenuTests.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/NewTabMenuEntry.h" +#include "../TerminalSettingsModel/FolderEntry.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "../types/inc/colorTable.hpp" +#include "JsonTestClass.h" + +#include + +using namespace Microsoft::Console; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace SettingsModelLocalTests +{ + // TODO:microsoft/terminal#3838: + // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for + // an updated TAEF that will let us install framework packages when the test + // package is deployed. Until then, these tests won't deploy in CI. + + class NewTabMenuTests : public JsonTestClass + { + // Use a custom AppxManifest to ensure that we can activate winrt types + // from our test. This property will tell taef to manually use this as + // the AppxManifest for this test class. + // This does not yet work for anything XAML-y. See TabTests.cpp for more + // details on that. + BEGIN_TEST_CLASS(NewTabMenuTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(DefaultsToRemainingProfiles); + TEST_METHOD(ParseEmptyFolder); + }; + + void NewTabMenuTests::DefaultsToRemainingProfiles() + { + Log::Comment(L"If the user doesn't customize the menu, put one entry for each profile"); + + static constexpr std::string_view settingsString{ R"json({ + })json" }; + + try + { + const auto settings{ winrt::make_self(settingsString, DefaultJson) }; + + VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); + + const auto& entries = settings->GlobalSettings().NewTabMenu(); + VERIFY_ARE_EQUAL(1u, entries.Size()); + VERIFY_ARE_EQUAL(winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType::RemainingProfiles, entries.GetAt(0).Type()); + } + catch (const SettingsException& ex) + { + auto loadError = ex.Error(); + loadError; + throw ex; + } + catch (const SettingsTypedDeserializationException& e) + { + auto deserializationErrorMessage = til::u8u16(e.what()); + Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); + throw e; + } + } + + void NewTabMenuTests::ParseEmptyFolder() + { + Log::Comment(L"GH #14557 - An empty folder entry shouldn't crash"); + + static constexpr std::string_view settingsString{ R"json({ + "newTabMenu": [ + { "type": "folder" } + ] + })json" }; + + try + { + const auto settings{ winrt::make_self(settingsString, DefaultJson) }; + + VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); + + const auto& entries = settings->GlobalSettings().NewTabMenu(); + VERIFY_ARE_EQUAL(1u, entries.Size()); + } + catch (const SettingsException& ex) + { + auto loadError = ex.Error(); + loadError; + throw ex; + } + catch (const SettingsTypedDeserializationException& e) + { + auto deserializationErrorMessage = til::u8u16(e.what()); + Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); + throw e; + } + } +} diff --git a/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp index fdb160fe27..a73b2bec3e 100644 --- a/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp @@ -38,6 +38,9 @@ namespace SettingsModelLocalTests TEST_METHOD(LayerProfileProperties); TEST_METHOD(LayerProfileIcon); TEST_METHOD(LayerProfilesOnArray); + TEST_METHOD(ProfileWithEnvVars); + TEST_METHOD(ProfileWithEnvVarsSameNameDifferentCases); + TEST_METHOD(DuplicateProfileTest); TEST_METHOD(TestGenGuidsForProfiles); TEST_METHOD(TestCorrectOldDefaultShellPaths); @@ -349,6 +352,50 @@ namespace SettingsModelLocalTests VERIFY_ARE_NOT_EQUAL(settings->AllProfiles().GetAt(0).Guid(), settings->AllProfiles().GetAt(1).Guid()); } + void ProfileTests::ProfileWithEnvVars() + { + const std::string profileString{ R"({ + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "environment": { + "VAR_1": "value1", + "VAR_2": "value2", + "VAR_3": "%VAR_3%;value3" + } + })" }; + const auto profile = implementation::Profile::FromJson(VerifyParseSucceeded(profileString)); + std::vector envVarMaps{}; + envVarMaps.emplace_back(profile->EnvironmentVariables()); + for (auto& envMap : envVarMaps) + { + VERIFY_ARE_EQUAL(static_cast(3), envMap.Size()); + VERIFY_ARE_EQUAL(L"value1", envMap.Lookup(L"VAR_1")); + VERIFY_ARE_EQUAL(L"value2", envMap.Lookup(L"VAR_2")); + VERIFY_ARE_EQUAL(L"%VAR_3%;value3", envMap.Lookup(L"VAR_3")); + } + } + + void ProfileTests::ProfileWithEnvVarsSameNameDifferentCases() + { + const std::string userSettings{ R"({ + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "environment": { + "FOO": "VALUE", + "Foo": "Value" + } + } + ] + })" }; + const auto settings = winrt::make_self(userSettings); + const auto warnings = settings->Warnings(); + VERIFY_ARE_EQUAL(static_cast(2), warnings.Size()); + uint32_t index; + VERIFY_IS_TRUE(warnings.IndexOf(SettingsLoadWarnings::InvalidProfileEnvironmentVariables, index)); + } + void ProfileTests::TestCorrectOldDefaultShellPaths() { static constexpr std::string_view inboxProfiles{ R"({ diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp index f0a3ed3d02..2488f3f812 100644 --- a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -42,6 +42,9 @@ namespace SettingsModelLocalTests TEST_METHOD(CascadiaSettings); TEST_METHOD(LegacyFontSettings); + TEST_METHOD(RoundtripReloadEnvVars); + TEST_METHOD(DontRoundtripNoReloadEnvVars); + private: // Method Description: // - deserializes and reserializes a json string representing a settings object model of type T @@ -134,7 +137,7 @@ namespace SettingsModelLocalTests "font": { "face": "Cascadia Mono", - "size": 12, + "size": 12.0, "weight": "normal" }, "padding": "8, 8, 8, 8", @@ -165,7 +168,13 @@ namespace SettingsModelLocalTests "historySize": 9001, "closeOnExit": "graceful", - "experimental.retroTerminalEffect": false + "experimental.retroTerminalEffect": false, + "environment": + { + "KEY_1": "VALUE_1", + "KEY_2": "%KEY_1%", + "KEY_3": "%PATH%" + } })" }; static constexpr std::string_view smallProfileString{ R"( @@ -250,8 +259,8 @@ namespace SettingsModelLocalTests // complex command with key chords static constexpr std::string_view actionsString4A{ R"([ - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+c" }, - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+d" } + { "command": { "action": "adjustFontSize", "delta": 1.0 }, "keys": "ctrl+c" }, + { "command": { "action": "adjustFontSize", "delta": 1.0 }, "keys": "ctrl+d" } ])" }; // GH#13323 - these can be fragile. In the past, the order these get // re-serialized as has been not entirely stable. We don't really care @@ -260,7 +269,7 @@ namespace SettingsModelLocalTests // itself. Feel free to change as needed. static constexpr std::string_view actionsString4B{ R"([ { "command": { "action": "findMatch", "direction": "prev" }, "keys": "ctrl+shift+r" }, - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+d" } + { "command": { "action": "adjustFontSize", "delta": 1.0 }, "keys": "ctrl+d" } ])" }; // command with name and icon and multiple key chords @@ -284,8 +293,8 @@ namespace SettingsModelLocalTests { "name": "Change font size...", "commands": [ - { "command": { "action": "adjustFontSize", "delta": 1 } }, - { "command": { "action": "adjustFontSize", "delta": -1 } }, + { "command": { "action": "adjustFontSize", "delta": 1.0 } }, + { "command": { "action": "adjustFontSize", "delta": -1.0 } }, { "command": "resetFontSize" }, ] } @@ -406,6 +415,12 @@ namespace SettingsModelLocalTests "$schema" : "https://aka.ms/terminal-profiles-schema", "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", "disabledProfileSources": [ "Windows.Terminal.Wsl" ], + "newTabMenu": + [ + { + "type": "remainingProfiles" + } + ], "profiles": { "defaults": { "font": { @@ -478,7 +493,7 @@ namespace SettingsModelLocalTests "name": "Profile with legacy font settings", "fontFace": "Cascadia Mono", - "fontSize": 12, + "fontSize": 12.0, "fontWeight": "normal" })" }; @@ -488,7 +503,7 @@ namespace SettingsModelLocalTests "font": { "face": "Cascadia Mono", - "size": 12, + "size": 12.0, "weight": "normal" } })" }; @@ -501,4 +516,114 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(toString(jsonOutput), toString(result)); } + + void SerializationTests::RoundtripReloadEnvVars() + { + static constexpr std::string_view oldSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "compatibility.reloadEnvironmentVariables": false, + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + static constexpr std::string_view newSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": + { + "defaults": + { + "compatibility.reloadEnvironmentVariables": false + }, + "list": + [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ] + }, + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, DefaultJson }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + VERIFY_IS_TRUE(oldLoader.FixupUserSettings(), L"Validate that this will indicate we need to write them back to disk"); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + implementation::SettingsLoader newLoader{ newSettingsJson, DefaultJson }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + const auto newResult{ newSettings->ToJson() }; + + VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); + } + + void SerializationTests::DontRoundtripNoReloadEnvVars() + { + // Kinda like the above test, but confirming that _nothing_ happens if + // we don't have a setting to migrate. + + static constexpr std::string_view oldSettingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "actions": [ + { + "name": "foo", + "command": "closePane", + "keys": "ctrl+shift+w" + } + ] + })" }; + + implementation::SettingsLoader oldLoader{ oldSettingsJson, DefaultJson }; + oldLoader.MergeInboxIntoUserSettings(); + oldLoader.FinalizeLayering(); + oldLoader.FixupUserSettings(); + const auto oldSettings = winrt::make_self(std::move(oldLoader)); + const auto oldResult{ oldSettings->ToJson() }; + + Log::Comment(L"Now, create a _new_ settings object from the re-serialization of the first"); + implementation::SettingsLoader newLoader{ toString(oldResult), DefaultJson }; + newLoader.MergeInboxIntoUserSettings(); + newLoader.FinalizeLayering(); + newLoader.FixupUserSettings(); + const auto newSettings = winrt::make_self(std::move(newLoader)); + VERIFY_IS_FALSE(newSettings->ProfileDefaults().HasReloadEnvironmentVariables(), + L"Ensure that the new settings object didn't find a reloadEnvironmentVariables"); + } } diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj index 87704cde00..dbd279005b 100644 --- a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -8,7 +8,7 @@ dependencies, like MUX, can be aggregated correctly, and resources properly combined into a resources.pri file. - TestHostApp will manually copy the output of this project into it's own + TestHostApp will manually copy the output of this project into its own OutDir, so we can run the tests from there. --> @@ -20,6 +20,7 @@ DynamicLibrary true + true @@ -39,6 +40,7 @@ + @@ -72,8 +74,6 @@ ..;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\jsoncpp\json;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettingsModel\Generated Files";%(AdditionalIncludeDirectories) pch.h - - %(AdditionalOptions) /Zc:twoPhase- 4702;%(DisableSpecificWarnings) @@ -98,15 +98,4 @@ - - - - x86 - $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.$(TerminalMUXVersion)\runtimes\win10-$(Native-Platform)\native\" - - - - - diff --git a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp index 8680e7f9cd..0ae0bcc164 100644 --- a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp @@ -779,21 +779,22 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(6u, settings->ActiveProfiles().Size()); VERIFY_ARE_EQUAL(2u, settings->GlobalSettings().ColorSchemes().Size()); - auto createTerminalSettings = [&](const auto& profile, const auto& schemes) { + auto createTerminalSettings = [&](const auto& profile, const auto& schemes, const auto& Theme) { auto terminalSettings{ winrt::make_self() }; terminalSettings->_ApplyProfileSettings(profile); - terminalSettings->_ApplyAppearanceSettings(profile.DefaultAppearance(), schemes); + terminalSettings->_ApplyAppearanceSettings(profile.DefaultAppearance(), schemes, Theme); return terminalSettings; }; const auto activeProfiles = settings->ActiveProfiles(); const auto colorSchemes = settings->GlobalSettings().ColorSchemes(); - const auto terminalSettings0 = createTerminalSettings(activeProfiles.GetAt(0), colorSchemes); - const auto terminalSettings1 = createTerminalSettings(activeProfiles.GetAt(1), colorSchemes); - const auto terminalSettings2 = createTerminalSettings(activeProfiles.GetAt(2), colorSchemes); - const auto terminalSettings3 = createTerminalSettings(activeProfiles.GetAt(3), colorSchemes); - const auto terminalSettings4 = createTerminalSettings(activeProfiles.GetAt(4), colorSchemes); - const auto terminalSettings5 = createTerminalSettings(activeProfiles.GetAt(5), colorSchemes); + const auto currentTheme = settings->GlobalSettings().CurrentTheme(); + const auto terminalSettings0 = createTerminalSettings(activeProfiles.GetAt(0), colorSchemes, currentTheme); + const auto terminalSettings1 = createTerminalSettings(activeProfiles.GetAt(1), colorSchemes, currentTheme); + const auto terminalSettings2 = createTerminalSettings(activeProfiles.GetAt(2), colorSchemes, currentTheme); + const auto terminalSettings3 = createTerminalSettings(activeProfiles.GetAt(3), colorSchemes, currentTheme); + const auto terminalSettings4 = createTerminalSettings(activeProfiles.GetAt(4), colorSchemes, currentTheme); + const auto terminalSettings5 = createTerminalSettings(activeProfiles.GetAt(5), colorSchemes, currentTheme); VERIFY_ARE_EQUAL(til::color(0x12, 0x34, 0x56), terminalSettings0->CursorColor()); // from color scheme VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings1->CursorColor()); // default diff --git a/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp index 327be5ad92..c7bf11326a 100644 --- a/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp @@ -60,11 +60,13 @@ namespace SettingsModelLocalTests "tabRow": { "background": "#FFFF8800", - "unfocusedBackground": "#FF8844" + "unfocusedBackground": "#FF8844", + "iconStyle": "default" }, "window": { - "applicationTheme": "light" + "applicationTheme": "light", + "useMica": true } })" }; @@ -80,6 +82,7 @@ namespace SettingsModelLocalTests VERIFY_IS_NOT_NULL(theme->Window()); VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Light, theme->Window().RequestedTheme()); + VERIFY_ARE_EQUAL(true, theme->Window().UseMica()); } void ThemeTests::ParseEmptyTheme() @@ -161,7 +164,8 @@ namespace SettingsModelLocalTests }, "window": { - "applicationTheme": "light" + "applicationTheme": "light", + "useMica": true } }, { @@ -172,14 +176,16 @@ namespace SettingsModelLocalTests }, "window": { - "applicationTheme": "light" + "applicationTheme": "light", + "useMica": true } }, { "name": "backgroundOmittedEntirely", "window": { - "applicationTheme": "light" + "applicationTheme": "light", + "useMica": true } } ] @@ -234,7 +240,8 @@ namespace SettingsModelLocalTests "tabRow": {}, "window": { - "applicationTheme": "light" + "applicationTheme": "light", + "useMica": true } } ] diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h index b018bdaa4a..b077abab8b 100644 --- a/src/cascadia/LocalTests_SettingsModel/pch.h +++ b/src/cascadia/LocalTests_SettingsModel/pch.h @@ -18,7 +18,7 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #define BLOCK_TIL // This includes support libraries from the CRT, STL, WIL, and GSL -#include "LibraryIncludes.h" +#include // This is inexplicable, but for whatever reason, cppwinrt conflicts with the // SDK definition of this function, so the only fix is to undef it. // from WinBase.h @@ -28,8 +28,9 @@ Author(s): #endif #include -#include +#include #include +#include #include #include @@ -44,6 +45,7 @@ Author(s): #include #include #include +#include #include #include #include @@ -62,6 +64,7 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include // Common includes for most tests: #include "../../inc/conattrs.hpp" diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index a3dcbb45f4..a940cc915f 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -98,6 +98,26 @@ namespace TerminalAppLocalTests } } } + void _logCommands(winrt::Windows::Foundation::Collections::IVector commands, const int indentation = 1) + { + if (indentation == 1) + { + Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); + } + for (const auto& cmd : commands) + { + Log::Comment(fmt::format(L"{0:>{1}}* {2}", + L"", + indentation, + cmd.Name()) + .c_str()); + + if (cmd.HasNestedCommands()) + { + _logCommandNames(cmd.NestedCommands(), indentation + 2); + } + } + } }; void SettingsTests::TestIterateCommands() @@ -164,14 +184,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -189,7 +210,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -207,7 +229,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -287,14 +310,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -312,7 +337,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile1", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -330,7 +357,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -412,14 +441,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -437,7 +468,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1\""); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1\"", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -455,7 +488,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -527,14 +562,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"Connect to ssh..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"Connect to ssh...", rootCommand.Name()); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -621,14 +657,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto grandparentCommand = expandedCommands.Lookup(L"grandparent"); + auto grandparentCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(grandparentCommand); + VERIFY_ARE_EQUAL(L"grandparent", grandparentCommand.Name()); + auto grandparentActionAndArgs = grandparentCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(grandparentActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, grandparentActionAndArgs.Action()); @@ -744,17 +782,22 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); - for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) + const std::vector profileNames{ L"profile0", L"profile1", L"profile2" }; + for (auto i = 0u; i < profileNames.size(); i++) { - winrt::hstring commandName{ name + L"..." }; - auto command = expandedCommands.Lookup(commandName); + const auto& name{ profileNames[i] }; + winrt::hstring commandName{ profileNames[i] + L"..." }; + + auto command = expandedCommands.GetAt(i); + VERIFY_ARE_EQUAL(commandName, command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -880,14 +923,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Tab With Profile..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Tab With Profile...", rootCommand.Name()); + auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -982,13 +1027,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Pane..."); + auto rootCommand = expandedCommands.GetAt(0); + VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Pane...", rootCommand.Name()); + VERIFY_IS_NOT_NULL(rootCommand); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); @@ -1205,8 +1253,8 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${scheme.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); @@ -1215,7 +1263,9 @@ namespace TerminalAppLocalTests // just easy tests to write. { - auto command = expandedCommands.Lookup(L"iterable command Campbell"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command Campbell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1233,7 +1283,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Campbell PowerShell"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command Campbell PowerShell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1251,7 +1303,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Vintage"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command Vintage", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 53013f9372..4a09828c68 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -4,11 +4,13 @@ #include "pch.h" #include "../TerminalApp/TerminalPage.h" +#include "../TerminalApp/TerminalWindow.h" #include "../TerminalApp/MinMaxCloseControl.h" #include "../TerminalApp/TabRowControl.h" #include "../TerminalApp/ShortcutActionDispatch.h" #include "../TerminalApp/TerminalTab.h" #include "../TerminalApp/CommandPalette.h" +#include "../TerminalApp/ContentManager.h" #include "CppWinrtTailored.h" using namespace Microsoft::Console; @@ -110,6 +112,8 @@ namespace TerminalAppLocalTests void _initializeTerminalPage(winrt::com_ptr& page, CascadiaSettings initialSettings); winrt::com_ptr _commonSetup(); + winrt::com_ptr _windowProperties; + winrt::com_ptr _contentManager; }; template @@ -194,8 +198,14 @@ namespace TerminalAppLocalTests { winrt::com_ptr page{ nullptr }; - auto result = RunOnUIThread([&page]() { - page = winrt::make_self(); + _windowProperties = winrt::make_self(); + winrt::TerminalApp::WindowProperties props = *_windowProperties; + + _contentManager = winrt::make_self(); + winrt::TerminalApp::ContentManager contentManager = *_contentManager; + + auto result = RunOnUIThread([&page, props, contentManager]() { + page = winrt::make_self(props, contentManager); VERIFY_IS_NOT_NULL(page); }); VERIFY_SUCCEEDED(result); @@ -239,9 +249,13 @@ namespace TerminalAppLocalTests // it's weird. winrt::TerminalApp::TerminalPage projectedPage{ nullptr }; + _windowProperties = winrt::make_self(); + winrt::TerminalApp::WindowProperties props = *_windowProperties; + _contentManager = winrt::make_self(); + winrt::TerminalApp::ContentManager contentManager = *_contentManager; Log::Comment(NoThrowString().Format(L"Construct the TerminalPage")); - auto result = RunOnUIThread([&projectedPage, &page, initialSettings]() { - projectedPage = winrt::TerminalApp::TerminalPage(); + auto result = RunOnUIThread([&projectedPage, &page, initialSettings, props, contentManager]() { + projectedPage = winrt::TerminalApp::TerminalPage(props, contentManager); page.copy_from(winrt::get_self(projectedPage)); page->_settings = initialSettings; }); @@ -503,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)); @@ -521,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)); @@ -843,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); @@ -861,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(); @@ -881,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(); }); @@ -1088,7 +1102,7 @@ namespace TerminalAppLocalTests // If you don't do this, the palette will just stay open, and the // next time we call _HandleNextTab, we'll continue traversing the // MRU list, instead of just hoping one entry. - page->CommandPalette().Visibility(Visibility::Collapsed); + page->LoadCommandPalette().Visibility(Visibility::Collapsed); }); TestOnUIThread([&page]() { @@ -1109,7 +1123,7 @@ namespace TerminalAppLocalTests // If you don't do this, the palette will just stay open, and the // next time we call _HandleNextTab, we'll continue traversing the // MRU list, instead of just hoping one entry. - page->CommandPalette().Visibility(Visibility::Collapsed); + page->LoadCommandPalette().Visibility(Visibility::Collapsed); }); TestOnUIThread([&page]() { @@ -1225,7 +1239,7 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title()); }); - const auto palette = winrt::get_self(page->CommandPalette()); + const auto palette = winrt::get_self(page->LoadCommandPalette()); VERIFY_ARE_EQUAL(winrt::TerminalApp::implementation::CommandPaletteMode::TabSwitchMode, palette->_currentMode, L"Verify we are in the tab switcher mode"); // At this point, the contents of the command palette's _mruTabs list is @@ -1242,14 +1256,16 @@ namespace TerminalAppLocalTests END_TEST_METHOD_PROPERTIES() auto page = _commonSetup(); - page->RenameWindowRequested([&page](auto&&, const winrt::TerminalApp::RenameWindowRequestedArgs args) { + page->RenameWindowRequested([&page, this](auto&&, const winrt::TerminalApp::RenameWindowRequestedArgs args) { // In the real terminal, this would bounce up to the monarch and // come back down. Instead, immediately call back and set the name. - page->WindowName(args.ProposedName()); + // + // This replicates how TerminalWindow works + _windowProperties->WindowName(args.ProposedName()); }); auto windowNameChanged = false; - page->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable { + _windowProperties->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable { if (args.PropertyName() == L"WindowNameForDisplay") { windowNameChanged = true; @@ -1260,7 +1276,7 @@ namespace TerminalAppLocalTests page->_RequestWindowRename(winrt::hstring{ L"Foo" }); }); TestOnUIThread([&]() { - VERIFY_ARE_EQUAL(L"Foo", page->_WindowName); + VERIFY_ARE_EQUAL(L"Foo", page->WindowProperties().WindowName()); VERIFY_IS_TRUE(windowNameChanged, L"The window name should have changed, and we should have raised a notification that WindowNameForDisplay changed"); }); diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index 4d9ec0ab83..e5a00d2d89 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -8,7 +8,7 @@ dependencies, like MUX, can be aggregated correctly, and resources properly combined into a resources.pri file. - TestHostApp will manually copy the output of this project into it's own + TestHostApp will manually copy the output of this project into its own OutDir, so we can run the tests from there. --> @@ -23,7 +23,7 @@ - true + true @@ -86,15 +86,4 @@ - - - - x86 - $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.$(TerminalMUXVersion)\runtimes\win10-$(Native-Platform)\native\" - - - - - diff --git a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj index f71e0ce53f..9e2029c9bc 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj @@ -14,6 +14,9 @@ true Application true + false + + true - - - Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies) - - - \ No newline at end of file diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters deleted file mode 100644 index 6865448d4a..0000000000 --- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj.filters +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;ipp;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - \ No newline at end of file diff --git a/src/cascadia/PublicTerminalCore/pch.cpp b/src/cascadia/PublicTerminalCore/pch.cpp deleted file mode 100644 index 398a99f665..0000000000 --- a/src/cascadia/PublicTerminalCore/pch.cpp +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" diff --git a/src/cascadia/PublicTerminalCore/pch.h b/src/cascadia/PublicTerminalCore/pch.h deleted file mode 100644 index 3834f83cde..0000000000 --- a/src/cascadia/PublicTerminalCore/pch.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN // If this is not defined, windows.h includes commdlg.h which defines FindText globally and conflicts with UIAutomation ITextRangeProvider. -#define NOMCX -#define NOHELP -#define NOCOMM -#endif - -#include -#include \ No newline at end of file diff --git a/src/cascadia/Remoting/CommandlineArgs.h b/src/cascadia/Remoting/CommandlineArgs.h index 5277e1c5f2..2f618e53ef 100644 --- a/src/cascadia/Remoting/CommandlineArgs.h +++ b/src/cascadia/Remoting/CommandlineArgs.h @@ -14,9 +14,13 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } CommandlineArgs(const winrt::array_view& args, - winrt::hstring currentDirectory) : + winrt::hstring currentDirectory, + const uint32_t showWindowCommand, + winrt::hstring envString) : _args{ args.begin(), args.end() }, - _cwd{ currentDirectory } + _cwd{ currentDirectory }, + _ShowWindowCommand{ showWindowCommand }, + CurrentEnvironment{ envString } { } @@ -25,6 +29,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void Commandline(const winrt::array_view& value); winrt::com_array Commandline(); + til::property CurrentEnvironment; + + WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); // SW_NORMAL is 1, 0 is SW_HIDE + private: winrt::com_array _args; winrt::hstring _cwd; diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index 8d69e2768e..cf2edd98fa 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -10,6 +10,7 @@ #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" +#include "WindowRequestedArgs.g.cpp" #include "../../types/inc/utils.hpp" using namespace winrt; @@ -301,6 +302,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { return 0; } + if (name == L"new") + { + return 0; + } uint64_t result = 0; @@ -658,6 +663,13 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + if (targetWindow == WindowingBehaviorUseNone) + { + // In this case, the targetWindow was UseNone, which means that we + // want to make a message box, but otherwise not make a Terminal + // window. + return winrt::make(false); + } // If there's a valid ID returned, then let's try and find the peasant // that goes with it. Alternatively, if we were given a magic windowing // constant, we can use that to look up an appropriate peasant. @@ -687,6 +699,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation case WindowingBehaviorUseName: windowID = _lookupPeasantIdForName(targetWindowName); break; + case WindowingBehaviorUseNone: + // This should be impossible. The if statement above should have + // prevented WindowingBehaviorUseNone from falling in here. + // Explode, because this is a programming error. + THROW_HR(E_UNEXPECTED); default: windowID = ::base::saturated_cast(targetWindow); break; @@ -724,6 +741,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation result->WindowName(targetWindowName); result->ShouldCreateWindow(true); + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + // If this fails, it'll be logged in the following // TraceLoggingWrite statement, with succeeded=false } @@ -759,6 +778,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation auto result{ winrt::make_self(true) }; result->Id(windowID); result->WindowName(targetWindowName); + + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + return *result; } } @@ -773,6 +795,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // In this case, no usable ID was provided. Return { true, nullopt } auto result = winrt::make_self(true); result->WindowName(targetWindowName); + + _RequestNewWindowHandlers(*this, *winrt::make_self(*result, args)); + return *result; } @@ -1034,4 +1059,96 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return winrt::single_threaded_vector(std::move(vec)); } + + void Monarch::RequestMoveContent(winrt::hstring window, + winrt::hstring content, + uint32_t tabIndex, + const Windows::Foundation::IReference& windowBounds) + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_MoveContent_Requested", + TraceLoggingWideString(window.c_str(), "window", "The name of the window we tried to move to"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + uint64_t windowId = _lookupPeasantIdForName(window); + if (windowId == 0) + { + // Try the name as an integer ID + uint32_t temp; + if (!Utils::StringToUint(window.c_str(), temp)) + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_MoveContent_FailedToParseId", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + else + { + windowId = temp; + } + } + + if (auto targetPeasant{ _getPeasant(windowId) }) + { + auto request = winrt::make_self(content, tabIndex); + targetPeasant.AttachContentToWindow(*request); + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_MoveContent_Completed", + TraceLoggingInt64(windowId, "windowId", "The ID of the peasant which we sent the content to"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_MoveContent_NoWindow", + TraceLoggingInt64(windowId, "windowId", "We could not find a peasant with this ID"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + // In the case where window couldn't be found, then create a window + // for that name / ID. + // + // Don't let the window literally be named "-1", because that's silly. Same with "new" + const bool nameIsReserved = window == L"-1" || window == L"new"; + auto request = winrt::make_self(nameIsReserved ? L"" : window, + content, + windowBounds); + _RequestNewWindowHandlers(*this, *request); + } + } + + // Very similar to the above. Someone came and told us that they were the target of a drag/drop, and they know who started it. + // We will go tell the person who started it that they should send that target the content which was dragged. + void Monarch::RequestSendContent(const Remoting::RequestReceiveContentArgs& args) + { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SendContent_Requested", + TraceLoggingUInt64(args.SourceWindow(), "source", "The window which started the drag"), + TraceLoggingUInt64(args.TargetWindow(), "target", "The window which was the target of the drop"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + if (auto senderPeasant{ _getPeasant(args.SourceWindow()) }) + { + senderPeasant.SendContent(args); + + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SendContent_Completed", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + else + { + // We couldn't find the peasant that started the drag. Well that + // sure is weird, but that would indicate that the sender closed + // after starting the drag. No matter. We can just do nothing. + + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SendContent_NoWindow", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index a9eac206da..203119a70c 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -6,6 +6,7 @@ #include "Monarch.g.h" #include "Peasant.h" #include "WindowActivatedArgs.h" +#include "WindowRequestedArgs.g.h" #include // We sure different GUIDs here depending on whether we're running a Release, @@ -14,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 { @@ -21,6 +23,8 @@ constexpr GUID Monarch_clsid 0x06171993, #elif defined(WT_BRANDING_PREVIEW) 0x04221993, +#elif defined(WT_BRANDING_CANARY) + 0x09222022, #else 0x08302020, #endif @@ -38,6 +42,40 @@ namespace RemotingUnitTests namespace winrt::Microsoft::Terminal::Remoting::implementation { + struct WindowRequestedArgs : public WindowRequestedArgsT + { + public: + WindowRequestedArgs(const Remoting::ProposeCommandlineResult& windowInfo, const Remoting::CommandlineArgs& command) : + _Id{ windowInfo.Id() ? windowInfo.Id().Value() : 0 }, // We'll use 0 as a sentinel, since no window will ever get to have that ID + _WindowName{ windowInfo.WindowName() }, + _args{ command.Commandline() }, + _CurrentDirectory{ command.CurrentDirectory() }, + _ShowWindowCommand{ command.ShowWindowCommand() }, + _CurrentEnvironment{ command.CurrentEnvironment() } {}; + + WindowRequestedArgs(const winrt::hstring& window, const winrt::hstring& content, const Windows::Foundation::IReference& bounds) : + _Id{ 0u }, + _WindowName{ window }, + _args{}, + _CurrentDirectory{}, + _Content{ content }, + _InitialBounds{ bounds } {}; + + void Commandline(const winrt::array_view& value) { _args = { value.begin(), value.end() }; }; + winrt::com_array Commandline() { return winrt::com_array{ _args.begin(), _args.end() }; } + + WINRT_PROPERTY(uint64_t, Id); + WINRT_PROPERTY(winrt::hstring, WindowName); + WINRT_PROPERTY(winrt::hstring, CurrentDirectory); + WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, ShowWindowCommand, SW_NORMAL); + WINRT_PROPERTY(winrt::hstring, CurrentEnvironment); + WINRT_PROPERTY(Windows::Foundation::IReference, InitialBounds); + + private: + winrt::com_array _args; + }; + struct Monarch : public MonarchT { Monarch(); @@ -60,6 +98,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation Windows::Foundation::Collections::IVectorView GetPeasantInfos(); Windows::Foundation::Collections::IVector GetAllWindowLayouts(); + void RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, const Windows::Foundation::IReference& windowBounds); + void RequestSendContent(const Remoting::RequestReceiveContentArgs& args); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -67,6 +108,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs); + private: uint64_t _ourPID; @@ -191,4 +234,5 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation namespace winrt::Microsoft::Terminal::Remoting::factory_implementation { BASIC_FACTORY(Monarch); + BASIC_FACTORY(WindowRequestedArgs); } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 2ef0980708..f57bd6b59b 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -18,6 +18,21 @@ namespace Microsoft.Terminal.Remoting Boolean ShouldCreateWindow { get; }; // If you name this `CreateWindow`, the compiler will explode } + [default_interface] runtimeclass WindowRequestedArgs { + WindowRequestedArgs(ProposeCommandlineResult windowInfo, CommandlineArgs command); + + UInt64 Id { get; }; + String WindowName { get; }; + + String[] Commandline { get; }; + String CurrentDirectory { get; }; + UInt32 ShowWindowCommand { get; }; + String CurrentEnvironment { get; }; + + String Content { get; }; + Windows.Foundation.IReference InitialBounds { get; }; + } + [default_interface] runtimeclass SummonWindowSelectionArgs { SummonWindowSelectionArgs(); SummonWindowSelectionArgs(String windowName); @@ -31,8 +46,7 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.IReference WindowID; } - [default_interface] runtimeclass QuitAllRequestedArgs - { + [default_interface] runtimeclass QuitAllRequestedArgs { QuitAllRequestedArgs(); Windows.Foundation.IAsyncAction BeforeQuitAllAction; } @@ -60,12 +74,17 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.Collections.IVectorView GetPeasantInfos { get; }; Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference bounds); + void RequestSendContent(RequestReceiveContentArgs args); + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; event Windows.Foundation.TypedEventHandler QuitAllRequested; + + event Windows.Foundation.TypedEventHandler RequestNewWindow; }; runtimeclass Monarch : [default] IMonarch diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index 801351fd63..94f8406c00 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -8,6 +8,8 @@ #include "GetWindowLayoutArgs.h" #include "Peasant.g.cpp" #include "../../types/inc/utils.hpp" +#include "AttachRequest.g.cpp" +#include "RequestReceiveContentArgs.g.cpp" using namespace winrt; using namespace winrt::Microsoft::Terminal; @@ -148,7 +150,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } // Method Description: - // - Tell this window to display it's window ID. We'll raise a + // - Tell this window to display its window ID. We'll raise a // DisplayWindowIdRequested event, which will get handled in the AppHost, // and used to tell the app to display the ID toast. // Arguments: @@ -275,6 +277,22 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + void Peasant::AttachContentToWindow(Remoting::AttachRequest request) + { + try + { + _AttachRequestedHandlers(*this, request); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_AttachContentToWindow", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + void Peasant::Quit() { try @@ -310,4 +328,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } return args->WindowLayoutJson(); } + + void Peasant::SendContent(const Remoting::RequestReceiveContentArgs& args) + { + _SendContentRequestedHandlers(*this, args); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index 2f9a84384d..26a82a17fe 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -5,6 +5,8 @@ #include "Peasant.g.h" #include "RenameRequestArgs.h" +#include "AttachRequest.g.h" +#include "RequestReceiveContentArgs.g.h" namespace RemotingUnitTests { @@ -12,6 +14,31 @@ namespace RemotingUnitTests }; namespace winrt::Microsoft::Terminal::Remoting::implementation { + struct AttachRequest : public AttachRequestT + { + WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, TabIndex); + + public: + AttachRequest(winrt::hstring content, + uint32_t tabIndex) : + _Content{ content }, + _TabIndex{ tabIndex } {}; + }; + + struct RequestReceiveContentArgs : RequestReceiveContentArgsT + { + WINRT_PROPERTY(uint64_t, SourceWindow); + WINRT_PROPERTY(uint64_t, TargetWindow); + WINRT_PROPERTY(uint32_t, TabIndex); + + public: + RequestReceiveContentArgs(const uint64_t src, const uint64_t tgt, const uint32_t tabIndex) : + _SourceWindow{ src }, + _TargetWindow{ tgt }, + _TabIndex{ tabIndex } {}; + }; + struct Peasant : public PeasantT { Peasant(); @@ -32,11 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void RequestQuitAll(); void Quit(); + void AttachContentToWindow(Remoting::AttachRequest request); + winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs(); winrt::hstring GetWindowLayout(); + void SendContent(const winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs& args); WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, ActiveTabTitle); @@ -47,12 +77,16 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); + TYPED_EVENT(AttachRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::AttachRequest); + TYPED_EVENT(SendContentRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs); + private: Peasant(const uint64_t testPID); uint64_t _ourPID; @@ -69,4 +103,5 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation namespace winrt::Microsoft::Terminal::Remoting::factory_implementation { BASIC_FACTORY(Peasant); + BASIC_FACTORY(RequestReceiveContentArgs); } diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index ec87c85188..f02d22da9c 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -7,10 +7,12 @@ namespace Microsoft.Terminal.Remoting runtimeclass CommandlineArgs { CommandlineArgs(); - CommandlineArgs(String[] args, String cwd); + CommandlineArgs(String[] args, String cwd, UInt32 showWindowCommand, String env); String[] Commandline { get; set; }; - String CurrentDirectory(); + String CurrentDirectory { get; }; + UInt32 ShowWindowCommand { get; }; + String CurrentEnvironment { get; }; }; runtimeclass RenameRequestArgs @@ -43,7 +45,6 @@ namespace Microsoft.Terminal.Remoting ToMouse, }; - [default_interface] runtimeclass SummonWindowBehavior { SummonWindowBehavior(); Boolean MoveToCurrentDesktop; @@ -52,6 +53,18 @@ namespace Microsoft.Terminal.Remoting MonitorBehavior ToMonitor; } + [default_interface] runtimeclass AttachRequest { + String Content { get; }; + UInt32 TabIndex { get; }; + } + [default_interface] runtimeclass RequestReceiveContentArgs { + RequestReceiveContentArgs(UInt64 src, UInt64 tgt, UInt32 tabIndex); + + UInt64 SourceWindow { get; }; + UInt64 TargetWindow { get; }; + UInt32 TabIndex { get; }; + }; + interface IPeasant { CommandlineArgs InitialArgs { get; }; @@ -70,23 +83,32 @@ namespace Microsoft.Terminal.Remoting void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested void Summon(SummonWindowBehavior behavior); + void RequestShowNotificationIcon(); void RequestHideNotificationIcon(); void RequestQuitAll(); void Quit(); String GetWindowLayout(); + void AttachContentToWindow(AttachRequest request); + void SendContent(RequestReceiveContentArgs args); + event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; event Windows.Foundation.TypedEventHandler DisplayWindowIdRequested; event Windows.Foundation.TypedEventHandler RenameRequested; event Windows.Foundation.TypedEventHandler SummonRequested; + event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler QuitRequested; + + event Windows.Foundation.TypedEventHandler AttachRequested; + + event Windows.Foundation.TypedEventHandler SendContentRequested; }; [default_interface] runtimeclass Peasant : IPeasant diff --git a/src/cascadia/Remoting/ProposeCommandlineResult.h b/src/cascadia/Remoting/ProposeCommandlineResult.h index 19903ddff0..2578ce8378 100644 --- a/src/cascadia/Remoting/ProposeCommandlineResult.h +++ b/src/cascadia/Remoting/ProposeCommandlineResult.h @@ -12,7 +12,7 @@ Abstract: ShouldCreateWindow is false, that implies that some other window process was given the commandline for handling, and the caller should just exit. - If ShouldCreateWindow is true, the Id property may or may not contain an ID - that the new window should use as it's ID. + that the new window should use as its ID. --*/ diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index f0f5696d93..aa9f67b0b3 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -2,51 +2,59 @@ // Licensed under the MIT license. #include "pch.h" + #include "WindowManager.h" -#include "MonarchFactory.h" -#include "CommandlineArgs.h" + #include "../inc/WindowingBehavior.h" +#include "MonarchFactory.h" + +#include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" #include "ProposeCommandlineResult.h" #include "WindowManager.g.cpp" #include "../../types/inc/utils.hpp" +#include + using namespace winrt; using namespace winrt::Microsoft::Terminal; using namespace winrt::Windows::Foundation; using namespace ::Microsoft::Console; +namespace +{ + const GUID& MonarchCLSID() + { + if (!IsPackaged()) [[unlikely]] + { + // Unpackaged installations don't have the luxury of magic package isolation + // to stop them from accidentally touching each other's monarchs. + // We need to enforce that ourselves by making their monarch CLSIDs unique + // per install. + // This applies in both portable mode and normal unpackaged mode. + // We'll use a v5 UUID based on the install folder to unique them. + static GUID processRootHashedGuid = []() { + // {5456C4DB-557D-4A22-B043-B1577418E4AF} + static constexpr GUID processRootHashedGuidBase = { 0x5456c4db, 0x557d, 0x4a22, { 0xb0, 0x43, 0xb1, 0x57, 0x74, 0x18, 0xe4, 0xaf } }; + + // Make a temporary monarch CLSID based on the unpackaged install root + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + modulePath.remove_filename(); + + return Utils::CreateV5Uuid(processRootHashedGuidBase, std::as_bytes(std::span{ modulePath.native() })); + }(); + return processRootHashedGuid; + } + return Monarch_clsid; + } +} + namespace winrt::Microsoft::Terminal::Remoting::implementation { WindowManager::WindowManager() { - _monarchWaitInterrupt.create(); - - // Register with COM as a server for the Monarch class - _registerAsMonarch(); - // Instantiate an instance of the Monarch. This may or may not be in-proc! - auto foundMonarch = false; - while (!foundMonarch) - { - try - { - _createMonarchAndCallbacks(); - // _createMonarchAndCallbacks will initialize _isKing - foundMonarch = true; - } - catch (...) - { - // If we fail to find the monarch, - // stay in this jail until we do. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInCtor", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } } - WindowManager::~WindowManager() { // IMPORTANT! Tear down the registration as soon as we exit. If we're not a @@ -55,32 +63,178 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // monarch! CoRevokeClassObject(_registrationHostClass); _registrationHostClass = 0; - SignalClose(); - _monarchWaitInterrupt.SetEvent(); - - // A thread is joinable once it's been started. Basically this just - // makes sure that the thread isn't just default-constructed. - if (_electionThread.joinable()) - { - _electionThread.join(); - } } - void WindowManager::SignalClose() + void WindowManager::_createMonarch() { + // Heads up! This only works because we're using + // "metadata-based-marshalling" for our WinRT types. That means the OS is + // using the .winmd file we generate to figure out the proxy/stub + // definitions for our types automatically. This only works in the following + // cases: + // + // * If we're running unpackaged: the .winmd must be a sibling of the .exe + // * If we're running packaged: the .winmd must be in the package root + _monarch = try_create_instance(MonarchCLSID(), + CLSCTX_LOCAL_SERVER); + } + + // Check if we became the king, and if we are, wire up callbacks. + void WindowManager::_createCallbacks() + { + assert(_monarch); + // Here, we're the king! + // + // This is where you should do any additional setup that might need to be + // done when we become the king. This will be called both for the first + // window, and when the current monarch dies. + + _monarch.WindowCreated({ get_weak(), &WindowManager::_WindowCreatedHandlers }); + _monarch.WindowClosed({ get_weak(), &WindowManager::_WindowClosedHandlers }); + _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); + + _monarch.RequestNewWindow({ get_weak(), &WindowManager::_raiseRequestNewWindow }); + } + + void WindowManager::_registerAsMonarch() + { + winrt::check_hresult(CoRegisterClassObject(MonarchCLSID(), + winrt::make<::MonarchFactory>().get(), + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + &_registrationHostClass)); + } + + void WindowManager::_raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args) + { + _FindTargetWindowRequestedHandlers(sender, args); + } + void WindowManager::_raiseRequestNewWindow(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args) + { + _RequestNewWindowHandlers(sender, args); + } + + Remoting::ProposeCommandlineResult WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args, const bool isolatedMode) + { + if (!isolatedMode) + { + // _createMonarch always attempts to connect an existing monarch. In + // isolated mode, we don't want to do that. + _createMonarch(); + } + if (_monarch) { - try + // We connected to a monarch instance, not us though. This won't hit + // in isolated mode. + + // Send the commandline over to the monarch process + if (_proposeToMonarch(args)) { - _monarch.SignalClose(_peasant.GetID()); + // If that succeeded, then we don't need to make a new window. + // Our job is done. Either the monarch is going to run the + // commandline in an existing window, or a new one, but either way, + // this process doesn't need to make a new window. + + return winrt::make(false); + } + // Otherwise, we'll try to handle this ourselves. + } + + // Theoretically, this condition is always true here: + // + // if (_monarch == nullptr) + // + // If we do still have a _monarch at this point, then we must have + // successfully proposed to it in _proposeToMonarch, so we can't get + // here with a monarch. + { + // No preexisting instance. + + // Raise an event, to ask how to handle this commandline. We can't ask + // the app ourselves - we exist isolated from that knowledge (and + // dependency hell). The WindowManager will raise this up to the app + // host, which will then ask the AppLogic, who will then parse the + // commandline and determine the provided ID of the window. + auto findWindowArgs{ winrt::make_self(args) }; + + // This is handled by some handler in-proc + _FindTargetWindowRequestedHandlers(*this, *findWindowArgs); + + // After the event was handled, ResultTargetWindow() will be filled with + // the parsed result. + const auto targetWindow = findWindowArgs->ResultTargetWindow(); + const auto targetWindowName = findWindowArgs->ResultTargetWindowName(); + + if (targetWindow == WindowingBehaviorUseNone) + { + // This commandline doesn't deserve a window. Don't make a monarch + // either. + return winrt::make(false); + } + else + { + // This commandline _does_ want a window, which means we do want + // to create a window, and a monarch. + // + // Congrats! This is now THE PROCESS. It's the only one that's + // getting any windows. + + // In isolated mode, we don't want to register as the monarch, + // we just want to make a local one. So we'll skip this step. + // The condition below it will handle making the unregistered + // local monarch. + + if (!isolatedMode) + { + _registerAsMonarch(); + _createMonarch(); + } + else + { + TraceLoggingWrite(g_hRemotingProvider, + "WindowManager_IntentionallyIsolated", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + + if (!_monarch) + { + // Something catastrophically bad happened here OR we were + // intentionally in isolated mode. We don't want to just + // exit immediately. Instead, we'll just instantiate a local + // Monarch instance, without registering it. We're firmly in + // the realm of undefined behavior, but better to have some + // window than not. + _monarch = winrt::make(); + TraceLoggingWrite(g_hRemotingProvider, + "WindowManager_FailedToCoCreate", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + _createCallbacks(); + + // So, we wanted a new peasant. Cool! + // + // We need to fill in args.ResultTargetWindow, + // args.ResultTargetWindowName so that we can create the new + // window with those values. Otherwise, the very first window + // won't obey the given name / ID. + // + // So let's just ask the monarch (ourselves) to get those values. + return _monarch.ProposeCommandline(args); } - CATCH_LOG() } } - void WindowManager::_proposeToMonarch(const Remoting::CommandlineArgs& args, - std::optional& givenID, - winrt::hstring& givenName) + // Method Description: + // - Helper attempting to call to the monarch multiple times. If the monarch + // fails to respond, or we encounter any sort of error, we'll try again + // until we find one, or decisively determine there isn't one. + bool WindowManager::_proposeToMonarch(const Remoting::CommandlineArgs& args) { // these two errors are Win32 errors, convert them to HRESULTS so we can actually compare below. static constexpr auto RPC_SERVER_UNAVAILABLE_HR = HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE); @@ -109,15 +263,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation try { // MSFT:38542548 _We believe_ that this is the source of the - // crash here. After we get the result, stash it's values into a + // crash here. After we get the result, stash its values into a // local copy, so that we can check them later. If the Monarch // dies between now and the inspection of // `result.ShouldCreateWindow` below, we don't want to explode // (since _proposeToMonarch is not try/caught). - auto outOfProcResult = _monarch.ProposeCommandline(args); - result = winrt::make(outOfProcResult); - proposedCommandline = true; + _monarch.ProposeCommandline(args); + return true; } catch (...) { @@ -154,560 +307,78 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _monarch = winrt::make(); - _createCallbacks(); + // Set the monarch to null, so that we'll create a new one + // (or just generally check if we need to even make a window + // for this commandline.) + _monarch = nullptr; + return false; } else { // We failed to ask the monarch. It must have died. Try and - // find the real monarch. Don't perform an election, that - // assumes we have a peasant, which we don't yet. - _createMonarchAndCallbacks(); - // _createMonarchAndCallbacks will initialize _isKing - } - if (_isKing) - { - // We became the king. We don't need to ProposeCommandline to ourself, we're just - // going to do it. - // - // Return early, because there's nothing else for us to do here. + // find another monarch. + _createMonarch(); + if (!_monarch) + { + // We failed to create a monarch. That means there + // aren't any other windows, and we can become the monarch. + return false; + } + // Go back around the loop. TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_proposeToMonarch_becameKing", + "WindowManager_proposeToMonarch_tryAgain", TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - // In WindowManager::ProposeCommandline, had we been the - // king originally, we would have started by setting - // this to true. We became the monarch here, so set it - // here as well. - _shouldCreateWindow = true; - return; } - - // Here, we created the new monarch, it wasn't us, so we're - // gonna go through the while loop again and ask the new - // king. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_proposeToMonarch_tryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } - // Here, the monarch (not us) has replied to the message. Get the - // valuables out of the response: - _shouldCreateWindow = result.ShouldCreateWindow(); - if (result.Id()) - { - givenID = result.Id().Value(); - } - givenName = result.WindowName(); - - // TraceLogging doesn't have a good solution for logging an - // optional. So we have to repeat the calls here: - if (givenID) - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(givenID.value(), "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingPointer(nullptr, "Id", "No ID provided"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - void WindowManager::ProposeCommandline(const Remoting::CommandlineArgs& args) - { - // If we're the king, we _definitely_ want to process the arguments, we were - // launched with them! - // - // Otherwise, the King will tell us if we should make a new window - _shouldCreateWindow = _isKing; - std::optional givenID; - winrt::hstring givenName{}; - if (!_isKing) - { - _proposeToMonarch(args, givenID, givenName); - } - - // During _proposeToMonarch, it's possible that we found that the king was dead, and we're the new king. Cool! Do this now. - if (_isKing) - { - // We're the monarch, we don't need to propose anything. We're just - // going to do it. - // - // However, we _do_ need to ask what our name should be. It's - // possible someone started the _first_ wt with something like `wt - // -w king` as the commandline - we want to make sure we set our - // name to "king". - // - // The FindTargetWindow event is the WindowManager's way of saying - // "I do not know how to figure out how to turn this list of args - // into a window ID/name. Whoever's listening to this event does, so - // I'll ask them". It's a convoluted way of hooking the - // WindowManager up to AppLogic without actually telling it anything - // about TerminalApp (or even WindowsTerminal) - auto findWindowArgs{ winrt::make_self(args) }; - _raiseFindTargetWindowRequested(nullptr, *findWindowArgs); - - const auto responseId = findWindowArgs->ResultTargetWindow(); - if (responseId > 0) - { - givenID = ::base::saturated_cast(responseId); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(givenID.value(), "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else if (responseId == WindowingBehaviorUseName) - { - givenName = findWindowArgs->ResultTargetWindowName(); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(0, "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(givenName.c_str(), "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - else - { - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ProposeCommandline_AsMonarch", - TraceLoggingBoolean(_shouldCreateWindow, "CreateWindow", "true iff we should create a new window"), - TraceLoggingUInt64(0, "Id", "The ID we should assign our peasant"), - TraceLoggingWideString(L"", "Name", "The name we should assign this window"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - - if (_shouldCreateWindow) - { - // If we should create a new window, then instantiate our Peasant - // instance, and tell that peasant to handle that commandline. - _createOurPeasant({ givenID }, givenName); - - // Spawn a thread to wait on the monarch, and handle the election - if (!_isKing) - { - _createPeasantThread(); - } - - // This right here will just tell us to stash the args away for the - // future. The AppHost hasnt yet set up the callbacks, and the rest - // of the app hasn't started at all. We'll note them and come back - // later. - _peasant.ExecuteCommandline(args); - } - // Otherwise, we'll do _nothing_. + // I don't think we can ever get here, but the compiler doesn't know + return false; } - bool WindowManager::ShouldCreateWindow() - { - return _shouldCreateWindow; - } - - void WindowManager::_registerAsMonarch() - { - winrt::check_hresult(CoRegisterClassObject(Monarch_clsid, - winrt::make<::MonarchFactory>().get(), - CLSCTX_LOCAL_SERVER, - REGCLS_MULTIPLEUSE, - &_registrationHostClass)); - } - - void WindowManager::_createMonarch() - { - // Heads up! This only works because we're using - // "metadata-based-marshalling" for our WinRT types. That means the OS is - // using the .winmd file we generate to figure out the proxy/stub - // definitions for our types automatically. This only works in the following - // cases: - // - // * If we're running unpackaged: the .winmd must be a sibling of the .exe - // * If we're running packaged: the .winmd must be in the package root - _monarch = create_instance(Monarch_clsid, - CLSCTX_LOCAL_SERVER); - } - - // Tries to instantiate a monarch, tries again, and eventually either throws - // (so that the caller will try again) or falls back to the isolated - // monarch. - void WindowManager::_redundantCreateMonarch() - { - _createMonarch(); - - if (_monarch == nullptr) - { - // See MSFT:38540483, GH#12774 for details. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchTryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // Here we're gonna just give it a quick second try.Probably not - // definitive, but might help. - _createMonarch(); - } - - if (_monarch == nullptr) - { - // See MSFT:38540483, GH#12774 for details. - if constexpr (Feature_IsolatedMonarchMode::IsEnabled()) - { - // Fall back to having a in proc monarch. Were now isolated from - // other windows. This is a pretty torn state, but at least we - // didn't just explode. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchIsolateMode", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - _monarch = winrt::make(); - } - else - { - // The monarch is null. We're hoping that we can find another, - // hopefully us. We're gonna go back around the loop again and - // see what happens. If this is really an infinite loop (where - // the OS won't even give us back US as the monarch), then I - // suppose we'll find out soon enough. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_NullMonarchTryAgain", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - winrt::hresult_error(E_UNEXPECTED, L"Did not expect the Monarch to ever be null"); - } - } - } - - // NOTE: This can throw! Callers include: - // - the constructor, who performs this in a loop until it successfully - // find a a monarch - // - the performElection method, which is called in the waitOnMonarch - // thread. All the calls in that thread are wrapped in try/catch's - // already. - // - _createOurPeasant, who might do this in a loop to establish us with the - // monarch. - void WindowManager::_createMonarchAndCallbacks() - { - _redundantCreateMonarch(); - // We're pretty confident that we have a Monarch here. - _createCallbacks(); - } - - // Check if we became the king, and if we are, wire up callbacks. - void WindowManager::_createCallbacks() - { - // Save the result of checking if we're the king. We want to avoid - // unnecessary calls back and forth if we can. - _isKing = _areWeTheKing(); - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ConnectedToMonarch", - TraceLoggingUInt64(_monarch.GetPID(), "monarchPID", "The PID of the new Monarch"), - TraceLoggingBoolean(_isKing, "isKing", "true if we are the new monarch"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - if (_peasant) - { - if (const auto& lastActivated{ _peasant.GetLastActivatedArgs() }) - { - // Inform the monarch of the time we were last activated - _monarch.HandleActivatePeasant(lastActivated); - } - } - - if (!_isKing) - { - return; - } - // Here, we're the king! - // - // This is where you should do any additional setup that might need to be - // done when we become the king. This will be called both for the first - // window, and when the current monarch dies. - - _monarch.WindowCreated({ get_weak(), &WindowManager::_WindowCreatedHandlers }); - _monarch.WindowClosed({ get_weak(), &WindowManager::_WindowClosedHandlers }); - _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); - _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); - - _BecameMonarchHandlers(*this, nullptr); - } - - bool WindowManager::_areWeTheKing() - { - const auto ourPID{ GetCurrentProcessId() }; - const auto kingPID{ _monarch.GetPID() }; - return (ourPID == kingPID); - } - - Remoting::IPeasant WindowManager::_createOurPeasant(std::optional givenID, - const winrt::hstring& givenName) + Remoting::Peasant WindowManager::CreatePeasant(const Remoting::WindowRequestedArgs& args) { auto p = winrt::make_self(); - if (givenID) + // This will be false if the Id is 0, which is our sentinel for "no specific ID was requested" + if (const auto id = args.Id()) { - p->AssignID(givenID.value()); + p->AssignID(id); } // If the name wasn't specified, this will be an empty string. - p->WindowName(givenName); - _peasant = *p; + p->WindowName(args.WindowName()); - // Try to add us to the monarch. If that fails, try to find a monarch - // again, until we find one (we will eventually find us) - while (true) - { - try - { - _monarch.AddPeasant(_peasant); - break; - } - catch (...) - { - try - { - // Wrap this in it's own try/catch, because this can throw. - _createMonarchAndCallbacks(); - } - catch (...) - { - } - } - } + p->ExecuteCommandline(*winrt::make_self(args.Commandline(), + args.CurrentDirectory(), + args.ShowWindowCommand(), + args.CurrentEnvironment())); - _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + _monarch.AddPeasant(*p); + + p->GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", - TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), + TraceLoggingUInt64(p->GetID(), "peasantID", "The ID of our new peasant"), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // If the peasant asks us to quit we should not try to act in future elections. - _peasant.QuitRequested([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto wm = weakThis.get()) - { - wm->_monarchWaitInterrupt.SetEvent(); - } - }); - - return _peasant; + return *p; } - // Method Description: - // - Attempt to connect to the monarch process. This might be us! - // - For the new monarch, add us to their list of peasants. - // Arguments: - // - - // Return Value: - // - true iff we're the new monarch process. - // NOTE: This can throw! - bool WindowManager::_performElection() + void WindowManager::SignalClose(const Remoting::Peasant& peasant) { - _createMonarchAndCallbacks(); - - // Tell the new monarch who we are. We might be that monarch! - _monarch.AddPeasant(_peasant); - - // This method is only called when a _new_ monarch is elected. So - // don't do anything here that needs to be done for all monarch - // windows. This should only be for work that's done when a window - // _becomes_ a monarch, after the death of the previous monarch. - return _isKing; - } - - void WindowManager::_createPeasantThread() - { - // If we catch an exception trying to get at the monarch ever, we can - // set the _monarchWaitInterrupt, and use that to trigger a new - // election. Though, we wouldn't be able to retry the function that - // caused the exception in the first place... - - _electionThread = std::thread([this] { - _waitOnMonarchThread(); - }); - } - - void WindowManager::_waitOnMonarchThread() - { - // This is the array of HANDLEs that we're going to wait on in - // WaitForMultipleObjects below. - // * waits[0] will be the handle to the monarch process. It gets - // signalled when the process exits / dies. - // * waits[1] is the handle to our _monarchWaitInterrupt event. Another - // thread can use that to manually break this loop. We'll do that when - // we're getting torn down. - HANDLE waits[2]; - waits[1] = _monarchWaitInterrupt.get(); - const auto peasantID = _peasant.GetID(); // safe: _peasant is in-proc. - - auto exitThreadRequested = false; - while (!exitThreadRequested) + if (_monarch) { - // At any point in all this, the current monarch might die. If it - // does, we'll go straight to a new election, in the "jail" - // try/catch below. Worst case, eventually, we'll become the new - // monarch. try { - // This might fail to even ask the monarch for it's PID. - wil::unique_handle hMonarch{ OpenProcess(PROCESS_ALL_ACCESS, - FALSE, - static_cast(_monarch.GetPID())) }; - - // If we fail to open the monarch, then they don't exist - // anymore! Go straight to an election. - if (hMonarch.get() == nullptr) - { - const auto gle = GetLastError(); - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_FailedToOpenMonarch", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingUInt64(gle, "lastError", "The result of GetLastError"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - exitThreadRequested = _performElection(); - continue; - } - - waits[0] = hMonarch.get(); - auto waitResult = WaitForMultipleObjects(2, waits, FALSE, INFINITE); - - switch (waitResult) - { - case WAIT_OBJECT_0 + 0: // waits[0] was signaled, the handle to the monarch process - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchDied", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - // Connect to the new monarch, which might be us! - // If we become the monarch, then we'll return true and exit this thread. - exitThreadRequested = _performElection(); - break; - - case WAIT_OBJECT_0 + 1: // waits[1] was signaled, our manual interrupt - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchWaitInterrupted", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - exitThreadRequested = true; - break; - - case WAIT_TIMEOUT: - // This should be impossible. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_MonarchWaitTimeout", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - exitThreadRequested = true; - break; - - default: - { - // Returning any other value is invalid. Just die. - const auto gle = GetLastError(); - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_WaitFailed", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingUInt64(gle, "lastError", "The result of GetLastError"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - ExitProcess(0); - } - } - } - catch (...) - { - // Theoretically, if window[1] dies when we're trying to get - // it's PID we'll get here. If we just try to do the election - // once here, it's possible we might elect window[2], but have - // it die before we add ourselves as a peasant. That - // _performElection call will throw, and we wouldn't catch it - // here, and we'd die. - - // Instead, we're going to have a resilient election process. - // We're going to keep trying an election, until one _doesn't_ - // throw an exception. That might mean burning through all the - // other dying monarchs until we find us as the monarch. But if - // this process is alive, then there's _someone_ in the line of - // succession. - - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInWaitThread", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - auto foundNewMonarch = false; - while (!foundNewMonarch) - { - try - { - exitThreadRequested = _performElection(); - // It doesn't matter if we're the monarch, or someone - // else is, but if we complete the election, then we've - // registered with a new one. We can escape this jail - // and re-enter society. - foundNewMonarch = true; - } - catch (...) - { - // If we fail to acknowledge the results of the election, - // stay in this jail until we do. - TraceLoggingWrite(g_hRemotingProvider, - "WindowManager_ExceptionInNestedWaitThread", - TraceLoggingUInt64(peasantID, "peasantID", "Our peasant ID"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } + _monarch.SignalClose(peasant.GetID()); } + CATCH_LOG() } } - Remoting::Peasant WindowManager::CurrentWindow() - { - return _peasant; - } - - void WindowManager::_raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args) - { - _FindTargetWindowRequestedHandlers(sender, args); - } - - bool WindowManager::IsMonarch() - { - return _isKing; - } - void WindowManager::SummonWindow(const Remoting::SummonWindowSelectionArgs& args) { // We should only ever get called when we are the monarch, because only @@ -741,42 +412,16 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return 0; } - // Method Description: - // - Ask the monarch to show a notification icon. - // Arguments: - // - - // Return Value: - // - - winrt::fire_and_forget WindowManager::RequestShowNotificationIcon() - { - co_await winrt::resume_background(); - _peasant.RequestShowNotificationIcon(); - } - - // Method Description: - // - Ask the monarch to hide its notification icon. - // Arguments: - // - - // Return Value: - // - - winrt::fire_and_forget WindowManager::RequestHideNotificationIcon() - { - auto strongThis{ get_strong() }; - co_await winrt::resume_background(); - _peasant.RequestHideNotificationIcon(); - } - // Method Description: // - Ask the monarch to quit all windows. // Arguments: // - // Return Value: // - - winrt::fire_and_forget WindowManager::RequestQuitAll() + winrt::fire_and_forget WindowManager::RequestQuitAll(Remoting::Peasant peasant) { - auto strongThis{ get_strong() }; co_await winrt::resume_background(); - _peasant.RequestQuitAll(); + peasant.RequestQuitAll(); } bool WindowManager::DoesQuakeWindowExist() @@ -784,9 +429,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation return _monarch.DoesQuakeWindowExist(); } - void WindowManager::UpdateActiveTabTitle(winrt::hstring title) + void WindowManager::UpdateActiveTabTitle(const winrt::hstring& title, const Remoting::Peasant& peasant) { - winrt::get_self(_peasant)->ActiveTabTitle(title); + winrt::get_self(peasant)->ActiveTabTitle(title); } Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() @@ -801,4 +446,19 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } return nullptr; } + + winrt::fire_and_forget WindowManager::RequestMoveContent(winrt::hstring window, + winrt::hstring content, + uint32_t tabIndex, + Windows::Foundation::IReference windowBounds) + { + co_await winrt::resume_background(); + _monarch.RequestMoveContent(window, content, tabIndex, windowBounds); + } + + winrt::fire_and_forget WindowManager::RequestSendContent(Remoting::RequestReceiveContentArgs args) + { + co_await winrt::resume_background(); + _monarch.RequestSendContent(args); + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 96c60710d7..015ab9f347 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -1,10 +1,8 @@ /*++ Copyright (c) Microsoft Corporation Licensed under the MIT license. - Class Name: - WindowManager.h - Abstract: - The Window Manager takes care of coordinating the monarch and peasant for this process. @@ -16,9 +14,7 @@ Abstract: - When the monarch needs to ask the TerminalApp about how to parse a commandline, it'll ask by raising an event that we'll bubble up to the AppHost. - --*/ - #pragma once #include "WindowManager.g.h" @@ -29,65 +25,51 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { struct WindowManager : public WindowManagerT { + public: WindowManager(); ~WindowManager(); + winrt::Microsoft::Terminal::Remoting::ProposeCommandlineResult ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args, const bool isolatedMode); + Remoting::Peasant CreatePeasant(const Remoting::WindowRequestedArgs& args); - void ProposeCommandline(const winrt::Microsoft::Terminal::Remoting::CommandlineArgs& args); - bool ShouldCreateWindow(); - - winrt::Microsoft::Terminal::Remoting::Peasant CurrentWindow(); - bool IsMonarch(); + void SignalClose(const Remoting::Peasant& peasant); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); - void SignalClose(); - void SummonAllWindows(); - uint64_t GetNumberOfPeasants(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); - winrt::fire_and_forget RequestShowNotificationIcon(); - winrt::fire_and_forget RequestHideNotificationIcon(); - winrt::fire_and_forget RequestQuitAll(); - bool DoesQuakeWindowExist(); - void UpdateActiveTabTitle(winrt::hstring title); + uint64_t GetNumberOfPeasants(); + + static winrt::fire_and_forget RequestQuitAll(Remoting::Peasant peasant); + void UpdateActiveTabTitle(const winrt::hstring& title, const Remoting::Peasant& peasant); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); + bool DoesQuakeWindowExist(); + + winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, Windows::Foundation::IReference windowBounds); + winrt::fire_and_forget RequestSendContent(Remoting::RequestReceiveContentArgs args); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); - TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs); + private: - bool _shouldCreateWindow{ false }; - bool _isKing{ false }; DWORD _registrationHostClass{ 0 }; winrt::Microsoft::Terminal::Remoting::IMonarch _monarch{ nullptr }; - winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; - wil::unique_event _monarchWaitInterrupt; - std::thread _electionThread; - - void _registerAsMonarch(); void _createMonarch(); - void _redundantCreateMonarch(); - void _createMonarchAndCallbacks(); - void _createCallbacks(); - bool _areWeTheKing(); - winrt::Microsoft::Terminal::Remoting::IPeasant _createOurPeasant(std::optional givenID, - const winrt::hstring& givenName); + void _registerAsMonarch(); - bool _performElection(); - void _createPeasantThread(); - void _waitOnMonarchThread(); + bool _proposeToMonarch(const Remoting::CommandlineArgs& args); + + void _createCallbacks(); void _raiseFindTargetWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); - - void _proposeToMonarch(const Remoting::CommandlineArgs& args, - std::optional& givenID, - winrt::hstring& givenName); + void _raiseRequestNewWindow(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); }; } diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 2fdfd7e343..5dd72b7f19 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -7,29 +7,36 @@ namespace Microsoft.Terminal.Remoting [default_interface] runtimeclass WindowManager { WindowManager(); - void ProposeCommandline(CommandlineArgs args); - void SignalClose(); - Boolean ShouldCreateWindow { get; }; - IPeasant CurrentWindow(); - Boolean IsMonarch { get; }; + + ProposeCommandlineResult ProposeCommandline(CommandlineArgs args, Boolean isolatedMode); + Peasant CreatePeasant(WindowRequestedArgs args); + + void SignalClose(Peasant p); + + void UpdateActiveTabTitle(String title, Peasant p); + static void RequestQuitAll(Peasant p); + void SummonWindow(SummonWindowSelectionArgs args); void SummonAllWindows(); - void RequestShowNotificationIcon(); - void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + Windows.Foundation.Collections.IVectorView GetPeasantInfos(); UInt64 GetNumberOfPeasants(); - void RequestQuitAll(); - void UpdateActiveTabTitle(String title); + Boolean DoesQuakeWindowExist(); - Windows.Foundation.Collections.IVectorView GetPeasantInfos(); + + void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference bounds); + void RequestSendContent(RequestReceiveContentArgs args); + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; - event Windows.Foundation.TypedEventHandler BecameMonarch; + event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; - event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; - event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + + event Windows.Foundation.TypedEventHandler RequestNewWindow; + }; } diff --git a/src/cascadia/Remoting/dll/Microsoft.Terminal.Remoting.vcxproj b/src/cascadia/Remoting/dll/Microsoft.Terminal.Remoting.vcxproj index f81877bf36..70369bb4a7 100644 --- a/src/cascadia/Remoting/dll/Microsoft.Terminal.Remoting.vcxproj +++ b/src/cascadia/Remoting/dll/Microsoft.Terminal.Remoting.vcxproj @@ -49,7 +49,7 @@ {18D09A24-8240-42D6-8CB6-236EEE820263} - diff --git a/src/cascadia/Remoting/pch.h b/src/cascadia/Remoting/pch.h index 3e65fdf103..4c4bcb1ac9 100644 --- a/src/cascadia/Remoting/pch.h +++ b/src/cascadia/Remoting/pch.h @@ -52,3 +52,4 @@ TRACELOGGING_DECLARE_PROVIDER(g_hRemotingProvider); #include "til.h" #include +#include diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 4aee550e5c..f1842c9cf5 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -27,6 +27,8 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, IBindCtx* /*pBindContext*/) try { + const auto runElevated = IsControlAndShiftPressed(); + wil::com_ptr_nothrow psi; RETURN_IF_FAILED(GetBestLocationFromSelectionOrSite(psiItemArray, psi.put())); if (!psi) @@ -42,10 +44,22 @@ try STARTUPINFOEX siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); + // Explicitly create the terminal window visible. + siEx.StartupInfo.dwFlags |= STARTF_USESHOWWINDOW; + siEx.StartupInfo.wShowWindow = SW_SHOWNORMAL; + + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; std::wstring cmdline; - RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + if (runElevated) + { + RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-(-d %s)-", QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + } + else + { + RETURN_IF_FAILED(wil::str_printf_nothrow(cmdline, LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str())); + } RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( - nullptr, // lpApplicationName + runElevated ? modulePath.replace_filename(ElevateShimExe).c_str() : nullptr, // if elevation requested pass the elevate-shim.exe as the application name cmdline.data(), nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes @@ -80,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 @@ -105,7 +121,8 @@ HRESULT OpenTerminalHere::GetState(IShellItemArray* psiItemArray, SFGAOF attributes; const bool isFileSystemItem = psi && (psi->GetAttributes(SFGAO_FILESYSTEM, &attributes) == S_OK); - *pCmdState = isFileSystemItem ? ECS_ENABLED : ECS_HIDDEN; + const bool isCompressed = psi && (psi->GetAttributes(SFGAO_FOLDER | SFGAO_STREAM, &attributes) == S_OK); + *pCmdState = isFileSystemItem && !isCompressed ? ECS_ENABLED : ECS_HIDDEN; return S_OK; } @@ -193,3 +210,15 @@ HRESULT OpenTerminalHere::GetBestLocationFromSelectionOrSite(IShellItemArray* ps RETURN_IF_FAILED(psi.copy_to(location)); return S_OK; } + +// Check is both ctrl and shift keys are pressed during activation of the shell extension +bool OpenTerminalHere::IsControlAndShiftPressed() +{ + short control = 0; + short shift = 0; + control = GetAsyncKeyState(VK_CONTROL); + shift = GetAsyncKeyState(VK_SHIFT); + + // GetAsyncKeyState returns a value with the most significant bit set to 1 if the key is pressed. This is the sign bit. + return control < 0 && shift < 0; +} diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.h b/src/cascadia/ShellExtension/OpenTerminalHere.h index bfcf660fae..de5d088643 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 @@ -58,6 +60,7 @@ struct private: HRESULT GetLocationFromSite(IShellItem** location) const noexcept; HRESULT GetBestLocationFromSelectionOrSite(IShellItemArray* psiArray, IShellItem** location) const noexcept; + bool IsControlAndShiftPressed(); wil::com_ptr_nothrow site_; }; diff --git a/src/cascadia/TerminalApp/AboutDialog.cpp b/src/cascadia/TerminalApp/AboutDialog.cpp new file mode 100644 index 0000000000..c443b73e09 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.cpp @@ -0,0 +1,150 @@ + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "AboutDialog.h" +#include "AboutDialog.g.cpp" + +#include +#include + +#include "../../types/inc/utils.hpp" +#include "Utils.h" + +using namespace winrt; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal; +using namespace ::TerminalApp; +using namespace std::chrono_literals; + +namespace winrt +{ + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::TerminalApp::implementation +{ + AboutDialog::AboutDialog() + { + InitializeComponent(); + _queueUpdateCheck(); + } + + winrt::hstring AboutDialog::ApplicationDisplayName() + { + return CascadiaSettings::ApplicationDisplayName(); + } + + winrt::hstring AboutDialog::ApplicationVersion() + { + return CascadiaSettings::ApplicationVersion(); + } + + void AboutDialog::_SendFeedbackOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& /*eventArgs*/) + { +#if defined(WT_BRANDING_RELEASE) + ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2125419", nullptr, nullptr, SW_SHOW); +#else + ShellExecute(nullptr, nullptr, L"https://go.microsoft.com/fwlink/?linkid=2204904", nullptr, nullptr, SW_SHOW); +#endif + } + + void AboutDialog::_ThirdPartyNoticesOnClick(const IInspectable& /*sender*/, const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/) + { + std::filesystem::path currentPath{ wil::GetModuleFileNameW(nullptr) }; + currentPath.replace_filename(L"NOTICE.html"); + ShellExecute(nullptr, nullptr, currentPath.c_str(), nullptr, nullptr, SW_SHOW); + } + + winrt::fire_and_forget AboutDialog::_queueUpdateCheck() + { + auto strongThis = get_strong(); + auto now{ std::chrono::system_clock::now() }; + if (now - _lastUpdateCheck < std::chrono::days{ 1 }) + { + co_return; + } + _lastUpdateCheck = now; + + if (!IsPackaged()) + { + co_return; + } + + co_await wil::resume_foreground(strongThis->Dispatcher()); + UpdatesAvailable(false); + CheckingForUpdates(true); + + try + { +#ifdef WT_BRANDING_DEV + // **DEV BRANDING**: Always sleep for three seconds and then report that + // there is an update available. This lets us test the system. + co_await winrt::resume_after(std::chrono::seconds{ 3 }); + co_await wil::resume_foreground(strongThis->Dispatcher()); + UpdatesAvailable(true); +#else // release build, likely has a store context + bool packageManagerAnswered{ false }; + + try + { + if (auto currentPackage{ winrt::Windows::ApplicationModel::Package::Current() }) + { + // We need to look up our package in the Package Manager; we cannot use Current + winrt::Windows::Management::Deployment::PackageManager pm; + if (auto lookedUpPackage{ pm.FindPackageForUser(winrt::hstring{}, currentPackage.Id().FullName()) }) + { + using winrt::Windows::ApplicationModel::PackageUpdateAvailability; + auto availabilityResult = co_await lookedUpPackage.CheckUpdateAvailabilityAsync(); + co_await wil::resume_foreground(strongThis->Dispatcher()); + auto availability = availabilityResult.Availability(); + switch (availability) + { + case PackageUpdateAvailability::Available: + case PackageUpdateAvailability::Required: + case PackageUpdateAvailability::NoUpdates: + UpdatesAvailable(availability != PackageUpdateAvailability::NoUpdates); + packageManagerAnswered = true; + break; + case PackageUpdateAvailability::Error: + case PackageUpdateAvailability::Unknown: + default: + // Do not set packageManagerAnswered, which will trigger the store check. + break; + } + } + } + } + catch (...) + { + } // Do nothing on failure + + if (!packageManagerAnswered) + { + if (auto storeContext{ winrt::Windows::Services::Store::StoreContext::GetDefault() }) + { + const auto updates = co_await storeContext.GetAppAndOptionalStorePackageUpdatesAsync(); + co_await wil::resume_foreground(strongThis->Dispatcher()); + if (updates) + { + const auto numUpdates = updates.Size(); + if (numUpdates > 0) + { + UpdatesAvailable(true); + } + } + } + } +#endif + } + catch (...) + { + // do nothing on failure + } + + co_await wil::resume_foreground(strongThis->Dispatcher()); + CheckingForUpdates(false); + } +} diff --git a/src/cascadia/TerminalApp/AboutDialog.h b/src/cascadia/TerminalApp/AboutDialog.h new file mode 100644 index 0000000000..c4dac5bdf7 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "AboutDialog.g.h" + +namespace winrt::TerminalApp::implementation +{ + struct AboutDialog : AboutDialogT + { + public: + AboutDialog(); + + winrt::hstring ApplicationDisplayName(); + winrt::hstring ApplicationVersion(); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(bool, UpdatesAvailable, _PropertyChangedHandlers, false); + WINRT_OBSERVABLE_PROPERTY(bool, CheckingForUpdates, _PropertyChangedHandlers, false); + + private: + friend struct AboutDialogT; // for Xaml to bind events + + std::chrono::system_clock::time_point _lastUpdateCheck{}; + + void _ThirdPartyNoticesOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _SendFeedbackOnClick(const IInspectable& sender, const Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& eventArgs); + winrt::fire_and_forget _queueUpdateCheck(); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(AboutDialog); +} diff --git a/src/cascadia/TerminalApp/AboutDialog.idl b/src/cascadia/TerminalApp/AboutDialog.idl new file mode 100644 index 0000000000..28b48530d9 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + [default_interface] runtimeclass AboutDialog : Windows.UI.Xaml.Controls.ContentDialog, Windows.UI.Xaml.Data.INotifyPropertyChanged + { + AboutDialog(); + + String ApplicationDisplayName { get; }; + String ApplicationVersion { get; }; + + Boolean CheckingForUpdates { get; }; + Boolean UpdatesAvailable { get; }; + } +} diff --git a/src/cascadia/TerminalApp/AboutDialog.xaml b/src/cascadia/TerminalApp/AboutDialog.xaml new file mode 100644 index 0000000000..a4257ec904 --- /dev/null +++ b/src/cascadia/TerminalApp/AboutDialog.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/ActionPaletteItem.cpp b/src/cascadia/TerminalApp/ActionPaletteItem.cpp index 6331a619cb..ecfd849c5d 100644 --- a/src/cascadia/TerminalApp/ActionPaletteItem.cpp +++ b/src/cascadia/TerminalApp/ActionPaletteItem.cpp @@ -24,27 +24,5 @@ namespace winrt::TerminalApp::implementation Name(command.Name()); KeyChordText(command.KeyChordText()); Icon(command.IconPath()); - - _commandChangedRevoker = command.PropertyChanged(winrt::auto_revoke, [weakThis{ get_weak() }](auto& sender, auto& e) { - auto item{ weakThis.get() }; - auto senderCommand{ sender.try_as() }; - - if (item && senderCommand) - { - auto changedProperty = e.PropertyName(); - if (changedProperty == L"Name") - { - item->Name(senderCommand.Name()); - } - else if (changedProperty == L"KeyChordText") - { - item->KeyChordText(senderCommand.KeyChordText()); - } - else if (changedProperty == L"IconPath") - { - item->Icon(senderCommand.IconPath()); - } - } - }); } } diff --git a/src/cascadia/TerminalApp/App.base.h b/src/cascadia/TerminalApp/App.base.h index 289ce39cab..bece632426 100644 --- a/src/cascadia/TerminalApp/App.base.h +++ b/src/cascadia/TerminalApp/App.base.h @@ -9,22 +9,35 @@ namespace winrt::TerminalApp::implementation IXamlType GetXamlType(const ::winrt::Windows::UI::Xaml::Interop::TypeName& type) { - return _appProvider.GetXamlType(type); + return AppProvider()->GetXamlType(type); } IXamlType GetXamlType(const ::winrt::hstring& fullName) { - return _appProvider.GetXamlType(fullName); + return AppProvider()->GetXamlType(fullName); } ::winrt::com_array<::winrt::Windows::UI::Xaml::Markup::XmlnsDefinition> GetXmlnsDefinitions() { - return _appProvider.GetXmlnsDefinitions(); + return AppProvider()->GetXmlnsDefinitions(); + } + + void AddOtherProvider(const ::winrt::Windows::UI::Xaml::Markup::IXamlMetadataProvider& provider) + { + AppProvider()->AddOtherProvider(provider); } private: bool _contentLoaded{ false }; - winrt::TerminalApp::XamlMetaDataProvider _appProvider; + winrt::com_ptr _appProvider; + winrt::com_ptr AppProvider() + { + if (!_appProvider) + { + _appProvider = winrt::make_self(); + } + return _appProvider; + } }; template diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index c143f2eced..017129ca5f 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "App.h" #include "App.g.cpp" +#include using namespace winrt; using namespace winrt::Windows::ApplicationModel::Activation; @@ -12,19 +13,12 @@ using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::UI::Xaml::Navigation; +namespace xaml = ::winrt::Windows::UI::Xaml; + namespace winrt::TerminalApp::implementation { App::App() { - // This is the same trick that Initialize() is about to use to figure out whether we're coming - // from a UWP context or from a Win32 context - // See https://github.com/windows-toolkit/Microsoft.Toolkit.Win32/blob/52611c57d89554f357f281d0c79036426a7d9257/Microsoft.Toolkit.Win32.UI.XamlApplication/XamlApplication.cpp#L42 - const auto dispatcherQueue = ::winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); - if (dispatcherQueue) - { - _isUwp = true; - } - Initialize(); // Disable XAML's automatic backplating of text when in High Contrast @@ -33,12 +27,75 @@ namespace winrt::TerminalApp::implementation HighContrastAdjustment(::winrt::Windows::UI::Xaml::ApplicationHighContrastAdjustment::None); } + void App::Initialize() + { + // LOAD BEARING + AddOtherProvider(winrt::Microsoft::Terminal::Control::XamlMetaDataProvider{}); + AddOtherProvider(winrt::Microsoft::UI::Xaml::XamlTypeInfo::XamlControlsXamlMetaDataProvider{}); + + const auto dispatcherQueue = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + if (!dispatcherQueue) + { + _windowsXamlManager = xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); + + // As of Process Model v3, terminal windows are all created on their + // own threads, but we still initiate XAML for the App on the main + // thread. Thing is, just initializing XAML creates a CoreWindow for + // us. On Windows 10, that CoreWindow will show up as a visible + // window on the taskbar, unless we hide it manually. So, go get it + // and do the SW_HIDE thing on it. + if (const auto& coreWindow{ winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread() }) + { + if (const auto& interop{ coreWindow.try_as() }) + { + HWND coreHandle{ 0 }; + interop->get_WindowHandle(&coreHandle); + if (coreHandle) + { + // This prevents an empty "DesktopWindowXamlSource" from + // appearing on the taskbar + ShowWindow(coreHandle, SW_HIDE); + } + } + } + } + else + { + FAIL_FAST_MSG("Terminal is not intended to run as a Universal Windows Application"); + } + } + AppLogic App::Logic() { static AppLogic logic; return logic; } + void App::Close() + { + if (_bIsClosed) + { + return; + } + + _bIsClosed = true; + + if (_windowsXamlManager) + { + _windowsXamlManager.Close(); + } + _windowsXamlManager = nullptr; + + Exit(); + { + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } + } + } + /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. @@ -46,22 +103,15 @@ namespace winrt::TerminalApp::implementation /// Details about the launch request and process. void App::OnLaunched(const LaunchActivatedEventArgs& /*e*/) { - // if this is a UWP... it means its our problem to hook up the content to the window here. - if (_isUwp) + // We used to support a pure UWP version of the Terminal. This method + // was only ever used to do UWP-specific setup of our App. + } + + void App::PrepareForSettingsUI() + { + if (!std::exchange(_preparedForSettingsUI, true)) { - auto content = Window::Current().Content(); - if (content == nullptr) - { - auto logic = Logic(); - logic.RunAsUwp(); // Must set UWP status first, settings might change based on it. - logic.LoadSettings(); - logic.Create(); - - auto page = logic.GetRoot().as(); - - Window::Current().Content(page); - Window::Current().Activate(); - } + AddOtherProvider(winrt::Microsoft::Terminal::Settings::Editor::XamlMetaDataProvider{}); } } } diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 259d317553..a2c9b6a7c0 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -5,6 +5,7 @@ #include "App.g.h" #include "App.base.h" +#include namespace winrt::TerminalApp::implementation { @@ -13,11 +14,22 @@ namespace winrt::TerminalApp::implementation public: App(); void OnLaunched(const Windows::ApplicationModel::Activation::LaunchActivatedEventArgs&); + void Initialize(); TerminalApp::AppLogic Logic(); + void Close(); + void PrepareForSettingsUI(); + + bool IsDisposed() const + { + return _bIsClosed; + } + private: - bool _isUwp = false; + winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager _windowsXamlManager = nullptr; + bool _bIsClosed = false; + bool _preparedForSettingsUI{ false }; }; } diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index 5e000c5a18..516442148c 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -7,10 +7,12 @@ namespace TerminalApp { // ADD ARBITRARY APP LOGIC TO AppLogic.idl, NOT HERE. // This is for XAML platform setup only. - [default_interface] runtimeclass App : Microsoft.Toolkit.Win32.UI.XamlHost.XamlApplication + [default_interface] runtimeclass App : Windows.UI.Xaml.Application, Windows.Foundation.IClosable { App(); AppLogic Logic { get; }; + + Boolean IsDisposed { get; }; } } diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index 24573ba619..1cda23ddd7 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -2,20 +2,19 @@ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. See LICENSE in the project root for license information. --> - + - + @@ -46,9 +45,27 @@ Color="{ThemeResource SystemErrorTextColor}" /> - 9,0,5,0 + 0,0,0,0 + 1,1,1,0 + + + @@ -139,7 +156,7 @@ evaluated as SolidBackgroundFillColorBase. If we try to use those resources directly though, we don't get the properly themed versions. Presumably because the - App itself can't have it's RequestedTheme changed at + App itself can't have its RequestedTheme changed at runtime. However, after more discussion with the WinUI @@ -153,10 +170,13 @@ Color="#2e2e2e" /> + ResourceKey="TabViewBackground" /> + + @@ -169,10 +189,13 @@ Color="#e8e8e8" /> + ResourceKey="TabViewBackground" /> + + @@ -193,6 +216,9 @@ + + @@ -202,5 +228,5 @@ - - + + diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index a5b1752372..481fce3362 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) @@ -209,12 +232,12 @@ namespace winrt::TerminalApp::implementation } else if (const auto& realArgs = args.ActionArgs().try_as()) { - auto moved = _MovePane(realArgs.TabIndex()); + const auto moved = _MovePane(realArgs); args.Handled(moved); } } - 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,6 +306,28 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } + void TerminalPage::_HandleEnablePaneReadOnly(const IInspectable& sender, + const ActionEventArgs& args) + { + if (const auto activeTab{ _senderOrFocusedTab(sender) }) + { + activeTab->SetPaneReadOnly(true); + } + + args.Handled(true); + } + + void TerminalPage::_HandleDisablePaneReadOnly(const IInspectable& sender, + const ActionEventArgs& args) + { + if (const auto activeTab{ _senderOrFocusedTab(sender) }) + { + activeTab->SetPaneReadOnly(false); + } + + args.Handled(true); + } + void TerminalPage::_HandleScrollUpPage(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -490,7 +533,7 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - const auto handled = _CopyText(realArgs.SingleLine(), realArgs.CopyFormatting()); + const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.CopyFormatting()); args.Handled(handled); } } @@ -507,10 +550,14 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_HandleFind(const IInspectable& /*sender*/, + void TerminalPage::_HandleFind(const IInspectable& sender, const ActionEventArgs& args) { - _Find(); + if (const auto activeTab{ _senderOrFocusedTab(sender) }) + { + _SetFocusedTab(*activeTab); + _Find(*activeTab); + } args.Handled(true); } @@ -588,10 +635,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - CommandPalette().EnableCommandPaletteMode(realArgs.LaunchMode()); - CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ? - Visibility::Collapsed : - Visibility::Visible); + const auto p = LoadCommandPalette(); + const auto v = p.Visibility() == Visibility::Visible ? Visibility::Collapsed : Visibility::Visible; + p.EnableCommandPaletteMode(realArgs.LaunchMode()); + p.Visibility(v); args.Handled(true); } } @@ -612,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; @@ -622,7 +669,7 @@ namespace winrt::TerminalApp::implementation tabColor = realArgs.TabColor(); } - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto activeTab{ _senderOrFocusedTab(sender) }) { if (tabColor) { @@ -636,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; @@ -656,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()) { @@ -670,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(); } @@ -774,29 +826,21 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleTabSearch(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CommandPalette().SetTabs(_tabs, _mruTabs); - CommandPalette().EnableTabSearchMode(); - CommandPalette().Visibility(Visibility::Visible); + const auto p = LoadCommandPalette(); + p.SetTabs(_tabs, _mruTabs); + p.EnableTabSearchMode(); + p.Visibility(Visibility::Visible); 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 direction = realArgs.Direction(); - if (direction != MoveTabDirection::None) - { - if (auto focusedTabIndex = _GetFocusedTabIndex()) - { - auto currentTabIndex = focusedTabIndex.value(); - auto delta = direction == MoveTabDirection::Forward ? 1 : -1; - _TryMoveTab(currentTabIndex, currentTabIndex + delta); - } - } - actionArgs.Handled(true); + const auto moved = _MoveTab(_senderOrFocusedTab(sender), realArgs); + actionArgs.Handled(moved); } } @@ -1004,6 +1048,59 @@ namespace winrt::TerminalApp::implementation args.Handled(true); } + void TerminalPage::_HandleDisplayWorkingDirectory(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + ShowTerminalWorkingDirectory(); + args.Handled(true); + } + } + + void TerminalPage::_HandleSearchForText(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (const auto termControl{ _GetActiveControl() }) + { + if (termControl.HasSelection()) + { + const auto selections{ termControl.SelectedText(true) }; + + // concatenate the selection into a single line + auto searchText = std::accumulate(selections.begin(), selections.end(), std::wstring()); + + // make it compact by replacing consecutive whitespaces with a single space + searchText = std::regex_replace(searchText, std::wregex(LR"(\s+)"), L" "); + + std::wstring queryUrl; + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + queryUrl = realArgs.QueryUrl().c_str(); + } + } + + // use global default if query URL is unspecified + if (queryUrl.empty()) + { + queryUrl = _settings.GlobalSettings().SearchWebDefaultQueryUrl().c_str(); + } + + constexpr std::wstring_view queryToken{ L"%s" }; + if (const auto pos{ queryUrl.find(queryToken) }; pos != std::wstring_view::npos) + { + queryUrl.replace(pos, queryToken.length(), Windows::Foundation::Uri::EscapeComponent(searchText)); + } + + winrt::Microsoft::Terminal::Control::OpenHyperlinkEventArgs shortcut{ queryUrl }; + _OpenHyperlinkHandler(termControl, shortcut); + args.Handled(true); + } + } + } + void TerminalPage::_HandleGlobalSummon(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -1031,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(); @@ -1047,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) { @@ -1118,46 +1225,142 @@ 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); } } - void TerminalPage::_HandleMarkMode(const IInspectable& /*sender*/, + void TerminalPage::_HandleSelectCommand(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + const auto res = _ApplyToActiveControls([&](auto& control) { + control.SelectCommand(realArgs.Direction() == Settings::Model::SelectOutputDirection::Previous); + }); + args.Handled(res); + } + } + } + void TerminalPage::_HandleSelectOutput(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + const auto res = _ApplyToActiveControls([&](auto& control) { + control.SelectOutput(realArgs.Direction() == Settings::Model::SelectOutputDirection::Previous); + }); + args.Handled(res); + } + } + } + + 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); } } + void TerminalPage::_HandleSuggestions(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + 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() }) + { + context = control.CommandHistory(); + if (context) + { + currentCommandline = context.CurrentCommandline(); + } + } + } + + // 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); + } + } + } + void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/, const ActionEventArgs& args) { @@ -1182,4 +1385,44 @@ namespace winrt::TerminalApp::implementation args.Handled(handled); } } + + void TerminalPage::_HandleToggleBroadcastInput(const IInspectable& sender, + const ActionEventArgs& args) + { + if (const auto activeTab{ _senderOrFocusedTab(sender) }) + { + activeTab->ToggleBroadcastInput(); + args.Handled(true); + } + // If the focused tab wasn't a TerminalTab, then leave handled=false + } + + void TerminalPage::_HandleRestartConnection(const IInspectable& sender, + const ActionEventArgs& args) + { + if (const auto activeTab{ _senderOrFocusedTab(sender) }) + { + if (const auto activePane{ activeTab->GetActivePane() }) + { + _restartPaneConnection(activePane); + } + } + args.Handled(true); + } + + void TerminalPage::_HandleShowContextMenu(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (const auto& control{ _GetActiveControl() }) + { + control.ShowContextMenu(); + } + args.Handled(true); + } + void TerminalPage::_HandleOpenAbout(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + _ShowAboutDialog(); + args.Handled(true); + } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 0ac4cf51af..5060b4ff2f 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -97,7 +97,7 @@ int AppCommandlineArgs::ParseCommand(const Commandline& command) } // Method Description: -// - Calls App::exit() for the provided command, and collects it's output into +// - Calls App::exit() for the provided command, and collects its output into // our _exitMessage buffer. // Arguments: // - command: Either the root App object, or a subcommand for which to call exit() on. @@ -340,7 +340,7 @@ void AppCommandlineArgs::_buildMovePaneParser() if (_movePaneTabIndex >= 0) { movePaneAction.Action(ShortcutAction::MovePane); - MovePaneArgs args{ static_cast(_movePaneTabIndex) }; + MovePaneArgs args{ static_cast(_movePaneTabIndex), L"" }; movePaneAction.Args(args); _startupActions.push_back(movePaneAction); } @@ -568,6 +568,14 @@ 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")); + + subcommand.inheritEnvOption = subcommand.subcommand->add_flag( + "--inheritEnvironment,!--reloadEnvironment", + _inheritEnvironment, + RS_A(L"CmdInheritEnvDesc")); + // Using positionals_at_end allows us to support "wt new-tab -d wsl -d Ubuntu" // without CLI11 thinking that we've specified -d twice. // There's an alternate construction where we make all subcommands "prefix commands", @@ -589,7 +597,8 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT { NewTerminalArgs args{}; - if (!_commandline.empty()) + const auto hasCommandline{ !_commandline.empty() }; + if (hasCommandline) { std::ostringstream cmdlineBuffer; @@ -654,6 +663,17 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT { args.ColorScheme(winrt::to_hstring(_startingColorScheme)); } + if (*subcommand.appendCommandLineOption) + { + args.AppendCommandLine(_appendCommandLineOption); + } + + bool inheritEnv = hasCommandline; + if (*subcommand.inheritEnvOption) + { + inheritEnv = _inheritEnvironment; + } + args.ReloadEnvironmentVariables(!inheritEnv); return args; } @@ -699,6 +719,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 31eac78168..9145697f45 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -67,6 +67,8 @@ private: CLI::Option* tabColorOption; CLI::Option* suppressApplicationTitleOption; CLI::Option* colorSchemeOption; + CLI::Option* appendCommandLineOption; + CLI::Option* inheritEnvOption; }; struct NewPaneSubcommand : public NewTerminalSubcommand @@ -99,12 +101,14 @@ private: std::string _startingTabColor; std::string _startingColorScheme; bool _suppressApplicationTitle{ false }; + bool _inheritEnvironment{ false }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _moveFocusDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; winrt::Microsoft::Terminal::Settings::Model::FocusDirection _swapPaneDirection{ winrt::Microsoft::Terminal::Settings::Model::FocusDirection::None }; // _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/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 7062bfd73a..fb39ebf72f 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -6,7 +6,7 @@ #include "../inc/WindowingBehavior.h" #include "AppLogic.g.cpp" #include "FindTargetWindowResult.g.cpp" -#include +#include "SettingsLoadEventArgs.h" #include #include @@ -31,35 +31,23 @@ namespace winrt using IInspectable = Windows::Foundation::IInspectable; } -static constexpr std::wstring_view StartupTaskName = L"StartTerminalOnLoginTask"; -// clang-format off +//////////////////////////////////////////////////////////////////////////////// +// Error message handling. This is in this file rather than with the warnings in +// TerminalWindow, because the error text might be: +// * A error we defined here +// * An error from deserializing the json +// * Any other fatal error loading the settings +// So all we pass on is the actual text of the error, rather than the +// combination of things that might have caused an error. + // !!! IMPORTANT !!! // Make sure that these keys are in the same order as the // SettingsLoadWarnings/Errors enum is! -static const std::array settingsLoadWarningsLabels { - USES_RESOURCE(L"MissingDefaultProfileText"), - USES_RESOURCE(L"DuplicateProfileText"), - USES_RESOURCE(L"UnknownColorSchemeText"), - USES_RESOURCE(L"InvalidBackgroundImage"), - USES_RESOURCE(L"InvalidIcon"), - USES_RESOURCE(L"AtLeastOneKeybindingWarning"), - USES_RESOURCE(L"TooManyKeysForChord"), - USES_RESOURCE(L"MissingRequiredParameter"), - USES_RESOURCE(L"FailedToParseCommandJson"), - USES_RESOURCE(L"FailedToWriteToSettings"), - USES_RESOURCE(L"InvalidColorSchemeInCmd"), - USES_RESOURCE(L"InvalidSplitSize"), - USES_RESOURCE(L"FailedToParseStartupActions"), - USES_RESOURCE(L"FailedToParseSubCommands"), - USES_RESOURCE(L"UnknownTheme"), -}; -static const std::array settingsLoadErrorsLabels { +static const std::array settingsLoadErrorsLabels{ USES_RESOURCE(L"NoProfilesText"), USES_RESOURCE(L"AllProfilesHiddenText") }; -// clang-format on -static_assert(settingsLoadWarningsLabels.size() == static_cast(SettingsLoadWarnings::WARNINGS_SIZE)); static_assert(settingsLoadErrorsLabels.size() == static_cast(SettingsLoadErrors::ERRORS_SIZE)); // Function Description: @@ -82,20 +70,6 @@ winrt::hstring _GetMessageText(uint32_t index, const T& keys) } return {}; } - -// Function Description: -// - Gets the text from our ResourceDictionary for the given -// SettingsLoadWarning. If there is no such text, we'll return nullptr. -// - The warning should have an entry in settingsLoadWarningsLabels. -// Arguments: -// - warning: the SettingsLoadWarnings value to get the localized text for. -// Return Value: -// - localized text for the given warning -static winrt::hstring _GetWarningText(SettingsLoadWarnings warning) -{ - return _GetMessageText(static_cast(warning), settingsLoadWarningsLabels); -} - // Function Description: // - Gets the text from our ResourceDictionary for the given // SettingsLoadError. If there is no such text, we'll return nullptr. @@ -109,30 +83,7 @@ static winrt::hstring _GetErrorText(SettingsLoadErrors error) return _GetMessageText(static_cast(error), settingsLoadErrorsLabels); } -// Function Description: -// - Creates a Run of text to display an error message. The text is yellow or -// red for dark/light theme, respectively. -// Arguments: -// - text: The text of the error message. -// - resources: The application's resource loader. -// Return Value: -// - The fully styled text run. -static Documents::Run _BuildErrorRun(const winrt::hstring& text, const ResourceDictionary& resources) -{ - Documents::Run textRun; - textRun.Text(text); - - // Color the text red (light theme) or yellow (dark theme) based on the system theme - auto key = winrt::box_value(L"ErrorTextBrush"); - if (resources.HasKey(key)) - { - auto g = resources.Lookup(key); - auto brush = g.try_as(); - textRun.Foreground(brush); - } - - return textRun; -} +static constexpr std::wstring_view StartupTaskName = L"StartTerminalOnLoginTask"; namespace winrt::TerminalApp::implementation { @@ -185,13 +136,14 @@ namespace winrt::TerminalApp::implementation // The TerminalPage has to be constructed during our construction, to // make sure that there's a terminal page for callers of // SetTitleBarContent - _isElevated = ::Microsoft::Console::Utils::IsElevated(); - _root = winrt::make_self(); + + _isElevated = ::Microsoft::Console::Utils::IsRunningElevated(); + _canDragDrop = ::Microsoft::Console::Utils::CanUwpDragDrop(); _reloadSettings = std::make_shared>(winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { if (auto self{ weakSelf.get() }) { - self->_ReloadSettings(); + self->ReloadSettings(); } }); @@ -200,45 +152,19 @@ namespace winrt::TerminalApp::implementation }); } - // Method Description: - // - Implements the IInitializeWithWindow interface from shobjidl_core. - HRESULT AppLogic::Initialize(HWND hwnd) - { - return _root->Initialize(hwnd); - } - - // Method Description: - // - Called around the codebase to discover if this is a UWP where we need to turn off specific settings. - // Arguments: - // - - reports internal state - // Return Value: - // - True if UWP, false otherwise. - bool AppLogic::IsUwp() const noexcept - { - return _isUwp; - } - // Method Description: // - Called around the codebase to discover if Terminal is running elevated // Arguments: // - - reports internal state // Return Value: // - True if elevated, false otherwise. - bool AppLogic::IsElevated() const noexcept + bool AppLogic::IsRunningElevated() const noexcept { return _isElevated; } - - // Method Description: - // - Called by UWP context invoker to let us know that we may have to change some of our behaviors - // for being a UWP - // Arguments: - // - (sets to UWP = true, one way change) - // Return Value: - // - - void AppLogic::RunAsUwp() + bool AppLogic::CanDragDrop() const noexcept { - _isUwp = true; + return _canDragDrop; } // Method Description: @@ -256,87 +182,7 @@ namespace winrt::TerminalApp::implementation // this as a MTA, before the app is Create()'d WINRT_ASSERT(_loadedInitialSettings); - _root->DialogPresenter(*this); - - // In UWP mode, we cannot handle taking over the title bar for tabs, - // so this setting is overridden to false no matter what the preference is. - if (_isUwp) - { - _settings.GlobalSettings().ShowTabsInTitlebar(false); - } - - // Pay attention, that even if some command line arguments were parsed (like launch mode), - // we will not use the startup actions from settings. - // While this simplifies the logic, we might want to reconsider this behavior in the future. - if (!_hasCommandLineArguments && _hasSettingsStartupActions) - { - _root->SetStartupActions(_settingsAppArgs.GetStartupActions()); - } - - _root->SetSettings(_settings, false); - _root->Loaded({ this, &AppLogic::_OnLoaded }); - _root->Initialized([this](auto&&, auto&&) { - // GH#288 - When we finish initialization, if the user wanted us - // launched _fullscreen_, toggle fullscreen mode. This will make sure - // that the window size is _first_ set up as something sensible, so - // leaving fullscreen returns to a reasonable size. - const auto launchMode = this->GetLaunchMode(); - if (IsQuakeWindow() || WI_IsFlagSet(launchMode, LaunchMode::FocusMode)) - { - _root->SetFocusMode(true); - } - - // The IslandWindow handles (creating) the maximized state - // we just want to record it here on the page as well. - if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) - { - _root->Maximized(true); - } - - if (WI_IsFlagSet(launchMode, LaunchMode::FullscreenMode) && !IsQuakeWindow()) - { - _root->SetFullscreen(true); - } - - // Both LoadSettings and ReloadSettings are supposed to call this function, - // but LoadSettings skips it, so that the UI starts up faster. - // Now that the UI is present we can do them with a less significant UX impact. - _ProcessLazySettingsChanges(); - - FILETIME creationTime, exitTime, kernelTime, userTime, now; - if (GetThreadTimes(GetCurrentThread(), &creationTime, &exitTime, &kernelTime, &userTime)) - { - static constexpr auto asInteger = [](const FILETIME& f) { - ULARGE_INTEGER i; - i.LowPart = f.dwLowDateTime; - i.HighPart = f.dwHighDateTime; - return i.QuadPart; - }; - static constexpr auto asSeconds = [](uint64_t v) { - return v * 1e-7f; - }; - - GetSystemTimeAsFileTime(&now); - - const auto latency = asSeconds(asInteger(now) - asInteger(creationTime)); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "AppInitialized", - TraceLoggingDescription("Event emitted once the app is initialized"), - TraceLoggingFloat32(latency, "latency"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }); - _root->Create(); - _ApplyLanguageSettingChange(); - _RefreshThemeRoutine(); - _ApplyStartupTaskStateChange(); - - auto args = winrt::make_self(RS_(L"SettingsMenuItem"), SystemMenuChangeAction::Add, SystemMenuItemHandler(this, &AppLogic::_OpenSettingsUI)); - _SystemMenuChangeRequestedHandlers(*this, *args); TraceLoggingWrite( g_hTerminalAppProvider, @@ -347,459 +193,6 @@ namespace winrt::TerminalApp::implementation TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); } - void AppLogic::Quit() - { - if (_root) - { - _root->CloseWindow(true); - } - } - - // Method Description: - // - Show a ContentDialog with buttons to take further action. Uses the - // FrameworkElements provided as the title and content of this dialog, and - // displays buttons (or a single button). Two buttons (primary and secondary) - // will be displayed if this is an warning dialog for closing the terminal, - // this allows the users to abandon the closing action. Otherwise, a single - // close button will be displayed. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. - // Arguments: - // - dialog: the dialog object that is going to show up - // Return value: - // - an IAsyncOperation with the dialog result - winrt::Windows::Foundation::IAsyncOperation AppLogic::ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog) - { - // DON'T release this lock in a wil::scope_exit. The scope_exit will get - // called when we await, which is not what we want. - std::unique_lock lock{ _dialogLock, std::try_to_lock }; - if (!lock) - { - // Another dialog is visible. - co_return ContentDialogResult::None; - } - - _dialog = dialog; - - // IMPORTANT: This is necessary as documented in the ContentDialog MSDN docs. - // Since we're hosting the dialog in a Xaml island, we need to connect it to the - // xaml tree somehow. - dialog.XamlRoot(_root->XamlRoot()); - - // IMPORTANT: Set the requested theme of the dialog, because the - // PopupRoot isn't directly in the Xaml tree of our root. So the dialog - // won't inherit our RequestedTheme automagically. - // GH#5195, GH#3654 Because we cannot set RequestedTheme at the application level, - // we occasionally run into issues where parts of our UI end up themed incorrectly. - // Dialogs, for example, live under a different Xaml root element than the rest of - // our application. This makes our popup menus and buttons "disappear" when the - // user wants Terminal to be in a different theme than the rest of the system. - // This hack---and it _is_ a hack--walks up a dialog's ancestry and forces the - // theme on each element up to the root. We're relying a bit on Xaml's implementation - // details here, but it does have the desired effect. - // It's not enough to set the theme on the dialog alone. - auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) { - auto theme{ _settings.GlobalSettings().CurrentTheme() }; - auto requestedTheme{ theme.RequestedTheme() }; - auto element{ sender.try_as() }; - while (element) - { - element.RequestedTheme(requestedTheme); - element = element.Parent().try_as(); - } - } }; - - themingLambda(dialog, nullptr); // if it's already in the tree - auto loadedRevoker{ dialog.Loaded(winrt::auto_revoke, themingLambda) }; // if it's not yet in the tree - - // Display the dialog. - co_return co_await dialog.ShowAsync(Controls::ContentDialogPlacement::Popup); - - // After the dialog is dismissed, the dialog lock (held by `lock`) will - // be released so another can be shown - } - - // Method Description: - // - Dismiss the (only) visible ContentDialog - void AppLogic::DismissDialog() - { - if (auto localDialog = std::exchange(_dialog, nullptr)) - { - localDialog.Hide(); - } - } - - // Method Description: - // - Displays a dialog for errors found while loading or validating the - // settings. Uses the resources under the provided title and content keys - // as the title and first content of the dialog, then also displays a - // message for whatever exception was found while validating the settings. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See ShowDialog for details - // Arguments: - // - titleKey: The key to use to lookup the title text from our resources. - // - contentKey: The key to use to lookup the content text from our resources. - void AppLogic::_ShowLoadErrorsDialog(const winrt::hstring& titleKey, - const winrt::hstring& contentKey, - HRESULT settingsLoadedResult) - { - auto title = GetLibraryResourceString(titleKey); - auto buttonText = RS_(L"Ok"); - - Controls::TextBlock warningsTextBlock; - // Make sure you can copy-paste - warningsTextBlock.IsTextSelectionEnabled(true); - // Make sure the lines of text wrap - warningsTextBlock.TextWrapping(TextWrapping::Wrap); - - winrt::Windows::UI::Xaml::Documents::Run errorRun; - const auto errorLabel = GetLibraryResourceString(contentKey); - errorRun.Text(errorLabel); - warningsTextBlock.Inlines().Append(errorRun); - warningsTextBlock.Inlines().Append(Documents::LineBreak{}); - - if (FAILED(settingsLoadedResult)) - { - if (!_settingsLoadExceptionText.empty()) - { - warningsTextBlock.Inlines().Append(_BuildErrorRun(_settingsLoadExceptionText, ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Resources())); - warningsTextBlock.Inlines().Append(Documents::LineBreak{}); - } - } - - // Add a note that we're using the default settings in this case. - winrt::Windows::UI::Xaml::Documents::Run usingDefaultsRun; - const auto usingDefaultsText = RS_(L"UsingDefaultSettingsText"); - usingDefaultsRun.Text(usingDefaultsText); - warningsTextBlock.Inlines().Append(Documents::LineBreak{}); - warningsTextBlock.Inlines().Append(usingDefaultsRun); - - Controls::ContentDialog dialog; - dialog.Title(winrt::box_value(title)); - dialog.Content(winrt::box_value(warningsTextBlock)); - dialog.CloseButtonText(buttonText); - dialog.DefaultButton(Controls::ContentDialogButton::Close); - - ShowDialog(dialog); - } - - // Method Description: - // - Displays a dialog for warnings found while loading or validating the - // settings. Displays messages for whatever warnings were found while - // validating the settings. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See ShowDialog for details - void AppLogic::_ShowLoadWarningsDialog() - { - auto title = RS_(L"SettingsValidateErrorTitle"); - auto buttonText = RS_(L"Ok"); - - Controls::TextBlock warningsTextBlock; - // Make sure you can copy-paste - warningsTextBlock.IsTextSelectionEnabled(true); - // Make sure the lines of text wrap - warningsTextBlock.TextWrapping(TextWrapping::Wrap); - - for (const auto& warning : _warnings) - { - // Try looking up the warning message key for each warning. - const auto warningText = _GetWarningText(warning); - if (!warningText.empty()) - { - warningsTextBlock.Inlines().Append(_BuildErrorRun(warningText, ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Resources())); - warningsTextBlock.Inlines().Append(Documents::LineBreak{}); - } - } - - Controls::ContentDialog dialog; - dialog.Title(winrt::box_value(title)); - dialog.Content(winrt::box_value(warningsTextBlock)); - dialog.CloseButtonText(buttonText); - dialog.DefaultButton(Controls::ContentDialogButton::Close); - - ShowDialog(dialog); - } - - // Method Description: - // - Triggered when the application is finished loading. If we failed to load - // the settings, then this will display the error dialog. This is done - // here instead of when loading the settings, because we need our UI to be - // visible to display the dialog, and when we're loading the settings, - // the UI might not be visible yet. - // Arguments: - // - - void AppLogic::_OnLoaded(const IInspectable& /*sender*/, - const RoutedEventArgs& /*eventArgs*/) - { - if (_settings.GlobalSettings().InputServiceWarning()) - { - const auto keyboardServiceIsDisabled = !_IsKeyboardServiceEnabled(); - if (keyboardServiceIsDisabled) - { - _root->ShowKeyboardServiceWarning(); - } - } - - if (FAILED(_settingsLoadedResult)) - { - const winrt::hstring titleKey = USES_RESOURCE(L"InitialJsonParseErrorTitle"); - const winrt::hstring textKey = USES_RESOURCE(L"InitialJsonParseErrorText"); - _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); - } - else if (_settingsLoadedResult == S_FALSE) - { - _ShowLoadWarningsDialog(); - } - } - - // Method Description: - // - Helper for determining if the "Touch Keyboard and Handwriting Panel - // Service" is enabled. If it isn't, we want to be able to display a - // warning to the user, because they won't be able to type in the - // Terminal. - // Return Value: - // - true if the service is enabled, or if we fail to query the service. We - // return true in that case, to be less noisy (though, that is unexpected) - bool AppLogic::_IsKeyboardServiceEnabled() - { - if (IsUwp()) - { - return true; - } - - // If at any point we fail to open the service manager, the service, - // etc, then just quick return true to disable the dialog. We'd rather - // not be noisy with this dialog if we failed for some reason. - - // Open the service manager. This will return 0 if it failed. - wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; - - if (LOG_LAST_ERROR_IF(!hManager.is_valid())) - { - return true; - } - - // Get a handle to the keyboard service - wil::unique_schandle hService{ OpenServiceW(hManager.get(), TabletInputServiceKey.data(), SERVICE_QUERY_STATUS) }; - - // Windows 11 doesn't have a TabletInputService. - // (It was renamed to TextInputManagementService, because people kept thinking that a - // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) - if (!hService.is_valid()) - { - return true; - } - - // Get the current state of the service - SERVICE_STATUS status{ 0 }; - if (!LOG_IF_WIN32_BOOL_FALSE(QueryServiceStatus(hService.get(), &status))) - { - return true; - } - - const auto state = status.dwCurrentState; - return (state == SERVICE_RUNNING || state == SERVICE_START_PENDING); - } - - // Method Description: - // - Get the size in pixels of the client area we'll need to launch this - // terminal app. This method will use the default profile's settings to do - // this calculation, as well as the _system_ dpi scaling. See also - // TermControl::GetProposedDimensions. - // Arguments: - // - - // Return Value: - // - a point containing the requested dimensions in pixels. - winrt::Windows::Foundation::Size AppLogic::GetLaunchDimensions(uint32_t dpi) - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - winrt::Windows::Foundation::Size proposedSize{}; - - const auto scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - if (const auto layout = _root->LoadPersistedLayout(_settings)) - { - if (layout.InitialSize()) - { - proposedSize = layout.InitialSize().Value(); - // The size is saved as a non-scaled real pixel size, - // so we need to scale it appropriately. - proposedSize.Height = proposedSize.Height * scale; - proposedSize.Width = proposedSize.Width * scale; - } - } - - if (_appArgs.GetSize().has_value() || (proposedSize.Width == 0 && proposedSize.Height == 0)) - { - // Use the default profile to determine how big of a window we need. - const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) }; - - const til::size emptySize{}; - const auto commandlineSize = _appArgs.GetSize().value_or(emptySize); - proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), - dpi, - commandlineSize.width, - commandlineSize.height); - } - - // GH#2061 - If the global setting "Always show tab bar" is - // set or if "Show tabs in title bar" is set, then we'll need to add - // the height of the tab bar here. - if (_settings.GlobalSettings().ShowTabsInTitlebar()) - { - // If we're showing the tabs in the titlebar, we need to use a - // TitlebarControl here to calculate how much space to reserve. - // - // We'll create a fake TitlebarControl, and we'll propose an - // available size to it with Measure(). After Measure() is called, - // the TitlebarControl's DesiredSize will contain the _unscaled_ - // size that the titlebar would like to use. We'll use that as part - // of the height calculation here. - auto titlebar = TitlebarControl{ static_cast(0) }; - titlebar.Measure({ SHRT_MAX, SHRT_MAX }); - proposedSize.Height += (titlebar.DesiredSize().Height) * scale; - } - else if (_settings.GlobalSettings().AlwaysShowTabs()) - { - // Otherwise, let's use a TabRowControl to calculate how much extra - // space we'll need. - // - // Similarly to above, we'll measure it with an arbitrarily large - // available space, to make sure we get all the space it wants. - auto tabControl = TabRowControl(); - tabControl.Measure({ SHRT_MAX, SHRT_MAX }); - - // For whatever reason, there's about 10px of unaccounted-for space - // in the application. I couldn't tell you where these 10px are - // coming from, but they need to be included in this math. - proposedSize.Height += (tabControl.DesiredSize().Height + 10) * scale; - } - - return proposedSize; - } - - // Method Description: - // - Get the launch mode in json settings file. Now there - // two launch mode: default, maximized. Default means the window - // will launch according to the launch dimensions provided. Maximized - // means the window will launch as a maximized window - // Arguments: - // - - // Return Value: - // - LaunchMode enum that indicates the launch mode - LaunchMode AppLogic::GetLaunchMode() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - // GH#4620/#5801 - If the user passed --maximized or --fullscreen on the - // commandline, then use that to override the value from the settings. - const auto valueFromSettings = _settings.GlobalSettings().LaunchMode(); - const auto valueFromCommandlineArgs = _appArgs.GetLaunchMode(); - if (const auto layout = _root->LoadPersistedLayout(_settings)) - { - if (layout.LaunchMode()) - { - return layout.LaunchMode().Value(); - } - } - return valueFromCommandlineArgs.has_value() ? - valueFromCommandlineArgs.value() : - valueFromSettings; - } - - // Method Description: - // - Get the user defined initial position from Json settings file. - // This position represents the top left corner of the Terminal window. - // This setting is optional, if not provided, we will use the system - // default size, which is provided in IslandWindow::MakeWindow. - // Arguments: - // - defaultInitialX: the system default x coordinate value - // - defaultInitialY: the system default y coordinate value - // Return Value: - // - a point containing the requested initial position in pixels. - TerminalApp::InitialPosition AppLogic::GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY) - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; - - if (const auto layout = _root->LoadPersistedLayout(_settings)) - { - if (layout.InitialPosition()) - { - initialPosition = layout.InitialPosition().Value(); - } - } - - // Commandline args trump everything else - if (_appArgs.GetPosition().has_value()) - { - initialPosition = _appArgs.GetPosition().value(); - } - - return { - initialPosition.X ? initialPosition.X.Value() : defaultInitialX, - initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY - }; - } - - bool AppLogic::CenterOnLaunch() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - // If the position has been specified on the commandline, don't center on launch - return _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.GetPosition().has_value(); - } - - winrt::Windows::UI::Xaml::ElementTheme AppLogic::GetRequestedTheme() - { - return Theme().RequestedTheme(); - } - - bool AppLogic::GetShowTabsInTitlebar() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().ShowTabsInTitlebar(); - } - - bool AppLogic::GetInitialAlwaysOnTop() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().AlwaysOnTop(); - } - - // Method Description: - // - See Pane::CalcSnappedDimension - float AppLogic::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const - { - return _root->CalcSnappedDimension(widthOrHeight, dimension); - } - // Method Description: // - Attempt to load the settings. If we fail for any reason, returns an error. // Return Value: @@ -810,7 +203,7 @@ namespace winrt::TerminalApp::implementation try { - auto newSettings = _isUwp ? CascadiaSettings::LoadUniversal() : CascadiaSettings::LoadAll(); + auto newSettings = CascadiaSettings::LoadAll(); if (newSettings.GetLoadingError()) { @@ -851,6 +244,7 @@ namespace winrt::TerminalApp::implementation } _settings = std::move(newSettings); + hr = _warnings.empty() ? S_OK : S_FALSE; } catch (const winrt::hresult_error& e) @@ -867,34 +261,9 @@ namespace winrt::TerminalApp::implementation return hr; } - // Method Description: - // - Initialized our settings. See CascadiaSettings for more details. - // Additionally hooks up our callbacks for keybinding events to the - // keybindings object. - // NOTE: This must be called from a MTA if we're running as a packaged - // application. The Windows.Storage APIs require a MTA. If this isn't - // happening during startup, it'll need to happen on a background thread. - void AppLogic::LoadSettings() + bool AppLogic::HasSettingsStartupActions() const noexcept { - // Attempt to load the settings. - // If it fails, - // - use Default settings, - // - don't persist them (LoadAll won't save them in this case). - // - _settingsLoadedResult will be set to an error, indicating that - // we should display the loading error. - // * We can't display the error now, because we might not have a - // UI yet. We'll display the error in _OnLoaded. - _settingsLoadedResult = _TryLoadSettings(); - - if (FAILED(_settingsLoadedResult)) - { - _settings = CascadiaSettings::LoadDefaults(); - } - - _loadedInitialSettings = true; - - // Register for directory change notification. - _RegisterSettingsChange(); + return _hasSettingsStartupActions; } // Call this function after loading your _settings. @@ -920,7 +289,7 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Registers for changes to the settings folder and upon a updated settings - // profile calls _ReloadSettings(). + // profile calls ReloadSettings(). // Arguments: // - // Return Value: @@ -986,45 +355,12 @@ namespace winrt::TerminalApp::implementation } CATCH_LOG() - // Method Description: - // - Update the current theme of the application. This will trigger our - // RequestedThemeChanged event, to have our host change the theme of the - // root of the application. - // Arguments: - // - newTheme: The ElementTheme to apply to our elements. - void AppLogic::_RefreshThemeRoutine() - { - // Propagate the event to the host layer, so it can update its own UI - _RequestedThemeChangedHandlers(*this, Theme()); - } - - // Function Description: - // Returns the current app package or nullptr. - // TRANSITIONAL - // Exists to work around a compiler bug. This function encapsulates the - // exception handling that we used to keep around calls to Package::Current, - // so that when it's called inside a coroutine and fails it doesn't explode - // terribly. - static winrt::Windows::ApplicationModel::Package GetCurrentPackageNoThrow() noexcept - { - try - { - return winrt::Windows::ApplicationModel::Package::Current(); - } - catch (...) - { - // discard any exception -- literally pretend we're not in a package - } - return nullptr; - } - fire_and_forget AppLogic::_ApplyStartupTaskStateChange() try { // First, make sure we're running in a packaged context. This method // won't work, and will crash mysteriously if we're running unpackaged. - const auto package{ GetCurrentPackageNoThrow() }; - if (package == nullptr) + if (!IsPackaged()) { co_return; } @@ -1055,7 +391,14 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Reloads the settings from the settings.json file. - void AppLogic::_ReloadSettings() + // - When this is called the first time, this initializes our settings. See + // CascadiaSettings for more details. Additionally hooks up our callbacks + // for keybinding events to the keybindings object. + // - NOTE: when called initially, this must be called from a MTA if we're + // running as a packaged application. The Windows.Storage APIs require a + // MTA. If this isn't happening during startup, it'll need to happen on + // a background thread. + void AppLogic::ReloadSettings() { // Attempt to load our settings. // If it fails, @@ -1064,36 +407,98 @@ namespace winrt::TerminalApp::implementation // - display a loading error _settingsLoadedResult = _TryLoadSettings(); + const auto initialLoad = !_loadedInitialSettings; + _loadedInitialSettings = true; + if (FAILED(_settingsLoadedResult)) { - const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); - const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); - _ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult); - return; + if (initialLoad) + { + _settings = CascadiaSettings::LoadDefaults(); + } + else + { + auto warnings{ winrt::multi_threaded_vector() }; + for (auto&& warn : _warnings) + { + warnings.Append(warn); + } + auto ev = winrt::make_self(true, + static_cast(_settingsLoadedResult), + _settingsLoadExceptionText, + warnings, + _settings); + _SettingsChangedHandlers(*this, *ev); + return; + } } - if (_settingsLoadedResult == S_FALSE) + if (initialLoad) { - _ShowLoadWarningsDialog(); + // Register for directory change notification. + _RegisterSettingsChange(); + return; } // Here, we successfully reloaded the settings, and created a new // TerminalSettings object. - // Update the settings in TerminalPage - _root->SetSettings(_settings, true); - _ApplyLanguageSettingChange(); - _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); _ProcessLazySettingsChanges(); - _SettingsChangedHandlers(*this, nullptr); + auto warnings{ winrt::multi_threaded_vector() }; + for (auto&& warn : _warnings) + { + warnings.Append(warn); + } + auto ev = winrt::make_self(!initialLoad, + _settingsLoadedResult, + _settingsLoadExceptionText, + warnings, + _settings); + _SettingsChangedHandlers(*this, *ev); } - void AppLogic::_OpenSettingsUI() + // This is a continuation of AppLogic::Create() and includes the more expensive parts. + void AppLogic::NotifyRootInitialized() { - _root->OpenSettingsUI(); + if (_notifyRootInitializedCalled.exchange(true, std::memory_order_relaxed)) + { + return; + } + + // Both LoadSettings and ReloadSettings are supposed to call this function, + // but LoadSettings skips it, so that the UI starts up faster. + // Now that the UI is present we can do them with a less significant UX impact. + _ApplyStartupTaskStateChange(); + _ProcessLazySettingsChanges(); + + FILETIME creationTime, exitTime, kernelTime, userTime, now; + if (GetThreadTimes(GetCurrentThread(), &creationTime, &exitTime, &kernelTime, &userTime)) + { + static constexpr auto asInteger = [](const FILETIME& f) { + ULARGE_INTEGER i; + i.LowPart = f.dwLowDateTime; + i.HighPart = f.dwHighDateTime; + return i.QuadPart; + }; + static constexpr auto asSeconds = [](uint64_t v) { + return v * 1e-7f; + }; + + GetSystemTimeAsFileTime(&now); + + const auto latency = asSeconds(asInteger(now) - asInteger(creationTime)); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "AppInitialized", + TraceLoggingDescription("Event emitted once the app is initialized"), + TraceLoggingFloat32(latency, "latency"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } } // Method Description: @@ -1103,255 +508,6 @@ namespace winrt::TerminalApp::implementation return _settings; } - UIElement AppLogic::GetRoot() noexcept - { - return _root.as(); - } - - // Method Description: - // - Gets the title of the currently focused terminal control. If there - // isn't a control selected for any reason, returns "Terminal" - // Arguments: - // - - // Return Value: - // - the title of the focused control if there is one, else "Terminal" - hstring AppLogic::Title() - { - if (_root) - { - return _root->Title(); - } - return { L"Terminal" }; - } - - // Method Description: - // - Used to tell the app that the titlebar has been clicked. The App won't - // actually receive any clicks in the titlebar area, so this is a helper - // to clue the app in that a click has happened. The App will use this as - // a indicator that it needs to dismiss any open flyouts. - // Arguments: - // - - // Return Value: - // - - void AppLogic::TitlebarClicked() - { - if (_root) - { - _root->TitlebarClicked(); - } - } - - // Method Description: - // - Used to tell the PTY connection that the window visibility has changed. - // The underlying PTY might need to expose window visibility status to the - // client application for the `::GetConsoleWindow()` API. - // Arguments: - // - showOrHide - True is show; false is hide. - // Return Value: - // - - void AppLogic::WindowVisibilityChanged(const bool showOrHide) - { - if (_root) - { - _root->WindowVisibilityChanged(showOrHide); - } - } - - // Method Description: - // - Implements the F7 handler (per GH#638) - // - Implements the Alt handler (per GH#6421) - // Return value: - // - whether the key was handled - bool AppLogic::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) - { - if (_root) - { - // Manually bubble the OnDirectKeyEvent event up through the focus tree. - auto xamlRoot{ _root->XamlRoot() }; - auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) }; - do - { - if (auto keyListener{ focusedObject.try_as() }) - { - if (keyListener.OnDirectKeyEvent(vkey, scanCode, down)) - { - return true; - } - // otherwise, keep walking. bubble the event manually. - } - - if (auto focusedElement{ focusedObject.try_as() }) - { - focusedObject = focusedElement.Parent(); - - // Parent() seems to return null when the focusedElement is created from an ItemTemplate. - // Use the VisualTreeHelper's GetParent as a fallback. - if (!focusedObject) - { - focusedObject = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElement); - } - } - else - { - break; // we hit a non-FE object, stop bubbling. - } - } while (focusedObject); - } - return false; - } - - // Method Description: - // - Used to tell the app that the 'X' button has been clicked and - // the user wants to close the app. We kick off the close warning - // experience. - // Arguments: - // - - // Return Value: - // - - void AppLogic::CloseWindow(LaunchPosition pos) - { - if (_root) - { - // If persisted layout is enabled and we are the last window closing - // we should save our state. - if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) - { - if (const auto layout = _root->GetWindowLayout()) - { - layout.InitialPosition(pos); - const auto state = ApplicationState::SharedInstance(); - state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); - } - } - - _root->CloseWindow(false); - } - } - - winrt::TerminalApp::TaskbarState AppLogic::TaskbarState() - { - if (_root) - { - return _root->TaskbarState(); - } - return {}; - } - - winrt::Windows::UI::Xaml::Media::Brush AppLogic::TitlebarBrush() - { - if (_root) - { - return _root->TitlebarBrush(); - } - return { nullptr }; - } - void AppLogic::WindowActivated(const bool activated) - { - _root->WindowActivated(activated); - } - - bool AppLogic::HasCommandlineArguments() const noexcept - { - return _hasCommandLineArguments; - } - - bool AppLogic::HasSettingsStartupActions() const noexcept - { - return _hasSettingsStartupActions; - } - - // Method Description: - // - Sets the initial commandline to process on startup, and attempts to - // parse it. Commands will be parsed into a list of ShortcutActions that - // will be processed on TerminalPage::Create(). - // - This function will have no effective result after Create() is called. - // - This function returns 0, unless a there was a non-zero result from - // trying to parse one of the commands provided. In that case, no commands - // after the failing command will be parsed, and the non-zero code - // returned. - // Arguments: - // - args: an array of strings to process as a commandline. These args can contain spaces - // Return Value: - // - the result of the first command who's parsing returned a non-zero code, - // or 0. (see AppLogic::_ParseArgs) - int32_t AppLogic::SetStartupCommandline(array_view args) - { - const auto result = _appArgs.ParseArgs(args); - if (result == 0) - { - // If the size of the arguments list is 1, - // then it contains only the executable name and no other arguments. - _hasCommandLineArguments = args.size() > 1; - _appArgs.ValidateStartupCommands(); - if (const auto idx = _appArgs.GetPersistedLayoutIdx()) - { - _root->SetPersistedLayoutIdx(idx.value()); - } - _root->SetStartupActions(_appArgs.GetStartupActions()); - - // Check if we were started as a COM server for inbound connections of console sessions - // coming out of the operating system default application feature. If so, - // tell TerminalPage to start the listener as we have to make sure it has the chance - // to register a handler to hear about the requests first and is all ready to receive - // them before the COM server registers itself. Otherwise, the request might come - // in and be routed to an event with no handlers or a non-ready Page. - if (_appArgs.IsHandoffListener()) - { - _root->SetInboundListener(true); - } - } - - return result; - } - - // Method Description: - // - Triggers the setup of the listener for incoming console connections - // from the operating system. - // Arguments: - // - - // Return Value: - // - - void AppLogic::SetInboundListener() - { - _root->SetInboundListener(false); - } - - // Method Description: - // - Parse the provided commandline arguments into actions, and try to - // perform them immediately. - // - This function returns 0, unless a there was a non-zero result from - // trying to parse one of the commands provided. In that case, no commands - // after the failing command will be parsed, and the non-zero code - // returned. - // - If a non-empty cwd is provided, the entire terminal exe will switch to - // that CWD while we handle these actions, then return to the original - // CWD. - // Arguments: - // - args: an array of strings to process as a commandline. These args can contain spaces - // - cwd: The directory to use as the CWD while performing these actions. - // Return Value: - // - the result of the first command who's parsing returned a non-zero code, - // or 0. (see AppLogic::_ParseArgs) - int32_t AppLogic::ExecuteCommandline(array_view args, - const winrt::hstring& cwd) - { - ::TerminalApp::AppCommandlineArgs appArgs; - auto result = appArgs.ParseArgs(args); - if (result == 0) - { - auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); - - _root->ProcessStartupActions(actions, false, cwd); - - if (appArgs.IsHandoffListener()) - { - _root->SetInboundListener(true); - } - } - // Return the result of parsing with commandline, though it may or may not be used. - return result; - } - // Method Description: // - Parse the given commandline args in an attempt to find the specified // window. The rest of the args are ignored for now (they'll be handled @@ -1375,7 +531,7 @@ namespace winrt::TerminalApp::implementation if (!_loadedInitialSettings) { // Load settings if we haven't already - LoadSettings(); + ReloadSettings(); } return AppLogic::_doFindTargetWindow(args, _settings.GlobalSettings().WindowingBehavior()); @@ -1391,7 +547,7 @@ namespace winrt::TerminalApp::implementation { if (!appArgs.GetExitMessage().empty()) { - return winrt::make(WindowingBehaviorUseNew); + return winrt::make(WindowingBehaviorUseNone); } const std::string parsedTarget{ appArgs.GetTargetWindow() }; @@ -1441,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); } @@ -1459,83 +615,14 @@ namespace winrt::TerminalApp::implementation } } - // Any unsuccessful parse will be a new window. That new window will try - // to handle the commandline itself, and find that the commandline - // failed to parse. When that happens, the new window will display the - // message box. + // Any unsuccessful parse will result in _no_ window. We will indicate + // to the caller that they shouldn't make a window. They can still find + // the commandline failed to parse and choose to display the message + // box. // // This will also work for the case where the user specifies an invalid - // commandline in conjunction with `-w 0`. This function will determine - // that the commandline has a parse error, and indicate that we should - // create a new window. Then, in that new window, we'll try to set the - // StartupActions, which will again fail, returning the correct error - // message. - return winrt::make(WindowingBehaviorUseNew); - } - - // Method Description: - // - If there were any errors parsing the commandline that was used to - // initialize the terminal, this will return a string containing that - // message. If there were no errors, this message will be blank. - // - If the user requested help on any command (using --help), this will - // contain the help message. - // - If the user requested the version number (using --version), this will - // contain the version string. - // Arguments: - // - - // Return Value: - // - the help text or error message for the provided commandline, if one - // exists, otherwise the empty string. - winrt::hstring AppLogic::ParseCommandlineMessage() - { - return winrt::to_hstring(_appArgs.GetExitMessage()); - } - - // Method Description: - // - Returns true if we should exit the application before even starting the - // window. We might want to do this if we're displaying an error message or - // the version string, or if we want to open the settings file. - // Arguments: - // - - // Return Value: - // - true iff we should exit the application before even starting the window - bool AppLogic::ShouldExitEarly() - { - return _appArgs.ShouldExitEarly(); - } - - bool AppLogic::FocusMode() const - { - return _root ? _root->FocusMode() : false; - } - - bool AppLogic::Fullscreen() const - { - return _root ? _root->Fullscreen() : false; - } - - void AppLogic::Maximized(bool newMaximized) - { - if (_root) - { - _root->Maximized(newMaximized); - } - } - - bool AppLogic::AlwaysOnTop() const - { - return _root ? _root->AlwaysOnTop() : false; - } - - bool AppLogic::AutoHideWindow() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().AutoHideWindow(); + // commandline in conjunction with `-w 0`. + return winrt::make(WindowingBehaviorUseNone); } Windows::Foundation::Collections::IMapView AppLogic::GlobalHotkeys() @@ -1543,27 +630,76 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } - bool AppLogic::ShouldUsePersistedLayout() + Microsoft::Terminal::Settings::Model::Theme AppLogic::Theme() { - return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false; + return _settings.GlobalSettings().CurrentTheme(); } - bool AppLogic::ShouldImmediatelyHandoffToElevated() + bool AppLogic::IsolatedMode() + { + if (!_loadedInitialSettings) + { + ReloadSettings(); + } + return _settings.GlobalSettings().IsolatedMode(); + } + bool AppLogic::RequestsTrayIcon() { if (!_loadedInitialSettings) { // Load settings if we haven't already - LoadSettings(); + ReloadSettings(); + } + const auto& globals{ _settings.GlobalSettings() }; + return globals.AlwaysShowNotificationIcon() || + globals.MinimizeToNotificationArea(); + } + + bool AppLogic::AllowHeadless() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + ReloadSettings(); + } + return _settings.GlobalSettings().AllowHeadless(); + } + + TerminalApp::TerminalWindow AppLogic::CreateNewWindow() + { + if (_settings == nullptr) + { + ReloadSettings(); } - return _root != nullptr ? _root->ShouldImmediatelyHandoffToElevated(_settings) : false; - } - void AppLogic::HandoffToElevated() - { - if (_root) + auto warnings{ winrt::multi_threaded_vector() }; + for (auto&& warn : _warnings) { - _root->HandoffToElevated(_settings); + warnings.Append(warn); } + auto ev = winrt::make_self(false, + _settingsLoadedResult, + _settingsLoadExceptionText, + warnings, + _settings); + + auto window = winrt::make_self(*ev, _contentManager); + + if (_hasSettingsStartupActions) + { + window->SetSettingsStartupArgs(_settingsAppArgs.GetStartupActions()); + } + return *window; + } + + winrt::TerminalApp::ContentManager AppLogic::ContentManager() + { + return _contentManager; + } + + bool AppLogic::ShouldUsePersistedLayout() const + { + return _settings.GlobalSettings().ShouldUsePersistedLayout(); } void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts) @@ -1582,120 +718,11 @@ namespace winrt::TerminalApp::implementation ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); } - hstring AppLogic::GetWindowLayoutJson(LaunchPosition position) + TerminalApp::ParseCommandlineResult AppLogic::GetParseCommandlineMessage(array_view args) { - if (_root != nullptr) - { - if (const auto layout = _root->GetWindowLayout()) - { - layout.InitialPosition(position); - return WindowLayout::ToJson(layout); - } - } - return L""; - } - - void AppLogic::IdentifyWindow() - { - if (_root) - { - _root->IdentifyWindow(); - } - } - - winrt::hstring AppLogic::WindowName() - { - return _root ? _root->WindowName() : L""; - } - void AppLogic::WindowName(const winrt::hstring& name) - { - if (_root) - { - _root->WindowName(name); - } - } - uint64_t AppLogic::WindowId() - { - return _root ? _root->WindowId() : 0; - } - void AppLogic::WindowId(const uint64_t& id) - { - if (_root) - { - _root->WindowId(id); - } - } - - void AppLogic::SetPersistedLayoutIdx(const uint32_t idx) - { - if (_root) - { - _root->SetPersistedLayoutIdx(idx); - } - } - - void AppLogic::SetNumberOfOpenWindows(const uint64_t num) - { - _numOpenWindows = num; - if (_root) - { - _root->SetNumberOfOpenWindows(num); - } - } - - void AppLogic::RenameFailed() - { - if (_root) - { - _root->RenameFailed(); - } - } - - bool AppLogic::IsQuakeWindow() const noexcept - { - return _root->IsQuakeWindow(); - } - - void AppLogic::RequestExitFullscreen() - { - _root->SetFullscreen(false); - } - - bool AppLogic::GetMinimizeToNotificationArea() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().MinimizeToNotificationArea(); - } - - bool AppLogic::GetAlwaysShowNotificationIcon() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().AlwaysShowNotificationIcon(); - } - - bool AppLogic::GetShowTitleInTitlebar() - { - return _settings.GlobalSettings().ShowTitleInTitlebar(); - } - - Microsoft::Terminal::Settings::Model::Theme AppLogic::Theme() - { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - return _settings.GlobalSettings().CurrentTheme(); + ::TerminalApp::AppCommandlineArgs _appArgs; + const auto r = _appArgs.ParseArgs(args); + return TerminalApp::ParseCommandlineResult{ winrt::to_hstring(_appArgs.GetExitMessage()), r }; } } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index f4abe8254f..695632da75 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -5,10 +5,12 @@ #include "AppLogic.g.h" #include "FindTargetWindowResult.g.h" -#include "SystemMenuChangeArgs.g.h" + #include "Jumplist.h" #include "LanguageProfileNotifier.h" -#include "TerminalPage.h" +#include "AppCommandlineArgs.h" +#include "TerminalWindow.h" +#include "ContentManager.h" #include #include @@ -36,192 +38,80 @@ namespace winrt::TerminalApp::implementation FindTargetWindowResult(id, L""){}; }; - struct SystemMenuChangeArgs : SystemMenuChangeArgsT - { - WINRT_PROPERTY(winrt::hstring, Name, L""); - WINRT_PROPERTY(SystemMenuChangeAction, Action, SystemMenuChangeAction::Add); - WINRT_PROPERTY(SystemMenuItemHandler, Handler, nullptr); - - public: - SystemMenuChangeArgs(const winrt::hstring& name, SystemMenuChangeAction action, SystemMenuItemHandler handler = nullptr) : - _Name{ name }, _Action{ action }, _Handler{ handler } {}; - }; - - struct AppLogic : AppLogicT + struct AppLogic : AppLogicT { public: static AppLogic* Current() noexcept; static const Microsoft::Terminal::Settings::Model::CascadiaSettings CurrentAppSettings(); AppLogic(); - ~AppLogic() = default; - - STDMETHODIMP Initialize(HWND hwnd); void Create(); - bool IsUwp() const noexcept; - void RunAsUwp(); - bool IsElevated() const noexcept; - void LoadSettings(); + bool IsRunningElevated() const noexcept; + bool CanDragDrop() const noexcept; + void ReloadSettings(); + void NotifyRootInitialized(); + + bool HasSettingsStartupActions() const noexcept; + + bool ShouldUsePersistedLayout() const; + void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); + [[nodiscard]] Microsoft::Terminal::Settings::Model::CascadiaSettings GetSettings() const noexcept; - void Quit(); - - bool HasCommandlineArguments() const noexcept; - bool HasSettingsStartupActions() const noexcept; - int32_t SetStartupCommandline(array_view actions); - int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); TerminalApp::FindTargetWindowResult FindTargetWindow(array_view actions); - winrt::hstring ParseCommandlineMessage(); - bool ShouldExitEarly(); - - bool FocusMode() const; - bool Fullscreen() const; - void Maximized(bool newMaximized); - bool AlwaysOnTop() const; - bool AutoHideWindow(); - - bool ShouldUsePersistedLayout(); - bool ShouldImmediatelyHandoffToElevated(); - void HandoffToElevated(); - hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); - void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); - void IdentifyWindow(); - void RenameFailed(); - winrt::hstring WindowName(); - void WindowName(const winrt::hstring& name); - uint64_t WindowId(); - void WindowId(const uint64_t& id); - void SetPersistedLayoutIdx(const uint32_t idx); - void SetNumberOfOpenWindows(const uint64_t num); - bool IsQuakeWindow() const noexcept; - void RequestExitFullscreen(); - - Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi); - bool CenterOnLaunch(); - TerminalApp::InitialPosition GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY); - winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); - Microsoft::Terminal::Settings::Model::LaunchMode GetLaunchMode(); - bool GetShowTabsInTitlebar(); - bool GetInitialAlwaysOnTop(); - float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; - - Windows::UI::Xaml::UIElement GetRoot() noexcept; - - void SetInboundListener(); - - hstring Title(); - void TitlebarClicked(); - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - - void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position); - void WindowVisibilityChanged(const bool showOrHide); - - winrt::TerminalApp::TaskbarState TaskbarState(); - winrt::Windows::UI::Xaml::Media::Brush TitlebarBrush(); - void WindowActivated(const bool activated); - - bool GetMinimizeToNotificationArea(); - bool GetAlwaysShowNotificationIcon(); - bool GetShowTitleInTitlebar(); - - winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); - void DismissDialog(); Windows::Foundation::Collections::IMapView GlobalHotkeys(); Microsoft::Terminal::Settings::Model::Theme Theme(); + bool IsolatedMode(); + bool AllowHeadless(); + bool RequestsTrayIcon(); - // -------------------------------- WinRT Events --------------------------------- - // PropertyChanged is surprisingly not a typed event, so we'll define that one manually. - // Usually we'd just do - // WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); - // - // But what we're doing here is exposing the Page's PropertyChanged _as - // our own event_. It's a FORWARDED_CALLBACK, essentially. - winrt::event_token PropertyChanged(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& handler) { return _root->PropertyChanged(handler); } - void PropertyChanged(winrt::event_token const& token) { _root->PropertyChanged(token); } + TerminalApp::TerminalWindow CreateNewWindow(); - TYPED_EVENT(RequestedThemeChanged, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Settings::Model::Theme); - TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(SystemMenuChangeRequested, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SystemMenuChangeArgs); + winrt::TerminalApp::ContentManager ContentManager(); + + TerminalApp::ParseCommandlineResult GetParseCommandlineMessage(array_view args); + + TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SettingsLoadEventArgs); private: - bool _isUwp{ false }; bool _isElevated{ false }; + bool _canDragDrop{ false }; + std::atomic _notifyRootInitializedCalled{ false }; - // If you add controls here, but forget to null them either here or in - // the ctor, you're going to have a bad time. It'll mysteriously fail to - // activate the AppLogic. - // ALSO: If you add any UIElements as roots here, make sure they're - // updated in _ApplyTheme. The root currently is _root. - winrt::com_ptr _root{ nullptr }; Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; winrt::hstring _settingsLoadExceptionText; HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; - uint64_t _numOpenWindows{ 0 }; - - std::shared_mutex _dialogLock; - winrt::Windows::UI::Xaml::Controls::ContentDialog _dialog; - - ::TerminalApp::AppCommandlineArgs _appArgs; + bool _hasSettingsStartupActions{ false }; ::TerminalApp::AppCommandlineArgs _settingsAppArgs; std::shared_ptr> _reloadSettings; til::throttled_func_trailing<> _reloadState; + std::vector _warnings{}; + // These fields invoke _reloadSettings and must be destroyed before _reloadSettings. // (C++ destroys members in reverse-declaration-order.) winrt::com_ptr _languageProfileNotifier; wil::unique_folder_change_reader_nothrow _reader; + TerminalApp::ContentManager _contentManager{ winrt::make() }; + static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view args, const Microsoft::Terminal::Settings::Model::WindowingMode& windowingBehavior); - void _ShowLoadErrorsDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey, HRESULT settingsLoadedResult); - void _ShowLoadWarningsDialog(); - bool _IsKeyboardServiceEnabled(); - void _ApplyLanguageSettingChange() noexcept; - void _RefreshThemeRoutine(); fire_and_forget _ApplyStartupTaskStateChange(); - void _OnLoaded(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - [[nodiscard]] HRESULT _TryLoadSettings() noexcept; void _ProcessLazySettingsChanges(); void _RegisterSettingsChange(); fire_and_forget _DispatchReloadSettings(); - void _ReloadSettings(); - void _OpenSettingsUI(); - - bool _hasCommandLineArguments{ false }; - bool _hasSettingsStartupActions{ false }; - std::vector _warnings; - - // These are events that are handled by the TerminalPage, but are - // exposed through the AppLogic. This macro is used to forward the event - // directly to them. - FORWARDED_TYPED_EVENT(SetTitleBarContent, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::UIElement, _root, SetTitleBarContent); - FORWARDED_TYPED_EVENT(TitleChanged, winrt::Windows::Foundation::IInspectable, winrt::hstring, _root, TitleChanged); - FORWARDED_TYPED_EVENT(LastTabClosed, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::LastTabClosedEventArgs, _root, LastTabClosed); - FORWARDED_TYPED_EVENT(FocusModeChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, FocusModeChanged); - FORWARDED_TYPED_EVENT(FullscreenChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, FullscreenChanged); - FORWARDED_TYPED_EVENT(ChangeMaximizeRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, ChangeMaximizeRequested); - FORWARDED_TYPED_EVENT(AlwaysOnTopChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, AlwaysOnTopChanged); - FORWARDED_TYPED_EVENT(RaiseVisualBell, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, RaiseVisualBell); - FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress); - FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested); - FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); - FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged); - FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); - FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested); - FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); - FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); - FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged); #ifdef UNIT_TESTING friend class TerminalAppLocalTests::CommandlineTest; diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 0f77af0c8c..1021e61b99 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -1,41 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. - -import "TerminalPage.idl"; -import "ShortcutActionDispatch.idl"; -import "IDirectKeyListener.idl"; +import "TerminalWindow.idl"; namespace TerminalApp { - struct InitialPosition - { - Int64 X; - Int64 Y; - }; - [default_interface] runtimeclass FindTargetWindowResult { Int32 WindowId { get; }; String WindowName { get; }; }; - delegate void SystemMenuItemHandler(); - - enum SystemMenuChangeAction + struct ParseCommandlineResult { - Add = 0, - Remove = 1 - }; - - [default_interface] runtimeclass SystemMenuChangeArgs { - String Name { get; }; - SystemMenuChangeAction Action { get; }; - SystemMenuItemHandler Handler { get; }; + String Message; + Int32 ExitCode; }; // See IDialogPresenter and TerminalPage's DialogPresenter for more // information. - [default_interface] runtimeclass AppLogic : IDirectKeyListener, IDialogPresenter, Windows.UI.Xaml.Data.INotifyPropertyChanged + [default_interface] runtimeclass AppLogic { AppLogic(); @@ -46,98 +29,33 @@ namespace TerminalApp // registered?" when it definitely is. void Create(); - Boolean IsUwp(); - void RunAsUwp(); - Boolean IsElevated(); + Boolean IsRunningElevated(); + Boolean CanDragDrop(); + + ContentManager ContentManager { get; }; - Boolean HasCommandlineArguments(); Boolean HasSettingsStartupActions(); - Int32 SetStartupCommandline(String[] commands); - Int32 ExecuteCommandline(String[] commands, String cwd); - String ParseCommandlineMessage { get; }; - Boolean ShouldExitEarly { get; }; - - void Quit(); - - void LoadSettings(); - Windows.UI.Xaml.UIElement GetRoot(); - - void SetInboundListener(); - - String Title { get; }; - - Boolean FocusMode { get; }; - Boolean Fullscreen { get; }; - void Maximized(Boolean newMaximized); - Boolean AlwaysOnTop { get; }; - Boolean AutoHideWindow { get; }; - - void IdentifyWindow(); - String WindowName; - UInt64 WindowId; - void SetPersistedLayoutIdx(UInt32 idx); - void SetNumberOfOpenWindows(UInt64 num); - void RenameFailed(); - void RequestExitFullscreen(); - Boolean IsQuakeWindow(); - - Windows.Foundation.Size GetLaunchDimensions(UInt32 dpi); - Boolean CenterOnLaunch { get; }; - - InitialPosition GetInitialPosition(Int64 defaultInitialX, Int64 defaultInitialY); - Windows.UI.Xaml.ElementTheme GetRequestedTheme(); - Microsoft.Terminal.Settings.Model.LaunchMode GetLaunchMode(); - Boolean GetShowTabsInTitlebar(); - Boolean GetInitialAlwaysOnTop(); - Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); - void TitlebarClicked(); - void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position); - void WindowVisibilityChanged(Boolean showOrHide); - - TaskbarState TaskbarState{ get; }; - Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; - void WindowActivated(Boolean activated); Boolean ShouldUsePersistedLayout(); - Boolean ShouldImmediatelyHandoffToElevated(); - void HandoffToElevated(); - String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector layouts); - Boolean GetMinimizeToNotificationArea(); - Boolean GetAlwaysShowNotificationIcon(); - Boolean GetShowTitleInTitlebar(); + void ReloadSettings(); + // Selected settings to expose Microsoft.Terminal.Settings.Model.Theme Theme { get; }; + Boolean IsolatedMode { get; }; + Boolean AllowHeadless { get; }; + Boolean RequestsTrayIcon { get; }; FindTargetWindowResult FindTargetWindow(String[] args); - Windows.Foundation.Collections.IMapView GlobalHotkeys(); + TerminalWindow CreateNewWindow(); - // See IDialogPresenter and TerminalPage's DialogPresenter for more - // information. - Windows.Foundation.IAsyncOperation ShowDialog(Windows.UI.Xaml.Controls.ContentDialog dialog); - void DismissDialog(); + ParseCommandlineResult GetParseCommandlineMessage(String[] args); + + IMapView GlobalHotkeys(); + + event Windows.Foundation.TypedEventHandler SettingsChanged; - event Windows.Foundation.TypedEventHandler SetTitleBarContent; - event Windows.Foundation.TypedEventHandler TitleChanged; - event Windows.Foundation.TypedEventHandler LastTabClosed; - event Windows.Foundation.TypedEventHandler RequestedThemeChanged; - event Windows.Foundation.TypedEventHandler FocusModeChanged; - event Windows.Foundation.TypedEventHandler FullscreenChanged; - event Windows.Foundation.TypedEventHandler ChangeMaximizeRequested; - event Windows.Foundation.TypedEventHandler AlwaysOnTopChanged; - event Windows.Foundation.TypedEventHandler RaiseVisualBell; - event Windows.Foundation.TypedEventHandler SetTaskbarProgress; - event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; - event Windows.Foundation.TypedEventHandler RenameWindowRequested; - event Windows.Foundation.TypedEventHandler SettingsChanged; - event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; - event Windows.Foundation.TypedEventHandler SummonWindowRequested; - event Windows.Foundation.TypedEventHandler CloseRequested; - event Windows.Foundation.TypedEventHandler OpenSystemMenu; - event Windows.Foundation.TypedEventHandler QuitRequested; - event Windows.Foundation.TypedEventHandler SystemMenuChangeRequested; - event Windows.Foundation.TypedEventHandler ShowWindowChanged; } } diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp index e582ca7fe6..f82d86feec 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp @@ -1,9 +1,6 @@ #include "pch.h" #include "ColorPickupFlyout.h" #include "ColorPickupFlyout.g.cpp" -#include "winrt/Windows.UI.Xaml.Media.h" -#include "winrt/Windows.UI.Xaml.Shapes.h" -#include "winrt/Windows.UI.Xaml.Interop.h" #include namespace winrt::TerminalApp::implementation diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml index bf7896cff2..215272472d 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml @@ -107,8 +107,7 @@ x:Uid="TabColorClearButton" Grid.Column="0" HorizontalAlignment="Stretch" - Click="ClearColorButton_Click" - Content="Reset" /> + Click="ClearColorButton_Click" /> + // fwdecl unittest classes namespace TerminalAppLocalTests { @@ -60,6 +62,14 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(PreviewAction, Windows::Foundation::IInspectable, Microsoft::Terminal::Settings::Model::Command); private: + struct winrt_object_hash + { + size_t operator()(const auto& value) const noexcept + { + return til::hash(winrt::get_abi(value)); + } + }; + friend struct CommandPaletteT; // for Xaml to bind events Windows::Foundation::Collections::IVector _allCommands{ nullptr }; @@ -141,7 +151,7 @@ namespace winrt::TerminalApp::implementation void _choosingItemContainer(const Windows::UI::Xaml::Controls::ListViewBase& sender, const Windows::UI::Xaml::Controls::ChoosingItemContainerEventArgs& args); void _containerContentChanging(const Windows::UI::Xaml::Controls::ListViewBase& sender, const Windows::UI::Xaml::Controls::ContainerContentChangingEventArgs& args); winrt::TerminalApp::PaletteItemTemplateSelector _itemTemplateSelector{ nullptr }; - std::unordered_map> _listViewItemsCache; + std::unordered_map, winrt_object_hash> _listViewItemsCache; Windows::UI::Xaml::DataTemplate _listItemTemplate; friend class TerminalAppLocalTests::TabTests; diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index 368de8e825..c8100e3568 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -75,6 +75,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" AutomationProperties.AccessibilityView="Raw" + Background="{ThemeResource FlyoutPresenterBackground}" Style="{ThemeResource KeyChordBorderStyle}" Visibility="{x:Bind Item.KeyChordText, Mode=OneWay, Converter={StaticResource CommandKeyChordVisibilityConverter}}"> @@ -204,6 +205,13 @@ Glyph="" Visibility="{x:Bind Item.(local:TabPaletteItem.TabStatus).IsReadOnlyActive, Mode=OneWay}" /> + + diff --git a/src/cascadia/TerminalApp/ContentManager.cpp b/src/cascadia/TerminalApp/ContentManager.cpp new file mode 100644 index 0000000000..edddc0b8ec --- /dev/null +++ b/src/cascadia/TerminalApp/ContentManager.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ContentManager.h" +#include "ContentManager.g.cpp" + +#include + +#include "../../types/inc/utils.hpp" + +using namespace winrt::Windows::ApplicationModel; +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace winrt::TerminalApp::implementation +{ + ControlInteractivity ContentManager::CreateCore(const Microsoft::Terminal::Control::IControlSettings& settings, + const IControlAppearance& unfocusedAppearance, + const TerminalConnection::ITerminalConnection& connection) + { + ControlInteractivity content{ settings, unfocusedAppearance, connection }; + content.Closed({ get_weak(), &ContentManager::_closedHandler }); + + _content.emplace(content.Id(), content); + + return content; + } + + ControlInteractivity ContentManager::TryLookupCore(uint64_t id) + { + const auto it = _content.find(id); + return it != _content.end() ? it->second : ControlInteractivity{ nullptr }; + } + + void ContentManager::Detach(const Microsoft::Terminal::Control::TermControl& control) + { + const auto contentId{ control.ContentId() }; + if (const auto& content{ TryLookupCore(contentId) }) + { + control.Detach(); + } + } + + void ContentManager::_closedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable&) + { + if (const auto& content{ sender.try_as() }) + { + const auto& contentId{ content.Id() }; + _content.erase(contentId); + } + } +} diff --git a/src/cascadia/TerminalApp/ContentManager.h b/src/cascadia/TerminalApp/ContentManager.h new file mode 100644 index 0000000000..38c0cbb7d3 --- /dev/null +++ b/src/cascadia/TerminalApp/ContentManager.h @@ -0,0 +1,49 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- ContentManager.h + +Abstract: +- This is a helper class for tracking all of the terminal "content" instances of + the Terminal. These are all the ControlInteractivity & ControlCore's of each + of our TermControls. These are each assigned a GUID on creation, and stored in + a map for later lookup. +- This is used to enable moving panes between windows. TermControl's are not + thread-agile, so they cannot be reused on other threads. However, the content + is. This helper, which exists as a singleton across all the threads in the + Terminal app, allows each thread to create content, assign it to a + TermControl, detach it from that control, and reattach to new controls on + other threads. +- When you want to create a new TermControl, call CreateCore to instantiate a + new content with a GUID for later reparenting. +- Detach can be used to temporarily remove a content from its hosted + TermControl. After detaching, you can still use LookupCore & + TermControl::AttachContent to re-attach to the content. +--*/ +#pragma once + +#include "ContentManager.g.h" + +#include +namespace winrt::TerminalApp::implementation +{ + struct ContentManager : ContentManagerT + { + public: + ContentManager() = default; + Microsoft::Terminal::Control::ControlInteractivity CreateCore(const Microsoft::Terminal::Control::IControlSettings& settings, + const Microsoft::Terminal::Control::IControlAppearance& unfocusedAppearance, + const Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); + Microsoft::Terminal::Control::ControlInteractivity TryLookupCore(uint64_t id); + + void Detach(const Microsoft::Terminal::Control::TermControl& control); + + private: + std::unordered_map _content; + + void _closedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& e); + }; +} diff --git a/src/cascadia/TerminalApp/Jumplist.cpp b/src/cascadia/TerminalApp/Jumplist.cpp index 4665a35b80..050fdbb429 100644 --- a/src/cascadia/TerminalApp/Jumplist.cpp +++ b/src/cascadia/TerminalApp/Jumplist.cpp @@ -149,19 +149,40 @@ winrt::com_ptr Jumplist::_createShellLink(const std::wstring_view n const auto module{ GetWtExePath() }; THROW_IF_FAILED(sh->SetPath(module.data())); THROW_IF_FAILED(sh->SetArguments(args.data())); + auto propStore{ sh.as() }; PROPVARIANT titleProp; titleProp.vt = VT_LPWSTR; titleProp.pwszVal = const_cast(name.data()); - PROPVARIANT iconProp; - iconProp.vt = VT_LPWSTR; - iconProp.pwszVal = const_cast(path.data()); + // Check for a comma in the path. If we find one we have an indirect icon. Parse the path into a file path and index/id. + auto commaPosition = path.find(L","); + if (commaPosition != std::wstring_view::npos) + { + const std::wstring iconPath{ path.substr(0, commaPosition) }; + + // We dont want the comma included so add 1 to its position + int iconIndex = til::to_int(path.substr(commaPosition + 1)); + if (iconIndex != til::to_int_error) + { + THROW_IF_FAILED(sh->SetIconLocation(iconPath.data(), iconIndex)); + } + } + else if (til::ends_with(path, L"exe") || til::ends_with(path, L"dll")) + { + // We have a binary path but no index/id. Default to 0 + THROW_IF_FAILED(sh->SetIconLocation(path.data(), 0)); + } + else + { + PROPVARIANT iconProp; + iconProp.vt = VT_LPWSTR; + iconProp.pwszVal = const_cast(path.data()); + + THROW_IF_FAILED(propStore->SetValue(PKEY_AppUserModel_DestListLogoUri, iconProp)); + } - auto propStore{ sh.as() }; THROW_IF_FAILED(propStore->SetValue(PKEY_Title, titleProp)); - THROW_IF_FAILED(propStore->SetValue(PKEY_AppUserModel_DestListLogoUri, iconProp)); - THROW_IF_FAILED(propStore->Commit()); return sh; diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml index c229580a2b..4bc516bc31 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml @@ -145,8 +145,8 @@ details. --> 40.0 - - 33.0 + + 32.0 diff --git a/src/cascadia/TerminalSettingsEditor/EditColorScheme.cpp b/src/cascadia/TerminalSettingsEditor/EditColorScheme.cpp index ab7333e0c5..4a2bdba6b0 100644 --- a/src/cascadia/TerminalSettingsEditor/EditColorScheme.cpp +++ b/src/cascadia/TerminalSettingsEditor/EditColorScheme.cpp @@ -34,6 +34,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation ToolTipService::SetToolTip(NameBox(), box_value(RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"))); Automation::AutomationProperties::SetName(RenameAcceptButton(), RS_(L"RenameAccept/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); Automation::AutomationProperties::SetName(RenameCancelButton(), RS_(L"RenameCancel/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); + Automation::AutomationProperties::SetName(SetAsDefaultButton(), RS_(L"ColorScheme_SetAsDefault/Header")); + Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"ColorScheme_DeleteButton/Text")); } void EditColorScheme::OnNavigatedTo(const NavigationEventArgs& e) @@ -45,7 +47,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (_ViewModel.IsInBoxScheme()) { // load disclaimer for in-box profiles - disclaimer = RS_(L"ColorScheme_RenameDisclaimerInBox"); + disclaimer = RS_(L"ColorScheme_DeleteButtonDisclaimerInBox/Text"); } RenameContainer().HelpText(disclaimer); diff --git a/src/cascadia/TerminalSettingsEditor/EditColorScheme.xaml b/src/cascadia/TerminalSettingsEditor/EditColorScheme.xaml index 2292d63998..50a0c1357b 100644 --- a/src/cascadia/TerminalSettingsEditor/EditColorScheme.xaml +++ b/src/cascadia/TerminalSettingsEditor/EditColorScheme.xaml @@ -415,6 +415,14 @@ + + + + diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp index f0509f1f87..27c5abbec8 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp @@ -43,6 +43,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation constexpr std::wstring_view systemThemeName{ L"system" }; constexpr std::wstring_view darkThemeName{ L"dark" }; constexpr std::wstring_view lightThemeName{ L"light" }; + constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" }; + constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" }; + constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" }; GlobalAppearanceViewModel::GlobalAppearanceViewModel(Model::GlobalAppSettings globalSettings) : _GlobalSettings{ globalSettings }, @@ -116,14 +119,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // is always "Use system language" ("und"). tags.emplace_back(systemLanguageTag); - // Add our hardcoded languages after the system definition. + // Add our hard-coded languages after the system definition. for (const auto& v : appLanguageTags) { tags.push_back(v); } } - // NOTE: The size of tags is always >0, due to tags[0] being hardcoded to "und". + // NOTE: The size of tags is always >0, due to tags[0] being hard-coded to "und". const auto tagsBegin = ++tags.begin(); const auto tagsEnd = tags.end(); @@ -218,7 +221,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { if (const auto& theme{ tag.try_as() }) { - _GlobalSettings.Theme(theme.Name()); + _GlobalSettings.Theme(Model::ThemePair{ theme.Name() }); } } @@ -248,6 +251,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { return RS_(L"Globals_ThemeSystem/Content"); } + else if (theme.Name() == legacyDarkThemeName) + { + return RS_(L"Globals_ThemeDarkLegacy/Content"); + } + else if (theme.Name() == legacyLightThemeName) + { + return RS_(L"Globals_ThemeLightLegacy/Content"); + } + else if (theme.Name() == legacySystemThemeName) + { + return RS_(L"Globals_ThemeSystemLegacy/Content"); + } return theme.Name(); } diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.xaml b/src/cascadia/TerminalSettingsEditor/Interaction.xaml index ed473e3120..295a584032 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.xaml +++ b/src/cascadia/TerminalSettingsEditor/Interaction.xaml @@ -88,6 +88,11 @@ + + + + diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h index 2cd31dea35..e6bcbd2b2e 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h @@ -27,6 +27,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, FocusFollowMouse); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, DetectURLs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, WordDelimiters); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ConfirmCloseAllTabs); private: Model::GlobalAppSettings _GlobalSettings; diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl index 34e4257429..ddd8750185 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl @@ -24,5 +24,6 @@ namespace Microsoft.Terminal.Settings.Editor PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, FocusFollowMouse); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, DetectURLs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(String, WordDelimiters); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ConfirmCloseAllTabs); } } diff --git a/src/cascadia/TerminalSettingsEditor/Launch.xaml b/src/cascadia/TerminalSettingsEditor/Launch.xaml index 3c4bb75b2a..71d0827e2b 100644 --- a/src/cascadia/TerminalSettingsEditor/Launch.xaml +++ b/src/cascadia/TerminalSettingsEditor/Launch.xaml @@ -30,6 +30,14 @@ + @@ -57,7 +65,7 @@ + IconSource="{x:Bind Icon, Converter={StaticResource IconSourceConverter}}" /> @@ -84,18 +92,18 @@ - - - + + + - + - - + + - + + IconSource="{x:Bind Icon, Converter={StaticResource IconSourceConverter}}" /> - - - - - + + + + + + + + + + + + - + (); + const auto xCoord = x.try_as(); return xCoord.has_value() ? gsl::narrow_cast(xCoord.value()) : NAN; } @@ -77,7 +77,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto y = _Settings.GlobalSettings().InitialPosition().Y; // If there's no value here, return NAN - XAML will ignore it and // put the placeholder text in the box instead - const auto yCoord = y.try_as(); + const auto yCoord = y.try_as(); return yCoord.has_value() ? gsl::narrow_cast(yCoord.value()) : NAN; } @@ -115,7 +115,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation InitialPosX(NAN); InitialPosY(NAN); } - _NotifyChanges(L"UseDefaultLaunchPosition", L"LaunchParametersCurrentValue"); + _NotifyChanges(L"UseDefaultLaunchPosition", L"LaunchParametersCurrentValue", L"InitialPosX", L"InitialPosY"); } bool LaunchViewModel::UseDefaultLaunchPosition() diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 19e1504b3a..2c18ce11c9 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -9,7 +9,7 @@ #include "Rendering.h" #include "RenderingViewModel.h" #include "Actions.h" -#include "Profiles.h" +#include "ProfileViewModel.h" #include "GlobalAppearance.h" #include "GlobalAppearanceViewModel.h" #include "ColorSchemes.h" @@ -17,8 +17,10 @@ #include "InteractionViewModel.h" #include "LaunchViewModel.h" #include "..\types\inc\utils.hpp" +#include <..\WinRTUtils\inc\Utils.h> #include +#include namespace winrt { @@ -55,8 +57,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _settingsClone{ settings.Copy() } { InitializeComponent(); - - _InitializeProfilesList(); + _UpdateBackgroundForMica(); _colorSchemesPageVM = winrt::make(_settingsClone); _colorSchemesPageViewModelChangedRevoker = _colorSchemesPageVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { @@ -70,6 +71,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto crumb = winrt::make(box_value(colorSchemesTag), currentScheme.Name(), BreadcrumbSubPage::ColorSchemes_Edit); _breadcrumbs.Append(crumb); } + else if (_colorSchemesPageVM.CurrentPage() == ColorSchemesSubPage::Base) + { + _Navigate(winrt::hstring{ colorSchemesTag }, BreadcrumbSubPage::None); + } } else if (settingName == L"CurrentSchemeName") { @@ -80,6 +85,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } }); + // Make sure to initialize the profiles _after_ we have initialized the color schemes page VM, because we pass + // that VM into the appearance VMs within the profiles + _InitializeProfilesList(); + Automation::AutomationProperties::SetHelpText(SaveButton(), RS_(L"Settings_SaveSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); Automation::AutomationProperties::SetHelpText(ResetButton(), RS_(L"Settings_ResetSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); Automation::AutomationProperties::SetHelpText(OpenJsonNavItem(), RS_(L"Nav_OpenJSON/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); @@ -98,6 +107,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _settingsSource = settings; _settingsClone = settings.Copy(); + _UpdateBackgroundForMica(); + // Deduce information about the currently selected item IInspectable lastBreadcrumb; const auto size = _breadcrumbs.Size(); @@ -106,46 +117,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation lastBreadcrumb = _breadcrumbs.GetAt(size - 1); } - auto menuItems{ SettingsNav().MenuItems() }; - - // We'll remove a bunch of items and iterate over it twice. - // --> Copy it into an STL vector to simplify our code and reduce COM overhead. - std::vector menuItemsSTL(menuItems.Size(), nullptr); - menuItems.GetMany(0, menuItemsSTL); - - // We want to refresh the list of profiles in the NavigationView. - // In order to add profiles we can use _InitializeProfilesList(); - // But before we can do that we have to remove existing profiles first of course. - // This "erase-remove" idiom will achieve just that. - menuItemsSTL.erase( - std::remove_if( - menuItemsSTL.begin(), - menuItemsSTL.end(), - [](const auto& item) -> bool { - if (const auto& navViewItem{ item.try_as() }) - { - if (const auto& tag{ navViewItem.Tag() }) - { - if (tag.try_as()) - { - // remove NavViewItem pointing to a Profile - return true; - } - if (const auto& stringTag{ tag.try_as() }) - { - if (stringTag == addProfileTag) - { - // remove the "Add Profile" item - return true; - } - } - } - } - return false; - }), - menuItemsSTL.end()); - - menuItems.ReplaceAll(menuItemsSTL); + // Collect only the first items out of the menu item source, the static + // ones that we don't want to regenerate. + // + // By manipulating a MenuItemsSource this way, rather than manipulating the + // MenuItems directly, we avoid a crash in WinUI. + // + // By making the vector only _originalNumItems big to start, GetMany + // will only fill that number of elements out of the current source. + std::vector menuItemsSTL(_originalNumItems, nullptr); + _menuItemSource.GetMany(0, menuItemsSTL); + // now, just stick them back in. + _menuItemSource.ReplaceAll(menuItemsSTL); // Repopulate profile-related menu items _InitializeProfilesList(); @@ -158,7 +141,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // refresh the current page using the breadcrumb data we collected before the refresh if (const auto& crumb{ lastBreadcrumb.try_as() }) { - for (const auto& item : menuItems) + for (const auto& item : _menuItemSource) { if (const auto& menuItem{ item.try_as() }) { @@ -198,7 +181,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // Couldn't find the selected item, fallback to first menu item // This happens when the selected item was a profile which doesn't exist in the new configuration // We can use menuItemsSTL here because the only things they miss are profile entries. - const auto& firstItem{ menuItemsSTL.at(0).as() }; + const auto& firstItem{ _menuItemSource.GetAt(0).as() }; SettingsNav().SelectedItem(firstItem); _Navigate(unbox_value(firstItem.Tag()), BreadcrumbSubPage::None); } @@ -206,6 +189,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void MainPage::SetHostingWindow(uint64_t hostingWindow) noexcept { _hostingHwnd.emplace(reinterpret_cast(hostingWindow)); + // Now that we have a HWND, update our own BG to account for if that + // window is using mica or not. + _UpdateBackgroundForMica(); } bool MainPage::TryPropagateHostingWindow(IInspectable object) noexcept @@ -229,8 +215,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { uint32_t insertIndex; auto selectedItem{ SettingsNav().SelectedItem() }; - auto menuItems{ SettingsNav().MenuItems() }; - menuItems.IndexOf(selectedItem, insertIndex); + if (_menuItemSource) + { + _menuItemSource.IndexOf(selectedItem, insertIndex); + } if (profileGuid != winrt::guid{}) { // if we were given a non-empty guid, we want to duplicate the corresponding profile @@ -309,7 +297,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _breadcrumbs.Clear(); } - void MainPage::_SetupProfileEventHandling(const Editor::ProfilePageNavigationState state) + void MainPage::_SetupProfileEventHandling(const Editor::ProfileViewModel profile) { // Add an event handler to navigate to Profiles_Appearance or Profiles_Advanced // Some notes on this: @@ -321,7 +309,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // We decided that it's better for the owner of the BreadcrumbBar to also be responsible // for navigation, so the navigation to Profiles_Advanced/Profiles_Appearance from // Profiles_Base got moved here. - const auto profile = state.Profile(); // If this is the base layer, the breadcrumb tag should be the globalProfileTag instead of the // ProfileViewModel, because the navigation menu item for this profile is the globalProfileTag. @@ -335,20 +322,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation const auto currentPage = profile.CurrentPage(); if (currentPage == ProfileSubPage::Base) { - contentFrame().Navigate(xaml_typename(), state); + contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); _breadcrumbs.Clear(); const auto crumb = winrt::make(breadcrumbTag, breadcrumbText, BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (currentPage == ProfileSubPage::Appearance) { - contentFrame().Navigate(xaml_typename(), state); + contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(breadcrumbTag, RS_(L"Profile_Appearance/Header"), BreadcrumbSubPage::Profile_Appearance); _breadcrumbs.Append(crumb); } else if (currentPage == ProfileSubPage::Advanced) { - contentFrame().Navigate(xaml_typename(), state); + contentFrame().Navigate(xaml_typename(), profile); const auto crumb = winrt::make(breadcrumbTag, RS_(L"Profile_Advanced/Header"), BreadcrumbSubPage::Profile_Advanced); _breadcrumbs.Append(crumb); } @@ -387,14 +374,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation else if (clickedItemTag == globalProfileTag) { auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone) }; + profileVM.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); profileVM.IsBaseLayer(true); - auto state{ winrt::make(profileVM, - _settingsClone.GlobalSettings().ColorSchemes(), - *this) }; - _SetupProfileEventHandling(state); + _SetupProfileEventHandling(profileVM); - contentFrame().Navigate(xaml_typename(), state); + contentFrame().Navigate(xaml_typename(), winrt::make(profileVM, *this)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ProfileDefaults/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -410,9 +395,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == colorSchemesTag) { - contentFrame().Navigate(xaml_typename(), _colorSchemesPageVM); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ColorSchemes/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); + contentFrame().Navigate(xaml_typename(), _colorSchemesPageVM); if (subPage == BreadcrumbSubPage::ColorSchemes_Edit) { @@ -440,18 +425,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // - NOTE: this does not update the selected item. // Arguments: // - profile - the profile object we are getting a view of - void MainPage::_Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage, const bool focusDeleteButton) + void MainPage::_Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage) { - auto state{ winrt::make(profile, - _settingsClone.GlobalSettings().ColorSchemes(), - *this) }; - state.FocusDeleteButton(focusDeleteButton); - _PreNavigateHelper(); - _SetupProfileEventHandling(state); + _SetupProfileEventHandling(profile); - contentFrame().Navigate(xaml_typename(), state); + contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(box_value(profile), profile.Name(), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -521,7 +501,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void MainPage::_InitializeProfilesList() { - const auto menuItems = SettingsNav().MenuItems(); + const auto& itemSource{ SettingsNav().MenuItemsSource() }; + if (!itemSource) + { + // There wasn't a MenuItemsSource set yet? The only way that's + // possible is if we haven't used + // _MoveXamlParsedNavItemsIntoItemSource to move the hardcoded menu + // entries from XAML into our runtime menu item source. Do that now. + + _MoveXamlParsedNavItemsIntoItemSource(); + } // Manually create a NavigationViewItem for each profile // and keep a reference to them in a map so that we @@ -531,8 +520,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { if (!profile.Deleted()) { - auto navItem = _CreateProfileNavViewItem(_viewModelForProfile(profile, _settingsClone)); - menuItems.Append(navItem); + auto profileVM = _viewModelForProfile(profile, _settingsClone); + profileVM.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); + auto navItem = _CreateProfileNavViewItem(profileVM); + _menuItemSource.Append(navItem); } } @@ -546,15 +537,53 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation icon.Glyph(L"\xE710"); addProfileItem.Icon(icon); - menuItems.Append(addProfileItem); + _menuItemSource.Append(addProfileItem); + } + + // BODGY + // Does the very wacky business of moving all our MenuItems that we + // hardcoded in XAML into a runtime MenuItemsSource. We'll then use _that_ + // MenuItemsSource as the source for our nav view entries instead. This + // lets us hardcode the initial entries in precompiled XAML, but then adjust + // the items at runtime. Without using a MenuItemsSource, the NavView just + // crashes when items are removed (see GH#13673) + void MainPage::_MoveXamlParsedNavItemsIntoItemSource() + { + if (SettingsNav().MenuItemsSource()) + { + // We've already copied over the original items to a source. We can + // just skip this now. + return; + } + + auto menuItems{ SettingsNav().MenuItems() }; + _originalNumItems = menuItems.Size(); + // Remove all the existing items, and move them to a separate vector + // that we'll use as a MenuItemsSource. By doing this, we avoid a WinUI + // bug (MUX#6302) where modifying the NavView.Items() directly causes a + // crash. By leaving these static entries in XAML, we maintain the + // benefit of instantiating them from the XBF, rather than at runtime. + // + // --> Copy it into an STL vector to simplify our code and reduce COM overhead. + auto original = std::vector{ _originalNumItems, nullptr }; + menuItems.GetMany(0, original); + + _menuItemSource = winrt::single_threaded_observable_vector(std::move(original)); + + SettingsNav().MenuItemsSource(_menuItemSource); } void MainPage::_CreateAndNavigateToNewProfile(const uint32_t index, const Model::Profile& profile) { const auto newProfile{ profile ? profile : _settingsClone.CreateNewProfile() }; const auto profileViewModel{ _viewModelForProfile(newProfile, _settingsClone) }; + profileViewModel.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); const auto navItem{ _CreateProfileNavViewItem(profileViewModel) }; - SettingsNav().MenuItems().InsertAt(index, navItem); + + if (_menuItemSource) + { + _menuItemSource.InsertAt(index, navItem); + } // Select and navigate to the new profile SettingsNav().SelectedItem(navItem); @@ -608,21 +637,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // remove selected item uint32_t index; auto selectedItem{ SettingsNav().SelectedItem() }; - auto menuItems{ SettingsNav().MenuItems() }; - menuItems.IndexOf(selectedItem, index); - menuItems.RemoveAt(index); + if (_menuItemSource) + { + _menuItemSource.IndexOf(selectedItem, index); + _menuItemSource.RemoveAt(index); - // navigate to the profile next to this one - const auto newSelectedItem{ menuItems.GetAt(index < menuItems.Size() - 1 ? index : index - 1) }; - SettingsNav().SelectedItem(newSelectedItem); - const auto newTag = newSelectedItem.as().Tag(); - if (const auto profileViewModel = newTag.try_as()) - { - _Navigate(*profileViewModel, BreadcrumbSubPage::None); - } - else - { - _Navigate(newTag.as(), BreadcrumbSubPage::None); + // navigate to the profile next to this one + const auto newSelectedItem{ _menuItemSource.GetAt(index < _menuItemSource.Size() - 1 ? index : index - 1) }; + SettingsNav().SelectedItem(newSelectedItem); + const auto newTag = newSelectedItem.as().Tag(); + if (const auto profileViewModel = newTag.try_as()) + { + profileViewModel->FocusDeleteButton(true); + _Navigate(*profileViewModel, BreadcrumbSubPage::None); + } + else + { + _Navigate(newTag.as(), BreadcrumbSubPage::None); + } } } @@ -636,4 +668,48 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return SettingsNav().Background(); } + // If the theme asks for Mica, then drop out our background, so that we + // can have mica too. + void MainPage::_UpdateBackgroundForMica() + { + bool isMicaAvailable = false; + + // Check to see if our hosting window supports Mica at all. We'll check + // to see if the window has Mica enabled - if it does, then we can + // assume that it supports Mica. + // + // We're doing this instead of checking if we're on Windows build 22621 + // or higher. + if (_hostingHwnd.has_value()) + { + int attribute = DWMSBT_NONE; + const auto hr = DwmGetWindowAttribute(*_hostingHwnd, DWMWA_SYSTEMBACKDROP_TYPE, &attribute, sizeof(attribute)); + if (SUCCEEDED(hr)) + { + isMicaAvailable = attribute == DWMSBT_MAINWINDOW; + } + } + + if (!isMicaAvailable) + { + return; + } + + const auto& theme = _settingsSource.GlobalSettings().CurrentTheme(); + const auto& requestedTheme = _settingsSource.GlobalSettings().CurrentTheme().RequestedTheme(); + + RequestedTheme(requestedTheme); + + const auto bgKey = (theme.Window() != nullptr && theme.Window().UseMica()) ? + L"SettingsPageMicaBackground" : + L"SettingsPageBackground"; + + // remember to use ThemeLookup to get the actual correct color for the + // currently requested theme. + if (const auto bgColor = ThemeLookup(Resources(), requestedTheme, winrt::box_value(bgKey))) + { + SettingsNav().Background(winrt::WUX::Media::SolidColorBrush(winrt::unbox_value(bgColor))); + } + } + } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index ba86c6d50f..90719eb09a 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -50,6 +50,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation private: Windows::Foundation::Collections::IObservableVector _breadcrumbs; + Windows::Foundation::Collections::IObservableVector _menuItemSource; + size_t _originalNumItems = 0u; + Model::CascadiaSettings _settingsSource; Model::CascadiaSettings _settingsClone; @@ -61,11 +64,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _DeleteProfile(const Windows::Foundation::IInspectable sender, const Editor::DeleteProfileEventArgs& args); void _AddProfileHandler(const winrt::guid profileGuid); - void _SetupProfileEventHandling(const winrt::Microsoft::Terminal::Settings::Editor::ProfilePageNavigationState state); + void _SetupProfileEventHandling(const winrt::Microsoft::Terminal::Settings::Editor::ProfileViewModel profile); void _PreNavigateHelper(); void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage); - void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage, const bool focusDeleteButton = false); + void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage); + + void _UpdateBackgroundForMica(); + void _MoveXamlParsedNavItemsIntoItemSource(); winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr }; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 719d1a6d57..ec690e49c9 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -9,6 +9,7 @@ xmlns:local="using:Microsoft.Terminal.Settings.Editor" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:tsm="using:Microsoft.Terminal.Settings.Model" mc:Ignorable="d"> @@ -31,16 +32,20 @@ #282828 + #00000000 #F9F9F9 + #00000000 + @@ -185,13 +190,29 @@ - + + + + + + + + + + + + MainPage.xaml - - Profiles.idl - Code - ProfileViewModel.idl Code @@ -323,9 +319,6 @@ MainPage.xaml - - Code - @@ -360,7 +353,6 @@ - diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp index 6aefd53d12..2c33b1b0f4 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp @@ -8,7 +8,22 @@ using namespace ::winrt::Microsoft::Terminal::TerminalConnection; using namespace ::winrt::Windows::Foundation; -static constexpr std::wstring_view PreviewText{ L"Windows Terminal\r\nCopyright (c) Microsoft Corporation\r\n\nC:\\Windows\\Terminal> " }; +static constexpr std::wstring_view PromptTextPlain{ L"C:\\> " }; +static constexpr std::wstring_view PromptTextPowerline{ L"\x1b[49;34m\xe0b6\x1b[1;97;44m C:\\ \x1b[m\x1b[46;34m\xe0b8\x1b[49;36m\xe0b8\x1b[m " }; + +// clang-format off +static constexpr std::wstring_view PreviewText{ + L"\x001b" + L"c" // Hard Reset (RIS); on separate lines to avoid becoming 0x01BC + L"Windows Terminal\r\n" + L"{0}\x1b[93m" L"git\x1b[m diff \x1b[90m-w\x1b[m\r\n" + L"\x1b[1m" L"diff --git a/win b/win\x1b[m\r\n" + L"\x1b[36m@@ -1 +1 @@\x1b[m\r\n" + L"\x1b[31m- Windows Console\x1b[m\r\n" + L"\x1b[32m+ Windows Terminal!\x1b[m\r\n" + L"{0}\x1b[93mWrite-Host \x1b[36m\"\xd83c\xdf2f!\"\x1b[1D\x1b[m" +}; +// clang-format on namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { @@ -16,10 +31,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void PreviewConnection::Start() noexcept { - // First send a sequence to disable cursor blinking - _TerminalOutputHandlers(L"\x1b[?12l"); // Send the preview text - _TerminalOutputHandlers(PreviewText); + _TerminalOutputHandlers(fmt::format(PreviewText, _displayPowerlineGlyphs ? PromptTextPowerline : PromptTextPlain)); } void PreviewConnection::Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) noexcept @@ -37,4 +50,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void PreviewConnection::Close() noexcept { } + + void PreviewConnection::DisplayPowerlineGlyphs(bool d) noexcept + { + if (_displayPowerlineGlyphs != d) + { + _displayPowerlineGlyphs = d; + Start(); + } + } } diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h index 1b8d980ad2..7958611ca5 100644 --- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h +++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h @@ -27,9 +27,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void Resize(uint32_t rows, uint32_t columns) noexcept; void Close() noexcept; + void DisplayPowerlineGlyphs(bool d) noexcept; + winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; } WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler); TYPED_EVENT(StateChanged, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, IInspectable); + + private: + bool _displayPowerlineGlyphs{ false }; }; } diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp index fadf3d0a6b..5f31f6be57 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp @@ -21,6 +21,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + static Editor::Font _FontObjectForDWriteFont(IDWriteFontFamily* family); + Windows::Foundation::Collections::IObservableVector ProfileViewModel::_MonospaceFontList{ nullptr }; Windows::Foundation::Collections::IObservableVector ProfileViewModel::_FontList{ nullptr }; @@ -51,22 +53,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // NOTE: this is similar to what is done with BackgroundImagePath above _NotifyChanges(L"UseParentProcessDirectory", L"UseCustomStartingDirectory"); } - else if (viewModelProperty == L"UseAcrylic") - { - // GH#11372: If we're on Windows 10, and someone turns off - // acrylic, we're going to disable opacity for them. Opacity - // doesn't work without acrylic on Windows 10. - // - // BODGY: CascadiaSettings's function IsDefaultTerminalAvailable - // is basically a "are we on Windows 11" check, because defterm - // only works on Win11. So we'll use that. - // - // Remove when we can remove the rest of GH#11285 - if (!UseAcrylic() && !CascadiaSettings::IsDefaultTerminalAvailable()) - { - Opacity(1.0); - } - } else if (viewModelProperty == L"AntialiasingMode") { _NotifyChanges(L"CurrentAntiAliasingMode"); @@ -120,7 +106,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation std::vector monospaceFontList; // get the font collection; subscribe to updates - const auto fontCollection = ::Microsoft::Console::Render::FontCache::GetFresh(); + wil::com_ptr factory; + THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof()))); + + wil::com_ptr fontCollection; + THROW_IF_FAILED(factory->GetSystemFontCollection(fontCollection.addressof(), TRUE)); for (UINT32 i = 0; i < fontCollection->GetFontFamilyCount(); ++i) { @@ -130,12 +120,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation com_ptr fontFamily; THROW_IF_FAILED(fontCollection->GetFontFamily(i, fontFamily.put())); - // get the font's localized names - com_ptr localizedFamilyNames; - THROW_IF_FAILED(fontFamily->GetFamilyNames(localizedFamilyNames.put())); - // construct a font entry for tracking - if (const auto fontEntry{ _GetFont(localizedFamilyNames) }) + if (const auto fontEntry{ _FontObjectForDWriteFont(fontFamily.get()) }) { // check if the font is monospaced try @@ -171,7 +157,32 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } CATCH_LOG(); - Editor::Font ProfileViewModel::_GetFont(com_ptr localizedFamilyNames) + Editor::Font ProfileViewModel::FindFontWithLocalizedName(const winrt::hstring& name) noexcept + { + // look for the current font in our shown list of fonts + Editor::Font fallbackFont{ nullptr }; + try + { + const auto& currentFontList{ CompleteFontList() }; + for (const auto& font : currentFontList) + { + if (font.LocalizedName() == name) + { + return font; + } + else if (font.LocalizedName() == L"Cascadia Mono") + { + fallbackFont = font; + } + } + } + CATCH_LOG(); + + // we couldn't find the desired font, set to "Cascadia Mono" if we found that since it ships by default + return fallbackFont; + } + + static Editor::Font _FontObjectForDWriteFont(IDWriteFontFamily* family) { // used for the font's name as an identifier (i.e. text block's font family property) std::wstring nameID; @@ -181,6 +192,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation std::wstring localizedName; UINT32 localizedNameIndex; + // get the font's localized names + winrt::com_ptr localizedFamilyNames; + THROW_IF_FAILED(family->GetFamilyNames(localizedFamilyNames.put())); + // use our current locale to find the localized name auto exists{ FALSE }; HRESULT hr; @@ -223,21 +238,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (!nameID.empty() && !localizedName.empty()) { - return make(nameID, localizedName); + return make(nameID, localizedName, family); } return nullptr; } - Windows::Foundation::Collections::IMapView ProfileViewModel::Schemes() const noexcept - { - return _Schemes; - } - - void ProfileViewModel::Schemes(const Windows::Foundation::Collections::IMapView& val) noexcept - { - _Schemes = val; - } - winrt::guid ProfileViewModel::OriginalProfileGuid() const noexcept { return _originalProfileGuid; @@ -273,8 +278,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _profile.CreateUnfocusedAppearance(); _unfocusedAppearanceViewModel = winrt::make(_profile.UnfocusedAppearance().try_as()); - _unfocusedAppearanceViewModel.Schemes(_Schemes); - _unfocusedAppearanceViewModel.WindowRoot(_WindowRoot); + _unfocusedAppearanceViewModel.SchemesList(DefaultAppearance().SchemesList()); _NotifyChanges(L"UnfocusedAppearance", L"HasUnfocusedAppearance", L"ShowUnfocusedAppearance"); } @@ -375,4 +379,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation auto deleteProfileArgs{ winrt::make_self(Guid()) }; _DeleteProfileHandlers(*this, *deleteProfileArgs); } + + void ProfileViewModel::SetupAppearances(Windows::Foundation::Collections::IObservableVector schemesList) + { + DefaultAppearance().SchemesList(schemesList); + if (UnfocusedAppearance()) + { + UnfocusedAppearance().SchemesList(schemesList); + } + } } diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 1b61c7062c..751645b79b 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -4,6 +4,7 @@ #pragma once #include "DeleteProfileEventArgs.g.h" +#include "NavigateToProfileArgs.g.h" #include "ProfileViewModel.g.h" #include "Utils.h" #include "ViewModelHelpers.h" @@ -11,6 +12,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + struct NavigateToProfileArgs : NavigateToProfileArgsT + { + public: + NavigateToProfileArgs(ProfileViewModel profile, Editor::IHostedInWindow windowRoot) : + _Profile(profile), + _WindowRoot(windowRoot) {} + + Editor::IHostedInWindow WindowRoot() const noexcept { return _WindowRoot; } + Editor::ProfileViewModel Profile() const noexcept { return _Profile; } + + private: + Editor::IHostedInWindow _WindowRoot; + Editor::ProfileViewModel _Profile{ nullptr }; + }; + struct ProfileViewModel : ProfileViewModelT, ViewModelHelper { public: @@ -18,13 +34,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation static void UpdateFontList() noexcept; static Windows::Foundation::Collections::IObservableVector CompleteFontList() noexcept { return _FontList; }; static Windows::Foundation::Collections::IObservableVector MonospaceFontList() noexcept { return _MonospaceFontList; }; + static Editor::Font FindFontWithLocalizedName(winrt::hstring const& name) noexcept; ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& settings); Model::TerminalSettings TermSettings() const; void DeleteProfile(); - Windows::Foundation::Collections::IMapView Schemes() const noexcept; - void Schemes(const Windows::Foundation::Collections::IMapView& val) noexcept; + void SetupAppearances(Windows::Foundation::Collections::IObservableVector schemesList); // bell style bits bool IsBellStyleFlagSet(const uint32_t flag); @@ -35,21 +51,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void SetAcrylicOpacityPercentageValue(double value) { Opacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(value)); - - // GH#11372: If we're on Windows 10, and someone wants opacity, then - // we'll turn acrylic on for them. Opacity doesn't work without - // acrylic on Windows 10. - // - // BODGY: CascadiaSettings's function IsDefaultTerminalAvailable - // is basically a "are we on Windows 11" check, because defterm - // only works on Win11. So we'll use that. - // - // Remove when we can remove the rest of GH#11285 - if (value < 100.0 && - !winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings::IsDefaultTerminalAvailable()) - { - UseAcrylic(true); - } }; void SetPadding(double value) @@ -86,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); @@ -97,16 +97,18 @@ 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); OBSERVABLE_PROJECTED_SETTING(_profile, BellStyle); OBSERVABLE_PROJECTED_SETTING(_profile, UseAtlasEngine); OBSERVABLE_PROJECTED_SETTING(_profile, Elevate); - OBSERVABLE_PROJECTED_SETTING(_profile, VtPassthrough) + OBSERVABLE_PROJECTED_SETTING(_profile, VtPassthrough); + OBSERVABLE_PROJECTED_SETTING(_profile, ReloadEnvironmentVariables); WINRT_PROPERTY(bool, IsBaseLayer, false); - WINRT_PROPERTY(IHostedInWindow, WindowRoot, nullptr); + WINRT_PROPERTY(bool, FocusDeleteButton, false); GETSET_BINDABLE_ENUM_SETTING(AntiAliasingMode, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode); GETSET_BINDABLE_ENUM_SETTING(CloseOnExitMode, Microsoft::Terminal::Settings::Model::CloseOnExitMode, CloseOnExit); GETSET_BINDABLE_ENUM_SETTING(ScrollState, Microsoft::Terminal::Control::ScrollbarState, ScrollState); @@ -119,13 +121,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation winrt::hstring _lastBgImagePath; winrt::hstring _lastStartingDirectoryPath; Editor::AppearanceViewModel _defaultAppearanceViewModel; - Windows::Foundation::Collections::IMapView _Schemes; static Windows::Foundation::Collections::IObservableVector _MonospaceFontList; static Windows::Foundation::Collections::IObservableVector _FontList; - static Editor::Font _GetFont(com_ptr localizedFamilyNames); - Model::CascadiaSettings _appSettings; Editor::AppearanceViewModel _unfocusedAppearanceViewModel; }; diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 143fa3ca47..e8517be399 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -4,6 +4,7 @@ import "EnumEntry.idl"; import "MainPage.idl"; import "Appearances.idl"; +import "ColorSchemesPageViewModel.idl"; #include "ViewModelHelpers.idl.h" @@ -13,6 +14,12 @@ import "Appearances.idl"; namespace Microsoft.Terminal.Settings.Editor { + runtimeclass NavigateToProfileArgs + { + ProfileViewModel Profile { get; }; + IHostedInWindow WindowRoot { get; }; + } + runtimeclass DeleteProfileEventArgs { Guid ProfileGuid { get; }; @@ -29,11 +36,14 @@ namespace Microsoft.Terminal.Settings.Editor { static Windows.Foundation.Collections.IObservableVector CompleteFontList { get; }; static Windows.Foundation.Collections.IObservableVector MonospaceFontList { get; }; + static Font FindFontWithLocalizedName(String name); Microsoft.Terminal.Settings.Model.TerminalSettings TermSettings { get; }; event Windows.Foundation.TypedEventHandler DeleteProfile; + void SetupAppearances(Windows.Foundation.Collections.IObservableVector schemesList); + void SetAcrylicOpacityPercentageValue(Double value); void SetPadding(Double value); @@ -52,6 +62,7 @@ namespace Microsoft.Terminal.Settings.Editor Windows.Foundation.Collections.IObservableVector ScrollStateList { get; }; Boolean CanDeleteProfile { get; }; + Boolean FocusDeleteButton; Boolean IsBaseLayer; ProfileSubPage CurrentPage; Boolean UseParentProcessDirectory; @@ -95,5 +106,6 @@ namespace Microsoft.Terminal.Settings.Editor OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, UseAtlasEngine); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, Elevate); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, VtPassthrough); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables); } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.h b/src/cascadia/TerminalSettingsEditor/Profiles.h deleted file mode 100644 index 45721540f9..0000000000 --- a/src/cascadia/TerminalSettingsEditor/Profiles.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "ProfilePageNavigationState.g.h" -#include "ProfileViewModel.h" -#include "Utils.h" -#include "ViewModelHelpers.h" -#include "Appearances.h" - -namespace winrt::Microsoft::Terminal::Settings::Editor::implementation -{ - struct ProfilePageNavigationState : ProfilePageNavigationStateT - { - public: - ProfilePageNavigationState(const Editor::ProfileViewModel& viewModel, - const Windows::Foundation::Collections::IMapView& schemes, - const IHostedInWindow& windowRoot) : - _Profile{ viewModel } - { - auto profile{ winrt::get_self(viewModel) }; - profile->Schemes(schemes); - profile->WindowRoot(windowRoot); - - viewModel.DefaultAppearance().Schemes(schemes); - viewModel.DefaultAppearance().WindowRoot(windowRoot); - - if (viewModel.UnfocusedAppearance()) - { - viewModel.UnfocusedAppearance().Schemes(schemes); - viewModel.UnfocusedAppearance().WindowRoot(windowRoot); - } - } - WINRT_PROPERTY(Editor::ProfileViewModel, Profile, nullptr); - WINRT_PROPERTY(bool, FocusDeleteButton, false); - }; -}; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.idl b/src/cascadia/TerminalSettingsEditor/Profiles.idl deleted file mode 100644 index 9bedfd3652..0000000000 --- a/src/cascadia/TerminalSettingsEditor/Profiles.idl +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import "MainPage.idl"; -import "ProfileViewModel.idl"; - -namespace Microsoft.Terminal.Settings.Editor -{ - runtimeclass ProfilePageNavigationState - { - ProfileViewModel Profile { get; }; - Boolean FocusDeleteButton; - }; -} diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.cpp index 79c0edffd7..00bdff6d0b 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "Profiles_Advanced.h" #include "Profiles_Advanced.g.cpp" +#include "ProfileViewModel.h" #include "EnumEntry.h" #include @@ -20,8 +21,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void Profiles_Advanced::OnNavigatedTo(const NavigationEventArgs& e) { - auto state{ e.Parameter().as() }; - _Profile = state.Profile(); + _Profile = e.Parameter().as(); } void Profiles_Advanced::OnNavigatedFrom(const NavigationEventArgs& /*e*/) diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.idl b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.idl index 44a1cbca91..befaf556aa 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.idl +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "Profiles.idl"; +import "ProfileViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml index b17296fe0a..31a9378254 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml @@ -136,6 +136,15 @@ + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp index 6a5963800f..aa6585e6a1 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp @@ -4,7 +4,7 @@ #include "pch.h" #include "Profiles_Appearance.h" #include "Profiles_Appearance.g.cpp" -#include "Profiles.h" +#include "ProfileViewModel.h" #include "PreviewConnection.h" #include "EnumEntry.h" @@ -16,20 +16,17 @@ using namespace winrt::Windows::UI::Xaml::Navigation; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - Profiles_Appearance::Profiles_Appearance() : - _previewControl{ Control::TermControl(Model::TerminalSettings{}, nullptr, make()) } + Profiles_Appearance::Profiles_Appearance() { InitializeComponent(); - - _previewControl.IsEnabled(false); - _previewControl.AllowFocusWhenDisabled(false); - ControlPreview().Child(_previewControl); + _previewConnection = winrt::make_self(); } void Profiles_Appearance::OnNavigatedTo(const NavigationEventArgs& e) { - auto state{ e.Parameter().as() }; - _Profile = state.Profile(); + const auto args = e.Parameter().as(); + _Profile = args.Profile(); + _windowRoot = args.WindowRoot(); // generate the font list, if we don't have one if (_Profile.CompleteFontList() || !_Profile.MonospaceFontList()) @@ -37,25 +34,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation ProfileViewModel::UpdateFontList(); } + if (!_previewControl) + { + const auto settings = _Profile.TermSettings(); + _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont()); + _previewControl = Control::TermControl(settings, settings, *_previewConnection); + _previewControl.IsEnabled(false); + _previewControl.AllowFocusWhenDisabled(false); + _previewControl.CursorVisibility(Microsoft::Terminal::Control::CursorDisplayState::Shown); + ControlPreview().Child(_previewControl); + } + // Subscribe to some changes in the view model // These changes should force us to update our own set of "Current" members, // and propagate those changes to the UI - _ViewModelChangedRevoker = _Profile.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& /*args*/) { - _previewControl.UpdateControlSettings(_Profile.TermSettings(), _Profile.TermSettings()); - }); - + _ViewModelChangedRevoker = _Profile.PropertyChanged(winrt::auto_revoke, { this, &Profiles_Appearance::_onProfilePropertyChanged }); // The Appearances object handles updating the values in the settings UI, but // we still need to listen to the changes here just to update the preview control - _AppearanceViewModelChangedRevoker = _Profile.DefaultAppearance().PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& /*args*/) { - _previewControl.UpdateControlSettings(_Profile.TermSettings(), _Profile.TermSettings()); - }); - - // There is a possibility that the control has not fully initialized yet, - // so wait for it to initialize before updating the settings (so we know - // that the renderer is set up) - _previewControl.Initialized([&](auto&& /*s*/, auto&& /*e*/) { - _previewControl.UpdateControlSettings(_Profile.TermSettings(), _Profile.TermSettings()); - }); + _AppearanceViewModelChangedRevoker = _Profile.DefaultAppearance().PropertyChanged(winrt::auto_revoke, { this, &Profiles_Appearance::_onProfilePropertyChanged }); } void Profiles_Appearance::OnNavigatedFrom(const NavigationEventArgs& /*e*/) @@ -73,4 +69,26 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _Profile.DeleteUnfocusedAppearance(); } + + bool Profiles_Appearance::_looksLikePowerlineFont() const + { + if (_Profile && _Profile.DefaultAppearance()) + { + if (const auto fontName = _Profile.DefaultAppearance().FontFace(); !fontName.empty()) + { + if (const auto font = ProfileViewModel::FindFontWithLocalizedName(fontName)) + { + return font.HasPowerlineCharacters(); + } + } + } + return false; + } + + void Profiles_Appearance::_onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const + { + const auto settings = _Profile.TermSettings(); + _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont()); + _previewControl.UpdateControlSettings(settings, settings); + } } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h index 2ddc393134..0eadfeb6a6 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h @@ -5,6 +5,7 @@ #include "Profiles_Appearance.g.h" #include "Utils.h" +#include "PreviewConnection.h" namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { @@ -19,13 +20,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void CreateUnfocusedAppearance_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void DeleteUnfocusedAppearance_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); + Editor::IHostedInWindow WindowRoot() { return _windowRoot; }; + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_PROPERTY(Editor::ProfileViewModel, Profile, nullptr); private: - Microsoft::Terminal::Control::TermControl _previewControl; + void _onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const; + bool _looksLikePowerlineFont() const; + + winrt::com_ptr _previewConnection{ nullptr }; + Microsoft::Terminal::Control::TermControl _previewControl{ nullptr }; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _AppearanceViewModelChangedRevoker; + Editor::IHostedInWindow _windowRoot; }; }; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.idl b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.idl index dff812912a..931edbe519 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.idl +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "Profiles.idl"; +import "ProfileViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { @@ -9,6 +9,7 @@ namespace Microsoft.Terminal.Settings.Editor { Profiles_Appearance(); ProfileViewModel Profile { get; }; + IHostedInWindow WindowRoot { get; }; Windows.UI.Xaml.Controls.Slider OpacitySlider { get; }; } diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.xaml index b8b334bcaf..710eb62ee1 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.xaml @@ -47,8 +47,8 @@ + SourceProfile="{x:Bind Profile, Mode=OneWay}" + WindowRoot="{x:Bind WindowRoot, Mode=OneTime}" /> @@ -174,7 +175,8 @@ + Visibility="{x:Bind Profile.ShowUnfocusedAppearance, Mode=OneWay}" + WindowRoot="{x:Bind WindowRoot, Mode=OneTime}" /> diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp index a0a0ab61e4..32570a016b 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp @@ -4,7 +4,7 @@ #include "pch.h" #include "Profiles_Base.h" #include "Profiles_Base.g.cpp" -#include "Profiles.h" +#include "ProfileViewModel.h" #include #include "..\WinRTUtils\inc\Utils.h" @@ -29,8 +29,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void Profiles_Base::OnNavigatedTo(const NavigationEventArgs& e) { - auto state{ e.Parameter().as() }; - _Profile = state.Profile(); + const auto args = e.Parameter().as(); + _Profile = args.Profile(); + _windowRoot = args.WindowRoot(); // Check the use parent directory box if the starting directory is empty if (_Profile.StartingDirectory().empty()) @@ -38,7 +39,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation StartingDirectoryUseParentCheckbox().IsChecked(true); } - _layoutUpdatedRevoker = LayoutUpdated(winrt::auto_revoke, [state, this](auto /*s*/, auto /*e*/) { + _layoutUpdatedRevoker = LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { // This event fires every time the layout changes, but it is always the last one to fire // in any layout change chain. That gives us great flexibility in finding the right point // at which to initialize our renderer (and our terminal). @@ -47,10 +48,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // Only let this succeed once. _layoutUpdatedRevoker.revoke(); - if (state.FocusDeleteButton()) + if (_Profile.FocusDeleteButton()) { DeleteButton().Focus(FocusState::Programmatic); - state.FocusDeleteButton(false); + _Profile.FocusDeleteButton(false); ProfilesBase_ScrollView().ChangeView(nullptr, ProfilesBase_ScrollView().ScrollableHeight(), nullptr); } }); @@ -86,7 +87,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation }; static constexpr winrt::guid clientGuidExecutables{ 0x2E7E4331, 0x0800, 0x48E6, { 0xB0, 0x17, 0xA1, 0x4C, 0xD8, 0x73, 0xDD, 0x58 } }; - const auto parentHwnd{ reinterpret_cast(winrt::get_self(_Profile)->WindowRoot().GetHostingWindow()) }; + const auto parentHwnd{ reinterpret_cast(_windowRoot.GetHostingWindow()) }; auto path = co_await OpenFilePicker(parentHwnd, [](auto&& dialog) { THROW_IF_FAILED(dialog->SetClientGuid(clientGuidExecutables)); try @@ -110,7 +111,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { auto lifetime = get_strong(); - const auto parentHwnd{ reinterpret_cast(winrt::get_self(_Profile)->WindowRoot().GetHostingWindow()) }; + const auto parentHwnd{ reinterpret_cast(_windowRoot.GetHostingWindow()) }; auto file = co_await OpenImagePicker(parentHwnd); if (!file.empty()) { @@ -121,7 +122,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation fire_and_forget Profiles_Base::StartingDirectory_Click(const IInspectable&, const RoutedEventArgs&) { auto lifetime = get_strong(); - const auto parentHwnd{ reinterpret_cast(winrt::get_self(_Profile)->WindowRoot().GetHostingWindow()) }; + const auto parentHwnd{ reinterpret_cast(_windowRoot.GetHostingWindow()) }; auto folder = co_await OpenFilePicker(parentHwnd, [](auto&& dialog) { static constexpr winrt::guid clientGuidFolderPicker{ 0xAADAA433, 0xB04D, 0x4BAE, { 0xB1, 0xEA, 0x1E, 0x6C, 0xD1, 0xCD, 0xA6, 0x8B } }; THROW_IF_FAILED(dialog->SetClientGuid(clientGuidFolderPicker)); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.h b/src/cascadia/TerminalSettingsEditor/Profiles_Base.h index 42067438ee..4f711f5bb1 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.h @@ -30,6 +30,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation private: Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker; winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; + Editor::IHostedInWindow _windowRoot; }; }; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.idl b/src/cascadia/TerminalSettingsEditor/Profiles_Base.idl index a0cd1b88ab..b076fe75a3 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.idl +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "Profiles.idl"; +import "ProfileViewModel.idl"; namespace Microsoft.Terminal.Settings.Editor { diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index f37004e808..630bcd87dd 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -135,6 +135,14 @@ Duplicate Button label that duplicates the selected profile and navigates to the duplicate's page. + + Set color scheme as default + This is the header for a control that allows the user to set the currently selected color scheme as their default. + + + Apply this color scheme to all your profiles, by default. Individual profiles can still select their own color schemes. + A description explaining how this control changes the user's default color scheme. + Rename color scheme This is the header for a control that allows the user to rename the currently selected color scheme. @@ -224,8 +232,8 @@ The header for a control allowing users to choose the app's language. - Sets an override for the app's preferred language. - A description explaining how this control changes the app's language. + Selects a display language for the application. This will override your default Windows interface language. + A description explaining how this control changes the app's language. {Locked="Windows"} Use system default @@ -236,15 +244,15 @@ Header for a control to toggle if the app should always show the tabs (similar to a website browser). - When disabled, the tab bar will appear when a new tab is created. - A description for what the "always show tabs" setting does. Presented near "Globals_AlwaysShowTabs.Header". + When disabled, the tab bar will appear when a new tab is created. Cannot be disabled while "Hide the title bar" is On. + A description for what the "always show tabs" setting does. Presented near "Globals_AlwaysShowTabs.Header". This setting cannot be disabled while "Globals_ShowTitlebar.Header" is enabled. Position of newly created tabs Header for a control to select position of newly created tabs. - Specifies where new tabs appear in the tab row + Specifies where new tabs appear in the tab row. A description for what the "Position of newly created tabs" setting does. @@ -348,8 +356,8 @@ The string to be displayed when there is no current value in the y-coordinate number box. - Use system default - A checkbox for the "launch position" setting. Toggling this control sets the launch position to the system default. + Let Windows decide + A checkbox for the "launch position" setting. Toggling this control sets the launch position to whatever the Windows operating system decides. If enabled, use the system default launch position. @@ -380,7 +388,7 @@ Launch parameters - Header for a set of settings that determine how terminal launches. These settings include the launch mode, launch position and whether the teminal should center itself on launch. + Header for a set of settings that determine how terminal launches. These settings include the launch mode, launch position and whether the terminal should center itself on launch. Settings that control how the terminal launches @@ -439,13 +447,9 @@ A description for what the "show titlebar" setting does. Presented near "Globals_ShowTitlebar.Header". - Use acrylic material in the tab row (requires relaunch) + Use acrylic material in the tab row Header for a control to toggle whether "acrylic material" is used. "Acrylic material" is a Microsoft-specific term: https://docs.microsoft.com/en-us/windows/apps/design/style/acrylic - - When checked, the tab row will have the acrylic material. - A description for the "Globals_AcrylicTabRow.Header" setting does. "Acrylic material" is a Microsoft-specific term: https://docs.microsoft.com/en-us/windows/apps/design/style/acrylic - Use active terminal title as application title Header for a control to toggle whether the terminal's title is shown as the application title, or not. @@ -475,8 +479,8 @@ Header for a control to toggle whether the app should launch when the user's machine starts up, or not. - When enabled, this enables the launch of Terminal at machine startup. - A description for what the "start on user login" setting does. Presented near "Globals_StartOnUserLogin.Header". + Automatically launch Terminal when you log in to Windows. + A description for what the "start on user login" setting does. Presented near "Globals_StartOnUserLogin.Header". {Locked="Windows"} Centered @@ -503,7 +507,7 @@ Header for a control to choose how wide the tabs are. - Compact will shrink unfocused tabs to the size of the icon. + Compact will shrink inactive tabs to the size of the icon. A description for what the "tab width mode" setting does. Presented near "Globals_TabWidthMode.Header". 'Compact' must match the value for <Globals_TabWidthModeCompact.Content>. @@ -519,32 +523,40 @@ An option to choose from for the "tab width mode" setting. When selected, each tab adjusts its width to the content within the tab. - Theme + Application Theme Header for a control to choose the theme colors used in the app. - - Sets the theme of the application. - A description for what the "theme" setting does. Presented near "Globals_Theme.Header". - Dark - An option to choose from for the "tab width mode" setting. When selected, the app is in dark theme and darker colors are used throughout the app. + An option to choose from for the "theme" setting. When selected, the app is in dark theme and darker colors are used throughout the app. Use Windows theme - An option to choose from for the "tab width mode" setting. When selected, the app uses the theme selected in the Windows operating system. + An option to choose from for the "theme" setting. When selected, the app uses the theme selected in the Windows operating system. Light - An option to choose from for the "tab width mode" setting. When selected, the app is in light theme and lighter colors are used throughout the app. + An option to choose from for the "theme" setting. When selected, the app is in light theme and lighter colors are used throughout the app. + + + Dark (Legacy) + An option to choose from for the "theme" setting. When selected, the app is in dark theme and darker colors are used throughout the app. This is an older version of the "dark" theme + + + Use Windows theme (Legacy) + An option to choose from for the "theme" setting. When selected, the app uses the theme selected in the Windows operating system. This is an older version of the "Use Windows theme" theme + + + Light (Legacy) + An option to choose from for the "theme" setting. When selected, the app is in light theme and lighter colors are used throughout the app. This is an older version of the "light" theme Word delimiters Header for a control to determine what delimiters to use to identify separate words during word selection. - Symbols used to define boundaries between words. - A description for what the "word delimiters" setting does. Presented near "Globals_WordDelimiters.Header". + These symbols will be used when you double-click text in the terminal or activate "Mark mode". + A description for what the "word delimiters" setting does. Presented near "Globals_WordDelimiters.Header". "Mark" is used in the sense of "choosing something to interact with." Appearance @@ -599,8 +611,8 @@ Header for a control to toggle whether the app treats ctrl+alt as the AltGr (also known as the Alt Graph) modifier key found on keyboards. {Locked="AltGr"} - By default Windows treats Ctrl+Alt as an alias for AltGr. When disabled, this behavior will be disabled. - A description for what the "AltGr aliasing" setting does. Presented near "Profile_AltGrAliasing.Header". + By default, Windows treats Ctrl+Alt as an alias for AltGr. This setting will override Windows' default behavior. + A description for what the "AltGr aliasing" setting does. Presented near "Profile_AltGrAliasing.Header". {Locked="Windows"} Text antialiasing @@ -766,10 +778,6 @@ Color scheme Header for a control to select the scheme (or set) of colors used in the session. This is selected from a list of options managed by the user. - - Name of the color scheme to use. - A description for what the "color scheme" setting does. Presented near "Profile_ColorScheme". - Command line Name for a control to determine commandline executable (i.e. a .exe file) to run when a terminal session of this profile is launched. @@ -815,7 +823,7 @@ An option to choose from for the "adjust indistinguishable colors" setting. When selected, we will adjust the text colors for visibility only when the colors are part of this profile's color scheme's color table. - Always (More performance intensive) + Always An option to choose from for the "adjust indistinguishable colors" setting. When selected, we will adjust the text colors for visibility. @@ -850,10 +858,6 @@ Font face Name for a control to select the font for text in the app. - - Name of the font face used in the profile. - A description for what the "font face" setting does. Presented near "Profile_FontFace". - Font size Header for a control to determine the size of the text in the app. @@ -866,6 +870,22 @@ Size of the font in points. A description for what the "font size" setting does. Presented near "Profile_FontSize". + + Line height + Header for a control that sets the text line height. + + + Line height + Header for a control that sets the text line height. + + + Override the line height of the terminal. Measured as a multiple of the font size. The default value depends on your font and is usually around 1.2. + A description for what the "line height" setting does. Presented near "Profile_LineHeight". + + + 1.2 + "1.2" is a decimal number. + Font weight Name for a control to select the weight (i.e. bold, thin, etc.) of the text in the app. @@ -951,15 +971,15 @@ Header for a control to toggle classic CRT display effects, which gives the terminal a retro look. - When enabled, enables retro terminal effects such as glowing text and scan lines. - A description for what the "retro terminal effects" setting does. Presented near "Profile_RetroTerminalEffect". + Show retro-style terminal effects such as glowing text and scan lines. + A description for what the "retro terminal effects" setting does. Presented near "Profile_RetroTerminalEffect". "Retro" is a common English prefix that suggests a nostalgic, dated appearance. Automatically adjust lightness of indistinguishable text Header for a control to toggle if we should adjust the foreground color's lightness to make it more visible when necessary, based on the background color. - When enabled, enables automatic adjustment of indistinguishable colors, which will, only when necessary, adjust the foreground color's lightness to make it more visible (based on the background color). + Automatically brightens or darkens text to make it more visible. Even when enabled, this adjustment will only occur when a combination of foreground and background colors would result in poor contrast. A description for what the "adjust indistinguishable colors" setting does. Presented near "Profile_AdjustIndistinguishableColors". @@ -1019,8 +1039,8 @@ Header for a control to toggle changes in the app title. - Use the tab title to override the default title of the tab and suppress any title change messages from the application. - A description for what the "suppress application title" setting does. Presented near "Profile_SuppressApplicationTitle". + Ignore application requests to change the title (OSC 2). + A description for what the "suppress application title" setting does. Presented near "Profile_SuppressApplicationTitle". "OSC 2" is a technical term that is understood in the industry. {Locked="OSC 2"} Tab title @@ -1039,7 +1059,7 @@ The header for the section where the unfocused appearance settings can be changed. - Add + Create Appearance Button label that adds an unfocused appearance for this profile. @@ -1059,7 +1079,7 @@ A supplementary setting to the "background image" setting. When enabled, the OS desktop wallpaper is used as the background image. Presented near "Profile_BackgroundImage". - When enabled, use the desktop wallpaper image as the background image for the terminal. + Use the desktop wallpaper image as the background image for the terminal. A description for what the supplementary "use desktop image" setting does. Presented near "Profile_UseDesktopImage". @@ -1122,6 +1142,14 @@ Use the new text renderer ("AtlasEngine") {Locked="AtlasEngine"} + + Launch this application with a new environment block + "environment variables" are user-definable values that can affect the way running processes will behave on a computer + + + When enabled, the Terminal will generate a new environment block when creating new tabs or panes with this profile. When disabled, the tab/pane will instead inherit the variables the Terminal was started with. + A description for what the "Reload environment variables" setting does. Presented near "Profile_ReloadEnvVars". + Enable experimental virtual terminal passthrough An option to enable experimental virtual terminal passthrough connectivity option with the underlying ConPTY @@ -1334,7 +1362,7 @@ Header for a control to choose how the tab switcher operates. - Defines the terminal behavior when switching tabs with the keyboard. + Selects which interface will be used when you switch tabs using the keyboard. A description for what the "tab switcher mode" setting does. Presented near "Globals_TabSwitcherMode.Header". @@ -1350,13 +1378,9 @@ An option to choose from for the "tab switcher mode" setting. The tab switcher overlay is hidden and does not appear in a separate window. - Text format when copying + Text formats to copy to the clipboard Header for a control to select the format of copied text. - - Defines the type of formatting in which selected text is copied to your clipboard. - A description for what the "copy formatting" setting does. Presented near "Globals_CopyFormat.Header". - Plain text only An option to choose from for the "copy formatting" setting. Store only plain text data. @@ -1427,7 +1451,7 @@ Create Appearance - Name for a control which creates an the unfocused appearance settings for this profile. + Name for a control which creates an the unfocused appearance settings for this profile. Text must match that of "Profile_AddAppearanceButton.Text". Create an unfocused appearance for this profile. This will be the appearance of the profile when it is inactive. @@ -1535,10 +1559,10 @@ Automatically hide window - Header for a control to toggle the "Automatically hide window" setting. If enabled, the window will be hidden as soon as it loses focus. + Header for a control to toggle the "Automatically hide window" setting. If enabled, the terminal will be hidden as soon as you switch to another window. - If enabled, the window will be hidden as soon as it loses focus. + If enabled, the terminal will be hidden as soon as you switch to another window. A description for what the "Automatically hide window" setting does. @@ -1557,4 +1581,28 @@ Set as default Text label for a button that, when invoked, sets the selected color scheme as the default scheme to use. - \ No newline at end of file + + Warn when closing more than one tab + Header for a control to toggle whether to show a confirm dialog box when closing the application with multiple tabs open. + + + Windows Terminal is running in portable mode. + A disclaimer that indicates that Terminal is running in a mode that saves settings to a different folder. + + + https://go.microsoft.com/fwlink/?linkid=2229086 + {Locked} + + + Learn more. + A hyperlink displayed near Settings_PortableModeNote.Text that the user can follow for more information. + + + Warning: + Title for the warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts + + + Choosing a non-monospaced font will likely result in visual artifacts. Use at your own discretion. + Warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts + + diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml index 5efeceeaec..9803220c6b 100644 --- a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml +++ b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml @@ -106,7 +106,8 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/Utils.h b/src/cascadia/TerminalSettingsEditor/Utils.h index 0eba77caa0..9a201a4e01 100644 --- a/src/cascadia/TerminalSettingsEditor/Utils.h +++ b/src/cascadia/TerminalSettingsEditor/Utils.h @@ -127,7 +127,7 @@ struct HasScrollViewer const winrt::Windows::UI::Xaml::Controls::ScrollViewerViewChangingEventArgs& /*e*/) { // Inside this struct, we can't get at the XamlRoot() that our subclass - // implements. I mean, _we_ can, but when XAML does it's code + // implements. I mean, _we_ can, but when XAML does its code // generation, _XAML_ won't be able to figure it out. // // Fortunately for us, we don't need to! The sender is a UIElement, so diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h index a131a53097..e306233ab6 100644 --- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h +++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h @@ -37,19 +37,25 @@ protected: winrt::event<::winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler> _propertyChangedHandlers; }; -#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \ -public: \ - auto name() const noexcept { return target.name(); }; \ - template \ - void name(const T& value) \ - { \ - if (name() != value) \ - { \ - target.name(value); \ - _NotifyChanges(L"Has" #name, L## #name); \ - } \ - } \ - bool Has##name() { return target.Has##name(); } +#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \ +public: \ + auto name() const noexcept \ + { \ + return target.name(); \ + }; \ + template \ + void name(const T& value) \ + { \ + if (name() != value) \ + { \ + target.name(value); \ + _NotifyChanges(L"Has" #name, L## #name); \ + } \ + } \ + bool Has##name() \ + { \ + return target.Has##name(); \ + } // Defines a setting that reflects another object's same-named // setting. @@ -64,7 +70,10 @@ public: \ _NotifyChanges(L"Has" #name, L## #name); \ } \ } \ - auto name##OverrideSource() { return target.name##OverrideSource(); } + auto name##OverrideSource() \ + { \ + return target.name##OverrideSource(); \ + } // Defines a setting that reflects another object's same-named // setting, but which cannot be erased. @@ -76,7 +85,10 @@ public: \ // except it leverages _NotifyChanges. #define VIEW_MODEL_OBSERVABLE_PROPERTY(type, name, ...) \ public: \ - type name() const noexcept { return _##name; }; \ + type name() const noexcept \ + { \ + return _##name; \ + }; \ void name(const type& value) \ { \ if (_##name != value) \ diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h index 9579f2e7c0..f0ea111507 100644 --- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h +++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.idl.h @@ -9,7 +9,10 @@ get; \ set; \ }; \ - Boolean Has##Name { get; } + Boolean Has##Name \ + { \ + get; \ + } #define OBSERVABLE_PROJECTED_SETTING(Type, Name) \ _BASE_OBSERVABLE_PROJECTED_SETTING(Type, Name); \ diff --git a/src/cascadia/TerminalSettingsEditor/packages.config b/src/cascadia/TerminalSettingsEditor/packages.config deleted file mode 100644 index 0661e59397..0000000000 --- a/src/cascadia/TerminalSettingsEditor/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 5f9f31386f..e90a2b8dcf 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -52,6 +52,7 @@ static constexpr std::string_view SwitchToTabKey{ "switchToTab" }; static constexpr std::string_view TabSearchKey{ "tabSearch" }; static constexpr std::string_view ToggleAlwaysOnTopKey{ "toggleAlwaysOnTop" }; static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; +static constexpr std::string_view SuggestionsKey{ "showSuggestions" }; static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" }; static constexpr std::string_view SetFocusModeKey{ "setFocusMode" }; static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" }; @@ -65,11 +66,15 @@ static constexpr std::string_view MoveTabKey{ "moveTab" }; static constexpr std::string_view BreakIntoDebuggerKey{ "breakIntoDebugger" }; static constexpr std::string_view FindMatchKey{ "findMatch" }; static constexpr std::string_view TogglePaneReadOnlyKey{ "toggleReadOnlyMode" }; +static constexpr std::string_view EnablePaneReadOnlyKey{ "enableReadOnlyMode" }; +static constexpr std::string_view DisablePaneReadOnlyKey{ "disableReadOnlyMode" }; static constexpr std::string_view NewWindowKey{ "newWindow" }; static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" }; static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" }; static constexpr std::string_view RenameWindowKey{ "renameWindow" }; static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" }; +static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" }; +static constexpr std::string_view SearchForTextKey{ "searchWeb" }; static constexpr std::string_view GlobalSummonKey{ "globalSummon" }; static constexpr std::string_view QuakeModeKey{ "quakeMode" }; static constexpr std::string_view FocusPaneKey{ "focusPane" }; @@ -81,11 +86,17 @@ static constexpr std::string_view QuitKey{ "quit" }; static constexpr std::string_view AdjustOpacityKey{ "adjustOpacity" }; static constexpr std::string_view RestoreLastClosedKey{ "restoreLastClosed" }; static constexpr std::string_view SelectAllKey{ "selectAll" }; +static constexpr std::string_view SelectCommandKey{ "selectCommand" }; +static constexpr std::string_view SelectOutputKey{ "selectOutput" }; static constexpr std::string_view MarkModeKey{ "markMode" }; static constexpr std::string_view ToggleBlockSelectionKey{ "toggleBlockSelection" }; static constexpr std::string_view SwitchSelectionEndpointKey{ "switchSelectionEndpoint" }; static constexpr std::string_view ColorSelectionKey{ "experimental.colorSelection" }; +static constexpr std::string_view ShowContextMenuKey{ "showContextMenu" }; static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWord" }; +static constexpr std::string_view RestartConnectionKey{ "restartConnection" }; +static constexpr std::string_view ToggleBroadcastInputKey{ "toggleBroadcastInput" }; +static constexpr std::string_view OpenAboutKey{ "openAbout" }; static constexpr std::string_view ActionKey{ "action" }; @@ -191,8 +202,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // * If json is a string, we'll attempt to treat it as an action name, // without arguments. // * If json is an object, we'll attempt to retrieve the action name from - // its "action" property, and we'll use that name to fine a deserializer - // to precess the rest of the arguments in the json object. + // its "action" property, and we'll use that name to find a deserializer + // to process the rest of the arguments in the json object. // - If the action name is null or "unbound", or we don't understand the // action name, or we failed to parse the arguments to this action, we'll // return null. This should indicate to the caller that the action should @@ -327,22 +338,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring ActionAndArgs::GenerateName() const { + // Sentinel used to indicate this command must ALWAYS be generated by GenerateName + static const winrt::hstring MustGenerate{ L"" }; // Use a magic static to initialize this map, because we won't be able // to load the resources at _init_, only at runtime. static const auto GeneratedActionNames = []() { return std::unordered_map{ { ShortcutAction::AdjustFontSize, RS_(L"AdjustFontSizeCommandKey") }, { ShortcutAction::CloseOtherPanes, RS_(L"CloseOtherPanesCommandKey") }, - { ShortcutAction::CloseOtherTabs, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::CloseOtherTabs, MustGenerate }, { ShortcutAction::ClosePane, RS_(L"ClosePaneCommandKey") }, - { ShortcutAction::CloseTab, L"" }, // Intentionally omitted, must be generated by GenerateName - { ShortcutAction::CloseTabsAfter, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::CloseTab, MustGenerate }, + { ShortcutAction::CloseTabsAfter, MustGenerate }, { ShortcutAction::CloseWindow, RS_(L"CloseWindowCommandKey") }, { ShortcutAction::CopyText, RS_(L"CopyTextCommandKey") }, { ShortcutAction::DuplicateTab, RS_(L"DuplicateTabCommandKey") }, { ShortcutAction::ExecuteCommandline, RS_(L"ExecuteCommandlineCommandKey") }, { ShortcutAction::Find, RS_(L"FindCommandKey") }, - { ShortcutAction::Invalid, L"" }, + { ShortcutAction::Invalid, MustGenerate }, { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, { ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") }, { ShortcutAction::SwapPane, RS_(L"SwapPaneCommandKey") }, @@ -367,47 +380,58 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::AddMark, RS_(L"AddMarkCommandKey") }, { ShortcutAction::ClearMark, RS_(L"ClearMarkCommandKey") }, { ShortcutAction::ClearAllMarks, RS_(L"ClearAllMarksCommandKey") }, - { ShortcutAction::SendInput, L"" }, - { ShortcutAction::SetColorScheme, L"" }, + { ShortcutAction::SendInput, MustGenerate }, + { ShortcutAction::SetColorScheme, MustGenerate }, { ShortcutAction::SetTabColor, RS_(L"ResetTabColorCommandKey") }, { ShortcutAction::SplitPane, RS_(L"SplitPaneCommandKey") }, { ShortcutAction::SwitchToTab, RS_(L"SwitchToTabCommandKey") }, { ShortcutAction::TabSearch, RS_(L"TabSearchCommandKey") }, { ShortcutAction::ToggleAlwaysOnTop, RS_(L"ToggleAlwaysOnTopCommandKey") }, - { ShortcutAction::ToggleCommandPalette, L"" }, + { ShortcutAction::ToggleCommandPalette, MustGenerate }, + { ShortcutAction::Suggestions, MustGenerate }, { ShortcutAction::ToggleFocusMode, RS_(L"ToggleFocusModeCommandKey") }, - { ShortcutAction::SetFocusMode, L"" }, + { ShortcutAction::SetFocusMode, MustGenerate }, { ShortcutAction::ToggleFullscreen, RS_(L"ToggleFullscreenCommandKey") }, - { ShortcutAction::SetFullScreen, L"" }, - { ShortcutAction::SetMaximized, L"" }, + { ShortcutAction::SetFullScreen, MustGenerate }, + { ShortcutAction::SetMaximized, MustGenerate }, { ShortcutAction::TogglePaneZoom, RS_(L"TogglePaneZoomCommandKey") }, { ShortcutAction::ToggleSplitOrientation, RS_(L"ToggleSplitOrientationCommandKey") }, { ShortcutAction::ToggleShaderEffects, RS_(L"ToggleShaderEffectsCommandKey") }, - { ShortcutAction::MoveTab, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::MoveTab, MustGenerate }, { ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") }, - { ShortcutAction::FindMatch, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::FindMatch, MustGenerate }, { ShortcutAction::TogglePaneReadOnly, RS_(L"TogglePaneReadOnlyCommandKey") }, + { ShortcutAction::EnablePaneReadOnly, RS_(L"EnablePaneReadOnlyCommandKey") }, + { ShortcutAction::DisablePaneReadOnly, RS_(L"DisablePaneReadOnlyCommandKey") }, { ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") }, { ShortcutAction::IdentifyWindow, RS_(L"IdentifyWindowCommandKey") }, { ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") }, { ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") }, { ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") }, - { ShortcutAction::GlobalSummon, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::DisplayWorkingDirectory, RS_(L"DisplayWorkingDirectoryCommandKey") }, + { ShortcutAction::GlobalSummon, MustGenerate }, + { ShortcutAction::SearchForText, MustGenerate }, { ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") }, - { ShortcutAction::FocusPane, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::FocusPane, MustGenerate }, { ShortcutAction::OpenSystemMenu, RS_(L"OpenSystemMenuCommandKey") }, - { ShortcutAction::ExportBuffer, L"" }, // Intentionally omitted, must be generated by GenerateName - { ShortcutAction::ClearBuffer, L"" }, // Intentionally omitted, must be generated by GenerateName - { ShortcutAction::MultipleActions, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::ExportBuffer, MustGenerate }, + { ShortcutAction::ClearBuffer, MustGenerate }, + { ShortcutAction::MultipleActions, MustGenerate }, { ShortcutAction::Quit, RS_(L"QuitCommandKey") }, - { ShortcutAction::AdjustOpacity, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::AdjustOpacity, MustGenerate }, { ShortcutAction::RestoreLastClosed, RS_(L"RestoreLastClosedCommandKey") }, + { ShortcutAction::SelectCommand, MustGenerate }, + { ShortcutAction::SelectOutput, MustGenerate }, { ShortcutAction::SelectAll, RS_(L"SelectAllCommandKey") }, { ShortcutAction::MarkMode, RS_(L"MarkModeCommandKey") }, { ShortcutAction::ToggleBlockSelection, RS_(L"ToggleBlockSelectionCommandKey") }, { ShortcutAction::SwitchSelectionEndpoint, RS_(L"SwitchSelectionEndpointCommandKey") }, - { ShortcutAction::ColorSelection, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::ColorSelection, MustGenerate }, + { ShortcutAction::ShowContextMenu, RS_(L"ShowContextMenuCommandKey") }, { ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") }, + { ShortcutAction::RestartConnection, RS_(L"RestartConnectionKey") }, + { ShortcutAction::ToggleBroadcastInput, RS_(L"ToggleBroadcastInputCommandKey") }, + { ShortcutAction::OpenAbout, RS_(L"OpenAboutCommandKey") }, }; }(); @@ -423,4 +447,30 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto found = GeneratedActionNames.find(_Action); return found != GeneratedActionNames.end() ? found->second : L""; } + + winrt::hstring ActionAndArgs::Serialize(const winrt::Windows::Foundation::Collections::IVector& args) + { + Json::Value json{ Json::objectValue }; + JsonUtils::SetValueForKey(json, "actions", args); + Json::StreamWriterBuilder wbuilder; + auto str = Json::writeString(wbuilder, json); + return winrt::to_hstring(str); + } + winrt::Windows::Foundation::Collections::IVector ActionAndArgs::Deserialize(winrt::hstring content) + { + auto data = winrt::to_string(content); + + std::string errs; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + + winrt::Windows::Foundation::Collections::IVector result{ nullptr }; + JsonUtils::GetValueForKey(root, "actions", result); + return result; + } + } diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h index 055bd1abcd..a2afefaa49 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.h @@ -16,6 +16,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation std::vector& warnings); static Json::Value ToJson(const Model::ActionAndArgs& val); + static winrt::hstring Serialize(const winrt::Windows::Foundation::Collections::IVector& args); + static winrt::Windows::Foundation::Collections::IVector Deserialize(winrt::hstring content); + ActionAndArgs() = default; ActionAndArgs(ShortcutAction action); ActionAndArgs(ShortcutAction action, IActionArgs args) : diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 64c4976af1..a276ebeff2 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -13,6 +13,7 @@ #include "ResizePaneArgs.g.cpp" #include "MoveFocusArgs.g.cpp" #include "MovePaneArgs.g.cpp" +#include "MoveTabArgs.g.cpp" #include "SwapPaneArgs.g.cpp" #include "AdjustFontSizeArgs.g.cpp" #include "SendInputArgs.g.cpp" @@ -28,21 +29,24 @@ #include "CloseOtherTabsArgs.g.cpp" #include "CloseTabsAfterArgs.g.cpp" #include "CloseTabArgs.g.cpp" -#include "MoveTabArgs.g.cpp" #include "ScrollToMarkArgs.g.cpp" #include "AddMarkArgs.g.cpp" #include "FindMatchArgs.g.cpp" #include "ToggleCommandPaletteArgs.g.cpp" +#include "SuggestionsArgs.g.cpp" #include "NewWindowArgs.g.cpp" #include "PrevTabArgs.g.cpp" #include "NextTabArgs.g.cpp" #include "RenameWindowArgs.g.cpp" +#include "SearchForTextArgs.g.cpp" #include "GlobalSummonArgs.g.cpp" #include "FocusPaneArgs.g.cpp" #include "ExportBufferArgs.g.cpp" #include "ClearBufferArgs.g.cpp" #include "MultipleActionsArgs.g.cpp" #include "AdjustOpacityArgs.g.cpp" +#include "SelectCommandArgs.g.cpp" +#include "SelectOutputArgs.g.cpp" #include "ColorSelectionArgs.g.cpp" #include @@ -194,6 +198,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ss << RS_(L"CopyTextCommandKey").c_str(); } + if (!DismissSelection()) + { + ss << L", dismissSelection: false"; + } + if (CopyFormatting()) { ss << L", copyFormatting: "; @@ -245,6 +254,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring MovePaneArgs::GenerateName() const { + if (!Window().empty()) + { + // Special case for moving to a new window. We can just ignore the + // tab index, because it _doesn't matter_. There won't be any tabs + // in the new window, till we get there. + if (Window() == L"new") + { + return RS_(L"MovePaneToNewWindowCommandKey"); + } + return winrt::hstring{ + fmt::format(L"{}, window:{}, tab index:{}", RS_(L"MovePaneCommandKey"), Window(), TabIndex()) + }; + } return winrt::hstring{ fmt::format(L"{}, tab index:{}", RS_(L"MovePaneCommandKey"), TabIndex()) }; @@ -648,6 +670,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::hstring MoveTabArgs::GenerateName() const { + if (!Window().empty()) + { + if (Window() == L"new") + { + return RS_(L"MoveTabToNewWindowCommandKey"); + } + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"MoveTabToWindowCommandKey")), + Window()) + }; + } + winrt::hstring directionString; switch (Direction()) { @@ -673,6 +707,46 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_(L"ToggleCommandPaletteCommandKey"); } + winrt::hstring SuggestionsArgs::GenerateName() const + { + std::wstringstream ss; + ss << RS_(L"SuggestionsCommandKey").c_str(); + + if (UseCommandline()) + { + ss << L", useCommandline:true"; + } + + // 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) + { + ss << L"all, "; + } + else if (source == static_cast(0)) + { + ss << L"none, "; + } + else + { + 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 { switch (Direction()) @@ -738,6 +812,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return RS_(L"ResetWindowNameCommandKey"); } + winrt::hstring SearchForTextArgs::GenerateName() const + { + if (QueryUrl().empty()) + { + // Return the default command name, because we'll just use the + // default search engine for this. + return RS_(L"SearchWebCommandKey"); + } + + try + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"SearchForTextCommandKey")), + Windows::Foundation::Uri(QueryUrl()).Domain().c_str()) + }; + } + CATCH_LOG(); + + // We couldn't parse a URL out of this. Return no string at all, so that + // we don't even put this into the command palette. + return L""; + } + winrt::hstring GlobalSummonArgs::GenerateName() const { // GH#10210 - Is this action literally the same thing as the `quakeMode` @@ -956,4 +1053,27 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation }; } } + + winrt::hstring SelectOutputArgs::GenerateName() const + { + switch (Direction()) + { + case SelectOutputDirection::Next: + return RS_(L"SelectOutputNextCommandKey"); + case SelectOutputDirection::Previous: + return RS_(L"SelectOutputPreviousCommandKey"); + } + return L""; + } + winrt::hstring SelectCommandArgs::GenerateName() const + { + switch (Direction()) + { + case SelectOutputDirection::Next: + return RS_(L"SelectCommandNextCommandKey"); + case SelectOutputDirection::Previous: + return RS_(L"SelectCommandPreviousCommandKey"); + } + return L""; + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 62fd9f9a08..6d83964cbb 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -34,17 +34,21 @@ #include "AddMarkArgs.g.h" #include "MoveTabArgs.g.h" #include "ToggleCommandPaletteArgs.g.h" +#include "SuggestionsArgs.g.h" #include "FindMatchArgs.g.h" #include "NewWindowArgs.g.h" #include "PrevTabArgs.g.h" #include "NextTabArgs.g.h" #include "RenameWindowArgs.g.h" +#include "SearchForTextArgs.g.h" #include "GlobalSummonArgs.g.h" #include "FocusPaneArgs.g.h" #include "ExportBufferArgs.g.h" #include "ClearBufferArgs.g.h" #include "MultipleActionsArgs.g.h" #include "AdjustOpacityArgs.g.h" +#include "SelectCommandArgs.g.h" +#include "SelectOutputArgs.g.h" #include "ColorSelectionArgs.g.h" #include "JsonUtils.h" @@ -56,12 +60,18 @@ #include "ActionArgsMagic.h" -#define ACTION_ARG(type, name, ...) \ -public: \ - type name() const noexcept { return _##name.has_value() ? _##name.value() : type{ __VA_ARGS__ }; } \ - void name(const type& value) noexcept { _##name = value; } \ - \ -private: \ +#define ACTION_ARG(type, name, ...) \ +public: \ + type name() const noexcept \ + { \ + return _##name.has_value() ? _##name.value() : type{ __VA_ARGS__ }; \ + } \ + void name(const type& value) noexcept \ + { \ + _##name = value; \ + } \ + \ +private: \ std::optional _##name{ std::nullopt }; // Notes on defining ActionArgs and ActionEventArgs: @@ -91,13 +101,15 @@ private: // false, if we don't really care if the parameter is required or not. //////////////////////////////////////////////////////////////////////////////// -#define COPY_TEXT_ARGS(X) \ - X(bool, SingleLine, "singleLine", false, false) \ +#define COPY_TEXT_ARGS(X) \ + X(bool, DismissSelection, "dismissSelection", false, true) \ + X(bool, SingleLine, "singleLine", false, false) \ X(Windows::Foundation::IReference, CopyFormatting, "copyFormatting", false, nullptr) //////////////////////////////////////////////////////////////////////////////// -#define MOVE_PANE_ARGS(X) \ - X(uint32_t, TabIndex, "index", false, 0) +#define MOVE_PANE_ARGS(X) \ + X(uint32_t, TabIndex, "index", false, 0) \ + X(winrt::hstring, Window, "window", false, L"") //////////////////////////////////////////////////////////////////////////////// #define SWITCH_TO_TAB_ARGS(X) \ @@ -172,8 +184,15 @@ private: X(Windows::Foundation::IReference, Index, "index", false, nullptr) //////////////////////////////////////////////////////////////////////////////// -#define MOVE_TAB_ARGS(X) \ - X(MoveTabDirection, Direction, "direction", args->Direction() == MoveTabDirection::None, MoveTabDirection::None) +// Interestingly, the order MATTERS here. Window has to be BEFORE Direction, +// because otherwise we won't have parsed the Window yet when we validate the +// Direction. +#define MOVE_TAB_ARGS(X) \ + X(winrt::hstring, Window, "window", false, L"") \ + X(MoveTabDirection, Direction, "direction", (args->Direction() == MoveTabDirection::None) && (args->Window().empty()), MoveTabDirection::None) + +// Other ideas: +// X(uint32_t, TabIndex, "index", false, 0) \ // target? source? //////////////////////////////////////////////////////////////////////////////// #define SCROLL_UP_ARGS(X) \ @@ -195,6 +214,11 @@ private: #define TOGGLE_COMMAND_PALETTE_ARGS(X) \ X(CommandPaletteLaunchMode, LaunchMode, "launchMode", false, CommandPaletteLaunchMode::Action) +//////////////////////////////////////////////////////////////////////////////// +#define SUGGESTIONS_ARGS(X) \ + X(SuggestionsSource, Source, "source", false, SuggestionsSource::Tasks) \ + X(bool, UseCommandline, "useCommandline", false, false) + //////////////////////////////////////////////////////////////////////////////// #define FIND_MATCH_ARGS(X) \ X(FindMatchDirection, Direction, "direction", args->Direction() == FindMatchDirection::None, FindMatchDirection::None) @@ -211,6 +235,10 @@ private: #define RENAME_WINDOW_ARGS(X) \ X(winrt::hstring, Name, "name", false, L"") +//////////////////////////////////////////////////////////////////////////////// +#define SEARCH_FOR_TEXT_ARGS(X) \ + X(winrt::hstring, QueryUrl, "queryUrl", false, L"") + //////////////////////////////////////////////////////////////////////////////// #define GLOBAL_SUMMON_ARGS(X) \ X(winrt::hstring, Name, "name", false, L"") \ @@ -236,6 +264,14 @@ private: X(int32_t, Opacity, "opacity", false, 0) \ X(bool, Relative, "relative", false, true) +//////////////////////////////////////////////////////////////////////////////// +#define SELECT_COMMAND_ARGS(X) \ + X(SelectOutputDirection, Direction, "direction", false, SelectOutputDirection::Previous) + +//////////////////////////////////////////////////////////////////////////////// +#define SELECT_OUTPUT_ARGS(X) \ + X(SelectOutputDirection, Direction, "direction", false, SelectOutputDirection::Previous) + //////////////////////////////////////////////////////////////////////////////// #define COLOR_SELECTION_ARGS(X) \ X(winrt::Microsoft::Terminal::Control::SelectionColor, Foreground, "foreground", false, nullptr) \ @@ -273,9 +309,12 @@ 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); + ACTION_ARG(Windows::Foundation::IReference, ReloadEnvironmentVariables, nullptr); + ACTION_ARG(uint64_t, ContentId); static constexpr std::string_view CommandlineKey{ "commandline" }; static constexpr std::string_view StartingDirectoryKey{ "startingDirectory" }; @@ -283,9 +322,12 @@ 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" }; + static constexpr std::string_view ReloadEnvironmentVariablesKey{ "reloadEnvironmentVariables" }; + static constexpr std::string_view ContentKey{ "__content" }; public: hstring GenerateName() const; @@ -302,9 +344,12 @@ 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; + otherAsUs->_Elevate == _Elevate && + otherAsUs->_ReloadEnvironmentVariables == _ReloadEnvironmentVariables && + otherAsUs->_ContentId == _ContentId; } return false; }; @@ -321,6 +366,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::GetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::GetValueForKey(json, ColorSchemeKey, args->_ColorScheme); JsonUtils::GetValueForKey(json, ElevateKey, args->_Elevate); + JsonUtils::GetValueForKey(json, ReloadEnvironmentVariablesKey, args->_ReloadEnvironmentVariables); + JsonUtils::GetValueForKey(json, ContentKey, args->_ContentId); return *args; } static Json::Value ToJson(const Model::NewTerminalArgs& val) @@ -340,6 +387,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation JsonUtils::SetValueForKey(json, SuppressApplicationTitleKey, args->_SuppressApplicationTitle); JsonUtils::SetValueForKey(json, ColorSchemeKey, args->_ColorScheme); JsonUtils::SetValueForKey(json, ElevateKey, args->_Elevate); + JsonUtils::SetValueForKey(json, ReloadEnvironmentVariablesKey, args->_ReloadEnvironmentVariables); + JsonUtils::SetValueForKey(json, ContentKey, args->_ContentId); return json; } Model::NewTerminalArgs Copy() const @@ -354,6 +403,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation copy->_SuppressApplicationTitle = _SuppressApplicationTitle; copy->_ColorScheme = _ColorScheme; copy->_Elevate = _Elevate; + copy->_ReloadEnvironmentVariables = _ReloadEnvironmentVariables; + copy->_ContentId = _ContentId; return *copy; } size_t Hash() const @@ -373,6 +424,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation h.write(SuppressApplicationTitle()); h.write(ColorScheme()); h.write(Elevate()); + h.write(ReloadEnvironmentVariables()); + h.write(ContentId()); } }; } @@ -436,6 +489,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); args->_TerminalArgs = NewTerminalArgs::FromJson(json); + + // Don't let the user specify the __content property in their + // settings. That's an internal-use-only property. + if (args->_TerminalArgs.ContentId()) + { + return { *args, { SettingsLoadWarnings::InvalidUseOfContent } }; + } return { *args, {} }; } static Json::Value ToJson(const IActionArgs& val) @@ -515,6 +575,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { return { nullptr, { SettingsLoadWarnings::InvalidSplitSize } }; } + + // Don't let the user specify the __content property in their + // settings. That's an internal-use-only property. + if (args->_TerminalArgs.ContentId()) + { + return { *args, { SettingsLoadWarnings::InvalidUseOfContent } }; + } + return { *args, {} }; } static Json::Value ToJson(const IActionArgs& val) @@ -574,6 +642,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); args->_TerminalArgs = NewTerminalArgs::FromJson(json); + + // Don't let the user specify the __content property in their + // settings. That's an internal-use-only property. + if (args->_TerminalArgs.ContentId()) + { + return { *args, { SettingsLoadWarnings::InvalidUseOfContent } }; + } + return { *args, {} }; } static Json::Value ToJson(const IActionArgs& val) @@ -649,6 +725,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(ToggleCommandPaletteArgs, TOGGLE_COMMAND_PALETTE_ARGS); + ACTION_ARGS_STRUCT(SuggestionsArgs, SUGGESTIONS_ARGS); + ACTION_ARGS_STRUCT(FindMatchArgs, FIND_MATCH_ARGS); ACTION_ARGS_STRUCT(PrevTabArgs, PREV_TAB_ARGS); @@ -657,6 +735,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(RenameWindowArgs, RENAME_WINDOW_ARGS); + ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS); + struct GlobalSummonArgs : public GlobalSummonArgsT { ACTION_ARG_BODY(GlobalSummonArgs, GLOBAL_SUMMON_ARGS) @@ -734,6 +814,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(AdjustOpacityArgs, ADJUST_OPACITY_ARGS); + ACTION_ARGS_STRUCT(SelectCommandArgs, SELECT_COMMAND_ARGS); + ACTION_ARGS_STRUCT(SelectOutputArgs, SELECT_OUTPUT_ARGS); + ACTION_ARGS_STRUCT(ColorSelectionArgs, COLOR_SELECTION_ARGS); } @@ -741,6 +824,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation { BASIC_FACTORY(ActionEventArgs); + BASIC_FACTORY(CopyTextArgs); BASIC_FACTORY(SwitchToTabArgs); BASIC_FACTORY(NewTerminalArgs); BASIC_FACTORY(NewTabArgs); @@ -770,4 +854,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(ClearBufferArgs); BASIC_FACTORY(MultipleActionsArgs); BASIC_FACTORY(AdjustOpacityArgs); + BASIC_FACTORY(SuggestionsArgs); + BASIC_FACTORY(SelectCommandArgs); + BASIC_FACTORY(SelectOutputArgs); } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index d24322072b..32f9005fde 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -80,6 +80,12 @@ namespace Microsoft.Terminal.Settings.Model Previous }; + enum SelectOutputDirection + { + Previous = 0, + Next, + }; + enum CommandPaletteLaunchMode { Action = 0, @@ -107,6 +113,15 @@ namespace Microsoft.Terminal.Settings.Model ToMouse, }; + [flags] + enum SuggestionsSource + { + Tasks = 0x1, + CommandHistory = 0x2, + DirectoryHistory = 0x4, + All = 0xffffffff, + }; + [default_interface] runtimeclass NewTerminalArgs { NewTerminalArgs(); NewTerminalArgs(Int32 profileIndex); @@ -117,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, @@ -131,6 +147,10 @@ namespace Microsoft.Terminal.Settings.Model // This needs to be an optional so that the default value (null) does // not modify whatever the profile's value is (either true or false) Windows.Foundation.IReference Elevate; + // Similarly with ReloadEnvironmentVariables + Windows.Foundation.IReference ReloadEnvironmentVariables; + + UInt64 ContentId{ get; set; }; Boolean Equals(NewTerminalArgs other); String GenerateName(); @@ -146,6 +166,8 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass CopyTextArgs : IActionArgs { + CopyTextArgs(); + Boolean DismissSelection { get; }; Boolean SingleLine { get; }; Windows.Foundation.IReference CopyFormatting { get; }; }; @@ -158,8 +180,9 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass MovePaneArgs : IActionArgs { - MovePaneArgs(UInt32 tabIndex); + MovePaneArgs(UInt32 tabIndex, String Window); UInt32 TabIndex; + String Window; }; [default_interface] runtimeclass SwitchToTabArgs : IActionArgs @@ -276,8 +299,9 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass MoveTabArgs : IActionArgs { - MoveTabArgs(MoveTabDirection direction); + MoveTabArgs(String window, MoveTabDirection direction); MoveTabDirection Direction { get; }; + String Window { get; }; }; [default_interface] runtimeclass ScrollUpArgs : IActionArgs @@ -306,6 +330,14 @@ namespace Microsoft.Terminal.Settings.Model CommandPaletteLaunchMode LaunchMode { get; }; }; + [default_interface] runtimeclass SuggestionsArgs : IActionArgs + { + SuggestionsArgs(); + SuggestionsArgs(SuggestionsSource source, Boolean useCommandline); + SuggestionsSource Source { get; }; + Boolean UseCommandline { get; }; + }; + [default_interface] runtimeclass FindMatchArgs : IActionArgs { FindMatchArgs(FindMatchDirection direction); @@ -338,6 +370,11 @@ namespace Microsoft.Terminal.Settings.Model String Name { get; }; }; + [default_interface] runtimeclass SearchForTextArgs : IActionArgs + { + String QueryUrl { get; }; + }; + [default_interface] runtimeclass GlobalSummonArgs : IActionArgs { String Name { get; }; @@ -384,4 +421,17 @@ namespace Microsoft.Terminal.Settings.Model Microsoft.Terminal.Control.SelectionColor Background; Microsoft.Terminal.Core.MatchMode MatchMode { get; }; }; + + [default_interface] runtimeclass SelectCommandArgs : IActionArgs + { + SelectCommandArgs(SelectOutputDirection direction); + SelectOutputDirection Direction { get; }; + } + [default_interface] runtimeclass SelectOutputArgs : IActionArgs + { + SelectOutputArgs(SelectOutputDirection direction); + SelectOutputDirection Direction { get; }; + } + + } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgsMagic.h b/src/cascadia/TerminalSettingsModel/ActionArgsMagic.h index 506083428f..5ec59c34f4 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgsMagic.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgsMagic.h @@ -41,7 +41,7 @@ struct InitListPlaceholder // some element of the class definition that will use the x-macro. // // You'll author a new arg by: -// 1: define a new x-macro above with all it's properties +// 1: define a new x-macro above with all its properties // 2. Define the class with: // // ACTION_ARGS_STRUCT(MyFooArgs, MY_FOO_ARGS); diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index a3a5124a3d..76565dff44 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -4,12 +4,14 @@ #include "pch.h" #include "AllShortcutActions.h" #include "ActionMap.h" +#include "Command.h" #include "AllShortcutActions.h" #include "ActionMap.g.cpp" using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Windows::Foundation::Collections; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { @@ -118,7 +120,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Method Description: // - Retrieves a map of actions that can be bound to a key - Windows::Foundation::Collections::IMapView ActionMap::AvailableActions() + IMapView ActionMap::AvailableActions() { if (!_AvailableActionsCache) { @@ -172,7 +174,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // - Retrieves a map of command names to the commands themselves // - These commands should not be modified directly because they may result in // an invalid state for the `ActionMap` - Windows::Foundation::Collections::IMapView ActionMap::NameMap() + IMapView ActionMap::NameMap() { if (!_NameMapCache) { @@ -283,7 +285,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return cumulativeActions; } - Windows::Foundation::Collections::IMapView ActionMap::GlobalHotkeys() + IMapView ActionMap::GlobalHotkeys() { if (!_GlobalHotkeysCache) { @@ -292,7 +294,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return _GlobalHotkeysCache.GetView(); } - Windows::Foundation::Collections::IMapView ActionMap::KeyBindings() + IMapView ActionMap::KeyBindings() { if (!_KeyBindingMapCache) { @@ -854,4 +856,147 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation cmd->ActionAndArgs(action); AddAction(*cmd); } + + void ActionMap::_recursiveUpdateCommandKeybindingLabels() + { + const auto& commands{ _ExpandedCommandsCache }; + + for (const auto& command : commands) + { + if (command.HasNestedCommands()) + { + _recursiveUpdateCommandKeybindingLabels(); + } + else + { + // If there's a keybinding that's bound to exactly this command, + // then get the keychord and display it as a + // part of the command in the UI. + // We specifically need to do this for nested commands. + const auto keyChord{ GetKeyBindingForAction(command.ActionAndArgs().Action(), + command.ActionAndArgs().Args()) }; + command.RegisterKey(keyChord); + } + } + } + + // This is a helper to aid in sorting commands by their `Name`s, alphabetically. + static bool _compareSchemeNames(const ColorScheme& lhs, const ColorScheme& rhs) + { + std::wstring leftName{ lhs.Name() }; + std::wstring rightName{ rhs.Name() }; + return leftName.compare(rightName) < 0; + } + + void ActionMap::ExpandCommands(const IVectorView& profiles, + const IMapView& schemes) + { + // TODO in review - It's a little weird to stash the expanded commands + // into a separate map. Is it possible to just replace the name map with + // the post-expanded commands? + // + // WHILE also making sure that upon re-saving the commands, we don't + // actually serialize the results of the expansion. I don't think it is. + + std::vector sortedSchemes; + sortedSchemes.reserve(schemes.Size()); + + for (const auto& nameAndScheme : schemes) + { + sortedSchemes.push_back(nameAndScheme.Value()); + } + std::sort(sortedSchemes.begin(), + sortedSchemes.end(), + _compareSchemeNames); + + auto copyOfCommands = winrt::single_threaded_map(); + + const auto& commandsToExpand{ NameMap() }; + for (auto nameAndCommand : commandsToExpand) + { + copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value()); + } + + implementation::Command::ExpandCommands(copyOfCommands, + profiles, + winrt::param::vector_view{ sortedSchemes }); + + _ExpandedCommandsCache = winrt::single_threaded_vector(); + for (const auto& [_, command] : copyOfCommands) + { + _ExpandedCommandsCache.Append(command); + } + } + IVector ActionMap::ExpandedCommands() + { + 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 6dbd2b04c2..4270a08111 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -75,6 +75,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void DeleteKeyBinding(const Control::KeyChord& keys); void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action); + Windows::Foundation::Collections::IVector ExpandedCommands(); + 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; @@ -90,11 +96,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd); void _TryUpdateKeyChord(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& consolidatedCmd); + void _recursiveUpdateCommandKeybindingLabels(); + Windows::Foundation::Collections::IMap _AvailableActionsCache{ nullptr }; Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; Windows::Foundation::Collections::IMap _GlobalHotkeysCache{ nullptr }; Windows::Foundation::Collections::IMap _KeyBindingMapCache{ nullptr }; + Windows::Foundation::Collections::IVector _ExpandedCommandsCache{ nullptr }; + std::unordered_map _NestedCommands; std::vector _IterableCommands; std::unordered_map _KeyMap; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index 806baa17a3..b84305b9d0 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -20,6 +20,10 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView NameMap { get; }; Windows.Foundation.Collections.IMapView KeyBindings { get; }; Windows.Foundation.Collections.IMapView GlobalHotkeys { get; }; + + IVector ExpandedCommands { get; }; + + IVector FilterToSendInput(String CurrentCommandline); }; [default_interface] runtimeclass ActionMap : IActionMapView diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index efb861667c..f9d934e36e 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -77,12 +77,16 @@ ON_ALL_ACTIONS(MoveTab) \ ON_ALL_ACTIONS(BreakIntoDebugger) \ ON_ALL_ACTIONS(TogglePaneReadOnly) \ + ON_ALL_ACTIONS(EnablePaneReadOnly) \ + ON_ALL_ACTIONS(DisablePaneReadOnly) \ ON_ALL_ACTIONS(FindMatch) \ ON_ALL_ACTIONS(NewWindow) \ ON_ALL_ACTIONS(IdentifyWindow) \ ON_ALL_ACTIONS(IdentifyWindows) \ ON_ALL_ACTIONS(RenameWindow) \ ON_ALL_ACTIONS(OpenWindowRenamer) \ + ON_ALL_ACTIONS(DisplayWorkingDirectory) \ + ON_ALL_ACTIONS(SearchForText) \ ON_ALL_ACTIONS(GlobalSummon) \ ON_ALL_ACTIONS(QuakeMode) \ ON_ALL_ACTIONS(FocusPane) \ @@ -94,12 +98,19 @@ ON_ALL_ACTIONS(AdjustOpacity) \ ON_ALL_ACTIONS(RestoreLastClosed) \ ON_ALL_ACTIONS(SelectAll) \ + ON_ALL_ACTIONS(SelectCommand) \ + ON_ALL_ACTIONS(SelectOutput) \ ON_ALL_ACTIONS(MarkMode) \ ON_ALL_ACTIONS(ToggleBlockSelection) \ ON_ALL_ACTIONS(SwitchSelectionEndpoint) \ + ON_ALL_ACTIONS(Suggestions) \ ON_ALL_ACTIONS(ColorSelection) \ + ON_ALL_ACTIONS(ShowContextMenu) \ ON_ALL_ACTIONS(ExpandSelectionToWord) \ - ON_ALL_ACTIONS(CloseOtherPanes) + ON_ALL_ACTIONS(CloseOtherPanes) \ + ON_ALL_ACTIONS(RestartConnection) \ + ON_ALL_ACTIONS(ToggleBroadcastInput) \ + ON_ALL_ACTIONS(OpenAbout) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize) \ @@ -109,6 +120,7 @@ ON_ALL_ACTIONS_WITH_ARGS(CopyText) \ ON_ALL_ACTIONS_WITH_ARGS(ExecuteCommandline) \ ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \ + ON_ALL_ACTIONS_WITH_ARGS(SearchForText) \ ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \ ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \ ON_ALL_ACTIONS_WITH_ARGS(MovePane) \ @@ -140,4 +152,7 @@ ON_ALL_ACTIONS_WITH_ARGS(ClearBuffer) \ ON_ALL_ACTIONS_WITH_ARGS(MultipleActions) \ ON_ALL_ACTIONS_WITH_ARGS(AdjustOpacity) \ + ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \ + ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \ + ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \ ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp index d48763eca1..90513b146c 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp @@ -18,6 +18,7 @@ static constexpr std::string_view SelectionBackgroundKey{ "selectionBackground" static constexpr std::string_view CursorColorKey{ "cursorColor" }; static constexpr std::string_view LegacyAcrylicTransparencyKey{ "acrylicOpacity" }; static constexpr std::string_view OpacityKey{ "opacity" }; +static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; AppearanceConfig::AppearanceConfig(winrt::weak_ref sourceProfile) : _sourceProfile(std::move(sourceProfile)) @@ -33,6 +34,9 @@ winrt::com_ptr AppearanceConfig::CopyAppearance(const Appearan appearance->_CursorColor = source->_CursorColor; appearance->_Opacity = source->_Opacity; + appearance->_DarkColorSchemeName = source->_DarkColorSchemeName; + appearance->_LightColorSchemeName = source->_LightColorSchemeName; + #define APPEARANCE_SETTINGS_COPY(type, name, jsonKey, ...) \ appearance->_##name = source->_##name; MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_COPY) @@ -50,6 +54,19 @@ Json::Value AppearanceConfig::ToJson() const JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground); JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor); JsonUtils::SetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); + if (HasDarkColorSchemeName() || HasLightColorSchemeName()) + { + // check if the setting is coming from the UI, if so grab the ColorSchemeName until the settings UI is fixed. + if (_LightColorSchemeName != _DarkColorSchemeName) + { + JsonUtils::SetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName); + JsonUtils::SetValueForKey(json["colorScheme"], "light", _LightColorSchemeName); + } + else + { + JsonUtils::SetValueForKey(json, "colorScheme", _DarkColorSchemeName); + } + } #define APPEARANCE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \ JsonUtils::SetValueForKey(json, jsonKey, _##name); @@ -79,6 +96,18 @@ void AppearanceConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, LegacyAcrylicTransparencyKey, _Opacity); JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter{}); + if (json["colorScheme"].isString()) + { + // to make the UI happy, set ColorSchemeName. + JsonUtils::GetValueForKey(json, ColorSchemeKey, _DarkColorSchemeName); + _LightColorSchemeName = _DarkColorSchemeName; + } + else if (json["colorScheme"].isObject()) + { + // to make the UI happy, set ColorSchemeName to whatever the dark value is. + JsonUtils::GetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName); + JsonUtils::GetValueForKey(json["colorScheme"], "light", _LightColorSchemeName); + } #define APPEARANCE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \ JsonUtils::GetValueForKey(json, jsonKey, _##name); diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h index bbc30927eb..7e8f35e663 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h @@ -42,6 +42,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, nullptr); INHERITABLE_SETTING(Model::IAppearanceConfig, double, Opacity, 1.0); + INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, DarkColorSchemeName, L"Campbell"); + INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, LightColorSchemeName, L"Campbell"); + #define APPEARANCE_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \ INHERITABLE_SETTING(Model::IAppearanceConfig, type, name, ##__VA_ARGS__) MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_INITIALIZE) diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp index 9aa3260582..6f684f91d6 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.cpp +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.cpp @@ -80,7 +80,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto data = til::u16u8(str); std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) @@ -137,7 +137,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // and resets it to the defaults. This will delete the state file! That's // the sure-fire way to make sure the data doesn't come back. If we leave // it untouched, then when we go to write the file back out, we'll first - // re-read it's contents and try to overlay our new state. However, + // re-read its contents and try to overlay our new state. However, // nullopts won't remove keys from the JSON, so we'll end up with the // original state in the file. // Arguments: @@ -161,7 +161,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation try { std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; // First get shared state out of `state.json`. const auto sharedData = _readSharedContents().value_or(std::string{}); @@ -177,7 +177,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // from state.json. We'll then load the Local props from // `elevated-state.json` // - If we're unelevated, then load _everything_ from state.json. - if (::Microsoft::Console::Utils::IsElevated()) + if (::Microsoft::Console::Utils::IsRunningElevated()) { // Only load shared properties if we're elevated FromJson(root, FileSource::Shared); @@ -225,10 +225,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // // After that's done, we'll write our Local properties into // elevated-state.json. - if (::Microsoft::Console::Utils::IsElevated()) + if (::Microsoft::Console::Utils::IsRunningElevated()) { std::string errs; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; // First load the contents of state.json into a json blob. This will @@ -353,7 +353,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // permissions, so we don't potentially read malicious data. std::optional ApplicationState::_readLocalContents() const { - return ::Microsoft::Console::Utils::IsElevated() ? + return ::Microsoft::Console::Utils::IsRunningElevated() ? ReadUTF8FileIfExists(_elevatedPath, true) : ReadUTF8FileIfExists(_sharedPath, false); } @@ -374,13 +374,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // will atomically write to `user-state.json` void ApplicationState::_writeLocalContents(const std::string_view content) const { - if (::Microsoft::Console::Utils::IsElevated()) + if (::Microsoft::Console::Utils::IsRunningElevated()) { // DON'T use WriteUTF8FileAtomic, which will write to a temporary file // then rename that file to the final filename. That actually lets us // overwrite the elevate file's contents even when unelevated, because // we're effectively deleting the original file, then renaming a - // different file in it's place. + // different file in its place. // // We're not worried about someone else doing that though, if they do // that with the wrong permissions, then we'll just ignore the file and diff --git a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp index a69f44eaec..948b1822f8 100644 --- a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp @@ -31,7 +31,8 @@ void AzureCloudShellGenerator::GenerateProfiles(std::vectorStartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY }); - azureCloudShellProfile->DefaultAppearance().ColorSchemeName(L"Vintage"); + azureCloudShellProfile->DefaultAppearance().DarkColorSchemeName(L"Vintage"); + azureCloudShellProfile->DefaultAppearance().LightColorSchemeName(L"Vintage"); azureCloudShellProfile->ConnectionType(AzureConnection::ConnectionType()); profiles.emplace_back(std::move(azureCloudShellProfile)); } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 9712c6e51c..851e667ac9 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -15,6 +15,7 @@ #include #include #include +#include using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings; @@ -295,7 +296,7 @@ Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source) MTSM_PROFILE_SETTINGS(DUPLICATE_PROFILE_SETTINGS) #undef DUPLICATE_PROFILE_SETTINGS - // These two aren't in MTSM_PROFILE_SETTINGS because they're special + // These aren't in MTSM_PROFILE_SETTINGS because they're special DUPLICATE_SETTING_MACRO(TabColor); DUPLICATE_SETTING_MACRO(Padding); @@ -326,6 +327,8 @@ Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source) DUPLICATE_SETTING_MACRO_SUB(appearance, target, SelectionBackground); DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorColor); DUPLICATE_SETTING_MACRO_SUB(appearance, target, Opacity); + DUPLICATE_SETTING_MACRO_SUB(appearance, target, DarkColorSchemeName); + DUPLICATE_SETTING_MACRO_SUB(appearance, target, LightColorSchemeName); } // UnfocusedAppearance is treated as a single setting, @@ -415,6 +418,7 @@ void CascadiaSettings::_validateSettings() _validateKeybindings(); _validateColorSchemesInCommands(); _validateThemeExists(); + _validateProfileEnvironmentVariables(); } // Method Description: @@ -430,22 +434,29 @@ void CascadiaSettings::_validateSettings() void CascadiaSettings::_validateAllSchemesExist() { const auto colorSchemes = _globals->ColorSchemes(); - auto foundInvalidScheme = false; + auto foundInvalidDarkScheme = false; + auto foundInvalidLightScheme = false; for (const auto& profile : _allProfiles) { for (const auto& appearance : std::array{ profile.DefaultAppearance(), profile.UnfocusedAppearance() }) { - if (appearance && !colorSchemes.HasKey(appearance.ColorSchemeName())) + if (appearance && !colorSchemes.HasKey(appearance.DarkColorSchemeName())) { - // Clear the user set color scheme. We'll just fallback instead. - appearance.ClearColorSchemeName(); - foundInvalidScheme = true; + // Clear the user set dark color scheme. We'll just fallback instead. + appearance.ClearDarkColorSchemeName(); + foundInvalidDarkScheme = true; + } + if (appearance && !colorSchemes.HasKey(appearance.LightColorSchemeName())) + { + // Clear the user set light color scheme. We'll just fallback instead. + appearance.ClearLightColorSchemeName(); + foundInvalidLightScheme = true; } } } - if (foundInvalidScheme) + if (foundInvalidDarkScheme || foundInvalidLightScheme) { _warnings.Append(SettingsLoadWarnings::UnknownColorScheme); } @@ -532,6 +543,30 @@ void CascadiaSettings::_validateMediaResources() } } +// Method Description: +// - Checks if the profiles contain multiple environment variables with the same name, but different +// cases +void CascadiaSettings::_validateProfileEnvironmentVariables() +{ + for (const auto& profile : _allProfiles) + { + std::set envVarNames{}; + if (profile.EnvironmentVariables() == nullptr) + { + continue; + } + for (const auto [key, value] : profile.EnvironmentVariables()) + { + const auto iterator = envVarNames.insert(key.c_str()); + if (!iterator.second) + { + _warnings.Append(SettingsLoadWarnings::InvalidProfileEnvironmentVariables); + return; + } + } + } +} + // Method Description: // - Helper to get the GUID of a profile, given an optional index and a possible // "profile" value to override that. @@ -937,24 +972,6 @@ bool CascadiaSettings::_hasInvalidColorScheme(const Model::Command& command) con return invalid; } -// Method Description: -// - Lookup the color scheme for a given profile. If the profile doesn't exist, -// or the scheme name listed in the profile doesn't correspond to a scheme, -// this will return `nullptr`. -// Arguments: -// - profileGuid: the GUID of the profile to find the scheme for. -// Return Value: -// - a non-owning pointer to the scheme. -Model::ColorScheme CascadiaSettings::GetColorSchemeForProfile(const Model::Profile& profile) const -{ - if (!profile) - { - return nullptr; - } - const auto schemeName = profile.DefaultAppearance().ColorSchemeName(); - return _globals->ColorSchemes().TryLookup(schemeName); -} - // Method Description: // - updates all references to that color scheme with the new name // Arguments: @@ -966,26 +983,41 @@ void CascadiaSettings::UpdateColorSchemeReferences(const winrt::hstring& oldName { // update profiles.defaults, if necessary if (_baseLayerProfile && - _baseLayerProfile->DefaultAppearance().HasColorSchemeName() && - _baseLayerProfile->DefaultAppearance().ColorSchemeName() == oldName) + _baseLayerProfile->DefaultAppearance().HasDarkColorSchemeName() && + _baseLayerProfile->DefaultAppearance().DarkColorSchemeName() == oldName) { - _baseLayerProfile->DefaultAppearance().ColorSchemeName(newName); + _baseLayerProfile->DefaultAppearance().DarkColorSchemeName(newName); + } + // NOT else-if, because both could match + if (_baseLayerProfile && + _baseLayerProfile->DefaultAppearance().HasLightColorSchemeName() && + _baseLayerProfile->DefaultAppearance().LightColorSchemeName() == oldName) + { + _baseLayerProfile->DefaultAppearance().LightColorSchemeName(newName); } // update all profiles referencing this color scheme for (const auto& profile : _allProfiles) { const auto defaultAppearance = profile.DefaultAppearance(); - if (defaultAppearance.HasColorSchemeName() && defaultAppearance.ColorSchemeName() == oldName) + if (defaultAppearance.HasLightColorSchemeName() && defaultAppearance.LightColorSchemeName() == oldName) { - defaultAppearance.ColorSchemeName(newName); + defaultAppearance.LightColorSchemeName(newName); + } + if (defaultAppearance.HasDarkColorSchemeName() && defaultAppearance.DarkColorSchemeName() == oldName) + { + defaultAppearance.DarkColorSchemeName(newName); } - if (profile.UnfocusedAppearance()) + if (auto unfocused{ profile.UnfocusedAppearance() }) { - if (profile.UnfocusedAppearance().HasColorSchemeName() && profile.UnfocusedAppearance().ColorSchemeName() == oldName) + if (unfocused.HasLightColorSchemeName() && unfocused.LightColorSchemeName() == oldName) { - profile.UnfocusedAppearance().ColorSchemeName(newName); + unfocused.LightColorSchemeName(newName); + } + if (unfocused.HasDarkColorSchemeName() && unfocused.DarkColorSchemeName() == oldName) + { + unfocused.DarkColorSchemeName(newName); } } } @@ -1000,7 +1032,7 @@ winrt::hstring CascadiaSettings::ApplicationDisplayName() } CATCH_LOG(); - return RS_(L"ApplicationDisplayNameUnpackaged"); + return IsPortableMode() ? RS_(L"ApplicationDisplayNamePortable") : RS_(L"ApplicationDisplayNameUnpackaged"); } winrt::hstring CascadiaSettings::ApplicationVersion() @@ -1076,7 +1108,21 @@ bool CascadiaSettings::IsDefaultTerminalAvailable() noexcept DWORDLONG dwlConditionMask = 0; VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); - return VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE; + if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) + { + return true; + } + + static bool isOtherwiseAvailable = [] { + wil::unique_hkey key; + const auto lResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\SystemSettings\\SettingId\\SystemSettings_Developer_Mode_Setting_DefaultTerminalApp", + 0, + KEY_READ, + &key); + return static_cast(key) && ERROR_SUCCESS == lResult; + }(); + return isOtherwiseAvailable; } bool CascadiaSettings::IsDefaultTerminalSet() noexcept @@ -1161,7 +1207,8 @@ void CascadiaSettings::ExportFile(winrt::hstring path, winrt::hstring content) void CascadiaSettings::_validateThemeExists() { - if (_globals->Themes().Size() == 0) + const auto& themes{ _globals->Themes() }; + if (themes.Size() == 0) { // We didn't even load the default themes. This should only be possible // if the defaults.json didn't include any themes, or if no @@ -1172,14 +1219,38 @@ void CascadiaSettings::_validateThemeExists() auto newTheme = winrt::make_self(); newTheme->Name(L"system"); _globals->AddTheme(*newTheme); - _globals->Theme(L"system"); + _globals->Theme(Model::ThemePair{ L"system" }); } - if (!_globals->Themes().HasKey(_globals->Theme())) + const auto& theme{ _globals->Theme() }; + if (theme.DarkName() == theme.LightName()) { - _warnings.Append(SettingsLoadWarnings::UnknownTheme); - - // safely fall back to system as the theme. - _globals->Theme(L"system"); + // Only one theme. We'll treat it as such. + if (!themes.HasKey(theme.DarkName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + // safely fall back to system as the theme. + _globals->Theme(*winrt::make_self(L"system")); + } + } + else + { + // Two different themes. Check each separately, and fall back to a + // reasonable default contextually + if (!themes.HasKey(theme.LightName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + theme.LightName(L"light"); + } + if (!themes.HasKey(theme.DarkName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + theme.DarkName(L"dark"); + } } } + +void CascadiaSettings::ExpandCommands() +{ + _globals->ExpandCommands(ActiveProfiles().GetView(), GlobalSettings().ColorSchemes()); +} diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 85e594f965..a9c4b9bb32 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -80,7 +80,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static void _rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString); static Json::Value _parseJSON(const std::string_view& content); static const Json::Value& _getJSONValue(const Json::Value& json, const std::string_view& key) noexcept; - gsl::span> _getNonUserOriginProfiles() const; + std::span> _getNonUserOriginProfiles() const; void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings); void _parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings); static JsonSettings _parseJson(const std::string_view& content); @@ -99,12 +99,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation public: static Model::CascadiaSettings LoadDefaults(); static Model::CascadiaSettings LoadAll(); - static Model::CascadiaSettings LoadUniversal(); static winrt::hstring SettingsPath(); static winrt::hstring DefaultSettingsPath(); static winrt::hstring ApplicationDisplayName(); static winrt::hstring ApplicationVersion(); + static bool IsPortableMode(); static void ExportFile(winrt::hstring path, winrt::hstring content); CascadiaSettings() noexcept = default; @@ -124,7 +124,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Model::Profile ProfileDefaults() const; Model::Profile CreateNewProfile(); Model::Profile FindProfile(const winrt::guid& guid) const noexcept; - Model::ColorScheme GetColorSchemeForProfile(const Model::Profile& profile) const; void UpdateColorSchemeReferences(const winrt::hstring& oldName, const winrt::hstring& newName); Model::Profile GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const; Model::Profile GetProfileByName(const winrt::hstring& name) const; @@ -144,6 +143,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Model::DefaultTerminal CurrentDefaultTerminal() noexcept; void CurrentDefaultTerminal(const Model::DefaultTerminal& terminal); + void ExpandCommands(); + private: static const std::filesystem::path& _settingsPath(); static const std::filesystem::path& _releaseSettingsPath(); @@ -154,15 +155,20 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _refreshDefaultTerminals(); void _resolveDefaultProfile() const; + void _resolveNewTabMenuProfiles() const; + void _resolveNewTabMenuProfilesSet(const winrt::Windows::Foundation::Collections::IVector entries, winrt::Windows::Foundation::Collections::IMap& remainingProfiles, Model::RemainingProfilesEntry& remainingProfilesEntry) const; void _validateSettings(); void _validateAllSchemesExist(); void _validateMediaResources(); + void _validateProfileEnvironmentVariables(); void _validateKeybindings() const; void _validateColorSchemesInCommands() const; bool _hasInvalidColorScheme(const Model::Command& command) const; void _validateThemeExists(); + void _researchOnLoad(); + // user settings winrt::hstring _hash; winrt::com_ptr _globals = winrt::make_self(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index fb69e58ffc..1889871bb3 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -11,10 +11,10 @@ namespace Microsoft.Terminal.Settings.Model [default_interface] runtimeclass CascadiaSettings { static CascadiaSettings LoadDefaults(); static CascadiaSettings LoadAll(); - static CascadiaSettings LoadUniversal(); static String SettingsPath { get; }; static String DefaultSettingsPath { get; }; + static Boolean IsPortableMode { get; }; static String ApplicationDisplayName { get; }; static String ApplicationVersion { get; }; @@ -45,7 +45,6 @@ namespace Microsoft.Terminal.Settings.Model Profile CreateNewProfile(); Profile FindProfile(Guid profileGuid); - ColorScheme GetColorSchemeForProfile(Profile profile); void UpdateColorSchemeReferences(String oldName, String newName); Profile GetProfileForArgs(NewTerminalArgs newTerminalArgs); @@ -54,5 +53,7 @@ namespace Microsoft.Terminal.Settings.Model static Boolean IsDefaultTerminalSet { get; }; IObservableVector DefaultTerminals { get; }; DefaultTerminal CurrentDefaultTerminal; + + void ExpandCommands(); } } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 0c6cf50309..8964169f20 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -13,11 +13,13 @@ #include "PowershellCoreProfileGenerator.h" #include "VisualStudioGenerator.h" #include "WslDistroGenerator.h" +#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED +#include "SshHostGenerator.h" +#endif // The following files are generated at build time into the "Generated Files" directory. -// defaults(-universal).h is a file containing the default json settings in a std::string_view. +// defaults.h is a file containing the default json settings in a std::string_view. #include "defaults.h" -#include "defaults-universal.h" // userDefault.h is like the above, but with a default template for the user's settings.json. #include @@ -28,6 +30,10 @@ #include "DefaultTerminal.h" #include "FileUtils.h" +#include "ProfileEntry.h" +#include "FolderEntry.h" +#include "MatchProfilesEntry.h" + using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::ApplicationModel::AppExtensions; using namespace winrt::Microsoft::Terminal::Settings; @@ -45,6 +51,18 @@ static constexpr std::string_view ThemesKey{ "themes" }; constexpr std::wstring_view systemThemeName{ L"system" }; constexpr std::wstring_view darkThemeName{ L"dark" }; constexpr std::wstring_view lightThemeName{ L"light" }; +constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" }; +constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" }; +constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" }; + +static constexpr std::array builtinThemes{ + systemThemeName, + lightThemeName, + darkThemeName, + legacySystemThemeName, + legacyLightThemeName, + legacyDarkThemeName, +}; static constexpr std::wstring_view jsonExtension{ L".json" }; static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" }; @@ -148,6 +166,9 @@ void SettingsLoader::GenerateProfiles() _executeGenerator(WslDistroGenerator{}); _executeGenerator(AzureCloudShellGenerator{}); _executeGenerator(VisualStudioGenerator{}); +#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED + _executeGenerator(SshHostGenerator{}); +#endif } // A new settings.json gets a special treatment: @@ -429,6 +450,17 @@ bool SettingsLoader::FixupUserSettings() } } + // Terminal 1.19: Migrate the global + // `compatibility.reloadEnvironmentVariables` to being a per-profile + // setting. If the user had it disabled in 1.18, then set the + // profiles.defaults value to false to match. + if (!userSettings.globals->LegacyReloadEnvironmentVariables()) + { + // migrate the user's opt-out to the profiles.defaults + userSettings.baseLayerProfile->ReloadEnvironmentVariables(false); + fixedUp = true; + } + return fixedUp; } @@ -490,7 +522,7 @@ Json::Value SettingsLoader::_parseJSON(const std::string_view& content) { Json::Value json; std::string errs; - const std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + const std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; if (!reader->parse(content.data(), content.data() + content.size(), &json, &errs)) { @@ -521,10 +553,10 @@ const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const // Thus no matter how many profiles are added later on, the following condition holds true: // The userSettings.profiles in the range [0, _userProfileCount) contain all profiles specified by the user. // In turn all profiles in the range [_userProfileCount, ∞) contain newly generated/added profiles. -// gsl::make_span(userSettings.profiles).subspan(_userProfileCount) gets us the latter range. -gsl::span> SettingsLoader::_getNonUserOriginProfiles() const +// std::span{ userSettings.profiles }.subspan(_userProfileCount) gets us the latter range. +std::span> SettingsLoader::_getNonUserOriginProfiles() const { - return gsl::make_span(userSettings.profiles).subspan(_userProfileCount); + return std::span{ userSettings.profiles }.subspan(_userProfileCount); } // Parses the given JSON string ("content") and fills a ParsedSettings instance with it. @@ -552,8 +584,10 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source { if (const auto theme = Theme::FromJson(themeJson)) { + const auto& name{ theme->Name() }; + if (origin != OriginTag::InBox && - (theme->Name() == systemThemeName || theme->Name() == lightThemeName || theme->Name() == darkThemeName)) + (std::ranges::find(builtinThemes, name) != builtinThemes.end())) { // If the theme didn't come from the in-box themes, and its // name was one of the reserved names, then just ignore it. @@ -781,7 +815,7 @@ void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator { const winrt::hstring source{ generatorNamespace }; - for (const auto& profile : gsl::span(inboxSettings.profiles).subspan(previousSize)) + for (const auto& profile : std::span(inboxSettings.profiles).subspan(previousSize)) { profile->Origin(OriginTag::Generated); profile->Source(source); @@ -812,9 +846,9 @@ try // read settings.json from the Release stable file path if it exists. // Otherwise use default settings file provided from original settings file bool releaseSettingExists = false; - if (firstTimeSetup) + if (firstTimeSetup && !IsPortableMode()) { -#if defined(WT_BRANDING_PREVIEW) +#if defined(WT_BRANDING_PREVIEW) || defined(WT_BRANDING_CANARY) { try { @@ -893,24 +927,7 @@ try settings->_hash = _calculateHash(settingsString, lastWriteTime); } - // GH#13936: We're interested in how many users opt out of useAtlasEngine, - // indicating major issues that would require us to disable it by default again. - { - size_t enabled[2]{}; - for (const auto& profile : settings->_activeProfiles) - { - enabled[profile.UseAtlasEngine()]++; - } - - TraceLoggingWrite( - g_hSettingsModelProvider, - "AtlasEngine_Usage", - TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in/out of useAtlasEngine"), - TraceLoggingUIntPtr(enabled[0], "UseAtlasEngineDisabled", "Number of profiles for which AtlasEngine is disabled"), - TraceLoggingUIntPtr(enabled[1], "UseAtlasEngineEnabled", "Number of profiles for which AtlasEngine is enabled"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } + settings->_researchOnLoad(); return *settings; } @@ -927,20 +944,105 @@ catch (const SettingsTypedDeserializationException& e) return *settings; } -// Function Description: -// - Loads a batch of settings curated for the Universal variant of the terminal app -// Arguments: -// - -// Return Value: -// - a unique_ptr to a CascadiaSettings with the connection types and settings for Universal terminal -Model::CascadiaSettings CascadiaSettings::LoadUniversal() +void CascadiaSettings::_researchOnLoad() { - return *winrt::make_self(std::string_view{}, DefaultUniversalJson); + // Only do this if we're actually being sampled + if (TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES)) + { + // GH#13936: We're interested in how many users opt out of useAtlasEngine, + // indicating major issues that would require us to disable it by default again. + { + size_t enabled[2]{}; + for (const auto& profile : _activeProfiles) + { + enabled[profile.UseAtlasEngine()]++; + } + + TraceLoggingWrite( + g_hSettingsModelProvider, + "AtlasEngine_Usage", + TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in/out of useAtlasEngine"), + TraceLoggingUIntPtr(enabled[0], "UseAtlasEngineDisabled", "Number of profiles for which AtlasEngine is disabled"), + TraceLoggingUIntPtr(enabled[1], "UseAtlasEngineEnabled", "Number of profiles for which AtlasEngine is enabled"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + // ----------------------------- RE: Themes ---------------------------- + const auto numThemes = GlobalSettings().Themes().Size(); + const auto themeInUse = GlobalSettings().CurrentTheme().Name(); + const auto changedTheme = GlobalSettings().HasTheme(); + + // system: 0 + // light: 1 + // dark: 2 + // a custom theme: 3 + // system (legacy): 4 + // light (legacy): 5 + // dark (legacy): 6 + const auto themeChoice = themeInUse == L"system" ? 0 : + themeInUse == L"light" ? 1 : + themeInUse == L"dark" ? 2 : + themeInUse == L"legacyDark" ? 4 : + themeInUse == L"legacyLight" ? 5 : + themeInUse == L"legacySystem" ? 6 : + 3; + + TraceLoggingWrite( + g_hSettingsModelProvider, + "ThemesInUse", + TraceLoggingDescription("Data about the themes in use"), + TraceLoggingBool(themeChoice, "Identifier for the theme chosen. 0 is system, 1 is light, 2 is dark, and 3 indicates any custom theme."), + TraceLoggingBool(changedTheme, "True if the user actually changed the theme from the default theme"), + TraceLoggingInt32(numThemes, "Number of themes in the user's settings"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + // --------------------------- RE: sendInput --------------------------- + auto collectSendInput = [&]() { + auto totalSendInput = 0; + const auto& allActions = GlobalSettings().ActionMap().AvailableActions(); + for (const auto&& [name, actionAndArgs] : allActions) + { + if (actionAndArgs.Action() == ShortcutAction::SendInput) + { + totalSendInput++; + } + } + return totalSendInput; + }; + + TraceLoggingWrite( + g_hSettingsModelProvider, + "SendInputUsage", + TraceLoggingDescription("Event emitted upon settings load, containing the number of sendInput actions a user has"), + TraceLoggingInt32(collectSendInput(), "Number of sendInput actions in the user's settings"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + // ------------------------ RE: autoMarkPrompts ------------------------ + auto totalAutoMark = 0; + auto totalShowMarks = 0; + for (const auto&& p : AllProfiles()) + { + totalAutoMark += p.AutoMarkPrompts() ? 1 : 0; + totalShowMarks += p.ShowMarks() ? 1 : 0; + } + + TraceLoggingWrite( + g_hSettingsModelProvider, + "MarksProfilesUsage", + TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in to scrollbar marks"), + TraceLoggingInt32(totalAutoMark, "Number of profiles for which AutoMarkPrompts is enabled"), + TraceLoggingInt32(totalShowMarks, "Number of profiles for which ShowMarks is enabled"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } } // Function Description: // - Creates a new CascadiaSettings object initialized with settings from the -// hardcoded defaults.json. +// hard-coded defaults.json. // Arguments: // - // Return Value: @@ -1025,7 +1127,10 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) : _warnings = winrt::single_threaded_vector(std::move(warnings)); _resolveDefaultProfile(); + _resolveNewTabMenuProfiles(); _validateSettings(); + + ExpandCommands(); } // Method Description: @@ -1076,6 +1181,11 @@ winrt::hstring CascadiaSettings::SettingsPath() return winrt::hstring{ _settingsPath().native() }; } +bool CascadiaSettings::IsPortableMode() +{ + return Model::IsPortableMode(); +} + winrt::hstring CascadiaSettings::DefaultSettingsPath() { // Both of these posts suggest getting the path to the exe, then removing @@ -1128,6 +1238,15 @@ void CascadiaSettings::WriteSettingsToDisk() } } +#ifndef NDEBUG +[[maybe_unused]] static std::string _getDevPathToSchema() +{ + std::filesystem::path filePath{ __FILE__ }; + auto schemaPath = filePath.parent_path().parent_path().parent_path().parent_path() / "doc" / "cascadia" / "profiles.schema.json"; + return "file:///" + schemaPath.generic_string(); +} +#endif + // Method Description: // - Create a new serialized JsonObject from an instance of this class // Arguments: @@ -1139,7 +1258,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 }; @@ -1173,7 +1302,8 @@ Json::Value CascadiaSettings::ToJson() const // Ignore the built in themes, when serializing the themes back out. We // don't want to re-include them in the user settings file. const auto theme{ winrt::get_self(entry.Value()) }; - if (theme->Name() == systemThemeName || theme->Name() == lightThemeName || theme->Name() == darkThemeName) + const auto& name{ theme->Name() }; + if (std::ranges::find(builtinThemes, name) != builtinThemes.end()) { continue; } @@ -1203,3 +1333,162 @@ void CascadiaSettings::_resolveDefaultProfile() const // Use the first profile as the new default. GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid()); } + +// Method Description: +// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile" +// fields, which can be a profile name, to a GUID and stores it back. +// - It finds any "source" entries and finds all profiles generated by that source +// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they +// represent (those that were not resolved before). It adds a warning when +// multiple of these entries are found. +void CascadiaSettings::_resolveNewTabMenuProfiles() const +{ + Model::RemainingProfilesEntry remainingProfilesEntry = nullptr; + + // The TerminalPage needs to know which profile has which profile ID. To prevent + // continuous lookups in the _activeProfiles vector, we create a map + // to store these indices in-flight. + auto remainingProfilesMap = std::map{}; + auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size()); + for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) + { + remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex)); + } + + // We keep track of the "remaining profiles" - those that have not yet been resolved + // in either a "profile" or "source" entry. They will possibly be assigned to a + // "remainingProfiles" entry + auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap)); + + // We call a recursive helper function to process the entries + auto entries = _globals->NewTabMenu(); + _resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry); + + // If a "remainingProfiles" entry has been found, assign to it the remaining profiles + if (remainingProfilesEntry != nullptr) + { + remainingProfilesEntry.Profiles(remainingProfiles); + } + + // If the configuration does not have a "newTabMenu" field, GlobalAppSettings + // will return a default value containing just a "remainingProfiles" entry. However, + // this value is regenerated on every "get" operation, so the effect of setting + // the remaining profiles above will be undone. So only in the case that no custom + // value is present in GlobalAppSettings, we will store the modified default value. + if (!_globals->HasNewTabMenu()) + { + _globals->NewTabMenu(entries); + } +} + +// Method Description: +// - Helper function that processes a set of tab menu entries and resolves any profile names +// or source fields as necessary - see function above for a more detailed explanation. +void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector entries, IMap& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const +{ + if (entries == nullptr || entries.Size() == 0) + { + return; + } + + for (const auto& entry : entries) + { + if (entry == nullptr) + { + continue; + } + + switch (entry.Type()) + { + // For a simple profile entry, the "profile" field can either be a name or a GUID. We + // use the GetProfileByName function to resolve this name to a profile instance, then + // find the index of that profile, and store this information in the entry. + case NewTabMenuEntryType::Profile: + { + // We need to access the unresolved profile name, a field that is not exposed + // in the projected class. So, we need to first obtain our implementation struct + // instance, to access this field. + const auto profileEntry{ winrt::get_self(entry.as()) }; + + // Find the profile by name + const auto profile = GetProfileByName(profileEntry->ProfileName()); + + // If not found, or if the profile is hidden, skip it + if (profile == nullptr || profile.Hidden()) + { + profileEntry->Profile(nullptr); // override "default" profile + break; + } + + // Find the index of the resulting profile and store the result in the entry + uint32_t profileIndex; + _activeProfiles.IndexOf(profile, profileIndex); + + profileEntry->Profile(profile); + profileEntry->ProfileIndex(profileIndex); + + // Remove from remaining profiles list (map) + remainingProfilesMap.TryRemove(profileIndex); + + break; + } + + // For a remainingProfiles entry, we store it in the variable that is passed back to our caller, + // except when that one has already been set (so we found a second/third/...) instance, which will + // trigger a warning. We then ignore this entry. + case NewTabMenuEntryType::RemainingProfiles: + { + if (remainingProfilesEntry != nullptr) + { + _warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry); + } + else + { + remainingProfilesEntry = entry.as(); + } + break; + } + + // For a folder, we simply call this method recursively + case NewTabMenuEntryType::Folder: + { + // We need to access the unfiltered entry list, a field that is not exposed + // in the projected class. So, we need to first obtain our implementation struct + // instance, to access this field. + const auto folderEntry{ winrt::get_self(entry.as()) }; + + auto folderEntries = folderEntry->RawEntries(); + _resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry); + break; + } + + // For a "matchProfiles" entry, we iterate through the list of all profiles and + // find all those matching: generated by the same source, having the same name, or + // having the same commandline. This can be expanded with regex support in the future. + // We make sure that none of the matches are included in the "remaining profiles" section. + case NewTabMenuEntryType::MatchProfiles: + { + // We need to access the matching function, which is not exposed in the projected class. + // So, we need to first obtain our implementation struct instance, to access this field. + const auto matchEntry{ winrt::get_self(entry.as()) }; + + matchEntry->Profiles(single_threaded_map()); + + auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size()); + for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) + { + const auto profile = _activeProfiles.GetAt(profileIndex); + + // On a match, we store it in the entry and remove it from the remaining list + if (matchEntry->MatchesProfile(profile)) + { + matchEntry->Profiles().Insert(profileIndex, profile); + remainingProfilesMap.TryRemove(profileIndex); + } + } + + break; + } + } + } +} diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index 8387413ee7..b211987b67 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 } @@ -478,23 +488,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // appended to this vector. // Return Value: // - - void Command::ExpandCommands(IMap commands, + void Command::ExpandCommands(IMap& commands, IVectorView profiles, - IVectorView schemes, - IVector warnings) + IVectorView schemes) { std::vector commandsToRemove; std::vector commandsToAdd; // First, collect up all the commands that need replacing. - for (const auto& nameAndCmd : commands) + for (const auto& [name, command] : commands) { - auto cmd{ get_self(nameAndCmd.Value()) }; + auto cmd{ get_self(command) }; - auto newCommands = _expandCommand(cmd, profiles, schemes, warnings); + auto newCommands = _expandCommand(cmd, profiles, schemes); if (newCommands.size() > 0) { - commandsToRemove.push_back(nameAndCmd.Key()); + commandsToRemove.push_back(name); commandsToAdd.insert(commandsToAdd.end(), newCommands.begin(), newCommands.end()); } } @@ -529,21 +538,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Arguments: // - expandable: the Command to potentially turn into more commands // - profiles: A list of all the profiles that this command should be expanded on. - // - warnings: If there were any warnings during parsing, they'll be - // appended to this vector. // Return Value: // - and empty vector if the command wasn't expandable, otherwise a list of // the newly-created commands. std::vector Command::_expandCommand(Command* const expandable, IVectorView profiles, - IVectorView schemes, - IVector& warnings) + IVectorView schemes) { std::vector newCommands; if (expandable->HasNestedCommands()) { - ExpandCommands(expandable->_subcommands, profiles, schemes, warnings); + ExpandCommands(expandable->_subcommands, profiles, schemes); } if (expandable->_IterateOn == ExpandCommandType::None) @@ -552,7 +558,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } std::string errs; // This string will receive any error text from failing to parse. - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; // First, get a string for the original Json::Value auto oldJsonString = expandable->_originalJson.toStyledString(); @@ -564,18 +570,19 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto actualDataEnd = newJsonString.data() + newJsonString.size(); if (!reader->parse(actualDataStart, actualDataEnd, &newJsonValue, &errs)) { - warnings.Append(SettingsLoadWarnings::FailedToParseCommandJson); // If we encounter a re-parsing error, just stop processing the rest of the commands. return false; } // Pass the new json back though FromJson, to get the new expanded value. - std::vector newWarnings; - if (auto newCmd{ Command::FromJson(newJsonValue, newWarnings) }) + // FromJson requires that we pass in a vector to hang on to the + // warnings, but ultimately, we don't care about warnings during + // expansion. + std::vector unused; + if (auto newCmd{ Command::FromJson(newJsonValue, unused) }) { newCommands.push_back(*newCmd); } - std::for_each(newWarnings.begin(), newWarnings.end(), [warnings](auto& warn) { warnings.Append(warn); }); return true; }; @@ -635,4 +642,152 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return newCommands; } + + winrt::Windows::Foundation::Collections::IVector Command::ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength) + { + if (json.empty()) + { + return nullptr; + } + auto data = winrt::to_string(json); + + std::string errs; + static std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; + Json::Value root; + if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) + { + throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); + } + + std::vector result; + + const auto parseElement = [&](const auto& element) { + winrt::hstring completionText; + winrt::hstring listText; + JsonUtils::GetValueForKey(element, "CompletionText", completionText); + JsonUtils::GetValueForKey(element, "ListItemText", listText); + + auto args = winrt::make_self( + winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"), + L"", + replaceLength, + static_cast(completionText)) }); + + Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args }; + + auto c = winrt::make_self(); + c->_name = listText; + c->_ActionAndArgs = actionAndArgs; + + // Try to assign a sensible icon based on the result type. These are + // roughly chosen to align with the icons in + // https://github.com/PowerShell/PowerShellEditorServices/pull/1738 + // as best as possible. + if (const auto resultType{ JsonUtils::GetValueForKey(element, "ResultType") }) + { + // PowerShell completion result -> Segoe Fluent icon value & name + switch (resultType) + { + case 1: // History -> 0xe81c History + c->_iconPath = L"\ue81c"; + break; + case 2: // Command -> 0xecaa AppIconDefault + c->_iconPath = L"\uecaa"; + break; + case 3: // ProviderItem -> 0xe8e4 AlignLeft + c->_iconPath = L"\ue8e4"; + break; + case 4: // ProviderContainer -> 0xe838 FolderOpen + c->_iconPath = L"\ue838"; + break; + case 5: // Property -> 0xe7c1 Flag + c->_iconPath = L"\ue7c1"; + break; + case 6: // Method -> 0xecaa AppIconDefault + c->_iconPath = L"\uecaa"; + break; + case 7: // ParameterName -> 0xe7c1 Flag + c->_iconPath = L"\ue7c1"; + break; + case 8: // ParameterValue -> 0xf000 KnowledgeArticle + c->_iconPath = L"\uf000"; + break; + case 10: // Namespace -> 0xe943 Code + c->_iconPath = L"\ue943"; + break; + case 13: // DynamicKeyword -> 0xe945 LightningBolt + c->_iconPath = L"\ue945"; + break; + } + } + + result.push_back(*c); + }; + + if (root.isArray()) + { + // If we got a whole array of suggestions, parse each one. + for (const auto& element : root) + { + parseElement(element); + } + } + else if (root.isObject()) + { + // If we instead only got a single element back, just parse the root element. + parseElement(root); + } + + return winrt::single_threaded_vector(std::move(result)); + } + + // Method description: + // * Convert the list of recent commands into a list of sendInput actions to + // send those commands. + // * We'll give each command a "history" icon. + // * If directories is true, we'll prepend "cd " to each command, so that + // the command will be run as a directory change instead. + IVector Command::HistoryToCommands(IVector history, + winrt::hstring currentCommandline, + bool directories) + { + std::wstring cdText = directories ? L"cd " : L""; + auto result = std::vector(); + + // Use this map to discard duplicates. + std::unordered_map foundCommands{}; + + auto backspaces = std::wstring(currentCommandline.size(), L'\x7f'); + + // Iterate in reverse over the history, so that most recent commands are first + for (auto i = history.Size(); i > 0; i--) + { + const auto& element{ history.GetAt(i - 1) }; + std::wstring_view line{ element }; + + if (line.empty()) + { + continue; + } + if (foundCommands.contains(line)) + { + continue; + } + auto args = winrt::make_self( + winrt::hstring{ fmt::format(L"{}{}{}", cdText, backspaces, line) }); + + Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args }; + + auto command = winrt::make_self(); + command->_ActionAndArgs = actionAndArgs; + command->_name = winrt::hstring{ line }; + command->_iconPath = directories ? + L"\ue8da" : // OpenLocal (a folder with an arrow pointing up) + L"\ue81c"; // History icon + result.push_back(*command); + foundCommands[line] = true; + } + + return winrt::single_threaded_vector(std::move(result)); + } } diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index 4f4dcbe204..41037bf1a2 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -41,10 +41,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::com_ptr FromJson(const Json::Value& json, std::vector& warnings); - static void ExpandCommands(Windows::Foundation::Collections::IMap commands, + static void ExpandCommands(Windows::Foundation::Collections::IMap& commands, Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IVectorView schemes, - Windows::Foundation::Collections::IVector warnings); + Windows::Foundation::Collections::IVectorView schemes); static std::vector LayerJson(Windows::Foundation::Collections::IMap& commands, const Json::Value& json); @@ -53,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; @@ -67,9 +67,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation hstring IconPath() const noexcept; void IconPath(const hstring& val); - winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker; + static Windows::Foundation::Collections::IVector ParsePowerShellMenuComplete(winrt::hstring json, int32_t replaceLength); + static Windows::Foundation::Collections::IVector HistoryToCommands(Windows::Foundation::Collections::IVector history, + winrt::hstring currentCommandline, + bool directories); - WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None); WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs); @@ -83,8 +85,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static std::vector _expandCommand(Command* const expandable, Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IVectorView schemes, - Windows::Foundation::Collections::IVector& warnings); + Windows::Foundation::Collections::IVectorView schemes); friend class SettingsModelLocalTests::DeserializationTests; friend class SettingsModelLocalTests::CommandTests; }; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index b749a79fb0..e92f459e69 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -24,11 +24,14 @@ namespace Microsoft.Terminal.Settings.Model ActionAndArgs(); ActionAndArgs(ShortcutAction action, IActionArgs args); + static String Serialize(IVector args); + static IVector Deserialize(String content); + IActionArgs Args; ShortcutAction Action; }; - [default_interface] runtimeclass Command : Windows.UI.Xaml.Data.INotifyPropertyChanged + [default_interface] runtimeclass Command { Command(); @@ -43,9 +46,8 @@ namespace Microsoft.Terminal.Settings.Model Boolean HasNestedCommands { get; }; Windows.Foundation.Collections.IMapView NestedCommands { get; }; - static void ExpandCommands(Windows.Foundation.Collections.IMap commands, - Windows.Foundation.Collections.IVectorView profiles, - Windows.Foundation.Collections.IVectorView schemes, - Windows.Foundation.Collections.IVector warnings); + static IVector ParsePowerShellMenuComplete(String json, Int32 replaceLength); + static IVector HistoryToCommands(IVector commandHistory, String commandline, Boolean directories); + } } diff --git a/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp b/src/cascadia/TerminalSettingsModel/DefaultTerminal.cpp index 0ce17eb725..754ed7cbd8 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, true)); + // 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", diff --git a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.cpp b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.cpp index 57f2e49f61..0df405a5a1 100644 --- a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.cpp @@ -17,7 +17,7 @@ static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" }; // - A Profile, ready to be filled in winrt::com_ptr CreateDynamicProfile(const std::wstring_view& name) { - const auto profileGuid = Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); + const auto profileGuid = Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, std::as_bytes(std::span{ name })); std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH }; iconPath.append(Microsoft::Console::Utils::GuidToString(profileGuid)); diff --git a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h index cf3e6e9e12..1e97e0bae5 100644 --- a/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h +++ b/src/cascadia/TerminalSettingsModel/DynamicProfileUtils.h @@ -21,6 +21,6 @@ Author(s): // will become disconnected from user settings. // {2bde4a90-d05f-401c-9492-e40884ead1d8} // uuidv5 properties: name format is UTF-16LE bytes -static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; +inline constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; winrt::com_ptr CreateDynamicProfile(const std::wstring_view& name); diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.cpp b/src/cascadia/TerminalSettingsModel/FileUtils.cpp index bc9100afab..ce219a6e0b 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.cpp +++ b/src/cascadia/TerminalSettingsModel/FileUtils.cpp @@ -15,14 +15,34 @@ static constexpr std::string_view Utf8Bom{ "\xEF\xBB\xBF", 3 }; static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" }; static constexpr std::wstring_view ReleaseSettingsFolder{ L"Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\" }; +static constexpr std::wstring_view PortableModeMarkerFile{ L".portable" }; +static constexpr std::wstring_view PortableModeSettingsFolder{ L"settings" }; namespace winrt::Microsoft::Terminal::Settings::Model { // Returns a path like C:\Users\\AppData\Local\Packages\\LocalState // You can put your settings.json or state.json in this directory. + bool IsPortableMode() + { + static auto portableMode = []() { + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + modulePath.replace_filename(PortableModeMarkerFile); + return std::filesystem::exists(modulePath); + }(); + return portableMode; + } + std::filesystem::path GetBaseSettingsPath() { static auto baseSettingsPath = []() { + if (!IsPackaged() && IsPortableMode()) + { + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + modulePath.replace_filename(PortableModeSettingsFolder); + std::filesystem::create_directories(modulePath); + return modulePath; + } + wil::unique_cotaskmem_string localAppDataFolder; // KF_FLAG_FORCE_APP_DATA_REDIRECTION, when engaged, causes SHGet... to return // the new AppModel paths (Packages/xxx/RoamingState, etc.) for standard path requests. @@ -258,7 +278,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model // Programs running in an elevated context will be free to write the // file, and unelevated processes will be able to read the file. An // unelevated process could always delete the file and rename a new - // file in it's place (a la the way `vim.exe` saves files), but if + // file in its place (a la the way `vim.exe` saves files), but if // they do that, the new file _won't_ be owned by Administrators, // failing the above check. } diff --git a/src/cascadia/TerminalSettingsModel/FileUtils.h b/src/cascadia/TerminalSettingsModel/FileUtils.h index f51ad2db9c..28e8c3027d 100644 --- a/src/cascadia/TerminalSettingsModel/FileUtils.h +++ b/src/cascadia/TerminalSettingsModel/FileUtils.h @@ -3,6 +3,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model { + bool IsPortableMode(); std::filesystem::path GetBaseSettingsPath(); std::filesystem::path GetReleaseSettingsPath(); std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly = false, FILETIME* lastWriteTime = nullptr); diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp new file mode 100644 index 0000000000..633f529a86 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "FolderEntry.h" +#include "JsonUtils.h" +#include "TerminalSettingsSerializationHelpers.h" + +#include "FolderEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; +using namespace winrt::Windows::Foundation::Collections; + +static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view IconKey{ "icon" }; +static constexpr std::string_view EntriesKey{ "entries" }; +static constexpr std::string_view InliningKey{ "inline" }; +static constexpr std::string_view AllowEmptyKey{ "allowEmpty" }; + +FolderEntry::FolderEntry() noexcept : + FolderEntry{ winrt::hstring{} } +{ +} + +FolderEntry::FolderEntry(const winrt::hstring& name) noexcept : + FolderEntryT(NewTabMenuEntryType::Folder), + _Name{ name } +{ +} + +Json::Value FolderEntry::ToJson() const +{ + auto json = NewTabMenuEntry::ToJson(); + + JsonUtils::SetValueForKey(json, NameKey, _Name); + JsonUtils::SetValueForKey(json, IconKey, _Icon); + JsonUtils::SetValueForKey(json, EntriesKey, _Entries); + JsonUtils::SetValueForKey(json, InliningKey, _Inlining); + JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty); + + return json; +} + +winrt::com_ptr FolderEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + JsonUtils::GetValueForKey(json, NameKey, entry->_Name); + JsonUtils::GetValueForKey(json, IconKey, entry->_Icon); + JsonUtils::GetValueForKey(json, EntriesKey, entry->_Entries); + JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining); + JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty); + + return entry; +} + +// A FolderEntry should only expose the entries to actually render to WinRT, +// to keep the logic for collapsing/expanding more centralised. +using NewTabMenuEntryModel = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntry; +IVector FolderEntry::Entries() const +{ + // We filter the full list of entries from JSON to just include the + // non-empty ones. + IVector result{ winrt::single_threaded_vector() }; + if (_Entries == nullptr) + { + return result; + } + + for (const auto& entry : _Entries) + { + if (entry == nullptr) + { + continue; + } + + switch (entry.Type()) + { + case NewTabMenuEntryType::Invalid: + continue; + + // A profile is filtered out if it is not valid, so if it was not resolved + case NewTabMenuEntryType::Profile: + { + const auto profileEntry = entry.as(); + if (profileEntry.Profile() == nullptr) + { + continue; + } + break; + } + + // Any profile collection is filtered out if there are no results + case NewTabMenuEntryType::RemainingProfiles: + case NewTabMenuEntryType::MatchProfiles: + { + const auto profileCollectionEntry = entry.as(); + if (profileCollectionEntry.Profiles().Size() == 0) + { + continue; + } + break; + } + + // A folder is filtered out if it has an effective size of 0 (calling + // this filtering method recursively), and if it is not allowed to be + // empty, or if it should auto-inline. + case NewTabMenuEntryType::Folder: + { + const auto folderEntry = entry.as(); + if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) + { + continue; + } + break; + } + } + + result.Append(entry); + } + + return result; +} diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h new file mode 100644 index 0000000000..619d228fa7 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/FolderEntry.h @@ -0,0 +1,55 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FolderEntry.h + +Abstract: +- A folder entry in the "new tab" dropdown menu, + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "pch.h" +#include "NewTabMenuEntry.h" +#include "FolderEntry.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct FolderEntry : FolderEntryT + { + public: + FolderEntry() noexcept; + explicit FolderEntry(const winrt::hstring& name) noexcept; + + Json::Value ToJson() const override; + static com_ptr FromJson(const Json::Value& json); + + // In JSON, we can set arbitrarily many profiles or nested profiles, that might not all + // be rendered; for example, when a profile entry is invalid, or when a folder is empty. + // Therefore, we will store the JSON entries list internally, and then expose only the + // entries to be rendered to WinRT. + winrt::Windows::Foundation::Collections::IVector Entries() const; + winrt::Windows::Foundation::Collections::IVector RawEntries() const + { + return _Entries; + }; + + WINRT_PROPERTY(winrt::hstring, Name); + WINRT_PROPERTY(winrt::hstring, Icon); + WINRT_PROPERTY(FolderEntryInlining, Inlining, FolderEntryInlining::Never); + WINRT_PROPERTY(bool, AllowEmpty, false); + + private: + winrt::Windows::Foundation::Collections::IVector _Entries{}; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(FolderEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp index 62ebc5aace..fafdf0976b 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp @@ -78,11 +78,6 @@ void FontConfig::LayerJson(const Json::Value& json) } } -bool FontConfig::HasAnyOptionSet() const -{ - return HasFontFace() || HasFontSize() || HasFontWeight(); -} - winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile() { return _sourceProfile.get(); diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h index 7b9b4663d2..8a789278eb 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.h +++ b/src/cascadia/TerminalSettingsModel/FontConfig.h @@ -35,7 +35,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::com_ptr CopyFontInfo(const FontConfig* source, winrt::weak_ref sourceProfile); Json::Value ToJson() const; void LayerJson(const Json::Value& json); - bool HasAnyOptionSet() const; Model::Profile SourceProfile(); diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl index 174a6ff74b..249bb94f8d 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.idl +++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl @@ -18,8 +18,9 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_FONT_SETTING(String, FontFace); INHERITABLE_FONT_SETTING(Single, FontSize); INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight); - INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontAxes); + INHERITABLE_FONT_SETTING(String, CellWidth); + INHERITABLE_FONT_SETTING(String, CellHeight); } } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 64ddea29f0..1caef96b2c 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -20,6 +20,7 @@ static constexpr std::string_view ActionsKey{ "actions" }; static constexpr std::string_view ThemeKey{ "theme" }; static constexpr std::string_view DefaultProfileKey{ "defaultProfile" }; static constexpr std::string_view LegacyUseTabSwitcherModeKey{ "useTabSwitcher" }; +static constexpr std::string_view LegacyReloadEnvironmentVariablesKey{ "compatibility.reloadEnvironmentVariables" }; // Method Description: // - Copies any extraneous data from the parent before completing a CreateChild call @@ -157,6 +158,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); } } + + JsonUtils::GetValueForKey(json, LegacyReloadEnvironmentVariablesKey, _legacyReloadEnvironmentVariables); } // Method Description: @@ -212,7 +215,22 @@ Json::Value GlobalAppSettings::ToJson() const winrt::Microsoft::Terminal::Settings::Model::Theme GlobalAppSettings::CurrentTheme() noexcept { - return _themes.TryLookup(Theme()); + auto requestedTheme = Model::Theme::IsSystemInDarkTheme() ? + winrt::Windows::UI::Xaml::ElementTheme::Dark : + winrt::Windows::UI::Xaml::ElementTheme::Light; + + switch (requestedTheme) + { + case winrt::Windows::UI::Xaml::ElementTheme::Light: + return _themes.TryLookup(Theme().LightName()); + + case winrt::Windows::UI::Xaml::ElementTheme::Dark: + return _themes.TryLookup(Theme().DarkName()); + + case winrt::Windows::UI::Xaml::ElementTheme::Default: + default: + return nullptr; + } } void GlobalAppSettings::AddTheme(const Model::Theme& theme) @@ -224,3 +242,14 @@ winrt::Windows::Foundation::Collections::IMapView& profiles, + const winrt::Windows::Foundation::Collections::IMapView& schemes) +{ + _actionMap->ExpandCommands(profiles, schemes); +} + +bool GlobalAppSettings::ShouldUsePersistedLayout() const +{ + return FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && !IsolatedMode(); +} diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index e90253a28c..3ccc0972be 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -23,6 +23,8 @@ Author(s): #include "Command.h" #include "ColorScheme.h" #include "Theme.h" +#include "NewTabMenuEntry.h" +#include "RemainingProfilesEntry.h" // fwdecl unittest classes namespace SettingsModelLocalTests @@ -60,6 +62,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMapView Themes() noexcept; void AddTheme(const Model::Theme& theme); Model::Theme CurrentTheme() noexcept; + bool ShouldUsePersistedLayout() const; + + void ExpandCommands(const Windows::Foundation::Collections::IVectorView& profiles, + const Windows::Foundation::Collections::IMapView& schemes); + + bool LegacyReloadEnvironmentVariables() const noexcept { return _legacyReloadEnvironmentVariables; } INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L""); @@ -76,6 +84,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation #endif winrt::guid _defaultProfile; + bool _legacyReloadEnvironmentVariables{ true }; winrt::com_ptr _actionMap{ winrt::make_self() }; std::vector _keybindingsWarnings; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index a6ff9c188c..20cb1d9052 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -6,6 +6,7 @@ import "Theme.idl"; import "ColorScheme.idl"; import "ActionMap.idl"; +import "NewTabMenuEntry.idl"; namespace Microsoft.Terminal.Settings.Model { @@ -94,7 +95,13 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon); INHERITABLE_SETTING(IVector, DisabledProfileSources); INHERITABLE_SETTING(Boolean, ShowAdminShield); + 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); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); @@ -104,7 +111,9 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView Themes(); void AddTheme(Theme theme); - INHERITABLE_SETTING(String, Theme); + INHERITABLE_SETTING(ThemePair, Theme); Theme CurrentTheme { get; }; + + Boolean ShouldUsePersistedLayout(); } } diff --git a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl index 2cfa305dec..5161a6716b 100644 --- a/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl +++ b/src/cascadia/TerminalSettingsModel/IAppearanceConfig.idl @@ -5,7 +5,7 @@ import "Profile.idl"; #include "IInheritable.idl.h" #define INHERITABLE_APPEARANCE_SETTING(Type, Name) \ - _BASE_INHERITABLE_SETTING(Type, Name); \ + _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.IAppearanceConfig Name##OverrideSource { get; } namespace Microsoft.Terminal.Settings.Model @@ -22,9 +22,7 @@ namespace Microsoft.Terminal.Settings.Model Vertical_Bottom = 0x20 }; - [flags] - enum IntenseStyle - { + [flags] enum IntenseStyle { Bold = 0x1, Bright = 0x2, All = 0xffffffff @@ -33,7 +31,10 @@ namespace Microsoft.Terminal.Settings.Model interface IAppearanceConfig { Microsoft.Terminal.Settings.Model.Profile SourceProfile { get; }; - INHERITABLE_APPEARANCE_SETTING(String, ColorSchemeName); + + INHERITABLE_APPEARANCE_SETTING(String, DarkColorSchemeName); + INHERITABLE_APPEARANCE_SETTING(String, LightColorSchemeName); + INHERITABLE_APPEARANCE_SETTING(Windows.Foundation.IReference, Foreground); INHERITABLE_APPEARANCE_SETTING(Windows.Foundation.IReference, Background); INHERITABLE_APPEARANCE_SETTING(Windows.Foundation.IReference, SelectionBackground); @@ -53,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/IInheritable.idl.h b/src/cascadia/TerminalSettingsModel/IInheritable.idl.h index b41822998e..a6f6529e22 100644 --- a/src/cascadia/TerminalSettingsModel/IInheritable.idl.h +++ b/src/cascadia/TerminalSettingsModel/IInheritable.idl.h @@ -9,7 +9,10 @@ get; \ set; \ }; \ - Boolean Has##Name { get; }; \ + Boolean Has##Name \ + { \ + get; \ + }; \ void Clear##Name() #define INHERITABLE_SETTING(Type, Name) \ diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp b/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp index e926efb7c7..a51ead6189 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.cpp @@ -72,7 +72,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Return Value: // - An IconElement with its IconSource set, if possible. template - TIconSource _getColoredBitmapIcon(const winrt::hstring& path) + TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome) { // FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters. // To skip throwing on Uri construction, we can quickly check if the first character is ASCII. @@ -81,11 +81,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation try { winrt::Windows::Foundation::Uri iconUri{ path }; - BitmapIconSource::type iconSource; + typename BitmapIconSource::type iconSource; // Make sure to set this to false, so we keep the RGB data of the // image. Otherwise, the icon will be white for all the // non-transparent pixels in the image. - iconSource.ShowAsMonochrome(false); + iconSource.ShowAsMonochrome(monochrome); iconSource.UriSource(iconUri); return iconSource; } @@ -95,6 +95,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return nullptr; } + _TIL_INLINEPREFIX winrt::hstring _expandIconPath(hstring iconPath) + { + if (iconPath.empty()) + { + return iconPath; + } + winrt::hstring envExpandedPath{ wil::ExpandEnvironmentStringsW(iconPath.c_str()) }; + return envExpandedPath; + } + // Method Description: // - Creates an IconSource for the given path. // * If the icon is a path to an image, we'll use that. @@ -111,14 +121,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Return Value: // - An IconElement with its IconSource set, if possible. template - TIconSource _getIconSource(const winrt::hstring& iconPath) + TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome) { TIconSource iconSource{ nullptr }; if (iconPath.size() != 0) { const auto expandedIconPath{ _expandIconPath(iconPath) }; - iconSource = _getColoredBitmapIcon(expandedIconPath); + iconSource = _getColoredBitmapIcon(expandedIconPath, monochrome); // If we fail to set the icon source using the "icon" as a path, // let's try it as a symbol/emoji. @@ -129,7 +139,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { try { - FontIconSource::type icon; + typename FontIconSource::type icon; const auto ch = iconPath[0]; // The range of MDL2 Icons isn't explicitly defined, but @@ -160,7 +170,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Swapping between nullptr IconSources and non-null IconSources causes a crash // to occur, but swapping between IconSources with a null source and non-null IconSources // work perfectly fine :shrug:. - BitmapIconSource::type icon; + typename BitmapIconSource::type icon; icon.UriSource(nullptr); iconSource = icon; } @@ -168,16 +178,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return iconSource; } - static winrt::hstring _expandIconPath(hstring iconPath) - { - if (iconPath.empty()) - { - return iconPath; - } - winrt::hstring envExpandedPath{ wil::ExpandEnvironmentStringsW(iconPath.c_str()) }; - return envExpandedPath; - } - // Method Description: // - Attempt to convert something into another type. For the // IconPathConverter, we support a variety of icons: @@ -197,7 +197,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const hstring& /* language */) { const auto& iconPath = winrt::unbox_value_or(value, L""); - return _getIconSource(iconPath); + return _getIconSource(iconPath, false); } // unused for one-way bindings @@ -211,12 +211,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::UI::Xaml::Controls::IconSource _IconSourceWUX(hstring path) { - return _getIconSource(path); + return _getIconSource(path, false); } - Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(hstring path) + Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(hstring path, bool monochrome) { - return _getIconSource(path); + return _getIconSource(path, monochrome); } SoftwareBitmap _convertToSoftwareBitmap(HICON hicon, @@ -296,9 +296,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation if (commaIndex != std::wstring::npos) { - // Convert the string iconIndex to an int - const auto index = til::to_ulong(pathView.substr(commaIndex + 1)); - if (index == til::to_ulong_error) + // Convert the string iconIndex to a signed int to support negative numbers which represent an Icon's ID. + const auto index{ til::to_int(pathView.substr(commaIndex + 1)) }; + if (index == til::to_int_error) { return std::nullopt; } @@ -329,13 +329,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return bitmapSource; } - MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath) + MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath, + const bool monochrome) { std::wstring_view iconPathWithoutIndex; const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); if (!indexOpt.has_value()) { - return _IconSourceMUX(iconPath); + return _IconSourceMUX(iconPath, monochrome); } const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value()); diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.h b/src/cascadia/TerminalSettingsModel/IconPathConverter.h index a9eee104b9..f91fdcad7b 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.h +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.h @@ -19,7 +19,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const hstring& language); static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath); - static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath); + static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, const bool convertToGrayscale); }; } diff --git a/src/cascadia/TerminalSettingsModel/IconPathConverter.idl b/src/cascadia/TerminalSettingsModel/IconPathConverter.idl index bbe1e055b8..b52367b6c3 100644 --- a/src/cascadia/TerminalSettingsModel/IconPathConverter.idl +++ b/src/cascadia/TerminalSettingsModel/IconPathConverter.idl @@ -16,7 +16,7 @@ namespace Microsoft.Terminal.Settings.Model IconPathConverter(); static Windows.UI.Xaml.Controls.IconElement IconWUX(String path); - static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path); + static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale); }; } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index c53a0afc59..6b1804d519 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -47,6 +47,18 @@ inline std::string JsonKey(const std::string_view key) namespace Microsoft::Terminal::Settings::Model::JsonUtils { + template + struct ConversionTrait + { + // Forward-declare these so the linker can pick up specializations from elsewhere! + T FromJson(const Json::Value&); + bool CanConvert(const Json::Value& json); + + Json::Value ToJson(const T& val); + + std::string TypeDescription() const { return ""; } + }; + namespace Detail { // Function Description: @@ -101,7 +113,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils { public: DeserializationError(const Json::Value& value) : - runtime_error(std::string("failed to deserialize ") + (value.isNull() ? "" : value.asString())), + runtime_error(std::string{ "failed to deserialize JSON value" }), jsonValue{ value } {} void SetKey(std::string_view newKey) @@ -238,18 +250,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils SetValueForKey(json, key, target, ConversionTrait>{}); } - template - struct ConversionTrait - { - // Forward-declare these so the linker can pick up specializations from elsewhere! - T FromJson(const Json::Value&); - bool CanConvert(const Json::Value& json); - - Json::Value ToJson(const T& val); - - std::string TypeDescription() const { return ""; } - }; - template<> struct ConversionTrait { @@ -298,6 +298,62 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; + template<> + struct ConversionTrait + { + GUID FromJson(const Json::Value& json) + { + return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json)).c_str()); + } + + bool CanConvert(const Json::Value& json) + { + if (!json.isString()) + { + return false; + } + + const auto string{ Detail::GetStringView(json) }; + return string.length() == 38 && string.front() == '{' && string.back() == '}'; + } + + Json::Value ToJson(const GUID& val) + { + return til::u16u8(::Microsoft::Console::Utils::GuidToString(val)); + } + + std::string TypeDescription() const + { + return "guid"; + } + }; + + // GUID and winrt::guid are mutually convertible, + // but IReference throws some of this off + template<> + struct ConversionTrait + { + winrt::guid FromJson(const Json::Value& json) const + { + return static_cast(ConversionTrait{}.FromJson(json)); + } + + bool CanConvert(const Json::Value& json) const + { + return ConversionTrait{}.CanConvert(json); + } + + Json::Value ToJson(const winrt::guid& val) + { + return ConversionTrait{}.ToJson(val); + } + + std::string TypeDescription() const + { + return ConversionTrait{}.TypeDescription(); + } + }; + template struct ConversionTrait> { @@ -636,6 +692,30 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; + template<> + struct ConversionTrait + { + unsigned int FromJson(const Json::Value& json) + { + return json.asUInt(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isUInt(); + } + + Json::Value ToJson(const uint64_t& val) + { + return val; + } + + std::string TypeDescription() const + { + return "number (>= 0)"; + } + }; + template<> struct ConversionTrait { @@ -684,62 +764,6 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; - template<> - struct ConversionTrait - { - GUID FromJson(const Json::Value& json) - { - return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json)).c_str()); - } - - bool CanConvert(const Json::Value& json) - { - if (!json.isString()) - { - return false; - } - - const auto string{ Detail::GetStringView(json) }; - return string.length() == 38 && string.front() == '{' && string.back() == '}'; - } - - Json::Value ToJson(const GUID& val) - { - return til::u16u8(::Microsoft::Console::Utils::GuidToString(val)); - } - - std::string TypeDescription() const - { - return "guid"; - } - }; - - // GUID and winrt::guid are mutually convertible, - // but IReference throws some of this off - template<> - struct ConversionTrait - { - winrt::guid FromJson(const Json::Value& json) const - { - return static_cast(ConversionTrait{}.FromJson(json)); - } - - bool CanConvert(const Json::Value& json) const - { - return ConversionTrait{}.CanConvert(json); - } - - Json::Value ToJson(const winrt::guid& val) - { - return ConversionTrait{}.ToJson(val); - } - - std::string TypeDescription() const - { - return ConversionTrait{}.TypeDescription(); - } - }; - template<> struct ConversionTrait { diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp index 50d2d13555..c4a5a0cfec 100644 --- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp @@ -123,8 +123,13 @@ static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring // - a newly constructed KeyChord static KeyChord _fromString(std::wstring_view wstr) { + if (wstr.empty()) + { + return nullptr; + } + using nameToVkeyPair = std::pair; - static const til::static_map nameToVkey{ + static constinit til::static_map nameToVkey{ // The above VKEY_NAME_PAIRS macro contains a list of key-binding names for each virtual key. // This god-awful macro inverts VKEY_NAME_PAIRS and creates a static map of key-binding names to virtual keys. // clang-format off @@ -240,7 +245,7 @@ static KeyChord _fromString(std::wstring_view wstr) static std::wstring _toString(const KeyChord& chord) { using vkeyToNamePair = std::pair; - static const til::static_map vkeyToName{ + static constinit til::static_map vkeyToName{ // The above VKEY_NAME_PAIRS macro contains a list of key-binding strings for each virtual key. // This macro picks the first (most preferred) name and creates a static map of virtual keys to key-binding names. #define GENERATOR(vkey, name1, ...) vkeyToNamePair{ vkey, name1 }, diff --git a/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h b/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h index 30d0f636ae..acb065fc0a 100644 --- a/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h +++ b/src/cascadia/TerminalSettingsModel/LegacyProfileGeneratorNamespaces.h @@ -16,6 +16,6 @@ Author(s): #pragma once -static constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; -static constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; -static constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; +inline constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; +inline constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; +inline constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 3cae191988..15e91a1d7a 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -18,56 +18,67 @@ Author(s): // Macro format (defaultArgs are optional): // (type, name, jsonKey, defaultArgs) -#define MTSM_GLOBAL_SETTINGS(X) \ - X(int32_t, InitialRows, "initialRows", 30) \ - X(int32_t, InitialCols, "initialCols", 80) \ - X(hstring, WordDelimiters, "wordDelimiters", DEFAULT_WORD_DELIMITERS) \ - X(bool, CopyOnSelect, "copyOnSelect", false) \ - X(bool, FocusFollowMouse, "focusFollowMouse", false) \ - X(bool, ForceFullRepaintRendering, "experimental.rendering.forceFullRepaint", false) \ - X(bool, SoftwareRendering, "experimental.rendering.software", false) \ - X(bool, UseBackgroundImageForWindow, "experimental.useBackgroundImageForWindow", false) \ - X(bool, ForceVTInput, "experimental.input.forceVT", false) \ - X(bool, TrimBlockSelection, "trimBlockSelection", true) \ - X(bool, DetectURLs, "experimental.detectURLs", true) \ - X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ - X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ - X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ - X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ - X(hstring, Theme, "theme") \ - X(hstring, Language, "language") \ - X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ - X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ - X(bool, ShowTabsInTitlebar, "showTabsInTitlebar", true) \ - X(bool, InputServiceWarning, "inputServiceWarning", true) \ - X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, "copyFormatting", 0) \ - X(bool, WarnAboutLargePaste, "largePasteWarning", true) \ - X(bool, WarnAboutMultiLinePaste, "multiLinePasteWarning", true) \ - X(Model::LaunchPosition, InitialPosition, "initialPosition", nullptr, nullptr) \ - X(bool, CenterOnLaunch, "centerOnLaunch", false) \ - X(Model::FirstWindowPreference, FirstWindowPreference, "firstWindowPreference", FirstWindowPreference::DefaultProfile) \ - X(Model::LaunchMode, LaunchMode, "launchMode", LaunchMode::DefaultMode) \ - X(bool, SnapToGridOnResize, "snapToGridOnResize", true) \ - X(bool, DebugFeaturesEnabled, "debugFeatures", debugFeaturesDefault) \ - X(bool, StartOnUserLogin, "startOnUserLogin", false) \ - X(bool, AlwaysOnTop, "alwaysOnTop", false) \ - X(bool, AutoHideWindow, "autoHideWindow", false) \ - X(Model::TabSwitcherMode, TabSwitcherMode, "tabSwitcherMode", Model::TabSwitcherMode::InOrder) \ - X(bool, DisableAnimations, "disableAnimations", false) \ - X(hstring, StartupActions, "startupActions", L"") \ - X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \ - X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \ - X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \ - X(winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, "disabledProfileSources", nullptr) \ - X(bool, ShowAdminShield, "showAdminShield", true) \ - X(bool, TrimPaste, "trimPaste", true) \ - X(bool, EnableColorSelection, "experimental.enableColorSelection", false) +#define MTSM_GLOBAL_SETTINGS(X) \ + X(int32_t, InitialRows, "initialRows", 30) \ + X(int32_t, InitialCols, "initialCols", 80) \ + X(hstring, WordDelimiters, "wordDelimiters", DEFAULT_WORD_DELIMITERS) \ + X(bool, CopyOnSelect, "copyOnSelect", false) \ + X(bool, FocusFollowMouse, "focusFollowMouse", false) \ + X(bool, ForceFullRepaintRendering, "experimental.rendering.forceFullRepaint", false) \ + X(bool, SoftwareRendering, "experimental.rendering.software", false) \ + X(bool, UseBackgroundImageForWindow, "experimental.useBackgroundImageForWindow", false) \ + X(bool, ForceVTInput, "experimental.input.forceVT", false) \ + X(bool, TrimBlockSelection, "trimBlockSelection", true) \ + X(bool, DetectURLs, "experimental.detectURLs", true) \ + X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ + X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ + X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ + X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ + X(Model::ThemePair, Theme, "theme") \ + X(hstring, Language, "language") \ + X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ + X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ + X(bool, ShowTabsInTitlebar, "showTabsInTitlebar", true) \ + X(bool, InputServiceWarning, "inputServiceWarning", true) \ + X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, "copyFormatting", 0) \ + X(bool, WarnAboutLargePaste, "largePasteWarning", true) \ + X(bool, WarnAboutMultiLinePaste, "multiLinePasteWarning", true) \ + X(Model::LaunchPosition, InitialPosition, "initialPosition", nullptr, nullptr) \ + X(bool, CenterOnLaunch, "centerOnLaunch", false) \ + X(Model::FirstWindowPreference, FirstWindowPreference, "firstWindowPreference", FirstWindowPreference::DefaultProfile) \ + X(Model::LaunchMode, LaunchMode, "launchMode", LaunchMode::DefaultMode) \ + X(bool, SnapToGridOnResize, "snapToGridOnResize", true) \ + X(bool, DebugFeaturesEnabled, "debugFeatures", debugFeaturesDefault) \ + X(bool, StartOnUserLogin, "startOnUserLogin", false) \ + X(bool, AlwaysOnTop, "alwaysOnTop", false) \ + X(bool, AutoHideWindow, "autoHideWindow", false) \ + X(Model::TabSwitcherMode, TabSwitcherMode, "tabSwitcherMode", Model::TabSwitcherMode::InOrder) \ + X(bool, DisableAnimations, "disableAnimations", false) \ + X(hstring, StartupActions, "startupActions", L"") \ + X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \ + X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \ + X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \ + X(winrt::Windows::Foundation::Collections::IVector, DisabledProfileSources, "disabledProfileSources", nullptr) \ + X(bool, ShowAdminShield, "showAdminShield", true) \ + 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) \ + X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22") +// Also add these settings to: +// * Profile.idl +// * TerminalSettings.h +// * TerminalSettings.cpp: TerminalSettings::_ApplyProfileSettings +// * IControlSettings.idl or ICoreSettings.idl +// * ControlProperties.h #define MTSM_PROFILE_SETTINGS(X) \ 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) \ @@ -78,12 +89,16 @@ Author(s): X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \ X(hstring, TabTitle, "tabTitle") \ X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ + X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \ X(bool, UseAtlasEngine, "useAtlasEngine", Feature_AtlasEngine::IsEnabled()) \ + X(bool, RightClickContextMenu, "experimental.rightClickContextMenu", false) \ X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) \ X(bool, Elevate, "elevate", false) \ X(bool, VtPassthrough, "experimental.connection.passthroughMode", false) \ X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false) \ - X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) + X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) \ + X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ + X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) // Intentionally omitted Profile settings: // * Name @@ -99,7 +114,9 @@ Author(s): X(float, FontSize, "size", DEFAULT_FONT_SIZE) \ X(winrt::Windows::UI::Text::FontWeight, FontWeight, "weight", DEFAULT_FONT_WEIGHT) \ X(IFontAxesMap, FontAxes, "axes") \ - X(IFontFeatureMap, FontFeatures, "features") + X(IFontFeatureMap, FontFeatures, "features") \ + X(winrt::hstring, CellWidth, "cellWidth") \ + X(winrt::hstring, CellHeight, "cellHeight") #define MTSM_APPEARANCE_SETTINGS(X) \ X(Core::CursorStyle, CursorShape, "cursorShape", Core::CursorStyle::Bar) \ @@ -109,10 +126,10 @@ Author(s): X(bool, RetroTerminalEffect, "experimental.retroTerminalEffect", false) \ X(hstring, PixelShaderPath, "experimental.pixelShaderPath") \ X(ConvergedAlignment, BackgroundImageAlignment, "backgroundImageAlignment", ConvergedAlignment::Horizontal_Center | ConvergedAlignment::Vertical_Center) \ - X(hstring, ColorSchemeName, "colorScheme", L"Campbell") \ 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 @@ -123,14 +140,19 @@ Author(s): X(winrt::Microsoft::Terminal::Settings::Model::TabRowTheme, TabRow, "tabRow", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::TabTheme, Tab, "tab", nullptr) -#define MTSM_THEME_WINDOW_SETTINGS(X) \ - X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "applicationTheme", winrt::Windows::UI::Xaml::ElementTheme::Default) +#define MTSM_THEME_WINDOW_SETTINGS(X) \ + X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "applicationTheme", winrt::Windows::UI::Xaml::ElementTheme::Default) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \ + X(bool, RainbowFrame, "experimental.rainbowFrame", false) \ + X(bool, UseMica, "useMica", false) #define MTSM_THEME_TABROW_SETTINGS(X) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) -#define MTSM_THEME_TAB_SETTINGS(X) \ - X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ - X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \ +#define MTSM_THEME_TAB_SETTINGS(X) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::IconStyle, IconStyle, "iconStyle", winrt::Microsoft::Terminal::Settings::Model::IconStyle::Default) \ X(winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility, ShowCloseButton, "showCloseButton", winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always) diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp new file mode 100644 index 0000000000..2873679018 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "MatchProfilesEntry.h" +#include "JsonUtils.h" + +#include "MatchProfilesEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; + +static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view CommandlineKey{ "commandline" }; +static constexpr std::string_view SourceKey{ "source" }; + +MatchProfilesEntry::MatchProfilesEntry() noexcept : + MatchProfilesEntryT(NewTabMenuEntryType::MatchProfiles) +{ +} + +Json::Value MatchProfilesEntry::ToJson() const +{ + auto json = NewTabMenuEntry::ToJson(); + + JsonUtils::SetValueForKey(json, NameKey, _Name); + JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline); + JsonUtils::SetValueForKey(json, SourceKey, _Source); + + return json; +} + +winrt::com_ptr MatchProfilesEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + JsonUtils::GetValueForKey(json, NameKey, entry->_Name); + JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline); + JsonUtils::GetValueForKey(json, SourceKey, entry->_Source); + + return entry; +} + +bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile) +{ + // We use an optional here instead of a simple bool directly, since there is no + // sensible default value for the desired semantics: the first property we want + // to match on should always be applied (so one would set "true" as a default), + // but if none of the properties are set, the default return value should be false + // since this entry type is expected to behave like a positive match/whitelist. + // + // The easiest way to deal with this neatly is to use an optional, then for any + // property to match we consider a null value to be "true", and for the return + // value of the function we consider the null value to be "false". + auto isMatching = std::optional{}; + + if (!_Name.empty()) + { + isMatching = { isMatching.value_or(true) && _Name == profile.Name() }; + } + + if (!_Source.empty()) + { + isMatching = { isMatching.value_or(true) && _Source == profile.Source() }; + } + + if (!_Commandline.empty()) + { + isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() }; + } + + return isMatching.value_or(false); +} diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h new file mode 100644 index 0000000000..464a6780c1 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h @@ -0,0 +1,42 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- MatchProfilesEntry.h + +Abstract: +- An entry in the "new tab" dropdown menu that represents a collection + of profiles that match a specified name, source, or command line. + +Author(s): +- Floris Westerman - November 2022 + +--*/ +#pragma once + +#include "ProfileCollectionEntry.h" +#include "MatchProfilesEntry.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct MatchProfilesEntry : MatchProfilesEntryT + { + public: + MatchProfilesEntry() noexcept; + + Json::Value ToJson() const override; + static com_ptr FromJson(const Json::Value& json); + + bool MatchesProfile(const Model::Profile& profile); + + WINRT_PROPERTY(winrt::hstring, Name); + WINRT_PROPERTY(winrt::hstring, Commandline); + WINRT_PROPERTY(winrt::hstring, Source); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(MatchProfilesEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index a1aaf301ce..2f77b74566 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -21,6 +21,27 @@ + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + DefaultTerminal.idl @@ -92,6 +113,7 @@ + @@ -162,10 +184,32 @@ EnumMappings.idl + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + + NewTabMenuEntry.idl + + @@ -180,6 +224,7 @@ + @@ -266,8 +311,6 @@ pch.h ..;$(OpenConsoleDir)\dep\jsoncpp\json;%(AdditionalIncludeDirectories); - - %(AdditionalOptions) /Zc:twoPhase- 4702;%(DisableSpecificWarnings) @@ -282,16 +325,19 @@ + + + + + + + - - - - diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters index fb21fccdc3..cd38077aee 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters @@ -18,6 +18,9 @@ profileGeneration + + profileGeneration + @@ -61,6 +64,9 @@ profileGeneration + + profileGeneration + @@ -94,6 +100,7 @@ profileGeneration + @@ -113,6 +120,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp new file mode 100644 index 0000000000..e346d9711c --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "NewTabMenuEntry.h" +#include "JsonUtils.h" +#include "TerminalSettingsSerializationHelpers.h" +#include "SeparatorEntry.h" +#include "FolderEntry.h" +#include "ProfileEntry.h" +#include "RemainingProfilesEntry.h" +#include "MatchProfilesEntry.h" + +#include "NewTabMenuEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; +using NewTabMenuEntryType = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType; + +static constexpr std::string_view TypeKey{ "type" }; + +NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept : + _Type{ type } +{ +} + +// This method will be overridden by the subclasses, which will then call this +// parent implementation for a "base" json object. +Json::Value NewTabMenuEntry::ToJson() const +{ + Json::Value json{ Json::ValueType::objectValue }; + + JsonUtils::SetValueForKey(json, TypeKey, _Type); + + return json; +} + +// Deserialize the JSON object based on the given type. We use the map from above for that. +winrt::com_ptr NewTabMenuEntry::FromJson(const Json::Value& json) +{ + const auto type = JsonUtils::GetValueForKey(json, TypeKey); + + switch (type) + { + case NewTabMenuEntryType::Separator: + return SeparatorEntry::FromJson(json); + case NewTabMenuEntryType::Folder: + return FolderEntry::FromJson(json); + case NewTabMenuEntryType::Profile: + return ProfileEntry::FromJson(json); + case NewTabMenuEntryType::RemainingProfiles: + return RemainingProfilesEntry::FromJson(json); + case NewTabMenuEntryType::MatchProfiles: + return MatchProfilesEntry::FromJson(json); + default: + return nullptr; + } +} diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h new file mode 100644 index 0000000000..57f6bc1a00 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h @@ -0,0 +1,72 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- NewTabMenuEntry.h + +Abstract: +- An entry in the "new tab" dropdown menu. These entries exist in a few varieties, + such as separators, folders, or profile links. + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "NewTabMenuEntry.g.h" +#include "JsonUtils.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct NewTabMenuEntry : NewTabMenuEntryT + { + public: + static com_ptr FromJson(const Json::Value& json); + virtual Json::Value ToJson() const; + + WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid); + + // We have a protected/hidden constructor so consumers cannot instantiate + // this base class directly and need to go through either FromJson + // or one of the subclasses. + protected: + explicit NewTabMenuEntry(const NewTabMenuEntryType type) noexcept; + }; +} + +namespace Microsoft::Terminal::Settings::Model::JsonUtils +{ + using namespace winrt::Microsoft::Terminal::Settings::Model; + + template<> + struct ConversionTrait + { + NewTabMenuEntry FromJson(const Json::Value& json) + { + const auto entry = implementation::NewTabMenuEntry::FromJson(json); + if (entry == nullptr) + { + return nullptr; + } + + return *entry; + } + + bool CanConvert(const Json::Value& json) const + { + return json.isObject(); + } + + Json::Value ToJson(const NewTabMenuEntry& val) + { + return winrt::get_self(val)->ToJson(); + } + + std::string TypeDescription() const + { + return "NewTabMenuEntry"; + } + }; +} diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl new file mode 100644 index 0000000000..64a3b1fdd9 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "Profile.idl"; + +namespace Microsoft.Terminal.Settings.Model +{ + enum NewTabMenuEntryType + { + Invalid = 0, + Profile, + Separator, + Folder, + RemainingProfiles, + MatchProfiles + }; + + [default_interface] unsealed runtimeclass NewTabMenuEntry + { + NewTabMenuEntryType Type; + } + + [default_interface] runtimeclass SeparatorEntry : NewTabMenuEntry + { + SeparatorEntry(); + } + + [default_interface] runtimeclass ProfileEntry : NewTabMenuEntry + { + ProfileEntry(); + ProfileEntry(String profile); + + Profile Profile; + Int32 ProfileIndex; + } + + enum FolderEntryInlining + { + Never = 0, + Auto + }; + + [default_interface] runtimeclass FolderEntry : NewTabMenuEntry + { + FolderEntry(); + FolderEntry(String name); + + String Name; + String Icon; + FolderEntryInlining Inlining; + Boolean AllowEmpty; + + IVector Entries(); + } + + [default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry + { + IMap Profiles; + } + + [default_interface] runtimeclass RemainingProfilesEntry : ProfileCollectionEntry + { + RemainingProfilesEntry(); + } + + [default_interface] runtimeclass MatchProfilesEntry : ProfileCollectionEntry + { + MatchProfilesEntry(); + + String Name; + String Commandline; + String Source; + } +} diff --git a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp index 5db52c810b..14878c8278 100644 --- a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp @@ -157,7 +157,7 @@ static void _accumulateTraditionalLayoutPowerShellInstancesInDirectory(std::wstr const auto executable = versionedPath / PWSH_EXE; if (std::filesystem::exists(executable)) { - const auto preview = versionedPath.filename().wstring().find(L"-preview") != std::wstring::npos; + const auto preview = versionedPath.filename().native().find(L"-preview") != std::wstring::npos; const auto previewFlag = preview ? PowerShellFlags::Preview : PowerShellFlags::None; out.emplace_back(PowerShellInstance{ std::stoi(versionedPath.filename()), PowerShellFlags::Traditional | flags | previewFlag, @@ -319,7 +319,8 @@ void PowershellCoreProfileGenerator::GenerateProfiles(std::vectorCommandline(winrt::hstring{ quotedCommandline }); profile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY }); - profile->DefaultAppearance().ColorSchemeName(L"Campbell"); + profile->DefaultAppearance().DarkColorSchemeName(L"Campbell"); + profile->DefaultAppearance().LightColorSchemeName(L"Campbell"); profile->Icon(winrt::hstring{ WI_IsFlagSet(psI.flags, PowerShellFlags::Preview) ? POWERSHELL_PREVIEW_ICON : POWERSHELL_ICON }); if (first) diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index 9db6741703..3aa896f50d 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -283,12 +283,12 @@ winrt::guid Profile::_GenerateGuidForProfile(const std::wstring_view& name, cons // our source to build the namespace guid, instead of using the default GUID. const auto namespaceGuid = !source.empty() ? - Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(source))) : + Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, std::as_bytes(std::span{ source })) : RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID; // Always use the name to generate the temporary GUID. That way, across // reloads, we'll generate the same static GUID. - return { Utils::CreateV5Uuid(namespaceGuid, gsl::as_bytes(gsl::make_span(name))) }; + return { Utils::CreateV5Uuid(namespaceGuid, std::as_bytes(std::span{ name })) }; } // Method Description: @@ -324,11 +324,9 @@ Json::Value Profile::ToJson() const MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_TO_JSON) #undef PROFILE_SETTINGS_TO_JSON - // Font settings - const auto fontInfoImpl = winrt::get_self(_FontInfo); - if (fontInfoImpl->HasAnyOptionSet()) + if (auto fontJSON = winrt::get_self(_FontInfo)->ToJson(); !fontJSON.empty()) { - json[JsonKey(FontInfoKey)] = winrt::get_self(_FontInfo)->ToJson(); + json[JsonKey(FontInfoKey)] = std::move(fontJSON); } if (_UnfocusedAppearance) diff --git a/src/cascadia/TerminalSettingsModel/Profile.h b/src/cascadia/TerminalSettingsModel/Profile.h index dbc33fc9ef..05bd77cc72 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.h +++ b/src/cascadia/TerminalSettingsModel/Profile.h @@ -67,6 +67,8 @@ namespace TerminalAppUnitTests class JsonTests; }; +using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap; + // GUID used for generating GUIDs at runtime, for profiles that did not have a // GUID specified manually. constexpr GUID RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID = { 0xf65ddb7e, 0x706b, 0x4499, { 0x8a, 0x50, 0x40, 0x31, 0x3c, 0xaf, 0x51, 0x0a } }; diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 608dfc5f91..4aa9af3422 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -9,6 +9,8 @@ import "FontConfig.idl"; _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.Profile Name##OverrideSource { get; } +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { // This tag is used to identify the context in which the Profile was created @@ -61,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); @@ -81,11 +82,20 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, SnapOnInput); INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing); INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle); + + INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap, EnvironmentVariables); + INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine); INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector, BellSound); INHERITABLE_PROFILE_SETTING(Boolean, Elevate); INHERITABLE_PROFILE_SETTING(Boolean, AutoMarkPrompts); INHERITABLE_PROFILE_SETTING(Boolean, ShowMarks); + + INHERITABLE_PROFILE_SETTING(Boolean, RightClickContextMenu); + INHERITABLE_PROFILE_SETTING(Boolean, RepositionCursorWithMouse); + + INHERITABLE_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables); + } } diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp new file mode 100644 index 0000000000..430f415301 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ProfileCollectionEntry.h" +#include "JsonUtils.h" + +#include "ProfileCollectionEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; + +ProfileCollectionEntry::ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept : + ProfileCollectionEntryT(type) +{ +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h new file mode 100644 index 0000000000..86cfa72344 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h @@ -0,0 +1,39 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SeparatorEntry.h + +Abstract: +- An entry in the "new tab" dropdown menu that represents some collection + of profiles. This is an abstract class that has concretizations like + "all profiles from a source" or "all remaining profiles" + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "NewTabMenuEntry.h" +#include "ProfileCollectionEntry.g.h" +#include "Profile.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct ProfileCollectionEntry : ProfileCollectionEntryT + { + public: + // Since a comma does not work very nicely in a macro and we need one + // for our map definition, we use a macro te define a comma. +#define COMMA , + WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IMap, Profiles); +#undef COMMA + + // We have a protected/hidden constructor so consumers cannot instantiate + // this class directly and need to go through one of the subclasses. + protected: + explicit ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept; + }; +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp new file mode 100644 index 0000000000..c857eea867 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ProfileEntry.h" +#include "JsonUtils.h" + +#include "ProfileEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; + +static constexpr std::string_view ProfileKey{ "profile" }; + +ProfileEntry::ProfileEntry() noexcept : + ProfileEntry{ winrt::hstring{} } +{ +} + +ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept : + ProfileEntryT(NewTabMenuEntryType::Profile), + _ProfileName{ profile } +{ +} + +Json::Value ProfileEntry::ToJson() const +{ + auto json = NewTabMenuEntry::ToJson(); + + // We will now return a profile reference to the JSON representation. Logic is + // as follows: + // - When Profile is null, this is typically because an existing profile menu entry + // in the JSON config is invalid (nonexistent or hidden profile). Then, we store + // the original profile string value as read from JSON, to improve portability + // of the settings file and limit modifications to the JSON. + // - Otherwise, we always store the GUID of the profile. This will effectively convert + // all name-matched profiles from the settings file to GUIDs. This might be unexpected + // to some users, but is less error-prone and will continue to work when profile + // names are changed. + if (_Profile == nullptr) + { + JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName); + } + else + { + JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid()); + } + + return json; +} + +winrt::com_ptr ProfileEntry::FromJson(const Json::Value& json) +{ + auto entry = winrt::make_self(); + + JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName); + + return entry; +} diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h new file mode 100644 index 0000000000..fe0f4573d2 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.h @@ -0,0 +1,53 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FolderEntry.h + +Abstract: +- A profile entry in the "new tab" dropdown menu, referring to + a single profile. + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "NewTabMenuEntry.h" +#include "ProfileEntry.g.h" + +#include "Profile.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct ProfileEntry : ProfileEntryT + { + public: + ProfileEntry() noexcept; + explicit ProfileEntry(const winrt::hstring& profile) noexcept; + + Json::Value ToJson() const override; + static com_ptr FromJson(const Json::Value& json); + + // In JSON, only a profile name (guid or string) can be set; + // but the consumers of this class would like to have direct access + // to the appropriate Model::Profile. Therefore, we have a read-only + // property ProfileName that corresponds to the JSON value, and + // then CascadiaSettings::_resolveNewTabMenuProfiles() will populate + // the Profile and ProfileIndex properties appropriately + winrt::hstring ProfileName() const noexcept { return _ProfileName; }; + + WINRT_PROPERTY(Model::Profile, Profile); + WINRT_PROPERTY(int, ProfileIndex); + + private: + winrt::hstring _ProfileName; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(ProfileEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp new file mode 100644 index 0000000000..1d2539da2d --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "RemainingProfilesEntry.h" +#include "NewTabMenuEntry.h" +#include "JsonUtils.h" + +#include "RemainingProfilesEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; + +RemainingProfilesEntry::RemainingProfilesEntry() noexcept : + RemainingProfilesEntryT(NewTabMenuEntryType::RemainingProfiles) +{ +} + +winrt::com_ptr RemainingProfilesEntry::FromJson(const Json::Value&) +{ + return winrt::make_self(); +} diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h new file mode 100644 index 0000000000..153669e4f7 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SeparatorEntry.h + +Abstract: +- An entry in the "new tab" dropdown menu that represents all profiles + that were not included explicitly elsewhere + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "ProfileCollectionEntry.h" +#include "RemainingProfilesEntry.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct RemainingProfilesEntry : RemainingProfilesEntryT + { + public: + RemainingProfilesEntry() noexcept; + + static com_ptr FromJson(const Json::Value& json); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(RemainingProfilesEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 09998b9093..d6d6d9565f 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -126,6 +126,10 @@ Split Pane... + + Terminal (Portable) + This display name is used when the Terminal application is running in a "portable" mode, where settings are not stored in a shared location. + Terminal (Unpackaged) This display name is used when the application's name cannot be determined @@ -166,6 +170,13 @@ Move tab {0} {0} will be replaced with a "forward" / "backward" + + Move tab to window "{0}" + {0} will be replaced with a user-specified name of a window + + + Move tab to a new window + forward @@ -414,6 +425,9 @@ Move pane + + Move pane to new window + Split pane @@ -435,6 +449,12 @@ Toggle command palette + + Recent commands... + + + Open suggestions... + Toggle command palette in command line mode @@ -472,6 +492,12 @@ Toggle pane read-only mode + + Enable pane read-only mode + + + Disable pane read-only mode + Toggle terminal visual effects @@ -491,6 +517,9 @@ Rename window... + + Display Terminal's current working directory + Show/Hide the Terminal window @@ -658,7 +687,41 @@ Expand selection to word + + Show context menu + Close all other panes + + Toggle broadcast input to all panes + When enabled, input will go to all panes in this tab simultaneously + + + Restart connection + + + Select next command output + + + Select previous command output + + + Select next command + + + Select previous command + + + Search {0} + {0} will be replaced with a user-specified URL to a web page + + + Search the web for selected text + This will open a web browser to search for some user-selected text + + + Open about dialog + This will open the "about" dialog, to display version info and other documentation + diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp new file mode 100644 index 0000000000..d6cee21090 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SeparatorEntry.h" +#include "JsonUtils.h" + +#include "SeparatorEntry.g.cpp" + +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; + +SeparatorEntry::SeparatorEntry() noexcept : + SeparatorEntryT(NewTabMenuEntryType::Separator) +{ +} + +winrt::com_ptr SeparatorEntry::FromJson(const Json::Value&) +{ + return winrt::make_self(); +} diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h new file mode 100644 index 0000000000..e074ce251c --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SeparatorEntry.h + +Abstract: +- A separator entry in the "new tab" dropdown menu + +Author(s): +- Floris Westerman - August 2022 + +--*/ +#pragma once + +#include "NewTabMenuEntry.h" +#include "SeparatorEntry.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct SeparatorEntry : SeparatorEntryT + { + public: + SeparatorEntry() noexcept; + + static com_ptr FromJson(const Json::Value& json); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(SeparatorEntry); +} diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp new file mode 100644 index 0000000000..30dc19039f --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "SshHostGenerator.h" +#include "../../inc/DefaultSettings.h" + +#include "DynamicProfileUtils.h" + +static constexpr std::wstring_view SshHostGeneratorNamespace{ L"Windows.Terminal.SSH" }; + +static constexpr std::wstring_view PROFILE_TITLE_PREFIX = L"SSH - "; +static constexpr std::wstring_view PROFILE_ICON_PATH = L"ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png"; + +// OpenSSH is installed under System32 when installed via Optional Features +static constexpr std::wstring_view SSH_EXE_PATH1 = L"%SystemRoot%\\System32\\OpenSSH\\ssh.exe"; + +// OpenSSH (x86/x64) is installed under Program Files when installed via MSI +static constexpr std::wstring_view SSH_EXE_PATH2 = L"%ProgramFiles%\\OpenSSH\\ssh.exe"; + +// OpenSSH (x86) is installed under Program Files x86 when installed via MSI on x64 machine +static constexpr std::wstring_view SSH_EXE_PATH3 = L"%ProgramFiles(x86)%\\OpenSSH\\ssh.exe"; + +static constexpr std::wstring_view SSH_SYSTEM_CONFIG_PATH = L"%ProgramData%\\ssh\\ssh_config"; +static constexpr std::wstring_view SSH_USER_CONFIG_PATH = L"%UserProfile%\\.ssh\\config"; + +static constexpr std::wstring_view SSH_CONFIG_HOST_KEY{ L"Host" }; +static constexpr std::wstring_view SSH_CONFIG_HOSTNAME_KEY{ L"HostName" }; + +using namespace ::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +/*static*/ const std::wregex SshHostGenerator::_configKeyValueRegex{ LR"(^\s*(\w+)\s+([^\s]+.*[^\s])\s*$)" }; + +/*static*/ std::wstring_view SshHostGenerator::_getProfileName(const std::wstring_view& hostName) noexcept +{ + return std::wstring_view{ L"" + PROFILE_TITLE_PREFIX + hostName }; +} + +/*static*/ std::wstring_view SshHostGenerator::_getProfileIconPath() noexcept +{ + return PROFILE_ICON_PATH; +} + +/*static*/ std::wstring_view SshHostGenerator::_getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept +{ + return std::wstring_view{ L"\"" + sshExePath + L"\" " + hostName }; +} + +/*static*/ bool SshHostGenerator::_tryFindSshExePath(std::wstring& sshExePath) noexcept +{ + try + { + for (const auto& path : { SSH_EXE_PATH1, SSH_EXE_PATH2, SSH_EXE_PATH3 }) + { + if (std::filesystem::exists(wil::ExpandEnvironmentStringsW(path.data()))) + { + sshExePath = path; + return true; + } + } + } + CATCH_LOG(); + + return false; +} + +/*static*/ bool SshHostGenerator::_tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept +{ + try + { + if (!line.empty() && !line.starts_with(L"#")) + { + std::wstring input{ line }; + std::wsmatch match; + if (std::regex_search(input, match, SshHostGenerator::_configKeyValueRegex)) + { + key = match[1]; + value = match[2]; + return true; + } + } + } + CATCH_LOG(); + + return false; +} + +/*static*/ void SshHostGenerator::_getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector& hostNames) noexcept +{ + try + { + const std::filesystem::path resolvedConfigPath{ wil::ExpandEnvironmentStringsW(configPath.data()) }; + if (std::filesystem::exists(resolvedConfigPath)) + { + std::wifstream inputStream(resolvedConfigPath); + + std::wstring line; + std::wstring key; + std::wstring value; + + std::wstring lastHost; + + while (std::getline(inputStream, line)) + { + if (_tryParseConfigKeyValue(line, key, value)) + { + if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOST_KEY)) + { + // Save potential Host value for later + lastHost = value; + } + else if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOSTNAME_KEY)) + { + // HostName was specified + if (!lastHost.empty()) + { + hostNames.emplace_back(lastHost); + lastHost = L""; + } + } + } + } + } + } + CATCH_LOG(); +} + +std::wstring_view SshHostGenerator::GetNamespace() const noexcept +{ + return SshHostGeneratorNamespace; +} + +// Method Description: +// - Generate a list of profiles for each detected OpenSSH host. +// Arguments: +// - +// Return Value: +// - +void SshHostGenerator::GenerateProfiles(std::vector>& profiles) const +{ + std::wstring sshExePath; + if (_tryFindSshExePath(sshExePath)) + { + std::vector hostNames; + + _getHostNamesFromConfigFile(SSH_SYSTEM_CONFIG_PATH, hostNames); + _getHostNamesFromConfigFile(SSH_USER_CONFIG_PATH, hostNames); + + for (const auto& hostName : hostNames) + { + const auto profile{ CreateDynamicProfile(_getProfileName(hostName)) }; + + profile->Commandline(winrt::hstring{ _getProfileCommandLine(sshExePath, hostName) }); + profile->Icon(winrt::hstring{ _getProfileIconPath() }); + + profiles.emplace_back(profile); + } + } +} diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h new file mode 100644 index 0000000000..a355b0c430 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h @@ -0,0 +1,40 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SshHostGenerator + +Abstract: +- This is the dynamic profile generator for SSH connections. Enumerates all the + SSH hosts to create profiles for them. + +Author(s): +- Jon Thysell - September 2022 + +--*/ + +#pragma once + +#include "IDynamicProfileGenerator.h" + +namespace winrt::Microsoft::Terminal::Settings::Model +{ + class SshHostGenerator final : public IDynamicProfileGenerator + { + public: + std::wstring_view GetNamespace() const noexcept override; + void GenerateProfiles(std::vector>& profiles) const override; + + private: + static const std::wregex _configKeyValueRegex; + + static std::wstring_view _getProfileName(const std::wstring_view& hostName) noexcept; + static std::wstring_view _getProfileIconPath() noexcept; + static std::wstring_view _getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept; + + static bool _tryFindSshExePath(std::wstring& sshExePath) noexcept; + static bool _tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept; + static void _getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector& hostNames) noexcept; + }; +}; diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 0ba5785730..617692691e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -57,7 +57,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto globals = appSettings.GlobalSettings(); settings->_ApplyProfileSettings(profile); settings->_ApplyGlobalSettings(globals); - settings->_ApplyAppearanceSettings(profile.DefaultAppearance(), globals.ColorSchemes()); + settings->_ApplyAppearanceSettings(profile.DefaultAppearance(), globals.ColorSchemes(), globals.CurrentTheme()); return settings; } @@ -91,7 +91,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { const auto globals = appSettings.GlobalSettings(); auto childImpl = settings->CreateChild(); - childImpl->_ApplyAppearanceSettings(unfocusedAppearance, globals.ColorSchemes()); + childImpl->_ApplyAppearanceSettings(unfocusedAppearance, globals.ColorSchemes(), globals.CurrentTheme()); child = *childImpl; } @@ -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()) { @@ -156,7 +163,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } if (newTerminalArgs.TabColor()) { - defaultSettings.StartingTabColor(winrt::Windows::Foundation::IReference{ til::color{ newTerminalArgs.TabColor().Value() } }); + defaultSettings.StartingTabColor(winrt::Windows::Foundation::IReference{ static_cast(til::color{ newTerminalArgs.TabColor().Value() }) }); } if (newTerminalArgs.SuppressApplicationTitle()) { @@ -178,22 +185,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { defaultSettings.Elevate(newTerminalArgs.Elevate().Value()); } + + if (newTerminalArgs.ReloadEnvironmentVariables()) + { + defaultSettings.ReloadEnvironmentVariables(newTerminalArgs.ReloadEnvironmentVariables().Value()); + } } return settingsPair; } - void TerminalSettings::_ApplyAppearanceSettings(const IAppearanceConfig& appearance, const Windows::Foundation::Collections::IMapView& schemes) + void TerminalSettings::_ApplyAppearanceSettings(const IAppearanceConfig& appearance, + const Windows::Foundation::Collections::IMapView& schemes, + const winrt::Microsoft::Terminal::Settings::Model::Theme currentTheme) { _CursorShape = appearance.CursorShape(); _CursorHeight = appearance.CursorHeight(); - if (!appearance.ColorSchemeName().empty()) + + auto requestedTheme = currentTheme.RequestedTheme(); + if (requestedTheme == winrt::Windows::UI::Xaml::ElementTheme::Default) { - if (const auto scheme = schemes.TryLookup(appearance.ColorSchemeName())) + requestedTheme = Model::Theme::IsSystemInDarkTheme() ? + winrt::Windows::UI::Xaml::ElementTheme::Dark : + winrt::Windows::UI::Xaml::ElementTheme::Light; + } + + switch (requestedTheme) + { + case winrt::Windows::UI::Xaml::ElementTheme::Light: + if (const auto scheme = schemes.TryLookup(appearance.LightColorSchemeName())) { ApplyColorScheme(scheme); } + break; + case winrt::Windows::UI::Xaml::ElementTheme::Dark: + if (const auto scheme = schemes.TryLookup(appearance.DarkColorSchemeName())) + { + ApplyColorScheme(scheme); + } + break; + case winrt::Windows::UI::Xaml::ElementTheme::Default: + // This shouldn't happen! + break; } + if (appearance.Foreground()) { _DefaultForeground = til::color{ appearance.Foreground().Value() }; @@ -227,6 +262,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _AdjustIndistinguishableColors = appearance.AdjustIndistinguishableColors(); _Opacity = appearance.Opacity(); + _UseAcrylic = appearance.UseAcrylic(); } // Method Description: @@ -246,13 +282,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // Fill in the remaining properties from the profile _ProfileName = profile.Name(); _ProfileSource = profile.Source(); - _UseAcrylic = profile.UseAcrylic(); - _FontFace = profile.FontInfo().FontFace(); - _FontSize = profile.FontInfo().FontSize(); - _FontWeight = profile.FontInfo().FontWeight(); - _FontFeatures = profile.FontInfo().FontFeatures(); - _FontAxes = profile.FontInfo().FontAxes(); + const auto fontInfo = profile.FontInfo(); + _FontFace = fontInfo.FontFace(); + _FontSize = fontInfo.FontSize(); + _FontWeight = fontInfo.FontWeight(); + _FontFeatures = fontInfo.FontFeatures(); + _FontAxes = fontInfo.FontAxes(); + _CellWidth = fontInfo.CellWidth(); + _CellHeight = fontInfo.CellHeight(); _Padding = profile.Padding(); _Commandline = profile.Commandline(); @@ -280,9 +318,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _TabColor = static_cast(colorRef); } + const auto profileEnvVars = profile.EnvironmentVariables(); + if (profileEnvVars == nullptr) + { + _EnvironmentVariables = std::nullopt; + } + else + { + _EnvironmentVariables = winrt::single_threaded_map(); + for (const auto& [key, value] : profileEnvVars) + { + _EnvironmentVariables.value().Insert(key, value); + } + } + _Elevate = profile.Elevate(); _AutoMarkPrompts = Feature_ScrollbarMarks::IsEnabled() && profile.AutoMarkPrompts(); _ShowMarks = Feature_ScrollbarMarks::IsEnabled() && profile.ShowMarks(); + + _RightClickContextMenu = profile.RightClickContextMenu(); + + _RepositionCursorWithMouse = profile.RepositionCursorWithMouse(); + + _ReloadEnvironmentVariables = profile.ReloadEnvironmentVariables(); } // Method Description: @@ -305,6 +363,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _ForceVTInput = globalSettings.ForceVTInput(); _TrimBlockSelection = globalSettings.TrimBlockSelection(); _DetectURLs = globalSettings.DetectURLs(); + _EnableUnfocusedAcrylic = globalSettings.EnableUnfocusedAcrylic(); } // Method Description: @@ -372,11 +431,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return colorTable; } - gsl::span TerminalSettings::_getColorTableImpl() + std::span TerminalSettings::_getColorTableImpl() { if (_ColorTable.has_value()) { - return gsl::make_span(*_ColorTable); + return std::span{ *_ColorTable }; } for (auto&& parent : _parents) { diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 0dd65cdfbb..60c9b5fb2c 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -22,6 +22,7 @@ Author(s): using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IEnvironmentVariableMap = winrt::Windows::Foundation::Collections::IMap; // fwdecl unittest classes namespace SettingsModelLocalTests @@ -117,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); @@ -126,6 +128,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Text::FontWeight, FontWeight); INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes); INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures); + INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellWidth); + INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellHeight); INHERITABLE_SETTING(Model::TerminalSettings, Model::ColorScheme, AppliedColorScheme); INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage); @@ -141,7 +145,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingDirectory); INHERITABLE_SETTING(Model::TerminalSettings, hstring, StartingTitle); INHERITABLE_SETTING(Model::TerminalSettings, bool, SuppressApplicationTitle); - INHERITABLE_SETTING(Model::TerminalSettings, hstring, EnvironmentVariables); + INHERITABLE_SETTING(Model::TerminalSettings, IEnvironmentVariableMap, EnvironmentVariables); INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible); INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAtlasEngine, false); @@ -160,17 +164,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, bool, AutoMarkPrompts, false); INHERITABLE_SETTING(Model::TerminalSettings, bool, ShowMarks, false); + INHERITABLE_SETTING(Model::TerminalSettings, bool, RightClickContextMenu, false); + INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false); + + INHERITABLE_SETTING(Model::TerminalSettings, bool, ReloadEnvironmentVariables, true); private: std::optional> _ColorTable; - gsl::span _getColorTableImpl(); + std::span _getColorTableImpl(); static winrt::com_ptr _CreateWithProfileCommon(const Model::CascadiaSettings& appSettings, const Model::Profile& profile); void _ApplyProfileSettings(const Model::Profile& profile); void _ApplyGlobalSettings(const Model::GlobalAppSettings& globalSettings) noexcept; void _ApplyAppearanceSettings(const Microsoft::Terminal::Settings::Model::IAppearanceConfig& appearance, - const Windows::Foundation::Collections::IMapView& schemes); + const Windows::Foundation::Collections::IMapView& schemes, + const winrt::Microsoft::Terminal::Settings::Model::Theme currentTheme); friend class SettingsModelLocalTests::TerminalSettingsTests; }; diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl index 514fcf7e36..23c305ee4e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.idl @@ -3,6 +3,8 @@ import "CascadiaSettings.idl"; +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { runtimeclass TerminalSettingsCreateResult @@ -26,6 +28,8 @@ namespace Microsoft.Terminal.Settings.Model { TerminalSettings(); + Windows.Foundation.Collections.IMap EnvironmentVariables; + static TerminalSettings CreateForPreview(CascadiaSettings appSettings, Profile profile); static TerminalSettingsCreateResult CreateWithProfile(CascadiaSettings appSettings, Profile profile, Microsoft.Terminal.Control.IKeyBindings keybindings); static TerminalSettingsCreateResult CreateWithNewTerminalArgs(CascadiaSettings appSettings, NewTerminalArgs newTerminalArgs, Microsoft.Terminal.Control.IKeyBindings keybindings); @@ -39,8 +43,8 @@ namespace Microsoft.Terminal.Settings.Model // able to change these at runtime (e.g. when duplicating a pane). String Commandline { set; }; String StartingDirectory { set; }; - String EnvironmentVariables { set; }; Boolean Elevate; + Boolean ReloadEnvironmentVariables; }; } diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 95e5a03cf8..f5431aabf3 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -499,6 +499,17 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FindMatchDirecti }; }; +JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::SuggestionsSource) +{ + 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{ "all", AllSet }, + }; +}; + JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::WindowingMode) { JSON_MAPPINGS(3) = { @@ -633,10 +644,20 @@ struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winrt::Microsoft::Terminal::Control::SelectionColor> { diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index 43dee4e5a6..aa02d18ff4 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -20,8 +20,11 @@ namespace Microsoft.Terminal.Settings.Model InvalidColorSchemeInCmd, InvalidSplitSize, FailedToParseStartupActions, + InvalidProfileEnvironmentVariables, FailedToParseSubCommands, UnknownTheme, + DuplicateRemainingProfilesEntry, + InvalidUseOfContent, WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder. }; diff --git a/src/cascadia/TerminalSettingsModel/Theme.cpp b/src/cascadia/TerminalSettingsModel/Theme.cpp index c7236a8463..353f9df599 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.cpp +++ b/src/cascadia/TerminalSettingsModel/Theme.cpp @@ -13,6 +13,7 @@ #include "WindowTheme.g.cpp" #include "TabRowTheme.g.cpp" #include "TabTheme.g.cpp" +#include "ThemePair.g.cpp" #include "Theme.g.cpp" using namespace ::Microsoft::Console; @@ -27,6 +28,8 @@ namespace winrt } static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view LightNameKey{ "light" }; +static constexpr std::string_view DarkNameKey{ "dark" }; static constexpr wchar_t RegKeyDwm[] = L"Software\\Microsoft\\Windows\\DWM"; static constexpr wchar_t RegKeyAccentColor[] = L"AccentColor"; @@ -125,16 +128,12 @@ winrt::WUX::Media::Brush ThemeColor::Evaluate(const winrt::WUX::ResourceDictiona { static const auto accentColorKey{ winrt::box_value(L"SystemAccentColor") }; - // NOTE: Currently, the DWM titlebar is always drawn, underneath our XAML - // content. If the opacity is <1.0, then you'll be able to see it, including - // the original caption buttons, which we don't want. - switch (ColorType()) { case ThemeColorType::Accent: { // NOTE: There is no canonical way to get the unfocused ACCENT titlebar - // color in Windows. Edge uses it's own heuristic, and in Windows 11, + // color in Windows. Edge uses its own heuristic, and in Windows 11, // much of this logic is rapidly changing. We're not gonna mess with // that, since it seems there's no good way to reverse engineer that. til::color accentColor = forTitlebar ? @@ -148,51 +147,10 @@ winrt::WUX::Media::Brush ThemeColor::Evaluate(const winrt::WUX::ResourceDictiona } case ThemeColorType::Color: { - return winrt::WUX::Media::SolidColorBrush{ forTitlebar ? - Color().with_alpha(255) : - Color() }; + return winrt::WUX::Media::SolidColorBrush{ Color() }; } case ThemeColorType::TerminalBackground: { - // If we're evaluating this color for the tab row, there are some rules - // we have to follow, unfortunately. We can't allow a transparent - // background, so we have to make sure to fill that in with Opacity(1.0) - // manually. - // - // So for that case, just make a new brush with the relevant properties - // set. - if (forTitlebar) - { - if (auto acrylic = terminalBackground.try_as()) - { - winrt::WUX::Media::AcrylicBrush newBrush{}; - newBrush.TintColor(acrylic.TintColor()); - newBrush.FallbackColor(acrylic.FallbackColor()); - newBrush.TintLuminosityOpacity(acrylic.TintLuminosityOpacity()); - - // Allow acrylic opacity, but it's gotta be HostBackdrop acrylic. - // - // For now, just always use 50% opacity for this. If we do ever - // figure out how to get rid of our titlebar under the XAML tab - // row (GH#10509), we can always get rid of the HostBackdrop - // thing, and all this copying, and just return the - // terminalBackground brush directly. - // - // Because we're wholesale copying the brush, we won't be able - // to adjust it's opacity with the mouse wheel. This seems like - // an acceptable tradeoff for now. - newBrush.TintOpacity(.5); - newBrush.BackgroundSource(winrt::WUX::Media::AcrylicBackgroundSource::HostBackdrop); - return newBrush; - } - else if (auto solidColor = terminalBackground.try_as()) - { - winrt::WUX::Media::SolidColorBrush newBrush{}; - newBrush.Color(til::color{ solidColor.Color() }.with_alpha(255)); - return newBrush; - } - } - return terminalBackground; } } @@ -365,3 +323,52 @@ winrt::WUX::ElementTheme Theme::RequestedTheme() const noexcept { return _Window ? _Window.RequestedTheme() : winrt::WUX::ElementTheme::Default; } + +winrt::com_ptr ThemePair::FromJson(const Json::Value& json) +{ + auto result = winrt::make_self(L"dark"); + + if (json.isString()) + { + result->_DarkName = result->_LightName = JsonUtils::GetValue(json); + } + else if (json.isObject()) + { + JsonUtils::GetValueForKey(json, DarkNameKey, result->_DarkName); + JsonUtils::GetValueForKey(json, LightNameKey, result->_LightName); + } + return result; +} + +Json::Value ThemePair::ToJson() const +{ + if (_DarkName == _LightName) + { + return JsonUtils::ConversionTrait().ToJson(DarkName()); + } + else + { + Json::Value json{ Json::ValueType::objectValue }; + + JsonUtils::SetValueForKey(json, DarkNameKey, _DarkName); + JsonUtils::SetValueForKey(json, LightNameKey, _LightName); + return json; + } +} +winrt::com_ptr ThemePair::Copy() const +{ + auto pair{ winrt::make_self() }; + pair->_DarkName = _DarkName; + pair->_LightName = _LightName; + return pair; +} + +// I'm not even joking, this is the recommended way to do this: +// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes#know-when-dark-mode-is-enabled +bool Theme::IsSystemInDarkTheme() +{ + static auto isColorLight = [](const winrt::Windows::UI::Color& clr) -> bool { + return (((5 * clr.G) + (2 * clr.R) + clr.B) > (8 * 128)); + }; + return isColorLight(winrt::Windows::UI::ViewManagement::UISettings().GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Foreground)); +}; diff --git a/src/cascadia/TerminalSettingsModel/Theme.h b/src/cascadia/TerminalSettingsModel/Theme.h index e39d42b8f2..ef0daa5d14 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.h +++ b/src/cascadia/TerminalSettingsModel/Theme.h @@ -22,10 +22,33 @@ Author(s): #include "WindowTheme.g.h" #include "TabRowTheme.g.h" #include "TabTheme.g.h" +#include "ThemePair.g.h" #include "Theme.g.h" +#include "JsonUtils.h" + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + struct ThemePair : ThemePairT + { + public: + ThemePair() = default; + explicit ThemePair(const winrt::hstring& name) noexcept : + _DarkName{ name }, + _LightName{ name } {}; + + explicit ThemePair(const winrt::hstring& lightName, const winrt::hstring& darkName) noexcept : + _DarkName{ darkName }, + _LightName{ lightName } {}; + + static com_ptr FromJson(const Json::Value& json); + Json::Value ToJson() const; + com_ptr Copy() const; + + WINRT_PROPERTY(winrt::hstring, DarkName); + WINRT_PROPERTY(winrt::hstring, LightName); + }; + struct ThemeColor : ThemeColorT { public: @@ -77,6 +100,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::Windows::UI::Xaml::ElementTheme RequestedTheme() const noexcept; + static bool IsSystemInDarkTheme(); + WINRT_PROPERTY(winrt::hstring, Name); MTSM_THEME_SETTINGS(THEME_SETTINGS_INITIALIZE) @@ -92,4 +117,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation { BASIC_FACTORY(ThemeColor); BASIC_FACTORY(Theme); + BASIC_FACTORY(ThemePair); +} + +namespace Microsoft::Terminal::Settings::Model::JsonUtils +{ + template<> + struct ConversionTrait + { + winrt::Microsoft::Terminal::Settings::Model::ThemePair FromJson(const Json::Value& json) + { + return *winrt::Microsoft::Terminal::Settings::Model::implementation::ThemePair::FromJson(json); + } + + bool CanConvert(const Json::Value& json) const + { + return json.isObject() || json.isString(); + } + + Json::Value ToJson(const winrt::Microsoft::Terminal::Settings::Model::ThemePair& val) + { + return winrt::get_self(val)->ToJson(); + } + + std::string TypeDescription() const + { + return "ThemePair{ string, string }"; + } + }; } diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 39260bb0cf..830bafef2a 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -4,6 +4,13 @@ namespace Microsoft.Terminal.Settings.Model { + enum IconStyle + { + Default, + Hidden, + Monochrome + }; + enum ThemeColorType { Accent, @@ -15,7 +22,18 @@ namespace Microsoft.Terminal.Settings.Model { Always, Hover, - Never + Never, + ActiveOnly + }; + + [default_interface] runtimeclass ThemePair + { + ThemePair(); + ThemePair(String name); + ThemePair(String darkName, String lightName); + + String DarkName; + String LightName; }; runtimeclass ThemeColor @@ -37,6 +55,10 @@ namespace Microsoft.Terminal.Settings.Model runtimeclass WindowTheme { Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; + Boolean UseMica { get; }; + Boolean RainbowFrame { get; }; + ThemeColor Frame { get; }; + ThemeColor UnfocusedFrame { get; }; } runtimeclass TabRowTheme { @@ -48,6 +70,7 @@ namespace Microsoft.Terminal.Settings.Model ThemeColor Background { get; }; ThemeColor UnfocusedBackground { get; }; TabCloseButtonVisibility ShowCloseButton { get; }; + IconStyle IconStyle { get; }; } [default_interface] runtimeclass Theme : Windows.Foundation.IStringable { @@ -67,6 +90,7 @@ namespace Microsoft.Terminal.Settings.Model // A helper for retrieving the RequestedTheme out of the window property Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; + static Boolean IsSystemInDarkTheme(); } } diff --git a/src/cascadia/TerminalSettingsModel/VsDevCmdGenerator.cpp b/src/cascadia/TerminalSettingsModel/VsDevCmdGenerator.cpp index e00f9f3841..19d6f50cc8 100644 --- a/src/cascadia/TerminalSettingsModel/VsDevCmdGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/VsDevCmdGenerator.cpp @@ -17,7 +17,7 @@ void VsDevCmdGenerator::GenerateProfiles(const VsSetupConfiguration::VsSetupInst } const auto seed = GetProfileGuidSeed(instance); - const winrt::guid profileGuid{ ::Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(seed))) }; + const winrt::guid profileGuid{ ::Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, std::as_bytes(std::span{ seed })) }; auto profile = winrt::make_self(profileGuid); profile->Name(winrt::hstring{ GetProfileName(instance) }); profile->Commandline(winrt::hstring{ GetProfileCommandLine(instance) }); @@ -42,10 +42,14 @@ std::wstring VsDevCmdGenerator::GetProfileCommandLine(const VsSetupConfiguration commandLine.reserve(256); commandLine.append(LR"(cmd.exe /k ")"); commandLine.append(GetDevCmdScriptPath(instance)); + // The "-startdir" parameter will prevent "vsdevcmd" from automatically + // setting the shell path so the path in the profile will be used instead. #if defined(_M_ARM64) - commandLine.append(LR"(" -arch=arm64 -host_arch=x64)"); + commandLine.append(LR"(" -startdir=none -arch=arm64 -host_arch=x64)"); #elif defined(_M_AMD64) - commandLine.append(LR"(" -arch=x64 -host_arch=x64)"); + commandLine.append(LR"(" -startdir=none -arch=x64 -host_arch=x64)"); +#else + commandLine.append(LR"(" -startdir=none)"); #endif return commandLine; } diff --git a/src/cascadia/TerminalSettingsModel/VsDevShellGenerator.cpp b/src/cascadia/TerminalSettingsModel/VsDevShellGenerator.cpp index a1b4ac67a1..22c17cd98a 100644 --- a/src/cascadia/TerminalSettingsModel/VsDevShellGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/VsDevShellGenerator.cpp @@ -18,7 +18,7 @@ void VsDevShellGenerator::GenerateProfiles(const VsSetupConfiguration::VsSetupIn } const auto seed = GetProfileGuidSeed(instance); - const winrt::guid profileGuid{ ::Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(seed))) }; + const winrt::guid profileGuid{ ::Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, std::as_bytes(std::span{ seed })) }; auto profile = winrt::make_self(profileGuid); profile->Name(winrt::hstring{ GetProfileName(instance) }); profile->Commandline(winrt::hstring{ GetProfileCommandLine(instance) }); diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp index 840835cf1f..2b656154ad 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp @@ -11,6 +11,7 @@ static constexpr std::wstring_view WslHomeDirectory{ L"~" }; static constexpr std::wstring_view DockerDistributionPrefix{ L"docker-desktop" }; +static constexpr std::wstring_view RancherDistributionPrefix{ L"rancher-desktop" }; // The WSL entries are structured as such: // HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss @@ -53,7 +54,8 @@ static winrt::com_ptr makeProfile(const std::wstring& d std::wstring command{}; THROW_IF_FAILED(wil::GetSystemDirectoryW(command)); WSLDistro->Commandline(winrt::hstring{ command + L"\\wsl.exe -d " + distName }); - WSLDistro->DefaultAppearance().ColorSchemeName(L"Campbell"); + WSLDistro->DefaultAppearance().DarkColorSchemeName(L"Campbell"); + WSLDistro->DefaultAppearance().LightColorSchemeName(L"Campbell"); if (isWslDashDashCdAvailableForLinuxPaths()) { WSLDistro->StartingDirectory(winrt::hstring{ WslHomeDirectory }); @@ -77,9 +79,9 @@ static void namesToProfiles(const std::vector& names, std::vector< { for (const auto& distName : names) { - if (til::starts_with(distName, DockerDistributionPrefix)) + if (til::starts_with(distName, DockerDistributionPrefix) || til::starts_with(distName, RancherDistributionPrefix)) { - // Docker for Windows creates some utility distributions to handle Docker commands. + // Docker for Windows and Rancher for Windows creates some utility distributions to handle Docker commands. // Pursuant to GH#3556, because they are _not_ user-facing we want to hide them. continue; } diff --git a/src/cascadia/TerminalSettingsModel/defaults-universal.json b/src/cascadia/TerminalSettingsModel/defaults-universal.json deleted file mode 100644 index 22507f5c00..0000000000 --- a/src/cascadia/TerminalSettingsModel/defaults-universal.json +++ /dev/null @@ -1,224 +0,0 @@ -// THIS IS AN AUTO-GENERATED FILE! Changes to this file will be ignored. -{ - "defaultProfile": "{70bfecf4-bcbb-443b-a8fa-d7ac4f7ad201}", - - // Launch Settings - "initialCols": 120, - "initialRows": 30, - "launchMode": "default", - "alwaysOnTop": false, - - // Selection - "copyOnSelect": false, - "copyFormatting": true, - "wordDelimiters": " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502", - - // Tab UI - "alwaysShowTabs": true, - "showTabsInTitlebar": true, - "showTerminalTitleInTitlebar": true, - "tabWidthMode": "equal", - "useTabSwitcher": true, - - // Miscellaneous - "confirmCloseAllTabs": true, - "startOnUserLogin": false, - "theme": "system", - "snapToGridOnResize": true, - - "profiles": - [ - { - "guid": "{70bfecf4-bcbb-443b-a8fa-d7ac4f7ad201}", - "name": "PowerShell", - "commandline": "pwsh.exe", - "icon": "ms-appx:///ProfileIcons/pwsh.png", - "colorScheme": "Campbell", - "antialiasingMode": "grayscale", - "closeOnExit": "graceful", - "cursorShape": "bar", - "fontFace": "Cascadia Mono", - "fontSize": 12, - "hidden": false, - "historySize": 9001, - "padding": "8, 8, 8, 8", - "snapOnInput": true, - "altGrAliasing": true, - "startingDirectory": "%SystemRoot%", - "useAcrylic": false, - "backgroundImage": "ms-appx:///internal-background.png", - "backgroundImageAlignment": "bottomRight", - "backgroundImageOpacity": 0.4, - "backgroundImageStretchMode": "none" - }, - { - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "Command Prompt", - "commandline": "%SystemRoot%\\System32\\cmd.exe", - "icon": "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png", - "colorScheme": "Campbell", - "antialiasingMode": "grayscale", - "closeOnExit": "graceful", - "cursorShape": "bar", - "fontFace": "Cascadia Mono", - "fontSize": 12, - "hidden": false, - "historySize": 9001, - "padding": "8, 8, 8, 8", - "snapOnInput": true, - "altGrAliasing": true, - "startingDirectory": "%SystemRoot%", - "useAcrylic": false, - "backgroundImage": "ms-appx:///internal-background.png", - "backgroundImageAlignment": "bottomRight", - "backgroundImageOpacity": 0.4, - "backgroundImageStretchMode": "none" - } - ], - "schemes": - [ - { - "name": "Campbell", - "foreground": "#CCCCCC", - "background": "#0C0C0C", - "cursorColor": "#FFFFFF", - "black": "#0C0C0C", - "red": "#C50F1F", - "green": "#13A10E", - "yellow": "#C19C00", - "blue": "#0037DA", - "purple": "#881798", - "cyan": "#3A96DD", - "white": "#CCCCCC", - "brightBlack": "#767676", - "brightRed": "#E74856", - "brightGreen": "#16C60C", - "brightYellow": "#F9F1A5", - "brightBlue": "#3B78FF", - "brightPurple": "#B4009E", - "brightCyan": "#61D6D6", - "brightWhite": "#F2F2F2" - } - ], - "actions": - [ - // Application-level Keys - { "command": "closeWindow", "keys": "alt+f4" }, - { "command": "toggleFullscreen", "keys": "alt+enter" }, - { "command": "toggleFullscreen", "keys": "f11" }, - { "command": "toggleFocusMode" }, - { "command": "toggleAlwaysOnTop" }, - { "command": "openNewTabDropdown", "keys": "ctrl+shift+space" }, - { "command": "openSettings", "keys": "ctrl+," }, - { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," }, - { "command": "find", "keys": "ctrl+shift+f" }, - { "command": "openTabColorPicker" }, - { "command": "commandPalette", "keys":"ctrl+shift+p" }, - - // Tab Management - // "command": "closeTab" is unbound by default. - // The closeTab command closes a tab without confirmation, even if it has multiple panes. - { "command": "closeOtherTabs" }, - { "command": "closeTabsAfter" }, - { "command": "newTab", "keys": "ctrl+shift+t" }, - { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1" }, - { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2" }, - { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3" }, - { "command": { "action": "newTab", "index": 3 }, "keys": "ctrl+shift+4" }, - { "command": { "action": "newTab", "index": 4 }, "keys": "ctrl+shift+5" }, - { "command": { "action": "newTab", "index": 5 }, "keys": "ctrl+shift+6" }, - { "command": { "action": "newTab", "index": 6 }, "keys": "ctrl+shift+7" }, - { "command": { "action": "newTab", "index": 7 }, "keys": "ctrl+shift+8" }, - { "command": { "action": "newTab", "index": 8 }, "keys": "ctrl+shift+9" }, - { "command": "duplicateTab", "keys": "ctrl+shift+d" }, - { "command": "nextTab", "keys": "ctrl+tab" }, - { "command": "prevTab", "keys": "ctrl+shift+tab" }, - { "command": { "action": "switchToTab", "index": 0 }, "keys": "ctrl+alt+1" }, - { "command": { "action": "switchToTab", "index": 1 }, "keys": "ctrl+alt+2" }, - { "command": { "action": "switchToTab", "index": 2 }, "keys": "ctrl+alt+3" }, - { "command": { "action": "switchToTab", "index": 3 }, "keys": "ctrl+alt+4" }, - { "command": { "action": "switchToTab", "index": 4 }, "keys": "ctrl+alt+5" }, - { "command": { "action": "switchToTab", "index": 5 }, "keys": "ctrl+alt+6" }, - { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7" }, - { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8" }, - { "command": { "action": "switchToTab", "index": 8 }, "keys": "ctrl+alt+9" }, - - // Pane Management - { "command": "closePane", "keys": "ctrl+shift+w" }, - { "command": { "action": "splitPane", "split": "horizontal" }, "keys": "alt+shift+-" }, - { "command": { "action": "splitPane", "split": "vertical" }, "keys": "alt+shift+plus" }, - { "command": { "action": "resizePane", "direction": "down" }, "keys": "alt+shift+down" }, - { "command": { "action": "resizePane", "direction": "left" }, "keys": "alt+shift+left" }, - { "command": { "action": "resizePane", "direction": "right" }, "keys": "alt+shift+right" }, - { "command": { "action": "resizePane", "direction": "up" }, "keys": "alt+shift+up" }, - { "command": { "action": "moveFocus", "direction": "down" }, "keys": "alt+down" }, - { "command": { "action": "moveFocus", "direction": "left" }, "keys": "alt+left" }, - { "command": { "action": "moveFocus", "direction": "right" }, "keys": "alt+right" }, - { "command": { "action": "moveFocus", "direction": "up" }, "keys": "alt+up" }, - { "command": "togglePaneZoom" }, - - // Clipboard Integration - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" }, - { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+insert" }, - { "command": "paste", "keys": "ctrl+shift+v" }, - { "command": "paste", "keys": "shift+insert" }, - - // Scrollback - { "command": "scrollDown", "keys": "ctrl+shift+down" }, - { "command": "scrollDownPage", "keys": "ctrl+shift+pgdn" }, - { "command": "scrollUp", "keys": "ctrl+shift+up" }, - { "command": "scrollUpPage", "keys": "ctrl+shift+pgup" }, - - // Visual Adjustments - { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, - { "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" }, - { "command": "resetFontSize", "keys": "ctrl+0" }, - - // Other commands - { - // Select color scheme... - "name": { "key": "SetColorSchemeParentCommandName" }, - "commands": [ - { - "iterateOn": "schemes", - "name": "${scheme.name}", - "command": { "action": "setColorScheme", "colorScheme": "${scheme.name}" } - } - ] - }, - { - // New tab... - "name": { "key": "NewTabParentCommandName" }, - "commands": [ - { - "iterateOn": "profiles", - "icon": "${profile.icon}", - "name": "${profile.name}", - "command": { "action": "newTab", "profile": "${profile.name}" } - } - ] - }, - { - // Split pane... - "name": { "key": "SplitPaneParentCommandName" }, - "commands": [ - { - "iterateOn": "profiles", - "icon": "${profile.icon}", - "name": "${profile.name}...", - "commands": [ - { - "command": { "action": "splitPane", "profile": "${profile.name}", "split": "auto" } - }, - { - "command": { "action": "splitPane", "profile": "${profile.name}", "split": "vertical" } - }, - { - "command": { "action": "splitPane", "profile": "${profile.name}", "split": "horizontal" } - } - ] - } - ] - } - ] -} diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 59a7f231c8..cdd5d6e78d 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -316,6 +316,36 @@ "background": "terminalBackground", "unfocusedBackground": "#00000000" } + }, + { + "name": "legacyDark", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "dark" + } + }, + { + "name": "legacyLight", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "light" + } + }, + { + "name": "legacySystem", + "tab": { + "background": null, + "unfocusedBackground": null + }, + "window": { + "applicationTheme": "system" + }, } ], "actions": @@ -343,7 +373,8 @@ { "command": "quakeMode", "keys":"win+sc(41)" }, { "command": "openSystemMenu", "keys": "alt+space" }, { "command": "quit" }, - { "command": "restoreLastClosed"}, + { "command": "restoreLastClosed" }, + { "command": "openAbout" }, // Tab Management // "command": "closeTab" is unbound by default. @@ -375,6 +406,7 @@ { "command": { "action": "switchToTab", "index": 6 }, "keys": "ctrl+alt+7" }, { "command": { "action": "switchToTab", "index": 7 }, "keys": "ctrl+alt+8" }, { "command": { "action": "switchToTab", "index": 4294967295 }, "keys": "ctrl+alt+9" }, + { "command": { "action": "moveTab", "window": "new" }, }, // Pane Management { "command": "closeOtherPanes" }, @@ -405,9 +437,12 @@ { "command": { "action": "swapPane", "direction": "previousInOrder"} }, { "command": { "action": "swapPane", "direction": "nextInOrder"} }, { "command": { "action": "swapPane", "direction": "first" } }, + { "command": "toggleBroadcastInput" }, { "command": "togglePaneZoom" }, { "command": "toggleSplitOrientation" }, { "command": "toggleReadOnlyMode" }, + { "command": "enableReadOnlyMode" }, + { "command": "disableReadOnlyMode" }, { "command": { "action": "movePane", "index": 0 } }, { "command": { "action": "movePane", "index": 1 } }, { "command": { "action": "movePane", "index": 2 } }, @@ -417,6 +452,8 @@ { "command": { "action": "movePane", "index": 6 } }, { "command": { "action": "movePane", "index": 7 } }, { "command": { "action": "movePane", "index": 8 } }, + { "command": { "action": "movePane", "window": "new" }, }, + { "command": "restartConnection" }, // Clipboard Integration { "command": { "action": "copy", "singleLine": false }, "keys": "ctrl+shift+c" }, @@ -429,6 +466,10 @@ { "command": "toggleBlockSelection" }, { "command": "switchSelectionEndpoint" }, { "command": "expandSelectionToWord" }, + { "command": "showContextMenu", "keys": "menu" }, + + // Web Search + { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" } }, // Scrollback { "command": "scrollDown", "keys": "ctrl+shift+down" }, diff --git a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj index 88132dc940..a9380bc704 100644 --- a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj +++ b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj @@ -38,6 +38,25 @@ + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + + + ../NewTabMenuEntry.h + @@ -78,7 +97,7 @@ false - diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h index f8266b66fd..133786e5da 100644 --- a/src/cascadia/TerminalSettingsModel/pch.h +++ b/src/cascadia/TerminalSettingsModel/pch.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hSettingsModelProvider); // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include #include #include diff --git a/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj b/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj index 1aba74045b..7685839407 100644 --- a/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj @@ -12,6 +12,7 @@ true + true diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index 658d4ca623..f34ca5a6bf 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -36,6 +36,12 @@ namespace ControlUnitTests TEST_METHOD(TestClearScrollback); TEST_METHOD(TestClearScreen); TEST_METHOD(TestClearAll); + TEST_METHOD(TestReadEntireBuffer); + + TEST_METHOD(TestSelectCommandSimple); + TEST_METHOD(TestSelectOutputSimple); + + TEST_METHOD(TestSimpleClickSelection); TEST_CLASS_SETUP(ModuleSetup) { @@ -142,8 +148,8 @@ namespace ControlUnitTests // requested acrylic. auto expectedUseAcrylic = expectedOpacity < 1.0; + VERIFY_IS_TRUE(core->_settings->UseAcrylic()); VERIFY_ARE_EQUAL(expectedUseAcrylic, core->UseAcrylic()); - VERIFY_ARE_EQUAL(true, core->_settings->UseAcrylic()); }; core->TransparencyChanged(opacityCallback); @@ -340,4 +346,223 @@ namespace ControlUnitTests // The ConptyRoundtripTests test the actual clearing of the contents. } + void ControlCoreTests::TestReadEntireBuffer() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + conn->WriteInput(L"This is some text \r\n"); + conn->WriteInput(L"with varying amounts \r\n"); + conn->WriteInput(L"of whitespace \r\n"); + + Log::Comment(L"Check the buffer contents"); + VERIFY_ARE_EQUAL(L"This is some text\r\nwith varying amounts\r\nof whitespace\r\n", + core->ReadEntireBuffer()); + } + void _writePrompt(const winrt::com_ptr& conn, const auto& path) + { + conn->WriteInput(L"\x1b]133;D\x7"); + conn->WriteInput(L"\x1b]133;A\x7"); + conn->WriteInput(L"\x1b]9;9;"); + conn->WriteInput(path); + conn->WriteInput(L"\x7"); + conn->WriteInput(L"PWSH "); + conn->WriteInput(path); + conn->WriteInput(L"> "); + conn->WriteInput(L"\x1b]133;B\x7"); + } + + void ControlCoreTests::TestSelectCommandSimple() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + + _writePrompt(conn, L"C:\\Windows"); + conn->WriteInput(L"Foo-bar"); + conn->WriteInput(L"\x1b]133;C\x7"); + + conn->WriteInput(L"\r\n"); + conn->WriteInput(L"This is some text \r\n"); + conn->WriteInput(L"with varying amounts \r\n"); + conn->WriteInput(L"of whitespace \r\n"); + + _writePrompt(conn, L"C:\\Windows"); + + Log::Comment(L"Check the buffer contents"); + const auto& buffer = core->_terminal->GetTextBuffer(); + const auto& cursor = buffer.GetCursor(); + + { + const til::point expectedCursor{ 17, 4 }; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + } + + VERIFY_IS_FALSE(core->HasSelection()); + core->SelectCommand(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 17, 0 }; + const til::point expectedEnd{ 23, 0 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + + core->_terminal->ClearSelection(); + conn->WriteInput(L"Boo-far"); + conn->WriteInput(L"\x1b]133;C\x7"); + + VERIFY_IS_FALSE(core->HasSelection()); + { + const til::point expectedCursor{ 24, 4 }; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + } + VERIFY_IS_FALSE(core->HasSelection()); + core->SelectCommand(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 17, 4 }; + const til::point expectedEnd{ 23, 4 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + core->SelectCommand(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 17, 0 }; + const til::point expectedEnd{ 23, 0 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + core->SelectCommand(false); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 17, 4 }; + const til::point expectedEnd{ 23, 4 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + } + void ControlCoreTests::TestSelectOutputSimple() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + + _writePrompt(conn, L"C:\\Windows"); + conn->WriteInput(L"Foo-bar"); + conn->WriteInput(L"\x1b]133;C\x7"); + + conn->WriteInput(L"\r\n"); + conn->WriteInput(L"This is some text \r\n"); + conn->WriteInput(L"with varying amounts \r\n"); + conn->WriteInput(L"of whitespace \r\n"); + + _writePrompt(conn, L"C:\\Windows"); + + Log::Comment(L"Check the buffer contents"); + const auto& buffer = core->_terminal->GetTextBuffer(); + const auto& cursor = buffer.GetCursor(); + + { + const til::point expectedCursor{ 17, 4 }; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + } + + VERIFY_IS_FALSE(core->HasSelection()); + core->SelectOutput(true); + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 24, 0 }; // The character after the prompt + const til::point expectedEnd{ 29, 3 }; // x = buffer.right + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + } + + void ControlCoreTests::TestSimpleClickSelection() + { + // Create a simple selection with the mouse, then click somewhere else, + // and confirm the selection got updated. + + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + // Here, we're using the UpdateSelectionMarkers as a stand-in to check + // if the selection got updated with the renderer. Standing up a whole + // dummy renderer for this test would be not very ergonomic. Instead, we + // are relying on ControlCore::_updateSelectionUI both + // TriggerSelection()'ing and also rasing this event + bool expectedSelectionUpdate = false; + bool gotSelectionUpdate = false; + core->UpdateSelectionMarkers([&](auto&& /*sender*/, auto&& /*args*/) { + VERIFY_IS_TRUE(expectedSelectionUpdate); + expectedSelectionUpdate = false; + gotSelectionUpdate = true; + }); + + auto needToCopy = false; + expectedSelectionUpdate = true; + core->LeftClickOnTerminal(til::point{ 1, 1 }, + 1, + false, + true, + false, + needToCopy); + + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 1, 1 }; + const til::point expectedEnd{ 1, 1 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + VERIFY_IS_TRUE(gotSelectionUpdate); + + expectedSelectionUpdate = true; + core->LeftClickOnTerminal(til::point{ 1, 2 }, + 1, + false, + true, + false, + needToCopy); + + VERIFY_IS_TRUE(core->HasSelection()); + { + const auto& start = core->_terminal->GetSelectionAnchor(); + const auto& end = core->_terminal->GetSelectionEnd(); + const til::point expectedStart{ 1, 1 }; + const til::point expectedEnd{ 1, 2 }; + VERIFY_ARE_EQUAL(expectedStart, start); + VERIFY_ARE_EQUAL(expectedEnd, end); + } + VERIFY_IS_TRUE(gotSelectionUpdate); + } } diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index d73b7b122c..a48aa96894 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -145,7 +145,7 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(0.5, settings->Opacity()); auto expectedUseAcrylic = expectedOpacity < 1.0 && - (!winrt::Microsoft::Terminal::Control::implementation::ControlCore::IsVintageOpacityAvailable() || useAcrylic); + (useAcrylic); VERIFY_ARE_EQUAL(useAcrylic, settings->UseAcrylic()); VERIFY_ARE_EQUAL(expectedUseAcrylic, core->UseAcrylic()); }; @@ -757,12 +757,14 @@ namespace ControlUnitTests _standardInit(core, interactivity); Log::Comment(L"Fill up the history buffer"); + const auto scrollbackLength = settings->HistorySize(); // Output lines equal to history size + viewport height to make sure we're // at the point where outputting more lines causes circular incrementing for (auto i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i) { conn->WriteInput(L"Foo\r\n"); } + VERIFY_ARE_EQUAL(scrollbackLength, core->_terminal->GetScrollOffset()); // For this test, don't use any modifiers const auto modifiers = ControlKeyStates(); @@ -810,9 +812,87 @@ namespace ControlUnitTests Log::Comment(L"Verify the location of the selection"); // The selection should now be 1 row lower - expectedAnchor.Y -= 1; - VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor()); - VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd()); + expectedAnchor.y -= 1; + { + const auto anchor{ core->_terminal->GetSelectionAnchor() }; + const auto end{ core->_terminal->GetSelectionEnd() }; + Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str()); + Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str()); + Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str()); + + VERIFY_ARE_EQUAL(expectedAnchor, anchor); + VERIFY_ARE_EQUAL(expectedAnchor, end); + } + VERIFY_ARE_EQUAL(scrollbackLength - 1, core->_terminal->GetScrollOffset()); + + Log::Comment(L"Output a line of text"); + conn->WriteInput(L"Foo\r\n"); + + Log::Comment(L"Verify the location of the selection"); + // The selection should now be 1 row lower + expectedAnchor.y -= 1; + { + const auto anchor{ core->_terminal->GetSelectionAnchor() }; + const auto end{ core->_terminal->GetSelectionEnd() }; + Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str()); + Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str()); + Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str()); + + VERIFY_ARE_EQUAL(expectedAnchor, anchor); + VERIFY_ARE_EQUAL(expectedAnchor, end); + } + VERIFY_ARE_EQUAL(scrollbackLength - 2, core->_terminal->GetScrollOffset()); + + Log::Comment(L"Move the mouse a little, to update the selection"); + // At this point, there should only be one selection region! The + // viewport moved up to keep the selection at the same relative spot. So + // wiggling the cursor should continue to select only the same + // character in the buffer (if, albeit in a new location). + // + // This helps test GH #14462, a regression from #10749. + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition0.to_core_point(), + true); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); + { + const auto anchor{ core->_terminal->GetSelectionAnchor() }; + const auto end{ core->_terminal->GetSelectionEnd() }; + Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str()); + Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str()); + Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str()); + + VERIFY_ARE_EQUAL(expectedAnchor, anchor); + VERIFY_ARE_EQUAL(expectedAnchor, end); + } + + Log::Comment(L"Output a line ant move the mouse a little to update the selection, all at once"); + // Same as above. The viewport has moved, so the mouse is still over the + // same character, even though it's at a new offset. + conn->WriteInput(L"Foo\r\n"); + expectedAnchor.y -= 1; + VERIFY_ARE_EQUAL(scrollbackLength - 3, core->_terminal->GetScrollOffset()); + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1.to_core_point(), + true); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); + { + const auto anchor{ core->_terminal->GetSelectionAnchor() }; + const auto end{ core->_terminal->GetSelectionEnd() }; + Log::Comment(fmt::format(L"expectedAnchor:({},{})", expectedAnchor.x, expectedAnchor.y).c_str()); + Log::Comment(fmt::format(L"anchor:({},{})", anchor.x, anchor.y).c_str()); + Log::Comment(fmt::format(L"end:({},{})", end.x, end.y).c_str()); + + VERIFY_ARE_EQUAL(expectedAnchor, anchor); + VERIFY_ARE_EQUAL(expectedAnchor, end); + } // Output enough text for the selection to get pushed off the buffer for (auto i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i) @@ -953,8 +1033,9 @@ namespace ControlUnitTests cursorPosition1.to_core_point()); Log::Comment(L" --- Resize the terminal to be 10 columns wider ---"); - const auto newSizeInDips{ til::size{ 40, 20 } * fontSize }; - core->SizeChanged(newSizeInDips.width, newSizeInDips.height); + const auto newWidth = 40.0f * fontSize.width; + const auto newHeight = 20.0f * fontSize.height; + core->SizeChanged(newWidth, newHeight); Log::Comment(L" --- Click on a spot that's NOW INSIDE the buffer ---"); // (32 + 35 + 1) = 68 = 'D' diff --git a/src/cascadia/UnitTests_Control/pch.h b/src/cascadia/UnitTests_Control/pch.h index f5aabcccc7..4833c11df0 100644 --- a/src/cascadia/UnitTests_Control/pch.h +++ b/src/cascadia/UnitTests_Control/pch.h @@ -46,6 +46,8 @@ Licensed under the MIT license. // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" +#include +#include #include "ThrottledFunc.h" diff --git a/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj b/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj index 25043dcdb1..1a546c76b6 100644 --- a/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj @@ -8,7 +8,7 @@ dependencies, like MUX, can be aggregated correctly, and resources properly combined into a resources.pri file. - TestHostApp will manually copy the output of this project into it's own + TestHostApp will manually copy the output of this project into its own OutDir, so we can run the tests from there. --> diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index f3fd19537e..7dec3c052e 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -85,6 +85,8 @@ namespace RemotingUnitTests winrt::hstring GetWindowLayout() DIE; void RequestQuitAll() DIE; void Quit() DIE; + void AttachContentToWindow(Remoting::AttachRequest) DIE; + void SendContent(winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs) DIE; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, Remoting::CommandlineArgs); TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -96,6 +98,8 @@ namespace RemotingUnitTests TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs); + TYPED_EVENT(AttachRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::AttachRequest); + TYPED_EVENT(SendContentRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs); }; // Same idea. @@ -114,6 +118,8 @@ namespace RemotingUnitTests bool DoesQuakeWindowExist() DIE; winrt::Windows::Foundation::Collections::IVectorView GetPeasantInfos() DIE; winrt::Windows::Foundation::Collections::IVector GetAllWindowLayouts() DIE; + void RequestMoveContent(winrt::hstring, winrt::hstring, uint32_t, winrt::Windows::Foundation::IReference) DIE; + void RequestSendContent(Remoting::RequestReceiveContentArgs) DIE; TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -121,6 +127,7 @@ namespace RemotingUnitTests TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, Remoting::QuitAllRequestedArgs); + TYPED_EVENT(RequestNewWindow, winrt::Windows::Foundation::IInspectable, Remoting::WindowRequestedArgs); }; class RemotingTests @@ -424,7 +431,7 @@ namespace RemotingUnitTests m0->FindTargetWindowRequested(&RemotingTests::_findTargetWindowHelper); std::vector args{}; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -461,7 +468,7 @@ namespace RemotingUnitTests }); std::vector args{ L"1", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -482,7 +489,7 @@ namespace RemotingUnitTests { std::vector args{ L"-1" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -490,7 +497,7 @@ namespace RemotingUnitTests } { std::vector args{ L"-2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -527,7 +534,7 @@ namespace RemotingUnitTests winrt::clock().now() }; p1->ActivateWindow(activatedArgs); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -552,7 +559,7 @@ namespace RemotingUnitTests p2->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -565,7 +572,7 @@ namespace RemotingUnitTests p1->ActivateWindow(activatedArgs); Log::Comment(L"Send a commandline to the current window, which should be p1 again"); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); @@ -586,7 +593,7 @@ namespace RemotingUnitTests { std::vector args{ L"2" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -595,7 +602,7 @@ namespace RemotingUnitTests } { std::vector args{ L"10" }; - Remoting::CommandlineArgs eventArgs{ { args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -641,7 +648,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to p2, who is still alive. We won't create a new window."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); @@ -649,7 +656,7 @@ namespace RemotingUnitTests } { Log::Comment(L"Send a commandline to p1, who is dead. We will create a new window."); - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); @@ -1352,7 +1359,7 @@ namespace RemotingUnitTests std::vector p2Args{ L"two", L"this is for p2" }; { - Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p1Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1361,7 +1368,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", which should be p2"); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(false, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -1373,7 +1380,7 @@ namespace RemotingUnitTests { Log::Comment(L"Send a commandline to \"two\", who is now dead."); - Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { p2Args }, { L"" }, SW_NORMAL, L"" }; auto result = m0->ProposeCommandline(eventArgs); VERIFY_ARE_EQUAL(true, result.ShouldCreateWindow()); VERIFY_ARE_EQUAL(false, (bool)result.Id()); // Casting to (bool) checks if the reference has a value @@ -2385,7 +2392,7 @@ namespace RemotingUnitTests VERIFY_ARE_EQUAL(p1->GetID(), m0->_mruPeasants[1].PeasantID()); std::vector commandlineArgs{ L"0", L"arg[1]" }; - Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" } }; + Remoting::CommandlineArgs eventArgs{ { commandlineArgs }, { L"" }, SW_NORMAL, L"" }; Log::Comment(L"When we attempt to send a commandline to the MRU window," L" we should find peasant 1 (who's name is \"one\"), not 2" @@ -2570,7 +2577,7 @@ namespace RemotingUnitTests auto m0 = make_private(monarch0PID); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" } }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL, L"" }; const auto result = m0->ProposeCommandline(args); auto shouldCreateWindow = result.ShouldCreateWindow(); VERIFY_IS_TRUE(shouldCreateWindow); @@ -2578,7 +2585,7 @@ namespace RemotingUnitTests auto m1 = make_self(); { - Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" } }; + Remoting::CommandlineArgs args{ { L"wt.exe" }, { L"-Embedding" }, SW_NORMAL, L"" }; try { diff --git a/src/cascadia/UnitTests_Remoting/pch.h b/src/cascadia/UnitTests_Remoting/pch.h index 4ad6732bf6..523f6305c3 100644 --- a/src/cascadia/UnitTests_Remoting/pch.h +++ b/src/cascadia/UnitTests_Remoting/pch.h @@ -49,3 +49,4 @@ Licensed under the MIT license. #include "../../inc/DefaultSettings.h" #include +#include diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index d5aee1d8b0..99c2bd02ec 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_LIMIT_BACKSPACE #include "test/CommonState.hpp" #include "../cascadia/TerminalCore/Terminal.hpp" @@ -479,8 +478,8 @@ void ConptyRoundtripTests::TestWrappingALongString() auto verifyBuffer = [&](const TextBuffer& tb) { auto& cursor = tb.GetCursor(); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(charsToWrite % initialTermView.Width(), cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(charsToWrite % initialTermView.Width(), cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); // Verify that we marked the 0th row as _wrapped_ const auto& row0 = tb.GetRowByOffset(0); @@ -523,8 +522,8 @@ void ConptyRoundtripTests::TestAdvancedWrapping() auto verifyBuffer = [&](const TextBuffer& tb) { auto& cursor = tb.GetCursor(); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(20, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(20, cursor.GetPosition().x); // Verify that we marked the 0th row as _wrapped_ const auto& row0 = tb.GetRowByOffset(0); @@ -595,8 +594,8 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces() auto verifyBuffer = [&](const TextBuffer& tb) { auto& cursor = tb.GetCursor(); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(10, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(10, cursor.GetPosition().x); // Verify that we marked the 0th row as _not wrapped_ const auto& row0 = tb.GetRowByOffset(0); @@ -657,8 +656,8 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces() auto verifyBuffer = [&](const TextBuffer& tb) { auto& cursor = tb.GetCursor(); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(20, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(20, cursor.GetPosition().x); // Verify that we marked the 0th row as _not wrapped_ const auto& row0 = tb.GetRowByOffset(0); @@ -735,8 +734,8 @@ void ConptyRoundtripTests::MoveCursorAtEOL() TestUtils::VerifySpanOfText(L" ", iter, 0, TerminalViewWidth); auto& cursor = tb.GetCursor(); - VERIFY_ARE_EQUAL(TerminalViewWidth - 1, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(TerminalViewWidth - 1, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); }; verifyData1(hostTb); @@ -798,7 +797,7 @@ void ConptyRoundtripTests::TestResizeHeight() hostSm.ProcessString(L"\r\n"); } - // Conpty doesn't have a scrollback, it's view's origin is always 0,0 + // Conpty doesn't have a scrollback, its view's origin is always 0,0 const auto secondHostView = si.GetViewport(); VERIFY_ARE_EQUAL(0, secondHostView.Top()); VERIFY_ARE_EQUAL(TerminalViewHeight, secondHostView.BottomExclusive()); @@ -905,7 +904,7 @@ void ConptyRoundtripTests::TestResizeHeight() // After we resize, make sure to get the new textBuffers std::tie(hostTb, termTb) = _performResize(newViewportSize); - // Conpty's doesn't have a scrollback, it's view's origin is always 0,0 + // Conpty's doesn't have a scrollback, its view's origin is always 0,0 const auto thirdHostView = si.GetViewport(); VERIFY_ARE_EQUAL(0, thirdHostView.Top()); VERIFY_ARE_EQUAL(newViewportSize.height, thirdHostView.BottomExclusive()); @@ -933,7 +932,7 @@ void ConptyRoundtripTests::TestResizeHeight() Log::Comment(NoThrowString().Format(L"Paint a frame to update the Terminal")); VERIFY_SUCCEEDED(renderer.PaintFrame()); - // Conpty's doesn't have a scrollback, it's view's origin is always 0,0 + // Conpty's doesn't have a scrollback, its view's origin is always 0,0 const auto fourthHostView = si.GetViewport(); VERIFY_ARE_EQUAL(0, fourthHostView.Top()); VERIFY_ARE_EQUAL(newViewportSize.height, fourthHostView.BottomExclusive()); @@ -1200,7 +1199,9 @@ void ConptyRoundtripTests::PassthroughHardReset() } // Write a Hard Reset VT sequence to the host, it should come through to the Terminal + // along with a DECSET sequence to re-enable win32 input and focus events. expectedOutput.push_back("\033c"); + expectedOutput.push_back("\033[?9001;1004h"); hostSm.ProcessString(L"\033c"); const auto termSecondView = term->GetViewport(); @@ -1588,8 +1589,8 @@ void ConptyRoundtripTests::ScrollWithMargins() auto verifyBuffer = [&](const TextBuffer& tb) { auto& cursor = tb.GetCursor(); // Verify the cursor is waiting in the bottom right corner - VERIFY_ARE_EQUAL(initialTermView.Height() - 1, cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(initialTermView.Width() - 1, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(initialTermView.Height() - 1, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(initialTermView.Width() - 1, cursor.GetPosition().x); // For all rows except the last one, verify that we have a run of four letters. for (auto i = 0; i < rowsToWrite; ++i) @@ -1700,56 +1701,59 @@ void ConptyRoundtripTests::ScrollWithMargins() hostSm.ProcessString(completeCursorAtPromptLine); // Set up the verifications like above. - auto verifyBufferAfter = [&](const TextBuffer& tb) { + auto verifyBufferAfter = [&](const TextBuffer& tb, const auto panOffset) { auto& cursor = tb.GetCursor(); // Verify the cursor is waiting on the freshly revealed line (1 above mode line) // and in the left most column. - VERIFY_ARE_EQUAL(initialTermView.Height() - 2, cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + const auto bottomLine = initialTermView.BottomInclusive() + panOffset; + VERIFY_ARE_EQUAL(bottomLine - 1, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); // For all rows except the last two, verify that we have a run of four letters. for (auto i = 0; i < rowsToWrite - 1; ++i) { // Start with B this time because the A line got scrolled off the top. const std::wstring expectedString(4, static_cast(L'B' + i)); - const til::point expectedPos{ 0, i }; + const til::point expectedPos{ 0, panOffset + i }; TestUtils::VerifyExpectedString(tb, expectedString, expectedPos); } // For the second to last row, verify that it is blank. { const std::wstring expectedBlankLine(initialTermView.Width(), L' '); - const til::point blankLinePos{ 0, rowsToWrite - 1 }; + const til::point blankLinePos{ 0, panOffset + rowsToWrite - 1 }; TestUtils::VerifyExpectedString(tb, expectedBlankLine, blankLinePos); } // For the last row, verify we have an entire row of asterisks for the mode line. { const std::wstring expectedModeLine(initialTermView.Width() - 1, L'*'); - const til::point modeLinePos{ 0, rowsToWrite }; + const til::point modeLinePos{ 0, panOffset + rowsToWrite }; TestUtils::VerifyExpectedString(tb, expectedModeLine, modeLinePos); } }; // This will verify the text emitted from the PTY. - expectedOutput.push_back("\x1b[H"); // cursor returns to top left corner. - for (auto i = 0; i < rowsToWrite - 1; ++i) + expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner + expectedOutput.push_back("\n"); // linefeed pans the viewport down { - const std::string expectedString(4, static_cast('B' + i)); + // Cursor gets reset into second line from bottom, left most column + std::stringstream ss; + ss << "\x1b[" << initialTermView.Height() - 1 << ";1H"; + expectedOutput.push_back(ss.str()); + } + { + // Bottom of the scroll region is replaced with a blank line + const std::string expectedString(initialTermView.Width(), ' '); expectedOutput.push_back(expectedString); - expectedOutput.push_back("\x1b[K"); // erase the rest of the line. - expectedOutput.push_back("\r\n"); - } - { - expectedOutput.push_back(""); // nothing for the empty line - expectedOutput.push_back("\x1b[K"); // erase the rest of the line. - expectedOutput.push_back("\r\n"); } + expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner { + // Mode line is redrawn at the bottom of the viewport const std::string expectedString(initialTermView.Width() - 1, '*'); - // There will be one extra blank space at the end of the line, to prevent delayed EOL wrapping - expectedOutput.push_back(expectedString + " "); + expectedOutput.push_back(expectedString); + expectedOutput.push_back(" "); } { // Cursor gets reset into second line from bottom, left most column @@ -1761,15 +1765,15 @@ void ConptyRoundtripTests::ScrollWithMargins() Log::Comment(L"Verify host buffer contains pattern moved up one and mode line still in place."); // Verify the host side. - verifyBufferAfter(hostTb); + verifyBufferAfter(hostTb, 0); Log::Comment(L"Emit PTY frame and validate it transmits the right data."); // Paint the frame VERIFY_SUCCEEDED(renderer.PaintFrame()); Log::Comment(L"Verify terminal buffer contains pattern moved up one and mode line still in place."); - // Verify the terminal side. - verifyBufferAfter(termTb); + // Verify the terminal side. Note the viewport has panned down a line. + verifyBufferAfter(termTb, 1); } void ConptyRoundtripTests::DontWrapMoveCursorInSingleFrame() @@ -2116,10 +2120,10 @@ void ConptyRoundtripTests::MarginsWithStatusLine() // Emulate calling ScrollConsoleScreenBuffer to scroll the B and C lines // down one line. til::inclusive_rect src; - src.Top = newBottom - 2; - src.Left = 0; - src.Right = si.GetViewport().Width(); - src.Bottom = originalBottom; + src.top = newBottom - 2; + src.left = 0; + src.right = si.GetViewport().Width(); + src.bottom = originalBottom; til::point tgt{ 0, newBottom - 1 }; TextAttribute useThisAttr(0x07); // We don't terribly care about the attributes so this is arbitrary ScrollRegion(si, src, std::nullopt, tgt, L' ', useThisAttr); @@ -2783,10 +2787,10 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest() _apiRoutines.GetConsoleScreenBufferInfoExImpl(si, csbiex); til::inclusive_rect src{ 0 }; - src.Top = 0; - src.Left = 0; - src.Right = csbiex.dwSize.X; - src.Bottom = csbiex.dwSize.Y; + src.top = 0; + src.left = 0; + src.right = csbiex.dwSize.X; + src.bottom = csbiex.dwSize.Y; til::point tgt{ 0, -csbiex.dwSize.Y }; VERIFY_SUCCEEDED(_apiRoutines.ScrollConsoleScreenBufferWImpl(si, @@ -2886,8 +2890,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()); @@ -3161,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_SUCCESS_NTSTATUS(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 @@ -3207,7 +3196,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() // GH#5839 - // This test does expose a real bug when using WriteCharsLegacy to emit - // wrapped lines in conpty without WC_DELAY_EOL_WRAP. However, this fix has + // wrapped lines in conpty without delayed EOL wrap. However, this fix has // not yet been made, so for now, we need to just skip the cases that cause // this. if (writingMethod == PrintWithWriteCharsLegacy && paintEachNewline == PaintEveryLine) @@ -3216,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 // @@ -3259,7 +3243,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, writeCharsLegacyMode); + WriteCharsLegacy(si, str, false, nullptr); } }; @@ -3417,7 +3401,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, WC_LIMIT_BACKSPACE); + WriteCharsLegacy(si, str, false, nullptr); } }; @@ -3430,7 +3414,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() // // The last line of the buffer will be used as a "prompt" line, with a // single ':' in it. This is similar to the way `less` typically displays - // it's prompt at the bottom of the buffer. + // its prompt at the bottom of the buffer. // First, print a whole viewport full of text. for (auto i = 0; i < (TerminalViewHeight) / 2; i++) @@ -3709,11 +3693,11 @@ void ConptyRoundtripTests::HyperlinkIdConsistency() auto verifyData = [](TextBuffer& tb) { // Check that all the linked cells still have the same ID - auto& attrRow = tb.GetRowByOffset(0).GetAttrRow(); - auto id = attrRow.GetAttrByColumn(0).GetHyperlinkId(); + auto& row = tb.GetRowByOffset(0); + auto id = row.GetAttrByColumn(0).GetHyperlinkId(); for (uint16_t i = 1; i < 4; ++i) { - VERIFY_ARE_EQUAL(id, attrRow.GetAttrByColumn(i).GetHyperlinkId()); + VERIFY_ARE_EQUAL(id, row.GetAttrByColumn(i).GetHyperlinkId()); } }; diff --git a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h index 1f3be0be99..fa5a4d7078 100644 --- a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h +++ b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h @@ -7,6 +7,7 @@ #include "../../inc/ControlProperties.h" #include +#include using namespace winrt::Microsoft::Terminal::Core; @@ -17,16 +18,17 @@ namespace TerminalCoreUnitTests // Color Table is special because it's an array std::array _ColorTable; -#define SETTINGS_GEN(type, name, ...) WINRT_PROPERTY(type, name, __VA_ARGS__); + public: +#define SETTINGS_GEN(type, name, ...) til::property name{ __VA_ARGS__ }; CORE_SETTINGS(SETTINGS_GEN) CORE_APPEARANCE_SETTINGS(SETTINGS_GEN) #undef SETTINGS_GEN public: MockTermSettings(int32_t historySize, int32_t initialRows, int32_t initialCols) : - _HistorySize(historySize), - _InitialRows(initialRows), - _InitialCols(initialCols) + HistorySize(historySize), + InitialRows(initialRows), + InitialCols(initialCols) { } diff --git a/src/cascadia/UnitTests_TerminalCore/ScreenSizeLimitsTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScreenSizeLimitsTest.cpp index 671fb167e2..8e9971abdf 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScreenSizeLimitsTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScreenSizeLimitsTest.cpp @@ -42,16 +42,16 @@ void ScreenSizeLimitsTest::ScreenWidthAndHeightAreClampedToBounds() DummyRenderer renderer{ &negativeColumnsTerminal }; negativeColumnsTerminal.CreateFromSettings(negativeColumnsSettings, renderer); auto actualDimensions = negativeColumnsTerminal.GetViewport().Dimensions(); - VERIFY_ARE_EQUAL(actualDimensions.Y, SHRT_MAX, L"Row count clamped to SHRT_MAX == " WCS(SHRT_MAX)); - VERIFY_ARE_EQUAL(actualDimensions.X, 1, L"Column count clamped to 1"); + VERIFY_ARE_EQUAL(actualDimensions.height, SHRT_MAX, L"Row count clamped to SHRT_MAX == " WCS(SHRT_MAX)); + VERIFY_ARE_EQUAL(actualDimensions.width, 1, L"Column count clamped to 1"); // Zero values are clamped to 1 as well. auto zeroRowsSettings = winrt::make(10000, 0, 9999999); Terminal zeroRowsTerminal; zeroRowsTerminal.CreateFromSettings(zeroRowsSettings, renderer); actualDimensions = zeroRowsTerminal.GetViewport().Dimensions(); - VERIFY_ARE_EQUAL(actualDimensions.Y, 1, L"Row count clamped to 1"); - VERIFY_ARE_EQUAL(actualDimensions.X, SHRT_MAX, L"Column count clamped to SHRT_MAX == " WCS(SHRT_MAX)); + VERIFY_ARE_EQUAL(actualDimensions.height, 1, L"Row count clamped to 1"); + VERIFY_ARE_EQUAL(actualDimensions.width, SHRT_MAX, L"Column count clamped to SHRT_MAX == " WCS(SHRT_MAX)); } void ScreenSizeLimitsTest::ScrollbackHistorySizeIsClampedToBounds() diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index f3f5db07e4..12223e94a6 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -56,7 +56,7 @@ namespace HRESULT InvalidateAll() noexcept { return S_OK; } HRESULT InvalidateCircling(_Out_ bool* /*pForcePaint*/) noexcept { return S_OK; } HRESULT PaintBackground() noexcept { return S_OK; } - HRESULT PaintBufferLine(gsl::span /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; } + HRESULT PaintBufferLine(std::span /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; } HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*color*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; } HRESULT PaintSelection(const til::rect& /*rect*/) noexcept { return S_OK; } HRESULT PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; } @@ -65,7 +65,7 @@ namespace HRESULT UpdateDpi(int /*iDpi*/) noexcept { return S_OK; } HRESULT UpdateViewport(const til::inclusive_rect& /*srNewViewport*/) noexcept { return S_OK; } HRESULT GetProposedFont(const FontInfoDesired& /*FontInfoDesired*/, _Out_ FontInfo& /*FontInfo*/, int /*iDpi*/) noexcept { return S_OK; } - HRESULT GetDirtyArea(gsl::span& /*area*/) noexcept { return S_OK; } + HRESULT GetDirtyArea(std::span& /*area*/) noexcept { return S_OK; } HRESULT GetFontSize(_Out_ til::size* /*pFontSize*/) noexcept { return S_OK; } HRESULT IsGlyphWideByFont(std::wstring_view /*glyph*/, _Out_ bool* /*pResult*/) noexcept { return S_OK; } @@ -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) { @@ -204,8 +213,8 @@ void ScrollTest::TestNotifyScrolling() fmt::format(L"Expected a 'trigger scroll' notification in Render Engine for row {}", currentRow).c_str()); til::point expectedDelta; - expectedDelta.X = 0; - expectedDelta.Y = -1; + expectedDelta.x = 0; + expectedDelta.y = -1; VERIFY_ARE_EQUAL(expectedDelta, _renderEngine->TriggerScrollDelta().value(), fmt::format(L"Wrong value in 'trigger scroll' notification in Render Engine for row {}", currentRow).c_str()); } else diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index dd6a1cd075..683740c155 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -97,7 +97,7 @@ void TerminalApiTest::PrintStringOfSurrogatePairs() [](LPVOID data) -> DWORD { const auto& baton = *reinterpret_cast(data); Log::Comment(L"Writing data."); - baton.pTerm->PrintString(baton.text); + baton.pTerm->_stateMachine->ProcessString(baton.text); Log::Comment(L"Setting event."); SetEvent(baton.done); return 0; @@ -235,7 +235,7 @@ void TerminalApiTest::CheckDoubleWidthCursor() singleWidthText.append(L"A"); } stateMachine.ProcessString(singleWidthText); - VERIFY_IS_TRUE(cursor.GetPosition().X == 98); + VERIFY_IS_TRUE(cursor.GetPosition().x == 98); // Stuff two double width characters. std::wstring doubleWidthText{ L"我愛" }; diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp index 808ce3cbd8..1ea9e0be16 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp @@ -122,8 +122,8 @@ void TerminalBufferTests::TestWrappingCharByChar() VERIFY_ARE_EQUAL(32, secondView.BottomExclusive()); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(charsToWrite % initialView.Width(), cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(charsToWrite % initialView.Width(), cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); // Verify that we marked the 0th row as _wrapped_ const auto& row0 = termTb.GetRowByOffset(0); @@ -156,8 +156,8 @@ void TerminalBufferTests::TestWrappingALongString() VERIFY_ARE_EQUAL(32, secondView.BottomExclusive()); // Verify the cursor wrapped to the second line - VERIFY_ARE_EQUAL(charsToWrite % initialView.Width(), cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(charsToWrite % initialView.Width(), cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); // Verify that we marked the 0th row as _wrapped_ const auto& row0 = termTb.GetRowByOffset(0); @@ -285,7 +285,7 @@ std::list TerminalBufferTests::_GetTabStops() for (;;) { termSm.ProcessCharacter(L'\t'); - auto column = cursor.GetPosition().X; + auto column = cursor.GetPosition().x; if (column >= lastColumn) { break; @@ -492,7 +492,7 @@ void TerminalBufferTests::TestGetForwardTab() cursor.SetXPosition(0); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = inputData.front(); + coordCursorExpected.x = inputData.front(); termSm.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -506,7 +506,7 @@ void TerminalBufferTests::TestGetForwardTab() cursor.SetXPosition(6); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = *std::next(inputData.begin(), 3); + coordCursorExpected.x = *std::next(inputData.begin(), 3); termSm.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -520,7 +520,7 @@ void TerminalBufferTests::TestGetForwardTab() cursor.SetXPosition(30); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = coordScreenBufferSize.X - 1; + coordCursorExpected.x = coordScreenBufferSize.width - 1; termSm.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -531,7 +531,7 @@ void TerminalBufferTests::TestGetForwardTab() Log::Comment(L"Find next tab from rightmost column."); { - cursor.SetXPosition(coordScreenBufferSize.X - 1); + cursor.SetXPosition(coordScreenBufferSize.width - 1); auto coordCursorExpected = cursor.GetPosition(); @@ -559,7 +559,7 @@ void TerminalBufferTests::TestGetReverseTab() cursor.SetXPosition(1); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = 0; + coordCursorExpected.x = 0; termSm.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); @@ -573,7 +573,7 @@ void TerminalBufferTests::TestGetReverseTab() cursor.SetXPosition(6); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = *std::next(inputData.begin()); + coordCursorExpected.x = *std::next(inputData.begin()); termSm.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); @@ -587,7 +587,7 @@ void TerminalBufferTests::TestGetReverseTab() cursor.SetXPosition(30); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = inputData.back(); + coordCursorExpected.x = inputData.back(); termSm.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); diff --git a/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp b/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp new file mode 100644 index 0000000000..89c2856b8e --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/TilWinRtHelpersTests.cpp @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include + +#include + +#include "../renderer/inc/DummyRenderer.hpp" +#include "../renderer/base/Renderer.hpp" +#include "../renderer/dx/DxRenderer.hpp" + +#include "../cascadia/TerminalCore/Terminal.hpp" +#include "MockTermSettings.h" +#include "consoletaeftemplates.hpp" +#include "../../inc/TestUtils.h" + +#include + +using namespace winrt::Microsoft::Terminal::Core; +using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console::Render; +using namespace ::Microsoft::Console::Types; + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace TerminalCoreUnitTests +{ + class TilWinRtHelpersTests; +}; +using namespace TerminalCoreUnitTests; + +class TerminalCoreUnitTests::TilWinRtHelpersTests final +{ + TEST_CLASS(TilWinRtHelpersTests); + TEST_METHOD(TestPropertySimple); + TEST_METHOD(TestPropertyHString); + TEST_METHOD(TestTruthiness); + TEST_METHOD(TestSimpleConstProperties); + TEST_METHOD(TestComposedConstProperties); + + TEST_METHOD(TestEvent); + + TEST_METHOD(TestTypedEvent); +}; + +void TilWinRtHelpersTests::TestPropertySimple() +{ + til::property Foo; + til::property Bar(11); + + VERIFY_ARE_EQUAL(11, Bar()); + + Foo(42); + VERIFY_ARE_EQUAL(42, Foo()); + + Foo(Foo() - 5); // 37 + VERIFY_ARE_EQUAL(37, Foo()); + + Foo(Foo() + Bar()); // 48 + VERIFY_ARE_EQUAL(48, Foo()); +} + +void TilWinRtHelpersTests::TestPropertyHString() +{ + til::property Foo{ L"Foo" }; + + VERIFY_ARE_EQUAL(L"Foo", Foo()); + + Foo(L"bar"); + VERIFY_ARE_EQUAL(L"bar", Foo()); +} + +void TilWinRtHelpersTests::TestTruthiness() +{ + til::property Foo{ false }; + til::property Bar(0); + til::property EmptyString; + til::property FullString{ L"Full" }; + + VERIFY_IS_FALSE(Foo()); + VERIFY_IS_FALSE((bool)Foo); + + VERIFY_IS_FALSE(Bar()); + VERIFY_IS_FALSE((bool)Bar); + + VERIFY_IS_FALSE((bool)EmptyString); + VERIFY_IS_FALSE(!EmptyString().empty()); + + Foo(true); + VERIFY_IS_TRUE(Foo()); + VERIFY_IS_TRUE((bool)Foo); + + Bar(11); + VERIFY_IS_TRUE(Bar()); + VERIFY_IS_TRUE((bool)Bar); + + VERIFY_IS_TRUE((bool)FullString); + VERIFY_IS_TRUE(!FullString().empty()); +} + +void TilWinRtHelpersTests::TestSimpleConstProperties() +{ + struct InnerType + { + int first{ 1 }; + int second{ 2 }; + }; + + struct Helper + { + til::property Foo{ 0 }; + til::property Composed; + til::property MyString; + }; + + struct Helper changeMe; + const struct Helper noTouching; + + VERIFY_ARE_EQUAL(0, changeMe.Foo()); + VERIFY_ARE_EQUAL(1, 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(1, noTouching.Composed().first); + VERIFY_ARE_EQUAL(2, noTouching.Composed().second); + VERIFY_ARE_EQUAL(L"", noTouching.MyString()); + + changeMe.Foo(42); + VERIFY_ARE_EQUAL(42, changeMe.Foo()); + // noTouching.Foo = 123; // will not compile + + // None of this compiles. + // Composed() doesn't return an l-value, it returns an _int_ + // + // changeMe.Composed().first = 5; + // VERIFY_ARE_EQUAL(5, changeMe.Composed().first); + // noTouching.Composed().first = 0x0f; // will not compile + + changeMe.MyString(L"Foo"); + VERIFY_ARE_EQUAL(L"Foo", changeMe.MyString()); + // noTouching.MyString = L"Bar"; // will not compile +} +void TilWinRtHelpersTests::TestComposedConstProperties() +{ + // This is an intentionally obtuse test, to show a weird edge case you + // should avoid. + // + // In this sample, `Helper` has a `property` of a raw struct + // `InnerType`, which itself is composed of two `property`s. This is not + // something that will actually occur in practice. In practice, the things + // inside the `property` will be WinRT types (or primitive types), and + // things that contain properties will THEMSELVES be WinRT types. + // + // But if you do it like this, you can't call + // + // changeMe.Composed().first(5); + // + // Or any variation of that, without ~ unexpected ~ behavior. This demonstrates that. + struct InnerType + { + til::property first{ 3 }; + til::property second{ 2 }; + }; + + struct Helper + { + til::property Foo{ 0 }; + til::property Composed; + til::property MyString; + }; + + struct Helper changeMe; + 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(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(L"", noTouching.MyString()); + + changeMe.Foo(42); + VERIFY_ARE_EQUAL(42, changeMe.Foo()); + // noTouching.Foo = 123; // will not compile + + // This test was authored to work through a potential foot gun. + // If you have property::operator() return `T`, then + // changeMe.Composed().first = 5; + // + // Roughly translates to: + // auto copy = changeMe.Composed(); + // copy.first(5); + // + // Which rather seems like a foot gun. + changeMe.Composed().first(5); + VERIFY_ARE_EQUAL(3, changeMe.Composed().first()); + + // IN PRACTICE, this shouldn't ever occur. Composed would be a WinRT type, + // and you'd get a ref to it, rather than a copy. + + changeMe.MyString(L"Foo"); + VERIFY_ARE_EQUAL(L"Foo", changeMe.MyString()); +} + +void TilWinRtHelpersTests::TestEvent() +{ + bool handledOne = false; + bool handledTwo = false; + auto handler = [&](const int& v) -> void { + VERIFY_ARE_EQUAL(42, v); + handledOne = true; + }; + + til::event> MyEvent; + MyEvent(handler); + MyEvent([&](int) { handledTwo = true; }); + MyEvent.raise(42); + VERIFY_ARE_EQUAL(true, handledOne); + VERIFY_ARE_EQUAL(true, handledTwo); +} + +void TilWinRtHelpersTests::TestTypedEvent() +{ + bool handledOne = false; + bool handledTwo = false; + + auto handler = [&](const winrt::hstring sender, const int& v) -> void { + VERIFY_ARE_EQUAL(L"sure", sender); + VERIFY_ARE_EQUAL(42, v); + handledOne = true; + }; + + til::typed_event MyEvent; + MyEvent(handler); + MyEvent([&](winrt::hstring, int) { handledTwo = true; }); + MyEvent.raise(L"sure", 42); + VERIFY_ARE_EQUAL(true, handledOne); + VERIFY_ARE_EQUAL(true, handledTwo); +} diff --git a/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj index d8ddd1fef7..94967d5b0e 100644 --- a/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj +++ b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj @@ -26,6 +26,7 @@ + diff --git a/src/cascadia/UnitTests_TerminalCore/pch.h b/src/cascadia/UnitTests_TerminalCore/pch.h index 8a2ed23a11..65ba14050a 100644 --- a/src/cascadia/UnitTests_TerminalCore/pch.h +++ b/src/cascadia/UnitTests_TerminalCore/pch.h @@ -17,6 +17,25 @@ Author(s): #pragma once +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // If this is not defined, windows.h includes commdlg.h which defines FindText globally and conflicts with UIAutomation ITextRangeProvider. +#endif + +#define NOMINMAX + +// Define and then undefine WIN32_NO_STATUS because windows.h has no guard to prevent it from double defing certain statuses +// when included with ntstatus.h +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS + +#include + +#pragma warning(push) +#pragma warning(disable : 4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. +#include +#pragma warning(pop) + #define BLOCK_TIL // This includes support libraries from the CRT, STL, WIL, and GSL #include "LibraryIncludes.h" @@ -48,14 +67,7 @@ Author(s): // // These are needed because the roundtrip tests included in this library also // re-use some conhost code that depends on these. - #include "conddkrefs.h" -// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs -typedef _Return_type_success_(return >= 0) LONG NTSTATUS; -/*lint -save -e624 */ // Don't complain about different typedefs. -typedef NTSTATUS* PNTSTATUS; -/*lint -restore */ // Resume checking for different typedefs. -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) // #include diff --git a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj index 853fc1eec4..959daaddf8 100644 --- a/src/cascadia/WinRTUtils/WinRTUtils.vcxproj +++ b/src/cascadia/WinRTUtils/WinRTUtils.vcxproj @@ -9,6 +9,7 @@ StaticLibrary Console true + false true diff --git a/src/cascadia/WinRTUtils/inc/Utils.h b/src/cascadia/WinRTUtils/inc/Utils.h index ec8233fcb3..036050a21b 100644 --- a/src/cascadia/WinRTUtils/inc/Utils.h +++ b/src/cascadia/WinRTUtils/inc/Utils.h @@ -3,6 +3,8 @@ #pragma once +#include + // Function Description: // - This function presents a File Open "common dialog" and returns its selected file asynchronously. // Parameters: diff --git a/src/cascadia/WinRTUtils/inc/WtExeUtils.h b/src/cascadia/WinRTUtils/inc/WtExeUtils.h index 0b0d40e23a..306af685e1 100644 --- a/src/cascadia/WinRTUtils/inc/WtExeUtils.h +++ b/src/cascadia/WinRTUtils/inc/WtExeUtils.h @@ -2,6 +2,7 @@ constexpr std::wstring_view WtExe{ L"wt.exe" }; constexpr std::wstring_view WtdExe{ L"wtd.exe" }; constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; +constexpr std::wstring_view ElevateShimExe{ L"elevate-shim.exe" }; _TIL_INLINEPREFIX bool IsPackaged() { diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 90524d9a14..36d47211b7 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -28,133 +28,70 @@ using namespace std::chrono_literals; // "If the high-order bit is 1, the key is down; otherwise, it is up." static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; -AppHost::AppHost() noexcept : - _app{}, - _windowManager{}, - _logic{ nullptr }, // don't make one, we're going to take a ref on app's - _window{ nullptr }, - _getWindowLayoutThrottler{} // this will get set if we become the monarch +constexpr const auto FrameUpdateInterval = std::chrono::milliseconds(16); + +AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + const Remoting::WindowManager& manager, + const Remoting::Peasant& peasant, + std::unique_ptr window) noexcept : + _appLogic{ logic }, + _windowLogic{ nullptr }, // don't make one, we're going to take a ref on app's + _windowManager{ manager }, + _peasant{ peasant }, + _desktopManager{ winrt::try_create_instance(__uuidof(VirtualDesktopManager)) } { - _logic = _app.Logic(); // get a ref to app's logic + _started = std::chrono::high_resolution_clock::now(); - // Inform the WindowManager that it can use us to find the target window for - // a set of commandline args. This needs to be done before - // _HandleCommandlineArgs, because WE might end up being the monarch. That - // would mean we'd need to be responsible for looking that up. - _windowManager.FindTargetWindowRequested({ this, &AppHost::_FindTargetWindow }); + _HandleCommandlineArgs(args); - // If there were commandline args to our process, try and process them here. - // Do this before AppLogic::Create, otherwise this will have no effect. - // - // This will send our commandline to the Monarch, to ask if we should make a - // new window or not. If not, exit immediately. - _HandleCommandlineArgs(); - if (!_shouldCreateWindow) + _HandleSessionRestore(!args.Content().empty()); + + // _HandleCommandlineArgs will create a _windowLogic + _useNonClientArea = _windowLogic.GetShowTabsInTitlebar(); + + const bool isWarmStart = window != nullptr; + if (isWarmStart) { - return; - } - - _useNonClientArea = _logic.GetShowTabsInTitlebar(); - if (_useNonClientArea) - { - _window = std::make_unique(_logic.GetRequestedTheme()); + _window = std::move(window); } else { - _window = std::make_unique(); + if (_useNonClientArea) + { + _window = std::make_unique(_windowLogic.GetRequestedTheme()); + } + else + { + _window = std::make_unique(); + } } // Update our own internal state tracking if we're in quake mode or not. _IsQuakeWindowChanged(nullptr, nullptr); - _window->SetMinimizeToNotificationAreaBehavior(_logic.GetMinimizeToNotificationArea()); + _window->SetMinimizeToNotificationAreaBehavior(_windowLogic.GetMinimizeToNotificationArea()); // Tell the window to callback to us when it's about to handle a WM_CREATE auto pfn = std::bind(&AppHost::_HandleCreateWindow, this, std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3); + std::placeholders::_2); _window->SetCreateCallback(pfn); - _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::CalcSnappedDimension, - _logic, - std::placeholders::_1, - std::placeholders::_2)); - - // Event handlers: - // MAKE SURE THEY ARE ALL: - // * winrt::auto_revoke - // * revoked manually in the dtor before the window is nulled out. - // - // If you don't, then it's possible for them to get triggered as the app is - // tearing down, after we've nulled out the window, during the dtor. That - // can cause unexpected AV's everywhere. - // - // _window callbacks don't need to be treated this way, because: - // * IslandWindow isn't a WinRT type (so it doesn't have neat revokers like this) - // * This particular bug scenario applies when we've already freed the window. - // - // (Most of these events are actually set up in AppHost::Initialize) - _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); - _window->WindowActivated({ this, &AppHost::_WindowActivated }); - _window->WindowMoved({ this, &AppHost::_WindowMoved }); - _window->HotkeyPressed({ this, &AppHost::_GlobalHotkeyPressed }); - _window->ShouldExitFullscreen({ &_logic, &winrt::TerminalApp::AppLogic::RequestExitFullscreen }); - - _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); - _window->SetAutoHideWindow(_logic.AutoHideWindow()); + _windowCallbacks.MouseScrolled = _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); + _windowCallbacks.WindowActivated = _window->WindowActivated({ this, &AppHost::_WindowActivated }); + _windowCallbacks.WindowMoved = _window->WindowMoved({ this, &AppHost::_WindowMoved }); + _windowCallbacks.ShouldExitFullscreen = _window->ShouldExitFullscreen({ &_windowLogic, &winrt::TerminalApp::TerminalWindow::RequestExitFullscreen }); _window->MakeWindow(); - - _GetWindowLayoutRequestedToken = _windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs& args) { - // The peasants are running on separate threads, so they'll need to - // swap what context they are in to the ui thread to get the actual layout. - args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); - }); - - _revokers.BecameMonarch = _windowManager.BecameMonarch(winrt::auto_revoke, { this, &AppHost::_BecomeMonarch }); - if (_windowManager.IsMonarch()) - { - _BecomeMonarch(nullptr, nullptr); - } - - // Create a throttled function for updating the window state, to match the - // one requested by the pty. A 200ms delay was chosen because it's the - // typical animation timeout in Windows. This does result in a delay between - // the PTY requesting a change to the window state and the Terminal - // realizing it, but should mitigate issues where the Terminal and PTY get - // de-sync'd. - _showHideWindowThrottler = std::make_shared>( - winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), - std::chrono::milliseconds(200), - [this](const bool show) { - _window->ShowWindowChanged(show); - }); -} - -AppHost::~AppHost() -{ - // destruction order is important for proper teardown here - - // revoke ALL our event handlers. There's a large class of bugs where we - // might get a callback to one of these when we call app.Close() below. Make - // sure to revoke these first, so we won't get any more callbacks, then null - // out the window, then close the app. - _revokers = {}; - - _showHideWindowThrottler.reset(); - - _window = nullptr; - _app.Close(); - _app = nullptr; } bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { - if (_logic) + if (_windowLogic) { - return _logic.OnDirectKeyEvent(vkey, scanCode, down); + return _windowLogic.OnDirectKeyEvent(vkey, scanCode, down); } return false; } @@ -169,34 +106,25 @@ bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, cons void AppHost::SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { - if (_logic) + if (_windowLogic) { - const auto state = _logic.TaskbarState(); + const auto state = _windowLogic.TaskbarState(); _window->SetTaskbarProgress(gsl::narrow_cast(state.State()), gsl::narrow_cast(state.Progress())); } } -void _buildArgsFromCommandline(std::vector& args) +void AppHost::s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResult& result) { - if (auto commandline{ GetCommandLineW() }) - { - auto argc = 0; - - // Get the argv, and turn them into a hstring array to pass to the app. - wil::unique_any argv{ CommandLineToArgvW(commandline, &argc) }; - if (argv) - { - for (auto& elem : wil::make_range(argv.get(), argc)) - { - args.emplace_back(elem); - } - } - } - if (args.empty()) - { - args.emplace_back(L"wt.exe"); - } + const auto displayHelp = result.ExitCode == 0; + const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE; + const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR; + // TODO:GH#4134: polish this dialog more, to make the text more + // like msiexec /? + MessageBoxW(nullptr, + result.Message.data(), + GetStringResource(messageTitle).data(), + MB_OK | messageIcon); } // Method Description: @@ -204,8 +132,6 @@ void _buildArgsFromCommandline(std::vector& args) // the WindowManager, to ask if we should become a window process. // - If we should create a window, then pass the arguments to the app logic for // processing. -// - If we shouldn't become a window, set _shouldCreateWindow to false and exit -// immediately. // - If the logic determined there's an error while processing that commandline, // display a message box to the user with the text of the error, and exit. // * We display a message box because we're a Win32 application (not a @@ -216,46 +142,37 @@ void _buildArgsFromCommandline(std::vector& args) // - // Return Value: // - -void AppHost::_HandleCommandlineArgs() +void AppHost::_HandleCommandlineArgs(const Remoting::WindowRequestedArgs& windowArgs) { - std::vector args; - _buildArgsFromCommandline(args); - auto cwd{ wil::GetCurrentDirectoryW() }; + // We did want to make a window, so let's instantiate it here. + // We don't have XAML yet, but we do have other stuff. + _windowLogic = _appLogic.CreateNewWindow(); - Remoting::CommandlineArgs eventArgs{ { args }, { cwd } }; - _windowManager.ProposeCommandline(eventArgs); - - _shouldCreateWindow = _windowManager.ShouldCreateWindow(); - if (!_shouldCreateWindow) + if (_peasant) { - return; - } - - if (auto peasant{ _windowManager.CurrentWindow() }) - { - if (auto args{ peasant.InitialArgs() }) + const auto& args{ _peasant.InitialArgs() }; + const bool startedForContent = !windowArgs.Content().empty(); + if (startedForContent) { - const auto result = _logic.SetStartupCommandline(args.Commandline()); - const auto message = _logic.ParseCommandlineMessage(); + _windowLogic.SetStartupContent(windowArgs.Content(), windowArgs.InitialBounds()); + } + else if (args) + { + const auto result = _windowLogic.SetStartupCommandline(args.Commandline(), args.CurrentDirectory(), args.CurrentEnvironment()); + const auto message = _windowLogic.ParseCommandlineMessage(); if (!message.empty()) { - const auto displayHelp = result == 0; - const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE; - const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR; - // TODO:GH#4134: polish this dialog more, to make the text more - // like msiexec /? - MessageBoxW(nullptr, - message.data(), - GetStringResource(messageTitle).data(), - MB_OK | messageIcon); + AppHost::s_DisplayMessageBox({ message, result }); - if (_logic.ShouldExitEarly()) + if (_windowLogic.ShouldExitEarly()) { - ExitProcess(result); + ExitThread(result); } } } + _launchShowWindowCommand = windowArgs.ShowWindowCommand(); + // This is a fix for GH#12190 and hopefully GH#12169. // // If the commandline we were provided is going to result in us only @@ -263,10 +180,9 @@ void AppHost::_HandleCommandlineArgs() // the window at all here. In that case, we're going through this // special escape hatch to dispatch all the calls to elevate-shim, and // then we're going to exit immediately. - if (_logic.ShouldImmediatelyHandoffToElevated()) + if (_windowLogic.ShouldImmediatelyHandoffToElevated()) { - _shouldCreateWindow = false; - _logic.HandoffToElevated(); + _windowLogic.HandoffToElevated(); return; } @@ -276,62 +192,73 @@ void AppHost::_HandleCommandlineArgs() // use to send the actions to the app. // // MORE EVENT HANDLERS, same rules as the ones above. - _revokers.peasantExecuteCommandlineRequested = peasant.ExecuteCommandlineRequested(winrt::auto_revoke, { this, &AppHost::_DispatchCommandline }); - _revokers.peasantSummonRequested = peasant.SummonRequested(winrt::auto_revoke, { this, &AppHost::_HandleSummon }); - _revokers.peasantDisplayWindowIdRequested = peasant.DisplayWindowIdRequested(winrt::auto_revoke, { this, &AppHost::_DisplayWindowId }); - _revokers.peasantQuitRequested = peasant.QuitRequested(winrt::auto_revoke, { this, &AppHost::_QuitRequested }); + _revokers.peasantExecuteCommandlineRequested = _peasant.ExecuteCommandlineRequested(winrt::auto_revoke, { this, &AppHost::_DispatchCommandline }); + _revokers.peasantSummonRequested = _peasant.SummonRequested(winrt::auto_revoke, { this, &AppHost::_HandleSummon }); + _revokers.peasantDisplayWindowIdRequested = _peasant.DisplayWindowIdRequested(winrt::auto_revoke, { this, &AppHost::_DisplayWindowId }); + _revokers.peasantQuitRequested = _peasant.QuitRequested(winrt::auto_revoke, { this, &AppHost::_QuitRequested }); - // We need this property to be set before we get the InitialSize/Position - // and BecameMonarch which normally sets it is only run after the window - // is created. - if (_windowManager.IsMonarch()) + _windowLogic.WindowName(_peasant.WindowName()); + _windowLogic.WindowId(_peasant.GetID()); + + _revokers.AttachRequested = _peasant.AttachRequested(winrt::auto_revoke, { this, &AppHost::_handleAttach }); + } +} + +void AppHost::_HandleSessionRestore(const bool startedForContent) +{ + const auto& args{ _peasant.InitialArgs() }; + + // This is logic that almost seems like it belongs on the WindowEmperor. + // It probably does. However, it needs to muck with our own window so + // much, that there was no reasonable way of moving this. Moving it also + // seemed to reorder bits of init so much that everything broke. So + // we'll leave it here. + const auto numPeasants = _windowManager.GetNumberOfPeasants(); + // Don't attempt to session restore if we're just making a window for tear-out + if (!startedForContent && numPeasants == 1) + { + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (_appLogic.ShouldUsePersistedLayout() && + layouts && + layouts.Size() > 0) { - const auto numPeasants = _windowManager.GetNumberOfPeasants(); - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - if (_logic.ShouldUsePersistedLayout() && - layouts && - layouts.Size() > 0) + uint32_t startIdx = 0; + // We want to create a window for every saved layout. + // If we are the only window, and no commandline arguments were provided + // then we should just use the current window to load the first layout. + // Otherwise create this window normally with its commandline, and create + // a new window using the first saved layout information. + // The 2nd+ layout will always get a new window. + if (!_windowLogic.HasCommandlineArguments() && + !_appLogic.HasSettingsStartupActions()) { - uint32_t startIdx = 0; - // We want to create a window for every saved layout. - // If we are the only window, and no commandline arguments were provided - // then we should just use the current window to load the first layout. - // Otherwise create this window normally with its commandline, and create - // a new window using the first saved layout information. - // The 2nd+ layout will always get a new window. - if (numPeasants == 1 && !_logic.HasCommandlineArguments() && !_logic.HasSettingsStartupActions()) - { - _logic.SetPersistedLayoutIdx(startIdx); - startIdx += 1; - } - - // Create new windows for each of the other saved layouts. - for (const auto size = layouts.Size(); startIdx < size; startIdx += 1) - { - auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args[0], startIdx); - - STARTUPINFO si; - memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); - wil::unique_process_information pi; - - LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, - newWindowArgs.data(), - nullptr, // lpProcessAttributes - nullptr, // lpThreadAttributes - false, // bInheritHandles - DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags - nullptr, // lpEnvironment - nullptr, // lpStartingDirectory - &si, // lpStartupInfo - &pi // lpProcessInformation - )); - } + _windowLogic.SetPersistedLayoutIdx(startIdx); + startIdx += 1; + } + + // Create new windows for each of the other saved layouts. + for (const auto size = layouts.Size(); startIdx < size; startIdx += 1) + { + auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args.Commandline()[0], startIdx); + + STARTUPINFO si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + wil::unique_process_information pi; + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + newWindowArgs.data(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags + nullptr, // lpEnvironment + nullptr, // lpStartingDirectory + &si, // lpStartupInfo + &pi // lpProcessInformation + )); } - _logic.SetNumberOfOpenWindows(numPeasants); } - _logic.WindowName(peasant.WindowName()); - _logic.WindowId(peasant.GetID()); } } @@ -348,10 +275,12 @@ void AppHost::_HandleCommandlineArgs() // - void AppHost::Initialize() { + // You aren't allowed to do ANY XAML before this line!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! _window->Initialize(); - if (auto withWindow{ _logic.try_as() }) + if (auto withWindow{ _windowLogic.try_as() }) { + // You aren't allowed to do anything with the TerminalPage before this line!!!!!!! withWindow->Initialize(_window->GetHandle()); } @@ -360,9 +289,17 @@ void AppHost::Initialize() // Register our callback for when the app's non-client content changes. // This has to be done _before_ App::Create, as the app might set the // content in Create. - _logic.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); + _windowLogic.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); } + // These call APIs that are reentrant on the window message loop. If + // you call them in the ctor, we might deadlock. The ctor for AppHost isn't + // always called on the window thread - for reheated windows, it could be + // called on a random COM thread. + + _window->SetAlwaysOnTop(_windowLogic.GetInitialAlwaysOnTop()); + _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); + // MORE EVENT HANDLERS HERE! // MAKE SURE THEY ARE ALL: // * winrt::auto_revoke @@ -372,64 +309,94 @@ void AppHost::Initialize() // tearing down, after we've nulled out the window, during the dtor. That // can cause unexpected AV's everywhere. // - // _window callbacks don't need to be treated this way, because: - // * IslandWindow isn't a WinRT type (so it doesn't have neat revokers like this) - // * This particular bug scenario applies when we've already freed the window. + // _window callbacks are a little special: + // * IslandWindow isn't a WinRT type (so it doesn't have neat revokers like + // this), so instead they go in their own special helper struct. + // * they all need to be manually revoked in _revokeWindowCallbacks. // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing - _window->WindowCloseButtonClicked([this]() { + _windowCallbacks.WindowCloseButtonClicked = _window->WindowCloseButtonClicked([this]() { _CloseRequested(nullptr, nullptr); }); // If the user requests a close in another way handle the same as if the 'X' // was clicked. - _revokers.CloseRequested = _logic.CloseRequested(winrt::auto_revoke, { this, &AppHost::_CloseRequested }); + _revokers.CloseRequested = _windowLogic.CloseRequested(winrt::auto_revoke, { this, &AppHost::_CloseRequested }); // Add an event handler to plumb clicks in the titlebar area down to the // application layer. - _window->DragRegionClicked([this]() { _logic.TitlebarClicked(); }); + _windowCallbacks.DragRegionClicked = _window->DragRegionClicked([this]() { _windowLogic.TitlebarClicked(); }); - _window->WindowVisibilityChanged([this](bool showOrHide) { _logic.WindowVisibilityChanged(showOrHide); }); + _windowCallbacks.WindowVisibilityChanged = _window->WindowVisibilityChanged([this](bool showOrHide) { _windowLogic.WindowVisibilityChanged(showOrHide); }); - _revokers.RequestedThemeChanged = _logic.RequestedThemeChanged(winrt::auto_revoke, { this, &AppHost::_UpdateTheme }); - _revokers.FullscreenChanged = _logic.FullscreenChanged(winrt::auto_revoke, { this, &AppHost::_FullscreenChanged }); - _revokers.FocusModeChanged = _logic.FocusModeChanged(winrt::auto_revoke, { this, &AppHost::_FocusModeChanged }); - _revokers.AlwaysOnTopChanged = _logic.AlwaysOnTopChanged(winrt::auto_revoke, { this, &AppHost::_AlwaysOnTopChanged }); - _revokers.RaiseVisualBell = _logic.RaiseVisualBell(winrt::auto_revoke, { this, &AppHost::_RaiseVisualBell }); - _revokers.SystemMenuChangeRequested = _logic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested }); - _revokers.ChangeMaximizeRequested = _logic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested }); + _windowCallbacks.UpdateSettingsRequested = _window->UpdateSettingsRequested({ this, &AppHost::_requestUpdateSettings }); - _window->MaximizeChanged([this](bool newMaximize) { - if (_logic) + _revokers.Initialized = _windowLogic.Initialized(winrt::auto_revoke, { this, &AppHost::_WindowInitializedHandler }); + _revokers.RequestedThemeChanged = _windowLogic.RequestedThemeChanged(winrt::auto_revoke, { this, &AppHost::_UpdateTheme }); + _revokers.FullscreenChanged = _windowLogic.FullscreenChanged(winrt::auto_revoke, { this, &AppHost::_FullscreenChanged }); + _revokers.FocusModeChanged = _windowLogic.FocusModeChanged(winrt::auto_revoke, { this, &AppHost::_FocusModeChanged }); + _revokers.AlwaysOnTopChanged = _windowLogic.AlwaysOnTopChanged(winrt::auto_revoke, { this, &AppHost::_AlwaysOnTopChanged }); + _revokers.RaiseVisualBell = _windowLogic.RaiseVisualBell(winrt::auto_revoke, { this, &AppHost::_RaiseVisualBell }); + _revokers.SystemMenuChangeRequested = _windowLogic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested }); + _revokers.ChangeMaximizeRequested = _windowLogic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested }); + + _windowCallbacks.MaximizeChanged = _window->MaximizeChanged([this](bool newMaximize) { + if (_windowLogic) { - _logic.Maximized(newMaximize); + _windowLogic.Maximized(newMaximize); } }); - _window->AutomaticShutdownRequested([this]() { + _windowCallbacks.AutomaticShutdownRequested = _window->AutomaticShutdownRequested([this]() { // Raised when the OS is beginning an update of the app. We will quit, // to save our state, before the OS manually kills us. - _windowManager.RequestQuitAll(); + Remoting::WindowManager::RequestQuitAll(_peasant); }); // Load bearing: make sure the PropertyChanged handler is added before we // call Create, so that when the app sets up the titlebar brush, we're // already prepared to listen for the change notification - _revokers.PropertyChanged = _logic.PropertyChanged(winrt::auto_revoke, { this, &AppHost::_PropertyChangedHandler }); + _revokers.PropertyChanged = _windowLogic.PropertyChanged(winrt::auto_revoke, { this, &AppHost::_PropertyChangedHandler }); - _logic.Create(); + _appLogic.Create(); + _windowLogic.Create(); - _revokers.TitleChanged = _logic.TitleChanged(winrt::auto_revoke, { this, &AppHost::AppTitleChanged }); - _revokers.LastTabClosed = _logic.LastTabClosed(winrt::auto_revoke, { this, &AppHost::LastTabClosed }); - _revokers.SetTaskbarProgress = _logic.SetTaskbarProgress(winrt::auto_revoke, { this, &AppHost::SetTaskbarProgress }); - _revokers.IdentifyWindowsRequested = _logic.IdentifyWindowsRequested(winrt::auto_revoke, { this, &AppHost::_IdentifyWindowsRequested }); - _revokers.RenameWindowRequested = _logic.RenameWindowRequested(winrt::auto_revoke, { this, &AppHost::_RenameWindowRequested }); - _revokers.SettingsChanged = _logic.SettingsChanged(winrt::auto_revoke, { this, &AppHost::_HandleSettingsChanged }); - _revokers.IsQuakeWindowChanged = _logic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged }); - _revokers.SummonWindowRequested = _logic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested }); - _revokers.OpenSystemMenu = _logic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu }); - _revokers.QuitRequested = _logic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll }); - _revokers.ShowWindowChanged = _logic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged }); + _revokers.TitleChanged = _windowLogic.TitleChanged(winrt::auto_revoke, { this, &AppHost::AppTitleChanged }); + _revokers.LastTabClosed = _windowLogic.LastTabClosed(winrt::auto_revoke, { this, &AppHost::LastTabClosed }); + _revokers.SetTaskbarProgress = _windowLogic.SetTaskbarProgress(winrt::auto_revoke, { this, &AppHost::SetTaskbarProgress }); + _revokers.IdentifyWindowsRequested = _windowLogic.IdentifyWindowsRequested(winrt::auto_revoke, { this, &AppHost::_IdentifyWindowsRequested }); + _revokers.RenameWindowRequested = _windowLogic.RenameWindowRequested(winrt::auto_revoke, { this, &AppHost::_RenameWindowRequested }); + + // A note: make sure to listen to our _window_'s settings changed, not the + // AppLogic's. We want to make sure the event has gone through the window + // logic _before_ we handle it, so we can ask the window about it's newest + // properties. + _revokers.SettingsChanged = _windowLogic.SettingsChanged(winrt::auto_revoke, { this, &AppHost::_HandleSettingsChanged }); + + _revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged }); + _revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested }); + _revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu }); + _revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll }); + _revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged }); + _revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent }); + _revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent }); + _revokers.SendContentRequested = _peasant.SendContentRequested(winrt::auto_revoke, { this, &AppHost::_handleSendContent }); + + // Add our GetWindowLayoutRequested handler AFTER the xaml island is + // started. Our _GetWindowLayoutAsync handler requires us to be able to work + // on our UI thread, which requires that we have a Dispatcher ready for us + // to move to. If we set up this callback in the ctor, then it is possible + // for there to be a time slice where + // * the monarch creates the peasant for us, + // * we get constructed (registering the callback) + // * then the monarch attempts to query all _peasants_ for their layout, + // coming back to ask us even before XAML has been created. + _GetWindowLayoutRequestedToken = _peasant.GetWindowLayoutRequested([this](auto&&, + const Remoting::GetWindowLayoutArgs& args) { + // The peasants are running on separate threads, so they'll need to + // swap what context they are in to the ui thread to get the actual layout. + args.WindowLayoutJsonAsync(_GetWindowLayoutAsync()); + }); // BODGY // On certain builds of Windows, when Terminal is set as the default @@ -438,32 +405,98 @@ void AppHost::Initialize() // applications. This call into TerminalThemeHelpers will tell our // compositor to automatically complete animations that are scheduled // while the screen is off. - TerminalTrySetAutoCompleteAnimationsWhenOccluded(static_cast<::IUnknown*>(winrt::get_abi(_logic.GetRoot())), true); + TerminalTrySetAutoCompleteAnimationsWhenOccluded(static_cast<::IUnknown*>(winrt::get_abi(_windowLogic.GetRoot())), true); - _window->UpdateTitle(_logic.Title()); + _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::TerminalWindow::CalcSnappedDimension, + _windowLogic, + std::placeholders::_1, + std::placeholders::_2)); + + // Create a throttled function for updating the window state, to match the + // one requested by the pty. A 200ms delay was chosen because it's the + // typical animation timeout in Windows. This does result in a delay between + // the PTY requesting a change to the window state and the Terminal + // realizing it, but should mitigate issues where the Terminal and PTY get + // de-sync'd. + _showHideWindowThrottler = std::make_shared>( + winrt::Windows::System::DispatcherQueue::GetForCurrentThread(), + std::chrono::milliseconds(200), + [this](const bool show) { + _window->ShowWindowChanged(show); + }); + + _window->UpdateTitle(_windowLogic.Title()); // Set up the content of the application. If the app has a custom titlebar, // set that content as well. - _window->SetContent(_logic.GetRoot()); + _window->SetContent(_windowLogic.GetRoot()); _window->OnAppInitialized(); +} - // BODGY +void AppHost::Close() +{ + // After calling _window->Close() we should avoid creating more WinUI related actions. + // I suspect WinUI wouldn't like that very much. As such unregister all event handlers first. + _revokers = {}; + if (_frameTimer) + { + _frameTimer.Tick(_frameTimerToken); + } + _showHideWindowThrottler.reset(); + + _revokeWindowCallbacks(); + + _window->Close(); + + if (_windowLogic) + { + _windowLogic.DismissDialog(); + _windowLogic = nullptr; + } +} + +void AppHost::_revokeWindowCallbacks() +{ + // You'll recall, IslandWindow isn't a WinRT type so it can't have auto-revokers. // - // We've got a weird crash that happens terribly inconsistently, but pretty - // readily on migrie's laptop, only in Debug mode. Apparently, there's some - // weird ref-counting magic that goes on during teardown, and our - // Application doesn't get closed quite right, which can cause us to crash - // into the debugger. This of course, only happens on exit, and happens - // somewhere in the XamlHost.dll code. - // - // Crazily, if we _manually leak the Application_ here, then the crash - // doesn't happen. This doesn't matter, because we really want the - // Application to live for _the entire lifetime of the process_, so the only - // time when this object would actually need to get cleaned up is _during - // exit_. So we can safely leak this Application object, and have it just - // get cleaned up normally when our process exits. - auto a{ _app }; - ::winrt::detach_abi(a); + // Instead, we need to manually remove our callbacks we registered on the window object. + _window->MouseScrolled(_windowCallbacks.MouseScrolled); + _window->WindowActivated(_windowCallbacks.WindowActivated); + _window->WindowMoved(_windowCallbacks.WindowMoved); + _window->ShouldExitFullscreen(_windowCallbacks.ShouldExitFullscreen); + _window->WindowCloseButtonClicked(_windowCallbacks.WindowCloseButtonClicked); + _window->DragRegionClicked(_windowCallbacks.DragRegionClicked); + _window->WindowVisibilityChanged(_windowCallbacks.WindowVisibilityChanged); + _window->UpdateSettingsRequested(_windowCallbacks.UpdateSettingsRequested); + _window->MaximizeChanged(_windowCallbacks.MaximizeChanged); + _window->AutomaticShutdownRequested(_windowCallbacks.AutomaticShutdownRequested); +} + +// revoke our callbacks, discard our XAML content (TerminalWindow & +// TerminalPage), and hand back our IslandWindow. This does _not_ close the XAML +// island for this thread. We should not be re-used after this, and our caller +// can destruct us like they normally would during a close. The returned +// IslandWindow will retain ownership of the DesktopWindowXamlSource, for later +// reuse. +[[nodiscard]] std::unique_ptr AppHost::Refrigerate() +{ + // After calling _window->Close() we should avoid creating more WinUI related actions. + // I suspect WinUI wouldn't like that very much. As such unregister all event handlers first. + _revokers = {}; + _showHideWindowThrottler.reset(); + + _revokeWindowCallbacks(); + + // DO NOT CLOSE THE WINDOW + _window->Refrigerate(); + + if (_windowLogic) + { + _windowLogic.DismissDialog(); + _windowLogic = nullptr; + } + + return std::move(_window); } // Method Description: @@ -477,11 +510,11 @@ void AppHost::Initialize() // - void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, winrt::hstring newTitle) { - if (_logic.GetShowTitleInTitlebar()) + if (_windowLogic.GetShowTitleInTitlebar()) { _window->UpdateTitle(newTitle); } - _windowManager.UpdateActiveTabTitle(newTitle); + _windowManager.UpdateActiveTabTitle(newTitle, _peasant); } // Method Description: @@ -491,31 +524,37 @@ void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /* // - LastTabClosedEventArgs: unused // Return Value: // - -void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::TerminalApp::LastTabClosedEventArgs& /*args*/) +void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::TerminalApp::LastTabClosedEventArgs& args) { - if (_windowManager.IsMonarch() && _notificationIcon) + // We don't want to try to save layouts if we are about to close. + _peasant.GetWindowLayoutRequested(_GetWindowLayoutRequestedToken); + + // If the user closes the last tab, in the last window, _by closing the tab_ + // (not by closing the whole window), we need to manually persist an empty + // window state here. That will cause the terminal to re-open with the usual + // settings (not the persisted state) + if (args.ClearPersistedState() && + _windowManager.GetNumberOfPeasants() == 1) { - _DestroyNotificationIcon(); - } - else if (_window->IsQuakeWindow()) - { - _HideNotificationIconRequested(nullptr, nullptr); + _windowLogic.ClearPersistedWindowState(); } - // We don't want to try to save layouts if we are about to close. - _getWindowLayoutThrottler.reset(); - _windowManager.GetWindowLayoutRequested(_GetWindowLayoutRequestedToken); - // We also don't need to update any of our bookkeeping on how many - // windows are open. - _windowManager.WindowCreated(_WindowCreatedToken); - _windowManager.WindowClosed(_WindowClosedToken); + // If the user closes the last tab, in the last window, _by closing the tab_ + // (not by closing the whole window), we need to manually persist an empty + // window state here. That will cause the terminal to re-open with the usual + // settings (not the persisted state) + if (args.ClearPersistedState() && + _windowManager.GetNumberOfPeasants() == 1) + { + _windowLogic.ClearPersistedWindowState(); + } // Remove ourself from the list of peasants so that we aren't included in // any future requests. This will also mean we block until any existing // event handler finishes. - _windowManager.SignalClose(); + _windowManager.SignalClose(_peasant); - _window->Close(); + PostQuitMessage(0); } LaunchPosition AppHost::_GetWindowLaunchPosition() @@ -547,12 +586,31 @@ LaunchPosition AppHost::_GetWindowLaunchPosition() return pos; } +// Method Description: +// - Callback for when the window is first being created (during WM_CREATE). +// Stash the proposed size for later. We'll need that once we're totally +// initialized, so that we can show the window in the right position *when we +// want to show it*. If we did the _initialResizeAndRepositionWindow work now, +// it would have no effect, because the window is not yet visible. +// Arguments: +// - hwnd: The HWND of the window we're about to create. +// - proposedRect: The location and size of the window that we're about to +// create. We'll use this rect to determine which monitor the window is about +// to appear on. +void AppHost::_HandleCreateWindow(const HWND /* hwnd */, const til::rect& proposedRect) +{ + // GH#11561: Hide the window until we're totally done being initialized. + // More commentary in TerminalPage::_CompleteInitialization + _initialResizeAndRepositionWindow(_window->GetHandle(), proposedRect, _launchMode); +} + // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as -// specified in the settings. This will be called during the handling of -// WM_CREATE. We'll load the settings for the app, then get the proposed size -// of the terminal from the app. Using that proposed size, we'll resize the -// window we're creating, so that it'll match the values in the settings. +// specified in the settings. This is called once the app has finished it's +// initial setup, once we have created all the tabs, panes, etc. We'll load +// the settings for the app, then get the proposed size of the terminal from +// the app. Using that proposed size, we'll resize the window we're creating, +// so that it'll match the values in the settings. // Arguments: // - hwnd: The HWND of the window we're about to create. // - proposedRect: The location and size of the window that we're about to @@ -561,13 +619,13 @@ LaunchPosition AppHost::_GetWindowLaunchPosition() // - launchMode: A LaunchMode enum reference that indicates the launch mode // Return Value: // - None -void AppHost::_HandleCreateWindow(const HWND hwnd, til::rect proposedRect, LaunchMode& launchMode) +void AppHost::_initialResizeAndRepositionWindow(const HWND hwnd, til::rect proposedRect, LaunchMode& launchMode) { - launchMode = _logic.GetLaunchMode(); + launchMode = _windowLogic.GetLaunchMode(); // Acquire the actual initial position - auto initialPos = _logic.GetInitialPosition(proposedRect.left, proposedRect.top); - const auto centerOnLaunch = _logic.CenterOnLaunch(); + auto initialPos = _windowLogic.GetInitialPosition(proposedRect.left, proposedRect.top); + const auto centerOnLaunch = _windowLogic.CenterOnLaunch(); proposedRect.left = gsl::narrow(initialPos.X); proposedRect.top = gsl::narrow(initialPos.Y); @@ -614,12 +672,10 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, til::rect proposedRect, Launc proposedRect.top = monitorInfo.rcWork.top; } - auto initialSize = _logic.GetLaunchDimensions(dpix); + auto initialSize = _windowLogic.GetLaunchDimensions(dpix); - const auto islandWidth = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Width)), 1); - const auto islandHeight = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Height)), 1); + const auto islandWidth = Utils::ClampToShortMax(lrintf(initialSize.Width), 1); + const auto islandHeight = Utils::ClampToShortMax(lrintf(initialSize.Height), 1); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. @@ -648,7 +704,7 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, til::rect proposedRect, Launc til::point origin{ (proposedRect.left + nonClientFrame.left), (proposedRect.top) }; - if (_logic.IsQuakeWindow()) + if (_windowLogic.IsQuakeWindow()) { // If we just use rcWork by itself, we'll fail to account for the invisible // space reserved for the resize handles. So retrieve that size here. @@ -705,7 +761,7 @@ void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspecta { auto nonClientWindow{ static_cast(_window.get()) }; nonClientWindow->SetTitlebarContent(arg); - nonClientWindow->SetTitlebarBackground(_logic.TitlebarBrush()); + nonClientWindow->SetTitlebarBackground(_windowLogic.TitlebarBrush()); } _updateTheme(); @@ -728,13 +784,13 @@ void AppHost::_UpdateTheme(const winrt::Windows::Foundation::IInspectable&, void AppHost::_FocusModeChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - _window->FocusModeChanged(_logic.FocusMode()); + _window->FocusModeChanged(_windowLogic.FocusMode()); } void AppHost::_FullscreenChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - _window->FullscreenChanged(_logic.Fullscreen()); + _window->FullscreenChanged(_windowLogic.Fullscreen()); } void AppHost::_ChangeMaximizeRequested(const winrt::Windows::Foundation::IInspectable&, @@ -772,7 +828,7 @@ void AppHost::_AlwaysOnTopChanged(const winrt::Windows::Foundation::IInspectable return; } - _window->SetAlwaysOnTop(_logic.AlwaysOnTop()); + _window->SetAlwaysOnTop(_windowLogic.AlwaysOnTop()); } // Method Description @@ -800,14 +856,14 @@ void AppHost::_RaiseVisualBell(const winrt::Windows::Foundation::IInspectable&, // - void AppHost::_WindowMouseWheeled(const til::point coord, const int32_t delta) { - if (_logic) + if (_windowLogic) { // Find all the elements that are underneath the mouse - auto elems = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::FindElementsInHostCoordinates(coord.to_winrt_point(), _logic.GetRoot()); + auto elems = Xaml::Media::VisualTreeHelper::FindElementsInHostCoordinates(coord.to_winrt_point(), _windowLogic.GetRoot()); for (const auto& e : elems) { // If that element has implemented IMouseWheelListener, call OnMouseWheel on that element. - if (auto control{ e.try_as() }) + if (auto control{ e.try_as() }) { try { @@ -835,11 +891,6 @@ void AppHost::_WindowMouseWheeled(const til::point coord, const int32_t delta) } } -bool AppHost::HasWindow() -{ - return _shouldCreateWindow; -} - // Method Description: // - Event handler for the Peasant::ExecuteCommandlineRequested event. Take the // provided commandline args, and attempt to parse them and perform the @@ -861,7 +912,51 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send // Summon the window whenever we dispatch a commandline to it. This will // make it obvious when a new tab/pane is created in a window. _HandleSummon(sender, summonArgs); - _logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); + _windowLogic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory(), args.CurrentEnvironment()); +} + +void AppHost::_WindowActivated(bool activated) +{ + _windowLogic.WindowActivated(activated); + + if (activated && _isWindowInitialized) + { + _peasantNotifyActivateWindow(); + } +} + +winrt::fire_and_forget AppHost::_peasantNotifyActivateWindow() +{ + const auto desktopManager = _desktopManager; + const auto peasant = _peasant; + const auto hwnd = _window->GetHandle(); + + auto weakThis{ weak_from_this() }; + + co_await winrt::resume_background(); + + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + + GUID currentDesktopGuid{}; + if (FAILED_LOG(desktopManager->GetWindowDesktopId(hwnd, ¤tDesktopGuid))) + { + co_return; + } + + // TODO: projects/5 - in the future, we'll want to actually get the + // desktop GUID in IslandWindow, and bubble that up here, then down to + // the Peasant. For now, we're just leaving space for it. + peasant.ActivateWindow({ + peasant.GetID(), + reinterpret_cast(hwnd), + currentDesktopGuid, + winrt::clock().now(), + }); } // Method Description: @@ -876,357 +971,29 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send // - The window layout as a json string. winrt::Windows::Foundation::IAsyncOperation AppHost::_GetWindowLayoutAsync() { - winrt::apartment_context peasant_thread; - winrt::hstring layoutJson = L""; + + auto weakThis{ weak_from_this() }; + // Use the main thread since we are accessing controls. - co_await wil::resume_foreground(_logic.GetRoot().Dispatcher()); + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); + + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return layoutJson; + } + try { const auto pos = _GetWindowLaunchPosition(); - layoutJson = _logic.GetWindowLayoutJson(pos); + layoutJson = _windowLogic.GetWindowLayoutJson(pos); } CATCH_LOG() - // go back to give the result to the peasant. - co_await peasant_thread; - co_return layoutJson; } -// Method Description: -// - Event handler for the WindowManager::FindTargetWindowRequested event. The -// manager will ask us how to figure out what the target window is for a set -// of commandline arguments. We'll take those arguments, and ask AppLogic to -// parse them for us. We'll then set ResultTargetWindow in the given args, so -// the sender can use that result. -// Arguments: -// - args: the bundle of a commandline and working directory to find the correct target window for. -// Return Value: -// - -void AppHost::_FindTargetWindow(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const Remoting::FindTargetWindowArgs& args) -{ - const auto targetWindow = _logic.FindTargetWindow(args.Args().Commandline()); - args.ResultTargetWindow(targetWindow.WindowId()); - args.ResultTargetWindowName(targetWindow.WindowName()); -} - -winrt::fire_and_forget AppHost::_WindowActivated(bool activated) -{ - _logic.WindowActivated(activated); - - if (!activated) - { - co_return; - } - - co_await winrt::resume_background(); - - if (auto peasant{ _windowManager.CurrentWindow() }) - { - const auto currentDesktopGuid{ _CurrentDesktopGuid() }; - - // TODO: projects/5 - in the future, we'll want to actually get the - // desktop GUID in IslandWindow, and bubble that up here, then down to - // the Peasant. For now, we're just leaving space for it. - Remoting::WindowActivatedArgs args{ peasant.GetID(), - (uint64_t)_window->GetHandle(), - currentDesktopGuid, - winrt::clock().now() }; - peasant.ActivateWindow(args); - } -} - -void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - // MSFT:35726322 - // - // Although we're manually revoking the event handler now in the dtor before - // we null out the window, let's be extra careful and check JUST IN CASE. - if (_window == nullptr) - { - return; - } - - _setupGlobalHotkeys(); - - if (_windowManager.DoesQuakeWindowExist() || - _window->IsQuakeWindow() || - (_logic.GetAlwaysShowNotificationIcon() || _logic.GetMinimizeToNotificationArea())) - { - _CreateNotificationIcon(); - } - - // Set the number of open windows (so we know if we are the last window) - // and subscribe for updates if there are any changes to that number. - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); - - _WindowCreatedToken = _windowManager.WindowCreated([this](auto&&, auto&&) { - if (_getWindowLayoutThrottler) { - _getWindowLayoutThrottler.value()(); - } - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); }); - - _WindowClosedToken = _windowManager.WindowClosed([this](auto&&, auto&&) { - if (_getWindowLayoutThrottler) - { - _getWindowLayoutThrottler.value()(); - } - _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); - }); - - // These events are coming from peasants that become or un-become quake windows. - _revokers.ShowNotificationIconRequested = _windowManager.ShowNotificationIconRequested(winrt::auto_revoke, { this, &AppHost::_ShowNotificationIconRequested }); - _revokers.HideNotificationIconRequested = _windowManager.HideNotificationIconRequested(winrt::auto_revoke, { this, &AppHost::_HideNotificationIconRequested }); - // If the monarch receives a QuitAll event it will signal this event to be - // ran before each peasant is closed. - _revokers.QuitAllRequested = _windowManager.QuitAllRequested(winrt::auto_revoke, { this, &AppHost::_QuitAllRequested }); - - // The monarch should be monitoring if it should save the window layout. - if (!_getWindowLayoutThrottler.has_value()) - { - // We want at least some delay to prevent the first save from overwriting - // the data as we try load windows initially. - _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _SaveWindowLayoutsRepeat(); })); - _getWindowLayoutThrottler.value()(); - } -} - -winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts() -{ - // Make sure we run on a background thread to not block anything. - co_await winrt::resume_background(); - - if (_logic.ShouldUsePersistedLayout()) - { - try - { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Collect", - TraceLoggingDescription("Logged when collecting window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - const auto layoutJsons = _windowManager.GetAllWindowLayouts(); - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Save", - TraceLoggingDescription("Logged when writing window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _logic.SaveWindowLayoutJsons(layoutJsons); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_SaveWindowLayouts_Failed", - TraceLoggingDescription("An error occurred when collecting or writing window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - } - } - - co_return; -} - -winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat() -{ - // Make sure we run on a background thread to not block anything. - co_await winrt::resume_background(); - - co_await _SaveWindowLayouts(); - - // Don't need to save too frequently. - co_await 30s; - - // As long as we are supposed to keep saving, request another save. - // This will be delayed by the throttler so that at most one save happens - // per 10 seconds, if a save is requested by another source simultaneously. - if (_getWindowLayoutThrottler.has_value()) - { - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_requestGetLayout", - TraceLoggingDescription("Logged when triggering a throttled write of the window state"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - _getWindowLayoutThrottler.value()(); - } -} - -void AppHost::_listenForInboundConnections() -{ - _logic.SetInboundListener(); -} - -winrt::fire_and_forget AppHost::_setupGlobalHotkeys() -{ - // The hotkey MUST be registered on the main thread. It will fail otherwise! - co_await wil::resume_foreground(_logic.GetRoot().Dispatcher()); - - if (!_window) - { - // MSFT:36797001 There's a surprising number of hits of this callback - // getting triggered during teardown. As a best practice, we really - // should make sure _window exists before accessing it on any coroutine. - // We might be getting called back after the app already began getting - // cleaned up. - co_return; - } - // Unregister all previously registered hotkeys. - // - // RegisterHotKey(), will not unregister hotkeys automatically. - // If a hotkey with a given HWND and ID combination already exists - // then a duplicate one will be added, which we don't want. - // (Additionally we want to remove hotkeys that were removed from the settings.) - for (auto i = 0, count = gsl::narrow_cast(_hotkeys.size()); i < count; ++i) - { - _window->UnregisterHotKey(i); - } - - _hotkeys.clear(); - - // Re-register all current hotkeys. - for (const auto& [keyChord, cmd] : _logic.GlobalHotkeys()) - { - if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) - { - auto index = gsl::narrow_cast(_hotkeys.size()); - const auto succeeded = _window->RegisterHotKey(index, keyChord); - - TraceLoggingWrite(g_hWindowsTerminalProvider, - "AppHost_setupGlobalHotkey", - TraceLoggingDescription("Emitted when setting a single hotkey"), - TraceLoggingInt64(index, "index", "the index of the hotkey to add"), - TraceLoggingWideString(cmd.Name().c_str(), "name", "the name of the command"), - TraceLoggingBoolean(succeeded, "succeeded", "true if we succeeded"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - _hotkeys.emplace_back(summonArgs); - } - } -} - -// Method Description: -// - Called whenever a registered hotkey is pressed. We'll look up the -// GlobalSummonArgs for the specified hotkey, then dispatch a call to the -// Monarch with the selection information. -// - If the monarch finds a match for the window name (or no name was provided), -// it'll set FoundMatch=true. -// - If FoundMatch is false, and a name was provided, then we should create a -// new window with the given name. -// Arguments: -// - hotkeyIndex: the index of the entry in _hotkeys that was pressed. -// Return Value: -// - -void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex) -{ - if (hotkeyIndex < 0 || static_cast(hotkeyIndex) > _hotkeys.size()) - { - return; - } - - const auto& summonArgs = til::at(_hotkeys, hotkeyIndex); - Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; - - // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false - // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false - // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true - args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); - args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); - args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); - args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); - - switch (summonArgs.Monitor()) - { - case Settings::Model::MonitorBehavior::Any: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); - break; - case Settings::Model::MonitorBehavior::ToCurrent: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); - break; - case Settings::Model::MonitorBehavior::ToMouse: - args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); - break; - } - - _windowManager.SummonWindow(args); - if (args.FoundMatch()) - { - // Excellent, the window was found. We have nothing else to do here. - } - else - { - // We should make the window ourselves. - _createNewTerminalWindow(summonArgs); - } -} - -// Method Description: -// - Called when the monarch failed to summon a window for a given set of -// SummonWindowSelectionArgs. In this case, we should create the specified -// window ourselves. -// - This is to support the scenario like `globalSummon(Name="_quake")` being -// used to summon the window if it already exists, or create it if it doesn't. -// Arguments: -// - args: Contains information on how we should name the window -// Return Value: -// - -winrt::fire_and_forget AppHost::_createNewTerminalWindow(Settings::Model::GlobalSummonArgs args) -{ - // Hop to the BG thread - co_await winrt::resume_background(); - - // This will get us the correct exe for dev/preview/release. If you - // don't stick this in a local, it'll get mangled by ShellExecute. I - // have no idea why. - const auto exePath{ GetWtExePath() }; - - // If we weren't given a name, then just use new to force the window to be - // unnamed. - winrt::hstring cmdline{ - fmt::format(L"-w {}", - args.Name().empty() ? L"new" : - args.Name()) - }; - - SHELLEXECUTEINFOW seInfo{ 0 }; - seInfo.cbSize = sizeof(seInfo); - seInfo.fMask = SEE_MASK_NOASYNC; - seInfo.lpVerb = L"open"; - seInfo.lpFile = exePath.c_str(); - seInfo.lpParameters = cmdline.c_str(); - seInfo.nShow = SW_SHOWNORMAL; - LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); - - co_return; -} - -// Method Description: -// - Helper to initialize our instance of IVirtualDesktopManager. If we already -// got one, then this will just return true. Otherwise, we'll try and init a -// new instance of one, and store that. -// - This will return false if we weren't able to initialize one, which I'm not -// sure is actually possible. -// Arguments: -// - -// Return Value: -// - true iff _desktopManager points to a non-null instance of IVirtualDesktopManager -bool AppHost::_LazyLoadDesktopManager() -{ - if (_desktopManager == nullptr) - { - try - { - _desktopManager = winrt::create_instance(__uuidof(VirtualDesktopManager)); - } - CATCH_LOG(); - } - - return _desktopManager != nullptr; -} - void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*sender*/, const Remoting::SummonWindowBehavior& args) { @@ -1234,7 +1001,7 @@ void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*se if (args != nullptr && args.MoveToCurrentDesktop()) { - if (_LazyLoadDesktopManager()) + if (_desktopManager) { // First thing - make sure that we're not on the current desktop. If // we are, then don't call MoveWindowToDesktop. This is to mitigate @@ -1262,23 +1029,6 @@ void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*se } } -// Method Description: -// - This gets the GUID of the desktop our window is currently on. It does NOT -// get the GUID of the desktop that's currently active. -// Arguments: -// - -// Return Value: -// - the GUID of the desktop our window is currently on -GUID AppHost::_CurrentDesktopGuid() -{ - GUID currentDesktopGuid{ 0 }; - if (_LazyLoadDesktopManager()) - { - LOG_IF_FAILED(_desktopManager->GetWindowDesktopId(_window->GetHandle(), ¤tDesktopGuid)); - } - return currentDesktopGuid; -} - // Method Description: // - Called when this window wants _all_ windows to display their // identification. We'll hop to the BG thread, and raise an event (eventually @@ -1290,13 +1040,22 @@ GUID AppHost::_CurrentDesktopGuid() winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable /*sender*/, const winrt::Windows::Foundation::IInspectable /*args*/) { + auto weakThis{ weak_from_this() }; + // We'll be raising an event that may result in a RPC call to the monarch - // make sure we're on the background thread, or this will silently fail co_await winrt::resume_background(); - if (auto peasant{ _windowManager.CurrentWindow() }) + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) { - peasant.RequestIdentifyWindows(); + co_return; + } + + if (_peasant) + { + _peasant.RequestIdentifyWindows(); } } @@ -1310,141 +1069,217 @@ winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows:: void AppHost::_DisplayWindowId(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { - _logic.IdentifyWindow(); + _windowLogic.IdentifyWindow(); } winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Foundation::IInspectable /*sender*/, const winrt::TerminalApp::RenameWindowRequestedArgs args) { - // Capture calling context. - winrt::apartment_context ui_thread; - // Switch to the BG thread - anything x-proc must happen on a BG thread co_await winrt::resume_background(); - if (auto peasant{ _windowManager.CurrentWindow() }) + if (_peasant) { Remoting::RenameRequestArgs requestArgs{ args.ProposedName() }; - peasant.RequestRename(requestArgs); - - // Switch back to the UI thread. Setting the WindowName needs to happen - // on the UI thread, because it'll raise a PropertyChanged event - co_await ui_thread; + _peasant.RequestRename(requestArgs); if (requestArgs.Succeeded()) { - _logic.WindowName(args.ProposedName()); + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); + _windowLogic.WindowName(args.ProposedName()); } else { - _logic.RenameFailed(); + _windowLogic.RenameFailed(); } } } +static double _opacityFromBrush(const winrt::Windows::UI::Xaml::Media::Brush& brush) +{ + if (auto acrylic = brush.try_as()) + { + return acrylic.TintOpacity(); + } + else if (auto solidColor = brush.try_as()) + { + return solidColor.Opacity(); + } + return 1.0; +} + +static bool _isActuallyDarkTheme(const auto requestedTheme) +{ + switch (requestedTheme) + { + case winrt::Windows::UI::Xaml::ElementTheme::Light: + return false; + case winrt::Windows::UI::Xaml::ElementTheme::Dark: + return true; + case winrt::Windows::UI::Xaml::ElementTheme::Default: + default: + return Theme::IsSystemInDarkTheme(); + } +} + +// DwmSetWindowAttribute(... DWMWA_BORDER_COLOR.. ) doesn't work on Windows 10, +// but it _will_ spew to the debug console. This helper just no-ops the call on +// Windows 10, so that we don't even get that spew +void _frameColorHelper(const HWND h, const COLORREF color) +{ + if (Utils::IsWindows11()) + { + LOG_IF_FAILED(DwmSetWindowAttribute(h, DWMWA_BORDER_COLOR, &color, sizeof(color))); + } +} + void AppHost::_updateTheme() { - auto theme = _logic.Theme(); + auto theme = _appLogic.Theme(); _window->OnApplicationThemeChanged(theme.RequestedTheme()); - // This block of code enables Mica for our window. By all accounts, this - // version of the code will only work on Windows 11, SV2. There's a slightly - // different API surface for enabling Mica on Windows 11 22000.0. - // - // This code is left here, commented out, for future enablement of Mica. - // We'll revisit this in GH#10509. Because we can't enable transparent - // titlebars for showing Mica currently, we're just gonna disable it - // entirely while we sort that out. - // - // const int attribute = theme.Window().UseMica() ? /*DWMSBT_MAINWINDOW*/ 2 : /*DWMSBT_NONE*/ 1; - // DwmSetWindowAttribute(_window->GetHandle(), /* DWMWA_SYSTEMBACKDROP_TYPE */ 38, &attribute, sizeof(attribute)); + const auto windowTheme{ theme.Window() }; + + const auto b = _windowLogic.TitlebarBrush(); + const auto color = ThemeColor::ColorFromBrush(b); + const auto colorOpacity = b ? color.A / 255.0 : 0.0; + const auto brushOpacity = _opacityFromBrush(b); + const auto opacity = std::min(colorOpacity, brushOpacity); + _window->UseMica(windowTheme ? windowTheme.UseMica() : false, opacity); + + // This is a hack to make the window borders dark instead of light. + // It must be done before WM_NCPAINT so that the borders are rendered with + // the correct theme. + // For more information, see GH#6620. + _window->UseDarkTheme(_isActuallyDarkTheme(theme.RequestedTheme())); + + // Update the window frame. If `rainbowFrame:true` is enabled, then that + // will be used. Otherwise we'll try to use the `FrameBrush` set in the + // terminal window, as that will have the right color for the ThemeColor for + // this setting. If that value is null, then revert to the default frame + // color. + if (windowTheme) + { + if (windowTheme.RainbowFrame()) + { + _startFrameTimer(); + } + else if (const auto b{ _windowLogic.FrameBrush() }) + { + _stopFrameTimer(); + const auto color = ThemeColor::ColorFromBrush(b); + COLORREF ref{ til::color{ color } }; + _frameColorHelper(_window->GetHandle(), ref); + } + else + { + _stopFrameTimer(); + // DWMWA_COLOR_DEFAULT is the magic "reset to the default" value + _frameColorHelper(_window->GetHandle(), DWMWA_COLOR_DEFAULT); + } + } +} + +void AppHost::_startFrameTimer() +{ + // Instantiate the frame color timer, if we haven't already. We'll only ever + // create one instance of this. We'll set up the callback for the timers as + // _updateFrameColor, which will actually handle setting the colors. If we + // already have a timer, just start that one. + + if (_frameTimer == nullptr) + { + _frameTimer = winrt::Windows::UI::Xaml::DispatcherTimer(); + _frameTimer.Interval(FrameUpdateInterval); + _frameTimerToken = _frameTimer.Tick({ this, &AppHost::_updateFrameColor }); + } + _frameTimer.Start(); +} + +void AppHost::_stopFrameTimer() +{ + if (_frameTimer) + { + _frameTimer.Stop(); + } +} + +// Method Description: +// - Updates the color of the window frame to cycle through all the colors. This +// is called as the `_frameTimer` Tick callback, roughly 60 times per second. +void AppHost::_updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) +{ + // First, a couple helper functions: + static const auto saturateAndToColor = [](const float a, const float b, const float c) -> til::color { + return til::color{ + base::saturated_cast(255.f * std::clamp(a, 0.f, 1.f)), + base::saturated_cast(255.f * std::clamp(b, 0.f, 1.f)), + base::saturated_cast(255.f * std::clamp(c, 0.f, 1.f)) + }; + }; + + // Helper for converting a hue [0, 1) to an RGB value. + // Credit to https://www.chilliant.com/rgb2hsv.html + static const auto hueToRGB = [&](const float H) -> til::color { + float R = abs(H * 6 - 3) - 1; + float G = 2 - abs(H * 6 - 2); + float B = 2 - abs(H * 6 - 4); + return saturateAndToColor(R, G, B); + }; + + // Now, the main body of work. + // - Convert the time delta between when we were started and now, to a hue. This will cycle us through all the colors. + // - Convert that hue to an RGB value. + // - Set the frame's color to that RGB color. + const auto now = std::chrono::high_resolution_clock::now(); + const std::chrono::duration delta{ now - _started }; + const auto seconds = delta.count() / 4; // divide by four, to make the effect slower. Otherwise it flashes way to fast. + float ignored; + const auto color = hueToRGB(modf(seconds, &ignored)); + + _frameColorHelper(_window->GetHandle(), color); } void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + const winrt::TerminalApp::SettingsLoadEventArgs& /*args*/) { - _setupGlobalHotkeys(); + // We don't need to call in to windowLogic here - it has its own SettingsChanged handler - // If we're monarch, we need to check some conditions to show the notification icon. - // If there's a Quake window somewhere, we'll want to keep the notification icon. - // There's two settings - MinimizeToNotificationArea and AlwaysShowNotificationIcon. If either - // one of them are true, we want to make sure there's a notification icon. - // If both are false, we want to remove our icon from the notification area. - // When we remove our icon from the notification area, we'll also want to re-summon - // any hidden windows, but right now we're not keeping track of who's hidden, - // so just summon them all. Tracking the work to do a "summon all minimized" in - // GH#10448 - if (_windowManager.IsMonarch()) - { - if (!_windowManager.DoesQuakeWindowExist()) - { - if (!_notificationIcon && (_logic.GetMinimizeToNotificationArea() || _logic.GetAlwaysShowNotificationIcon())) - { - _CreateNotificationIcon(); - } - else if (_notificationIcon && !_logic.GetMinimizeToNotificationArea() && !_logic.GetAlwaysShowNotificationIcon()) - { - _windowManager.SummonAllWindows(); - _DestroyNotificationIcon(); - } - } - } - - _window->SetMinimizeToNotificationAreaBehavior(_logic.GetMinimizeToNotificationArea()); - _window->SetAutoHideWindow(_logic.AutoHideWindow()); + _window->SetMinimizeToNotificationAreaBehavior(_windowLogic.GetMinimizeToNotificationArea()); + _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); _updateTheme(); } void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - // We want the quake window to be accessible through the notification icon. - // This means if there's a quake window _somewhere_, we want the notification icon - // to show regardless of the notification icon settings. - // This also means we'll need to destroy the notification icon if it was created - // specifically for the quake window. If not, it should not be destroyed. - if (!_window->IsQuakeWindow() && _logic.IsQuakeWindow()) - { - _ShowNotificationIconRequested(nullptr, nullptr); - } - else if (_window->IsQuakeWindow() && !_logic.IsQuakeWindow()) - { - _HideNotificationIconRequested(nullptr, nullptr); - } - - _window->IsQuakeWindow(_logic.IsQuakeWindow()); + _window->IsQuakeWindow(_windowLogic.IsQuakeWindow()); } +// Raised from our Peasant. We handle by propagating the call to our terminal window. winrt::fire_and_forget AppHost::_QuitRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { + auto weakThis{ weak_from_this() }; // Need to be on the main thread to close out all of the tabs. - co_await wil::resume_foreground(_logic.GetRoot().Dispatcher()); + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); - _logic.Quit(); + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + + _windowLogic.Quit(); } +// Raised from TerminalWindow. We handle by bubbling the request to the window manager. void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - _windowManager.RequestQuitAll(); -} - -void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&, - const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) -{ - // Make sure that the current timer is destroyed so that it doesn't attempt - // to run while we are in the middle of quitting. - if (_getWindowLayoutThrottler.has_value()) - { - _getWindowLayoutThrottler.reset(); - } - - // Tell the monarch to wait for the window layouts to save before - // everyone quits. - args.BeforeQuitAllAction(_SaveWindowLayouts()); + Remoting::WindowManager::RequestQuitAll(_peasant); } void AppHost::_ShowWindowChanged(const winrt::Windows::Foundation::IInspectable&, @@ -1454,10 +1289,7 @@ void AppHost::_ShowWindowChanged(const winrt::Windows::Foundation::IInspectable& // should prevent scenarios where the Terminal window state and PTY window // state get de-sync'd, and cause the window to minimize/restore constantly // in a loop. - if (_showHideWindowThrottler) - { - _showHideWindowThrottler->Run(args.ShowOrHide()); - } + _showHideWindowThrottler->Run(args.ShowOrHide()); } void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, @@ -1498,76 +1330,6 @@ void AppHost::_SystemMenuChangeRequested(const winrt::Windows::Foundation::IInsp } } -// Method Description: -// - Creates a Notification Icon and hooks up its handlers -// Arguments: -// - -// Return Value: -// - -void AppHost::_CreateNotificationIcon() -{ - _notificationIcon = std::make_unique(_window->GetHandle()); - - // Hookup the handlers, save the tokens for revoking if settings change. - _ReAddNotificationIconToken = _window->NotifyReAddNotificationIcon([this]() { _notificationIcon->ReAddNotificationIcon(); }); - _NotificationIconPressedToken = _window->NotifyNotificationIconPressed([this]() { _notificationIcon->NotificationIconPressed(); }); - _ShowNotificationIconContextMenuToken = _window->NotifyShowNotificationIconContextMenu([this](til::point coord) { _notificationIcon->ShowContextMenu(coord, _windowManager.GetPeasantInfos()); }); - _NotificationIconMenuItemSelectedToken = _window->NotifyNotificationIconMenuItemSelected([this](HMENU hm, UINT idx) { _notificationIcon->MenuItemSelected(hm, idx); }); - _notificationIcon->SummonWindowRequested([this](auto& args) { _windowManager.SummonWindow(args); }); -} - -// Method Description: -// - Deletes our notification icon if we have one. -// Arguments: -// - -// Return Value: -// - -void AppHost::_DestroyNotificationIcon() -{ - _window->NotifyReAddNotificationIcon(_ReAddNotificationIconToken); - _window->NotifyNotificationIconPressed(_NotificationIconPressedToken); - _window->NotifyShowNotificationIconContextMenu(_ShowNotificationIconContextMenuToken); - _window->NotifyNotificationIconMenuItemSelected(_NotificationIconMenuItemSelectedToken); - - _notificationIcon->RemoveIconFromNotificationArea(); - _notificationIcon = nullptr; -} - -void AppHost::_ShowNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - if (_windowManager.IsMonarch()) - { - if (!_notificationIcon) - { - _CreateNotificationIcon(); - } - } - else - { - _windowManager.RequestShowNotificationIcon(); - } -} - -void AppHost::_HideNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) -{ - if (_windowManager.IsMonarch()) - { - // Destroy it only if our settings allow it - if (_notificationIcon && - !_logic.GetAlwaysShowNotificationIcon() && - !_logic.GetMinimizeToNotificationArea()) - { - _DestroyNotificationIcon(); - } - } - else - { - _windowManager.RequestHideNotificationIcon(); - } -} - // Method Description: // - BODGY workaround for GH#9320. When the window moves, dismiss all the popups // in the UI tree. Xaml Islands unfortunately doesn't do this for us, see @@ -1578,14 +1340,18 @@ void AppHost::_HideNotificationIconRequested(const winrt::Windows::Foundation::I // - void AppHost::_WindowMoved() { - if (_logic) + if (_isWindowInitialized < WindowInitializedState::Initialized) + { + return; + } + if (_windowLogic) { // Ensure any open ContentDialog is dismissed. // Closing the popup in the UI tree as done below is not sufficient because // it does not terminate the dialog's async operation. - _logic.DismissDialog(); + _windowLogic.DismissDialog(); - const auto root{ _logic.GetRoot() }; + const auto root{ _windowLogic.GetRoot() }; if (root && root.XamlRoot()) { try @@ -1614,7 +1380,8 @@ void AppHost::_CloseRequested(const winrt::Windows::Foundation::IInspectable& /* const winrt::Windows::Foundation::IInspectable& /*args*/) { const auto pos = _GetWindowLaunchPosition(); - _logic.CloseWindow(pos); + const bool isLastWindow = _windowManager.GetNumberOfPeasants() == 1; + _windowLogic.CloseWindow(pos, isLastWindow); } void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, @@ -1625,7 +1392,183 @@ void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspect if (_useNonClientArea) { auto nonClientWindow{ static_cast(_window.get()) }; - nonClientWindow->SetTitlebarBackground(_logic.TitlebarBrush()); + nonClientWindow->SetTitlebarBackground(_windowLogic.TitlebarBrush()); + _updateTheme(); } } + else if (e.PropertyName() == L"FrameBrush") + { + _updateTheme(); + } +} + +winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*arg*/) +{ + _isWindowInitialized = WindowInitializedState::Initializing; + + // GH#11561: We're totally done being initialized. Resize the window to + // match the initial settings, and then call ShowWindow to finally make us + // visible. + + // Use the visibility that we were originally requested with as a base. We + // can't just use SW_SHOWDEFAULT, because that is set on a per-process + // basis. That means that a second window needs to have its STARTUPINFO's + // wShowCmd passed into the original process. + auto nCmdShow = _launchShowWindowCommand; + + if (WI_IsFlagSet(_launchMode, LaunchMode::MaximizedMode)) + { + nCmdShow = SW_MAXIMIZE; + } + + auto weakThis{ weak_from_this() }; + // For inexplicable reasons, again, hop to the BG thread, then back to the + // UI thread. This is shockingly load bearing - without this, then + // sometimes, we'll _still_ show the HWND before the XAML island actually + // paints. + co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher(), winrt::Windows::UI::Core::CoreDispatcherPriority::Low); + + // If we're gone on the other side of this co_await, well, that's fine. Just bail. + const auto strongThis = weakThis.lock(); + if (!strongThis) + { + co_return; + } + + ShowWindow(_window->GetHandle(), nCmdShow); + + // If we didn't start the window hidden (in one way or another), then try to + // pull ourselves to the foreground. Don't necessarily do a whole "summon", + // we don't really want to STEAL foreground if someone rightfully took it + + const bool noForeground = nCmdShow == SW_SHOWMINIMIZED || + nCmdShow == SW_SHOWNOACTIVATE || + nCmdShow == SW_SHOWMINNOACTIVE || + nCmdShow == SW_SHOWNA || + nCmdShow == SW_FORCEMINIMIZE; + if (!noForeground) + { + SetForegroundWindow(_window->GetHandle()); + _peasantNotifyActivateWindow(); + } + + // Don't set our state to Initialized until after the call to ShowWindow. + // When we call ShowWindow, the OS will also send us a WM_MOVE, which we'll + // then use to try and dismiss an open dialog. This creates the unintended + // side effect of immediately dismissing the initial warning dialog, if + // there were settings load warnings. + // + // In AppHost::_WindowMoved, we'll make sure we're at least initialized + // before dismissing open dialogs. + _isWindowInitialized = WindowInitializedState::Initialized; +} + +winrt::TerminalApp::TerminalWindow AppHost::Logic() +{ + return _windowLogic; +} + +// Method Description: +// - Raised from Page -> us -> manager -> monarch +// - Called when the user attempts to move a tab or pane to another window. +// `args` will contain info about the structure of the content being moved, +// and where it should go. +// - If the WindowPosition is filled in, then the user was dragging a tab out of +// this window and dropping it in empty space, indicating it should create a +// new window. In that case, we'll make some adjustments using that info and +// our own window info, so that the new window will be created in the right +// place and with the same size. +void AppHost::_handleMoveContent(const winrt::Windows::Foundation::IInspectable& /*sender*/, + winrt::TerminalApp::RequestMoveContentArgs args) +{ + winrt::Windows::Foundation::IReference windowBoundsReference{ nullptr }; + + if (args.WindowPosition() && _window) + { + // The WindowPosition is in DIPs. We need to convert it to pixels. + const til::point dragPositionInDips{ til::math::rounding, args.WindowPosition().Value() }; + const auto scale = _window->GetCurrentDpiScale(); + + til::point dragPositionInPixels{ + til::math::rounding, + dragPositionInDips.x * scale, + dragPositionInDips.y * scale, + }; + + // Fortunately, the window position is already in pixels. + til::rect windowBoundsInPixels{ _window->GetWindowRect() }; + til::size windowSize{ windowBoundsInPixels.size() }; + + const auto dpi = _window->GetCurrentDpi(); + const auto nonClientFrame = _window->GetNonClientFrame(dpi); + + // If this window is maximized, you don't _really_ want the new window + // showing up at the same size (the size of a maximized window). You + // want it to just make a normal-sized window. This logic was taken out + // of AppHost::_HandleCreateWindow. + if (IsZoomed(_window->GetHandle())) + { + const auto initialSize = _windowLogic.GetLaunchDimensions(dpi); + + const auto islandWidth = Utils::ClampToShortMax(lrintf(initialSize.Width), 1); + const auto islandHeight = Utils::ClampToShortMax(lrintf(initialSize.Height), 1); + + // Get the size of a window we'd need to host that client rect. This will + // add the titlebar space. + const til::size nonClientSize{ _window->GetTotalNonClientExclusiveSize(dpi) }; + + const auto adjustedWidth = islandWidth + nonClientSize.width; + const auto adjustedHeight = islandHeight + nonClientSize.height; + + windowSize = til::size{ Utils::ClampToShortMax(adjustedWidth, 1), + Utils::ClampToShortMax(adjustedHeight, 1) }; + } + + // Adjust for the non-client bounds + dragPositionInPixels.x -= nonClientFrame.left; + dragPositionInPixels.y -= nonClientFrame.top; + windowSize = windowSize - nonClientFrame.size(); + + // Convert to DIPs for the size, so that dragging across a DPI boundary + // retains the correct dimensions. + const auto sizeInDips = windowSize.scale(til::math::rounding, 1.0f / scale); + til::rect inDips{ dragPositionInPixels, sizeInDips }; + + // Use the drag event as the new position, and the size of the actual window. + windowBoundsReference = inDips.to_winrt_rect(); + } + + _windowManager.RequestMoveContent(args.Window(), args.Content(), args.TabIndex(), windowBoundsReference); +} + +void AppHost::_handleAttach(const winrt::Windows::Foundation::IInspectable& /*sender*/, + winrt::Microsoft::Terminal::Remoting::AttachRequest args) +{ + _windowLogic.AttachContent(args.Content(), args.TabIndex()); +} + +// Page -> us -> manager -> monarch +// The page wants to tell the monarch that it was the drop target for a drag drop. +// The manager will tell the monarch to tell the _other_ window to send its content to us. +void AppHost::_handleReceiveContent(const winrt::Windows::Foundation::IInspectable& /* sender */, + winrt::TerminalApp::RequestReceiveContentArgs args) +{ + _windowManager.RequestSendContent(winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs{ args.SourceWindow(), args.TargetWindow(), args.TabIndex() }); +} + +// monarch -> Peasant -> us -> Page +// The Monarch was told to tell us to send our dragged content to someone else. +void AppHost::_handleSendContent(const winrt::Windows::Foundation::IInspectable& /* sender */, + winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args) +{ + _windowLogic.SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs{ args.SourceWindow(), args.TargetWindow(), args.TabIndex() }); +} + +// Bubble the update settings request up to the emperor. We're being called on +// the Window thread, but the Emperor needs to update the settings on the _main_ +// thread. +void AppHost::_requestUpdateSettings() +{ + _UpdateSettingsRequestedHandlers(); } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 2de8474218..fd912aee67 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -6,41 +6,71 @@ #include "NotificationIcon.h" #include -class AppHost +class AppHost : public std::enable_shared_from_this { public: - AppHost() noexcept; - virtual ~AppHost(); + AppHost(const winrt::TerminalApp::AppLogic& logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + const winrt::Microsoft::Terminal::Remoting::WindowManager& manager, + const winrt::Microsoft::Terminal::Remoting::Peasant& peasant, + std::unique_ptr window = nullptr) noexcept; void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle); void LastTabClosed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::LastTabClosedEventArgs& args); void Initialize(); + void Close(); + + [[nodiscard]] std::unique_ptr Refrigerate(); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); void SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - bool HasWindow(); + winrt::TerminalApp::TerminalWindow Logic(); + + static void s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResult& message); + + WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); private: std::unique_ptr _window; - winrt::TerminalApp::App _app; - winrt::TerminalApp::AppLogic _logic; - winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr }; - std::vector _hotkeys; + winrt::TerminalApp::AppLogic _appLogic; + winrt::TerminalApp::TerminalWindow _windowLogic; + + winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr }; + winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; + winrt::com_ptr _desktopManager{ nullptr }; - bool _shouldCreateWindow{ false }; + enum WindowInitializedState : uint32_t + { + NotInitialized = 0, + Initializing = 1, + Initialized = 2, + }; + + WindowInitializedState _isWindowInitialized{ WindowInitializedState::NotInitialized }; bool _useNonClientArea{ false }; + winrt::Microsoft::Terminal::Settings::Model::LaunchMode _launchMode{}; - std::optional> _getWindowLayoutThrottler; std::shared_ptr> _showHideWindowThrottler; - winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts(); - winrt::fire_and_forget _SaveWindowLayoutsRepeat(); - void _HandleCommandlineArgs(); + std::chrono::time_point _started; + winrt::Windows::UI::Xaml::DispatcherTimer _frameTimer{ nullptr }; + + uint32_t _launchShowWindowCommand{ SW_NORMAL }; + + void _preInit(); + + void _revokeWindowCallbacks(); + + void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); + void _HandleSessionRestore(const bool startedForContent); + winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); - void _HandleCreateWindow(const HWND hwnd, til::rect proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + void _HandleCreateWindow(const HWND hwnd, const til::rect& proposedRect); + void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::UIElement& arg); void _UpdateTheme(const winrt::Windows::Foundation::IInspectable&, @@ -53,10 +83,14 @@ private: const winrt::Windows::Foundation::IInspectable& arg); void _AlwaysOnTopChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); + winrt::fire_and_forget _WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& arg); + void _RaiseVisualBell(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); void _WindowMouseWheeled(const til::point coord, const int32_t delta); - winrt::fire_and_forget _WindowActivated(bool activated); + void _WindowActivated(bool activated); + winrt::fire_and_forget _peasantNotifyActivateWindow(); void _WindowMoved(); void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, @@ -64,12 +98,6 @@ private: winrt::Windows::Foundation::IAsyncOperation _GetWindowLayoutAsync(); - void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args); - - void _BecomeMonarch(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); - void _GlobalHotkeyPressed(const long hotkeyIndex); void _HandleSummon(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior& args); @@ -84,11 +112,8 @@ private: bool _LazyLoadDesktopManager(); - void _listenForInboundConnections(); - winrt::fire_and_forget _setupGlobalHotkeys(); - winrt::fire_and_forget _createNewTerminalWindow(winrt::Microsoft::Terminal::Settings::Model::GlobalSummonArgs args); void _HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + const winrt::TerminalApp::SettingsLoadEventArgs& args); void _IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); @@ -110,34 +135,37 @@ private: void _CloseRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - void _QuitAllRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args); - void _ShowWindowChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs& args); - void _CreateNotificationIcon(); - void _DestroyNotificationIcon(); - void _ShowNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); - void _HideNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); - void _updateTheme(); void _PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); - void _initialResizeAndRepositionWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + void _initialResizeAndRepositionWindow(const HWND hwnd, til::rect proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + + void _handleMoveContent(const winrt::Windows::Foundation::IInspectable& sender, + winrt::TerminalApp::RequestMoveContentArgs args); + void _handleAttach(const winrt::Windows::Foundation::IInspectable& sender, + winrt::Microsoft::Terminal::Remoting::AttachRequest args); + + void _requestUpdateSettings(); + + // Page -> us -> monarch + void _handleReceiveContent(const winrt::Windows::Foundation::IInspectable& sender, + winrt::TerminalApp::RequestReceiveContentArgs args); + + // monarch -> us -> Page + void _handleSendContent(const winrt::Windows::Foundation::IInspectable& sender, + winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args); + + void _startFrameTimer(); + void _stopFrameTimer(); + void _updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); - std::unique_ptr _notificationIcon; - winrt::event_token _ReAddNotificationIconToken; - winrt::event_token _NotificationIconPressedToken; - winrt::event_token _ShowNotificationIconContextMenuToken; - winrt::event_token _NotificationIconMenuItemSelectedToken; winrt::event_token _GetWindowLayoutRequestedToken; - winrt::event_token _WindowCreatedToken; - winrt::event_token _WindowClosedToken; + winrt::event_token _frameTimerToken; // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll @@ -146,33 +174,58 @@ private: struct Revokers { // Event handlers to revoke in ~AppHost, before calling App.Close - winrt::Microsoft::Terminal::Remoting::WindowManager::BecameMonarch_revoker BecameMonarch; winrt::Microsoft::Terminal::Remoting::Peasant::ExecuteCommandlineRequested_revoker peasantExecuteCommandlineRequested; winrt::Microsoft::Terminal::Remoting::Peasant::SummonRequested_revoker peasantSummonRequested; winrt::Microsoft::Terminal::Remoting::Peasant::DisplayWindowIdRequested_revoker peasantDisplayWindowIdRequested; winrt::Microsoft::Terminal::Remoting::Peasant::QuitRequested_revoker peasantQuitRequested; - winrt::TerminalApp::AppLogic::CloseRequested_revoker CloseRequested; - winrt::TerminalApp::AppLogic::RequestedThemeChanged_revoker RequestedThemeChanged; - winrt::TerminalApp::AppLogic::FullscreenChanged_revoker FullscreenChanged; - winrt::TerminalApp::AppLogic::FocusModeChanged_revoker FocusModeChanged; - winrt::TerminalApp::AppLogic::AlwaysOnTopChanged_revoker AlwaysOnTopChanged; - winrt::TerminalApp::AppLogic::RaiseVisualBell_revoker RaiseVisualBell; - winrt::TerminalApp::AppLogic::SystemMenuChangeRequested_revoker SystemMenuChangeRequested; - winrt::TerminalApp::AppLogic::ChangeMaximizeRequested_revoker ChangeMaximizeRequested; - winrt::TerminalApp::AppLogic::TitleChanged_revoker TitleChanged; - winrt::TerminalApp::AppLogic::LastTabClosed_revoker LastTabClosed; - winrt::TerminalApp::AppLogic::SetTaskbarProgress_revoker SetTaskbarProgress; - winrt::TerminalApp::AppLogic::IdentifyWindowsRequested_revoker IdentifyWindowsRequested; - winrt::TerminalApp::AppLogic::RenameWindowRequested_revoker RenameWindowRequested; - winrt::TerminalApp::AppLogic::SettingsChanged_revoker SettingsChanged; - winrt::TerminalApp::AppLogic::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; - winrt::TerminalApp::AppLogic::SummonWindowRequested_revoker SummonWindowRequested; - winrt::TerminalApp::AppLogic::OpenSystemMenu_revoker OpenSystemMenu; - winrt::TerminalApp::AppLogic::QuitRequested_revoker QuitRequested; - winrt::TerminalApp::AppLogic::ShowWindowChanged_revoker ShowWindowChanged; - winrt::Microsoft::Terminal::Remoting::WindowManager::ShowNotificationIconRequested_revoker ShowNotificationIconRequested; - winrt::Microsoft::Terminal::Remoting::WindowManager::HideNotificationIconRequested_revoker HideNotificationIconRequested; + + winrt::Microsoft::Terminal::Remoting::Peasant::AttachRequested_revoker AttachRequested; + + winrt::TerminalApp::TerminalWindow::Initialized_revoker Initialized; + winrt::TerminalApp::TerminalWindow::CloseRequested_revoker CloseRequested; + winrt::TerminalApp::TerminalWindow::RequestedThemeChanged_revoker RequestedThemeChanged; + winrt::TerminalApp::TerminalWindow::FullscreenChanged_revoker FullscreenChanged; + winrt::TerminalApp::TerminalWindow::FocusModeChanged_revoker FocusModeChanged; + winrt::TerminalApp::TerminalWindow::AlwaysOnTopChanged_revoker AlwaysOnTopChanged; + winrt::TerminalApp::TerminalWindow::RaiseVisualBell_revoker RaiseVisualBell; + winrt::TerminalApp::TerminalWindow::SystemMenuChangeRequested_revoker SystemMenuChangeRequested; + winrt::TerminalApp::TerminalWindow::ChangeMaximizeRequested_revoker ChangeMaximizeRequested; + winrt::TerminalApp::TerminalWindow::TitleChanged_revoker TitleChanged; + winrt::TerminalApp::TerminalWindow::LastTabClosed_revoker LastTabClosed; + winrt::TerminalApp::TerminalWindow::SetTaskbarProgress_revoker SetTaskbarProgress; + winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested; + winrt::TerminalApp::TerminalWindow::RenameWindowRequested_revoker RenameWindowRequested; + winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged; + winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested; + winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu; + winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested; + winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged; + winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent; + winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent; + winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged; + winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged; + winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; - winrt::TerminalApp::AppLogic::PropertyChanged_revoker PropertyChanged; + winrt::Microsoft::Terminal::Remoting::Peasant::SendContentRequested_revoker SendContentRequested; } _revokers{}; + + // our IslandWindow is not a WinRT type. It can't make auto_revokers like + // the above. We also need to make sure to unregister ourself from the + // window when we refrigerate the window thread so that the window can later + // be re-used. + struct WindowRevokers + { + winrt::event_token MouseScrolled; + winrt::event_token WindowActivated; + winrt::event_token WindowMoved; + winrt::event_token ShouldExitFullscreen; + winrt::event_token WindowCloseButtonClicked; + winrt::event_token DragRegionClicked; + winrt::event_token WindowVisibilityChanged; + winrt::event_token UpdateSettingsRequested; + winrt::event_token MaximizeChanged; + winrt::event_token AutomaticShutdownRequested; + // LOAD BEARING!! + //If you add events here, make sure they're revoked in AppHost::_revokeWindowCallbacks + } _windowCallbacks{}; }; diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 98538c82b7..36c4eabb80 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -28,7 +28,7 @@ public: WINRT_ASSERT(!that->_window); that->_window = wil::unique_hwnd(window); - return that->_OnNcCreate(wparam, lparam); + return that->OnNcCreate(wparam, lparam); } else if (T* that = GetThisFromHandle(window)) { @@ -168,8 +168,8 @@ public: { const auto scale = GetCurrentDpiScale(); // 0.5 is to ensure that we pixel snap correctly at the edges, this is necessary with odd DPIs like 1.25, 1.5, 1, .75 - const auto logicalWidth = (physicalSize.cx / scale) + 0.5f; - const auto logicalHeight = (physicalSize.cy / scale) + 0.5f; + const auto logicalWidth = (physicalSize.width / scale) + 0.5f; + const auto logicalHeight = (physicalSize.height / scale) + 0.5f; return winrt::Windows::Foundation::Size(logicalWidth, logicalHeight); } @@ -210,13 +210,17 @@ protected: bool _minimized = false; + void _setupUserData() + { + SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + } // Method Description: // - This method is called when the window receives the WM_NCCREATE message. // Return Value: // - The value returned from the window proc. - virtual [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept + [[nodiscard]] virtual LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept { - SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + _setupUserData(); EnableNonClientDpiScaling(_window.get()); _currentDpi = GetDpiForWindow(_window.get()); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index cb3425c1ef..9bea05acfe 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -9,6 +9,7 @@ #include "NotificationIcon.h" #include #include +#include extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -26,8 +27,6 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" #define IDM_SYSTEM_MENU_BEGIN 0x1000 -const UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); - IslandWindow::IslandWindow() noexcept : _interopWindowHandle{ nullptr }, _rootGrid{ nullptr }, @@ -38,7 +37,47 @@ IslandWindow::IslandWindow() noexcept : IslandWindow::~IslandWindow() { - _source.Close(); + Close(); +} + +void IslandWindow::Close() +{ + // GH#15454: Unset the user data for the window. This will prevent future + // callbacks that come onto our window message loop from being sent to the + // IslandWindow (or other derived class's) implementation. + // + // Specifically, this prevents a pending coroutine from being able to call + // something like ShowWindow, and have that come back on the IslandWindow + // message loop, where it'll end up asking XAML something that XAML is no + // longer able to answer. + SetWindowLongPtr(_window.get(), GWLP_USERDATA, 0); + + if (_source) + { + _source.Close(); + _source = nullptr; + } +} + +// Clear out any state that might be associated with this app instance, so that +// we can later re-use this HWND for another instance. +// +// This doesn't actually close out our HWND or DesktopWindowXamlSource, but it +// will remove all our content, and SW_HIDE the window, so it isn't accessible. +void IslandWindow::Refrigerate() noexcept +{ + // Similar to in Close - unset our HWND's user data. We'll re-set this when + // we get re-heated, so that while we're refrigerated, we won't have + // unexpected callbacks into us while we don't have content. + // + // This pointer will get re-set in _warmInitialize + SetWindowLongPtr(_window.get(), GWLP_USERDATA, 0); + + _pfnCreateCallback = nullptr; + _pfnSnapDimensionCallback = nullptr; + + _rootGrid.Children().Clear(); + ShowWindow(_window.get(), SW_HIDE); } HWND IslandWindow::GetInteropHandle() const @@ -54,6 +93,12 @@ HWND IslandWindow::GetInteropHandle() const // - void IslandWindow::MakeWindow() noexcept { + if (_window) + { + // no-op if we already have a window. + return; + } + WNDCLASS wc{}; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hInstance = reinterpret_cast(&__ImageBase); @@ -90,17 +135,6 @@ void IslandWindow::MakeWindow() noexcept WINRT_ASSERT(_window); } -// Method Description: -// - Called when no tab is remaining to close the window. -// Arguments: -// - -// Return Value: -// - -void IslandWindow::Close() -{ - PostQuitMessage(0); -} - // Method Description: // - Set a callback to be called when we process a WM_CREATE message. This gives // the AppHost a chance to resize the window to the proper size. @@ -112,7 +146,7 @@ void IslandWindow::Close() // window. // Return Value: // - -void IslandWindow::SetCreateCallback(std::function pfn) noexcept +void IslandWindow::SetCreateCallback(std::function pfn) noexcept { _pfnCreateCallback = pfn; } @@ -154,23 +188,19 @@ void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexce rc.right = rc.left + pcs->cx; rc.bottom = rc.top + pcs->cy; - auto launchMode = LaunchMode::DefaultMode; if (_pfnCreateCallback) { - _pfnCreateCallback(_window.get(), rc, launchMode); + _pfnCreateCallback(_window.get(), rc); } - auto nCmdShow = SW_SHOW; - if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) - { - nCmdShow = SW_MAXIMIZE; - } - - ShowWindow(_window.get(), nCmdShow); + // GH#11561: DO NOT call ShowWindow here. The AppHost will call ShowWindow + // once the app has completed its initialization. UpdateWindow(_window.get()); UpdateWindowIconForActiveMetrics(_window.get()); + + _currentSystemThemeIsDark = Theme::IsSystemInDarkTheme(); } // Method Description: @@ -211,10 +241,10 @@ LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) const auto nonClientSize = GetTotalNonClientExclusiveSize(dpix); - auto clientWidth = winRect->right - winRect->left - nonClientSize.cx; + auto clientWidth = winRect->right - winRect->left - nonClientSize.width; clientWidth = std::max(minWidthScaled, clientWidth); - auto clientHeight = winRect->bottom - winRect->top - nonClientSize.cy; + auto clientHeight = winRect->bottom - winRect->top - nonClientSize.height; // If we're the quake window, prevent resizing on all sides except the // bottom. This also applies to resizing with the Alt+Space menu @@ -251,12 +281,12 @@ LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) case WMSZ_LEFT: case WMSZ_TOPLEFT: case WMSZ_BOTTOMLEFT: - winRect->left = winRect->right - (clientWidth + nonClientSize.cx); + winRect->left = winRect->right - (clientWidth + nonClientSize.width); break; case WMSZ_RIGHT: case WMSZ_TOPRIGHT: case WMSZ_BOTTOMRIGHT: - winRect->right = winRect->left + (clientWidth + nonClientSize.cx); + winRect->right = winRect->left + (clientWidth + nonClientSize.width); break; } @@ -266,12 +296,12 @@ LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) case WMSZ_BOTTOM: case WMSZ_BOTTOMLEFT: case WMSZ_BOTTOMRIGHT: - winRect->bottom = winRect->top + (clientHeight + nonClientSize.cy); + winRect->bottom = winRect->top + (clientHeight + nonClientSize.height); break; case WMSZ_TOP: case WMSZ_TOPLEFT: case WMSZ_TOPRIGHT: - winRect->top = winRect->bottom - (clientHeight + nonClientSize.cy); + winRect->top = winRect->bottom - (clientHeight + nonClientSize.height); break; } @@ -304,7 +334,30 @@ LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam) return false; } -void IslandWindow::Initialize() +// return true if this was a "cold" initialize, that didn't start XAML before. +bool IslandWindow::Initialize() +{ + if (!_source) + { + _coldInitialize(); + return true; + } + else + { + // This was a "warm" initialize - we've already got an HWND, but we need + // to move it to the new correct place, new size, and reset any leftover + // runtime state. + _warmInitialize(); + return false; + } +} + +// Method Description: +// - Start this window for the first time. This will instantiate our XAML +// island, set up our root grid, and initialize some other members that only +// need to be initialized once. +// - This should only be called once. +void IslandWindow::_coldInitialize() { _source = DesktopWindowXamlSource{}; @@ -314,6 +367,11 @@ void IslandWindow::Initialize() // stash the child interop handle so we can resize it when the main hwnd is resized interop->get_WindowHandle(&_interopWindowHandle); + // Immediately hide our XAML island hwnd. On earlier versions of Windows, + // this HWND could sometimes appear as an actual window in the taskbar + // without this! + ShowWindow(_interopWindowHandle, SW_HIDE); + _rootGrid = winrt::Windows::UI::Xaml::Controls::Grid(); _source.Content(_rootGrid); @@ -332,9 +390,32 @@ void IslandWindow::Initialize() // We don't really care if this failed or not. TerminalTrySetTransparentBackground(true); } +void IslandWindow::_warmInitialize() +{ + // re-add the pointer to us to our HWND's user data, so that we can start + // getting window proc callbacks again. + _setupUserData(); + + // Manually ask how we want to be created. + if (_pfnCreateCallback) + { + til::rect rc{ GetWindowRect() }; + _pfnCreateCallback(_window.get(), rc); + } + + // Don't call IslandWindow::OnSize - that will set the Width/Height members + // of the _rootGrid. However, NonClientIslandWindow doesn't use those! If you set them, here, + // the contents of the window will never resize. + UpdateWindow(_window.get()); + ForceResize(); +} void IslandWindow::OnSize(const UINT width, const UINT height) { + // NOTE: This _isn't_ called by NonClientIslandWindow::OnSize. The + // NonClientIslandWindow has very different logic for positioning the + // DesktopWindowXamlSource inside its HWND. + // update the interop window size SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE); @@ -389,8 +470,8 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam const auto nonClientSizeScaled = GetTotalNonClientExclusiveSize(dpix); auto lpMinMaxInfo = reinterpret_cast(lParam); - lpMinMaxInfo->ptMinTrackSize.x = _calculateTotalSize(true, minimumWidth * dpix / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.cx); - lpMinMaxInfo->ptMinTrackSize.y = _calculateTotalSize(false, minimumHeight * dpiy / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.cy); + lpMinMaxInfo->ptMinTrackSize.x = _calculateTotalSize(true, minimumWidth * dpix / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.width); + lpMinMaxInfo->ptMinTrackSize.y = _calculateTotalSize(false, minimumHeight * dpiy / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.height); } // Method Description: @@ -404,18 +485,19 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam // - The total dimension long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize) { - return gsl::narrow_cast(_pfnSnapDimensionCallback(isWidth, gsl::narrow_cast(clientSize)) + nonClientSize); + if (_pfnSnapDimensionCallback) + { + return gsl::narrow_cast(_pfnSnapDimensionCallback(isWidth, gsl::narrow_cast(clientSize)) + nonClientSize); + } + // We might have been called in WM_CREATE, before we've initialized XAML or + // our page. That's okay. + return clientSize + nonClientSize; } [[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { - case WM_HOTKEY: - { - _HotkeyPressedHandlers(static_cast(wparam)); - return 0; - } case WM_GETMINMAXINFO: { _OnGetMinMaxInfo(wparam, lparam); @@ -499,6 +581,24 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize return 0; } } + + // BODGY This is a fix for the upstream: + // + // https://github.com/microsoft/microsoft-ui-xaml/issues/3577 + // + // ContentDialogs don't resize themselves when the XAML island resizes. + // However, if we manually resize our CoreWindow, that'll actually + // trigger a resize of the ContentDialog. + if (const auto& coreWindow{ winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread() }) + { + if (const auto& interop{ coreWindow.as() }) + { + HWND coreWindowInterop; + interop->get_WindowHandle(&coreWindowInterop); + PostMessage(coreWindowInterop, message, wparam, lparam); + } + } + break; } case WM_MOVING: @@ -613,30 +713,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize } break; } - case CM_NOTIFY_FROM_NOTIFICATION_AREA: - { - switch (LOWORD(lparam)) - { - case NIN_SELECT: - case NIN_KEYSELECT: - { - _NotifyNotificationIconPressedHandlers(); - return 0; - } - case WM_CONTEXTMENU: - { - const til::point eventPoint{ GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) }; - _NotifyShowNotificationIconContextMenuHandlers(eventPoint); - return 0; - } - } - break; - } - case WM_MENUCOMMAND: - { - _NotifyNotificationIconMenuItemSelectedHandlers((HMENU)lparam, (UINT)wparam); - return 0; - } case WM_SYSCOMMAND: { // the low 4 bits contain additional information (that we don't care about) @@ -658,6 +734,33 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize } break; } + case WM_SETTINGCHANGE: + { + // Currently, we only support checking when the OS theme changes. In + // that case, wParam is 0. Re-evaluate when we decide to reload env vars + // (GH#1125) + if (wparam == 0 && lparam != 0) + { + const std::wstring param{ (wchar_t*)lparam }; + // ImmersiveColorSet seems to be the notification that the OS theme + // changed. If that happens, let the app know, so it can hot-reload + // themes, color schemes that might depend on the OS theme + if (param == L"ImmersiveColorSet") + { + // GH#15732: Don't update the settings, unless the theme + // _actually_ changed. ImmersiveColorSet gets sent more often + // than just on a theme change. It notably gets sent when the PC + // is locked, or the UAC prompt opens. + auto isCurrentlyDark = Theme::IsSystemInDarkTheme(); + if (isCurrentlyDark != _currentSystemThemeIsDark) + { + _currentSystemThemeIsDark = isCurrentlyDark; + _UpdateSettingsRequestedHandlers(); + } + } + } + break; + } case WM_ENDSESSION: { // For WM_QUERYENDSESSION and WM_ENDSESSION, refer to: @@ -695,16 +798,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize _AutomaticShutdownRequestedHandlers(); return true; } - default: - // We'll want to receive this message when explorer.exe restarts - // so that we can re-add our icon to the notification area. - // This unfortunately isn't a switch case because we register the - // message at runtime. - if (message == WM_TASKBARCREATED) - { - _NotifyReAddNotificationIconHandlers(); - return 0; - } } // TODO: handle messages here... @@ -793,7 +886,7 @@ void IslandWindow::OnAppInitialized() { // Do a quick resize to force the island to paint const auto size = GetPhysicalSize(); - OnSize(size.cx, size.cy); + OnSize(size.width, size.height); } // Method Description: @@ -1221,65 +1314,6 @@ void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled) } } -// Method Description: -// - Call UnregisterHotKey once for each previously registered hotkey. -// Return Value: -// - -void IslandWindow::UnregisterHotKey(const int index) noexcept -{ - TraceLoggingWrite( - g_hWindowsTerminalProvider, - "UnregisterHotKey", - TraceLoggingDescription("Emitted when clearing previously set hotkeys"), - TraceLoggingInt64(index, "index", "the index of the hotkey to remove"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index)); -} - -// Method Description: -// - Call RegisterHotKey to attempt to register that keybinding as a global hotkey. -// - When these keys are pressed, we'll get a WM_HOTKEY message with the payload -// containing the index we registered here. -// - Call UnregisterHotKey() before registering your hotkeys. -// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey#remarks -// Arguments: -// - hotkey: The key-combination to register. -// Return Value: -// - -bool IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept -{ - const auto vkey = hotkey.Vkey(); - auto hotkeyFlags = MOD_NOREPEAT; - { - const auto modifiers = hotkey.Modifiers(); - WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)); - WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)); - WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)); - WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)); - } - - // TODO GH#8888: We should display a warning of some kind if this fails. - // This can fail if something else already bound this hotkey. - const auto result = ::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey); - - TraceLoggingWrite(g_hWindowsTerminalProvider, - "RegisterHotKey", - TraceLoggingDescription("Emitted when setting hotkeys"), - TraceLoggingInt64(index, "index", "the index of the hotkey to add"), - TraceLoggingUInt64(vkey, "vkey", "the key"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_WIN), "win", "is WIN in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_ALT), "alt", "is ALT in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_CONTROL), "control", "is CONTROL in the modifiers"), - TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_SHIFT), "shift", "is SHIFT in the modifiers"), - TraceLoggingBool(result, "succeeded", "true if we succeeded"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - - return result; -} - // Method Description: // - Summon the window, or possibly dismiss it. If toggleVisibility is true, // then we'll dismiss (minimize) the window if it's currently active. @@ -1509,7 +1543,7 @@ void IslandWindow::_globalActivateWindow(const uint32_t dropdownDuration, else { // Try first to send a message to the current foreground window. If it's not responding, it may - // be waiting on us to finsh launching. Passing SMTO_NOTIMEOUTIFNOTHUNG means that we get the same + // be waiting on us to finish launching. Passing SMTO_NOTIMEOUTIFNOTHUNG means that we get the same // behavior as before--that is, waiting for the message loop--but we've done an early return if // it turns out that it was hung. // SendMessageTimeoutW returns nonzero if it succeeds. @@ -1882,3 +1916,48 @@ void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel) } _systemMenuItems.erase(it->first); } + +void IslandWindow::UseDarkTheme(const bool v) +{ + const BOOL attribute = v ? TRUE : FALSE; + std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_USE_IMMERSIVE_DARK_MODE, &attribute, sizeof(attribute)); +} + +void IslandWindow::UseMica(const bool newValue, const double /*titlebarOpacity*/) +{ + // This block of code enables Mica for our window. By all accounts, this + // version of the code will only work on Windows 11, SV2. There's a slightly + // different API surface for enabling Mica on Windows 11 22000.0. + // + // This API was only publicly supported as of Windows 11 SV2, 22621. Before + // that version, this API will just return an error and do nothing silently. + + const int attribute = newValue ? DWMSBT_MAINWINDOW : DWMSBT_NONE; + std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_SYSTEMBACKDROP_TYPE, &attribute, sizeof(attribute)); +} + +// Method Description: +// - This method is called when the window receives the WM_NCCREATE message. +// Return Value: +// - The value returned from the window proc. +[[nodiscard]] LRESULT IslandWindow::OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept +{ + const auto ret = BaseWindow::OnNcCreate(wParam, lParam); + if (!ret) + { + return FALSE; + } + + // This is a hack to make the window borders dark instead of light. + // It must be done before WM_NCPAINT so that the borders are rendered with + // the correct theme. + // For more information, see GH#6620. + // + // Theoretically, we don't need this anymore, since _updateTheme will update + // the darkness of our window. However, we're keeping this call to prevent + // the window from appearing as a white rectangle for a frame before we load + // the rest of the settings. + UseDarkTheme(true); + + return TRUE; +} diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 396bfd04b8..eee013c529 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -3,7 +3,7 @@ #include "pch.h" #include "BaseWindow.h" -#include +#include void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept; @@ -21,11 +21,17 @@ public: virtual ~IslandWindow() override; virtual void MakeWindow() noexcept; - void Close(); + virtual void Close(); + + virtual void Refrigerate() noexcept; + virtual void OnSize(const UINT width, const UINT height); HWND GetInteropHandle() const; [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + + [[nodiscard]] LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept override; + void OnResize(const UINT width, const UINT height) override; void OnMinimize() override; void OnRestore() override; @@ -35,9 +41,10 @@ public: virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; - virtual void Initialize(); + virtual bool Initialize(); + + void SetCreateCallback(std::function pfn) noexcept; - void SetCreateCallback(std::function pfn) noexcept; void SetSnapDimensionCallback(std::function pfn) noexcept; void FocusModeChanged(const bool focusMode); @@ -48,9 +55,6 @@ public: void FlashTaskbar(); void SetTaskbarProgress(const size_t state, const size_t progress); - void UnregisterHotKey(const int index) noexcept; - bool RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; - winrt::fire_and_forget SummonWindow(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); bool IsQuakeWindow() const noexcept; @@ -65,11 +69,13 @@ public: void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); void RemoveFromSystemMenu(const winrt::hstring& itemLabel); + void UseDarkTheme(const bool v); + virtual void UseMica(const bool newValue, const double titlebarOpacity); + WINRT_CALLBACK(DragRegionClicked, winrt::delegate<>); WINRT_CALLBACK(WindowCloseButtonClicked, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); WINRT_CALLBACK(WindowActivated, winrt::delegate); - WINRT_CALLBACK(HotkeyPressed, winrt::delegate); WINRT_CALLBACK(NotifyNotificationIconPressed, winrt::delegate); WINRT_CALLBACK(NotifyWindowHidden, winrt::delegate); WINRT_CALLBACK(NotifyShowNotificationIconContextMenu, winrt::delegate); @@ -81,22 +87,23 @@ public: WINRT_CALLBACK(WindowMoved, winrt::delegate); WINRT_CALLBACK(WindowVisibilityChanged, winrt::delegate); + WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); protected: void ForceResize() { // Do a quick resize to force the island to paint const auto size = GetPhysicalSize(); - OnSize(size.cx, size.cy); + OnSize(size.width, size.height); } HWND _interopWindowHandle; - winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; - winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor wil::com_ptr _taskbar; - std::function _pfnCreateCallback; + std::function _pfnCreateCallback; std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; @@ -110,6 +117,10 @@ protected: RECT _rcWindowBeforeFullscreen{}; RECT _rcWorkBeforeFullscreen{}; UINT _dpiBeforeFullscreen{ 96 }; + bool _currentSystemThemeIsDark{ true }; + + void _coldInitialize(); + void _warmInitialize(); virtual void _SetIsBorderless(const bool borderlessEnabled); virtual void _SetIsFullscreen(const bool fullscreenEnabled); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 4952909a75..ee538bfa71 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -26,7 +26,13 @@ NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme) { } -NonClientIslandWindow::~NonClientIslandWindow() = default; +void NonClientIslandWindow::Close() +{ + // Avoid further callbacks into XAML/WinUI-land after we've Close()d the DesktopWindowXamlSource + // inside `IslandWindow::Close()`. XAML thanks us for doing that by not crashing. Thank you XAML. + SetWindowLongPtr(_dragBarWindow.get(), GWLP_USERDATA, 0); + IslandWindow::Close(); +} static constexpr const wchar_t* dragBarClassName{ L"DRAG_BAR_WINDOW_CLASS" }; @@ -51,6 +57,12 @@ static constexpr const wchar_t* dragBarClassName{ L"DRAG_BAR_WINDOW_CLASS" }; void NonClientIslandWindow::MakeWindow() noexcept { + if (_window) + { + // no-op if we already have a window. + return; + } + IslandWindow::MakeWindow(); static auto dragBarWindowClass{ []() { @@ -329,9 +341,17 @@ void NonClientIslandWindow::OnAppInitialized() IslandWindow::OnAppInitialized(); } -void NonClientIslandWindow::Initialize() +void NonClientIslandWindow::Refrigerate() noexcept { - IslandWindow::Initialize(); + IslandWindow::Refrigerate(); + + // Revoke all our XAML callbacks. + _callbacks = {}; +} + +bool NonClientIslandWindow::Initialize() +{ + const bool coldInit = IslandWindow::Initialize(); _UpdateFrameMargins(); @@ -343,6 +363,7 @@ void NonClientIslandWindow::Initialize() Controls::RowDefinition contentRow{}; titlebarRow.Height(GridLengthHelper::Auto()); + _rootGrid.RowDefinitions().Clear(); _rootGrid.RowDefinitions().Append(titlebarRow); _rootGrid.RowDefinitions().Append(contentRow); @@ -350,17 +371,25 @@ void NonClientIslandWindow::Initialize() _titlebar = winrt::TerminalApp::TitlebarControl{ reinterpret_cast(GetHandle()) }; _dragBar = _titlebar.DragBar(); - _dragBar.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); - _rootGrid.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); + _callbacks.dragBarSizeChanged = _dragBar.SizeChanged(winrt::auto_revoke, { this, &NonClientIslandWindow::_OnDragBarSizeChanged }); + _callbacks.rootGridSizeChanged = _rootGrid.SizeChanged(winrt::auto_revoke, { this, &NonClientIslandWindow::_OnDragBarSizeChanged }); _rootGrid.Children().Append(_titlebar); Controls::Grid::SetRow(_titlebar, 0); // GH#3440 - When the titlebar is loaded (officially added to our UI tree), - // then make sure to update it's visual state to reflect if we're in the + // then make sure to update its visual state to reflect if we're in the // maximized state on launch. - _titlebar.Loaded([this](auto&&, auto&&) { _OnMaximizeChange(); }); + _callbacks.titlebarLoaded = _titlebar.Loaded(winrt::auto_revoke, [this](auto&&, auto&&) { _OnMaximizeChange(); }); + + // LOAD BEARING: call _ResizeDragBarWindow to update the position of our + // XAML island to reflect our current bounds. In the case of a "warm init" + // (i.e. re-using an existing window), we need to manually update the + // island's position to fill the new window bounds. + _ResizeDragBarWindow(); + + return coldInit; } // Method Description: @@ -398,7 +427,7 @@ void NonClientIslandWindow::SetTitlebarContent(winrt::Windows::UI::Xaml::UIEleme // GH#4288 - add a SizeChanged handler to this content. It's possible that // this element's size will change after the dragbar's. When that happens, // the drag bar won't send another SizeChanged event, because the dragbar's - // _size_ didn't change, only it's position. + // _size_ didn't change, only its position. const auto fwe = content.try_as(); if (fwe) { @@ -442,11 +471,21 @@ til::rect NonClientIslandWindow::_GetDragAreaRect() const noexcept static_cast(_rootGrid.ActualWidth()), static_cast(_dragBar.ActualHeight()) }; + const auto clientDragBarRect = transform.TransformBounds(logicalDragBarRect); + + // Make sure to trim the right side of the rectangle, so that it doesn't + // hang off the right side of the root window. This normally wouldn't + // matter, but UIA will still think its bounds can extend past the right + // of the parent HWND. + // + // x here is the width of the tabs. + const auto x = gsl::narrow_cast(clientDragBarRect.X * scale); + return { - gsl::narrow_cast(clientDragBarRect.X * scale), + x, gsl::narrow_cast(clientDragBarRect.Y * scale), - gsl::narrow_cast((clientDragBarRect.Width + clientDragBarRect.X) * scale), + gsl::narrow_cast((clientDragBarRect.Width + clientDragBarRect.X) * scale) - x, gsl::narrow_cast((clientDragBarRect.Height + clientDragBarRect.Y) * scale), }; } @@ -542,8 +581,8 @@ void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const winrt::check_bool(SetWindowPos(_interopWindowHandle, HWND_BOTTOM, - newIslandPos.X, - newIslandPos.Y, + newIslandPos.x, + newIslandPos.y, windowWidth, windowHeight - topBorderHeight, SWP_SHOWWINDOW | SWP_NOACTIVATE)); @@ -879,7 +918,26 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept // at the top) in the WM_PAINT handler. This eliminates the transparency // bug and it's what a lot of Win32 apps that customize the title bar do // so it should work fine. - margins.cyTopHeight = -frame.top; + // + // Notes #3 (circa late 2022): We want to make some changes here to + // support Mica. This introduces some complications. + // - If we leave the titlebar visible AT ALL, then a transparent + // titlebar (theme.tabRow.background:#ff00ff00 for example) will allow + // the DWM titlebar to be visible, underneath our content. EVEN MORE + // SO: Mica + "show accent color on title bars" will _always_ show the + // accent-colored strip of the titlebar, even on top of the Mica. + // - It _seems_ like we can just set this to 0, and have it work. You'd + // be wrong. On Windows 10, setting this to 0 will cause the topmost + // pixel of our window to be just a little darker than the rest of the + // frame. So ONLY set this to 0 when the user has explicitly asked for + // Mica. Though it won't do anything on Windows 10, they should be + // able to opt back out of having that weird dark pixel. + // - This is LOAD-BEARING. By having the titlebar a totally empty rect, + // DWM will know that we don't have the traditional titlebar, and will + // use NCHITTEST to determine where to place the Snap Flyout. The drag + // rect will handle that. + + margins.cyTopHeight = (_useMica || _titlebarOpacity < 1.0) ? 0 : -frame.top; } // Extend the frame into the client area. microsoft/terminal#2735 - Just log @@ -1015,27 +1073,6 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept return 0; } -// Method Description: -// - This method is called when the window receives the WM_NCCREATE message. -// Return Value: -// - The value returned from the window proc. -[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept -{ - const auto ret = IslandWindow::_OnNcCreate(wParam, lParam); - if (!ret) - { - return FALSE; - } - - // This is a hack to make the window borders dark instead of light. - // It must be done before WM_NCPAINT so that the borders are rendered with - // the correct theme. - // For more information, see GH#6620. - LOG_IF_FAILED(TerminalTrySetDarkTheme(_window.get(), true)); - - return TRUE; -} - // Method Description: // - Called when the app wants to change its theme. We'll update the frame // theme to match the new theme. @@ -1132,3 +1169,16 @@ void NonClientIslandWindow::SetTitlebarBackground(winrt::Windows::UI::Xaml::Medi { _titlebar.Background(brush); } + +void NonClientIslandWindow::UseMica(const bool newValue, const double titlebarOpacity) +{ + // Stash internally if we're using Mica. If we aren't, we don't want to + // totally blow away our titlebar with DwmExtendFrameIntoClientArea, + // especially on Windows 10 + _useMica = newValue; + _titlebarOpacity = titlebarOpacity; + + IslandWindow::UseMica(newValue, titlebarOpacity); + + _UpdateFrameMargins(); +} diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 20b9bde43e..d173ad68c2 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -30,8 +30,10 @@ public: static constexpr const int topBorderVisibleHeight = 1; NonClientIslandWindow(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) noexcept; - virtual ~NonClientIslandWindow() override; + void Refrigerate() noexcept override; + + virtual void Close() override; void MakeWindow() noexcept override; virtual void OnSize(const UINT width, const UINT height) override; @@ -40,7 +42,7 @@ public: virtual til::rect GetNonClientFrame(UINT dpi) const noexcept override; virtual til::size GetTotalNonClientExclusiveSize(UINT dpi) const noexcept override; - void Initialize() override; + bool Initialize() override; void OnAppInitialized() override; void SetContent(winrt::Windows::UI::Xaml::UIElement content) override; @@ -49,6 +51,8 @@ public: void SetTitlebarBackground(winrt::Windows::UI::Xaml::Media::Brush brush); + virtual void UseMica(const bool newValue, const double titlebarOpacity) override; + private: std::optional _oldIslandPos; @@ -63,6 +67,9 @@ private: winrt::Windows::UI::Xaml::ElementTheme _theme; + bool _useMica{ false }; + double _titlebarOpacity{ 1.0 }; + bool _isMaximized; bool _trackingMouse{ false }; @@ -76,7 +83,6 @@ private: int _GetTopBorderHeight() const noexcept; LRESULT _dragBarNcHitTest(const til::point pointer); - [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept override; [[nodiscard]] LRESULT _OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept; [[nodiscard]] LRESULT _OnNcHitTest(POINT ptMouse) const noexcept; [[nodiscard]] LRESULT _OnPaint() noexcept; @@ -91,4 +97,11 @@ private: void _UpdateFrameMargins() const noexcept; void _UpdateMaximizedState(); void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight); + + struct Revokers + { + winrt::Windows::UI::Xaml::Controls::Border::SizeChanged_revoker dragBarSizeChanged; + winrt::Windows::UI::Xaml::Controls::Grid::SizeChanged_revoker rootGridSizeChanged; + winrt::TerminalApp::TitlebarControl::Loaded_revoker titlebarLoaded; + } _callbacks{}; }; diff --git a/src/cascadia/WindowsTerminal/NotificationIcon.h b/src/cascadia/WindowsTerminal/NotificationIcon.h index cc71a88b41..3277a1e845 100644 --- a/src/cascadia/WindowsTerminal/NotificationIcon.h +++ b/src/cascadia/WindowsTerminal/NotificationIcon.h @@ -3,6 +3,7 @@ #include "pch.h" +#pragma once // This enumerates all the possible actions // that our notification icon context menu could do. enum class NotificationIconMenuItemAction diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp new file mode 100644 index 0000000000..47a11bba38 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -0,0 +1,888 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "WindowEmperor.h" + +#include "../inc/WindowingBehavior.h" + +#include "../../types/inc/utils.hpp" + +#include "../WinRTUtils/inc/WtExeUtils.h" + +#include "resource.h" +#include "NotificationIcon.h" +#include + +using namespace winrt; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Windows::Foundation; +using namespace ::Microsoft::Console; +using namespace std::chrono_literals; +using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; + +#define TERMINAL_MESSAGE_CLASS_NAME L"TERMINAL_MESSAGE_CLASS" +extern "C" IMAGE_DOS_HEADER __ImageBase; + +WindowEmperor::WindowEmperor() noexcept : + _app{} +{ + _manager.FindTargetWindowRequested([this](const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& findWindowArgs) { + { + const auto targetWindow = _app.Logic().FindTargetWindow(findWindowArgs.Args().Commandline()); + findWindowArgs.ResultTargetWindow(targetWindow.WindowId()); + findWindowArgs.ResultTargetWindowName(targetWindow.WindowName()); + } + }); + + _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + + // BODGY + // + // There's a mysterious crash in XAML on Windows 10 if you just let the App + // get dtor'd. By all accounts, it doesn't make sense. To mitigate this, we + // need to intentionally leak a reference to our App. Crazily, if you just + // let the app get cleaned up with the rest of the process when the process + // exits, then it doesn't crash. But if you let it get explicitly dtor'd, it + // absolutely will crash on exit. + // + // GH#15410 has more details. + + auto a{ _app }; + ::winrt::detach_abi(a); +} + +WindowEmperor::~WindowEmperor() +{ + _app.Close(); + _app = nullptr; +} + +void _buildArgsFromCommandline(std::vector& args) +{ + if (auto commandline{ GetCommandLineW() }) + { + auto argc = 0; + + // Get the argv, and turn them into a hstring array to pass to the app. + wil::unique_any argv{ CommandLineToArgvW(commandline, &argc) }; + if (argv) + { + for (auto& elem : wil::make_range(argv.get(), argc)) + { + args.emplace_back(elem); + } + } + } + if (args.empty()) + { + args.emplace_back(L"wt.exe"); + } +} + +bool WindowEmperor::HandleCommandlineArgs() +{ + std::vector args; + _buildArgsFromCommandline(args); + const auto cwd{ wil::GetCurrentDirectoryW() }; + + { + // ALWAYS change the _real_ CWD of the Terminal to system32, so that we + // don't lock the directory we were spawned in. + std::wstring system32{}; + if (SUCCEEDED_LOG(wil::GetSystemDirectoryW(system32))) + { + LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectoryW(system32.c_str())); + } + } + + // Get the requested initial state of the window from our startup info. For + // something like `start /min`, this will set the wShowWindow member to + // SW_SHOWMINIMIZED. We'll need to make sure is bubbled all the way through, + // so we can open a new window with the same state. + STARTUPINFOW si; + GetStartupInfoW(&si); + const uint32_t showWindow = WI_IsFlagSet(si.dwFlags, STARTF_USESHOWWINDOW) ? si.wShowWindow : SW_SHOW; + + const auto currentEnv{ til::env::from_current_environment() }; + + Remoting::CommandlineArgs eventArgs{ { args }, { cwd }, showWindow, winrt::hstring{ currentEnv.to_string() } }; + + const auto isolatedMode{ _app.Logic().IsolatedMode() }; + + const auto result = _manager.ProposeCommandline(eventArgs, isolatedMode); + + const bool makeWindow = result.ShouldCreateWindow(); + if (makeWindow) + { + _createNewWindowThread(Remoting::WindowRequestedArgs{ result, eventArgs }); + + _becomeMonarch(); + } + else + { + const auto res = _app.Logic().GetParseCommandlineMessage(eventArgs.Commandline()); + if (!res.Message.empty()) + { + AppHost::s_DisplayMessageBox(res); + std::quick_exit(res.ExitCode); + } + } + + return makeWindow; +} + +void WindowEmperor::WaitForWindows() +{ + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } +} + +void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs& args) +{ + Remoting::Peasant peasant{ _manager.CreatePeasant(args) }; + std::shared_ptr window{ nullptr }; + + // FIRST: Attempt to reheat an existing window that we refrigerated for + // later. If we have an existing unused window, then we don't need to create + // a new WindowThread & HWND for this request. + { // Add a scope to minimize lock duration. + auto fridge{ _oldThreads.lock() }; + if (!fridge->empty()) + { + // Look at that, a refrigerated thread ready to be used. Let's use that! + window = std::move(fridge->back()); + fridge->pop_back(); + } + } + + // Did we find one? + if (window) + { + // Cool! Let's increment the number of active windows, and re-heat it. + _windowThreadInstances.fetch_add(1, std::memory_order_relaxed); + + window->Microwave(args, peasant); + // This will unblock the event we're waiting on in KeepWarm, and the + // window thread (started below) will continue through it's loop + return; + } + + // At this point, there weren't any pending refrigerated threads we could + // just use. That's fine. Let's just go create a new one. + + window = std::make_shared(_app.Logic(), args, _manager, peasant); + + std::weak_ptr weakThis{ weak_from_this() }; + + // Increment our count of window instances _now_, immediately. We're + // starting a window now, we shouldn't exit (due to having 0 windows) till + // this window has a chance to actually start. + // * We can't just put the window immediately into _windows right now, + // because there are multiple async places where we iterate over all + // _windows assuming that they have initialized and we can call methods + // that might hit the TerminalPage. + // * If we don't somehow track this window now, before it has been actually + // started, there's a possible race. As an example, it would be possible + // to drag a tab out of the single window, which would create a new + // window, but have the original window exit before the new window has + // started, causing the app to exit. + // Hence: increment the number of total windows now. + _windowThreadInstances.fetch_add(1, std::memory_order_relaxed); + + std::thread t([weakThis, window]() { + try + { + window->CreateHost(); + + if (auto self{ weakThis.lock() }) + { + self->_windowStartedHandlerPostXAML(window); + } + while (window->KeepWarm()) + { + // Now that the window is ready to go, we can add it to our list of windows, + // because we know it will be well behaved. + // + // Be sure to only modify the list of windows under lock. + + if (auto self{ weakThis.lock() }) + { + auto lockedWindows{ self->_windows.lock() }; + lockedWindows->push_back(window); + } + auto removeWindow = wil::scope_exit([&]() { + if (auto self{ weakThis.lock() }) + { + self->_removeWindow(window->PeasantID()); + } + }); + + auto decrementWindowCount = wil::scope_exit([&]() { + if (auto self{ weakThis.lock() }) + { + self->_decrementWindowCount(); + } + }); + + window->RunMessagePump(); + + // Manually trigger the cleanup callback. This will ensure that we + // remove the window from our list of windows, before we release the + // AppHost (and subsequently, the host's Logic() member that we use + // elsewhere). + removeWindow.reset(); + + // On Windows 11, we DONT want to refrigerate the window. There, + // we can just close it like normal. Break out of the loop, so + // we don't try to put this window in the fridge. + if (Utils::IsWindows11()) + { + decrementWindowCount.reset(); + break; + } + else + { + window->Refrigerate(); + decrementWindowCount.reset(); + + if (auto self{ weakThis.lock() }) + { + auto fridge{ self->_oldThreads.lock() }; + fridge->push_back(window); + } + } + } + + // Now that we no longer care about this thread's window, let it + // release it's app host and flush the rest of the XAML queue. + window->RundownForExit(); + } + CATCH_LOG() + }); + LOG_IF_FAILED(SetThreadDescription(t.native_handle(), L"Window Thread")); + + t.detach(); +} + +// Handler for a WindowThread's Started event, which it raises once the window +// thread starts and XAML is ready to go on that thread. Set up some callbacks +// now that we know this window is set up and ready to go. +// Q: Why isn't adding these callbacks just a part of _createNewWindowThread? +// A: Until the thread actually starts, the AppHost (and its Logic()) haven't +// been ctor'd or initialized, so trying to add callbacks immediately will A/V +void WindowEmperor::_windowStartedHandlerPostXAML(const std::shared_ptr& sender) +{ + // Add a callback to the window's logic to let us know when the window's + // quake mode state changes. We'll use this to check if we need to add + // or remove the notification icon. + sender->Logic().IsQuakeWindowChanged({ this, &WindowEmperor::_windowIsQuakeWindowChanged }); + sender->UpdateSettingsRequested({ this, &WindowEmperor::_windowRequestUpdateSettings }); + + // DON'T Summon the window to the foreground, since we might not _currently_ be in + // the foreground, but we should act like the new window is. + // + // If you summon here, the resulting code will call ShowWindow(SW_SHOW) on + // the Terminal window, making it visible BEFORE the XAML island is actually + // ready to be drawn. We want to wait till the app's Initialized event + // before we make the window visible. +} + +void WindowEmperor::_removeWindow(uint64_t senderID) +{ + auto lockedWindows{ _windows.lock() }; + + // find the window in _windows who's peasant's Id matches the peasant's Id + // and remove it + std::erase_if(*lockedWindows, [&](const auto& w) { + return w->PeasantID() == senderID; + }); +} + +void WindowEmperor::_decrementWindowCount() +{ + // When we run out of windows, exit our process if and only if: + // * We're not allowed to run headless OR + // * we've explicitly been told to "quit", which should fully exit the Terminal. + const bool quitWhenLastWindowExits{ !_app.Logic().AllowHeadless() }; + const bool noMoreWindows{ _windowThreadInstances.fetch_sub(1, std::memory_order_relaxed) == 1 }; + if (noMoreWindows && + (_quitting || quitWhenLastWindowExits)) + { + _close(); + } +} + +// Method Description: +// - Set up all sorts of handlers now that we've determined that we're a process +// that will end up hosting the windows. These include: +// - Setting up a message window to handle hotkeys and notification icon +// invokes. +// - Setting up the global hotkeys. +// - Setting up the notification icon. +// - Setting up callbacks for when the settings change. +// - Setting up callbacks for when the number of windows changes. +// - Setting up the throttled func for layout persistence. Arguments: +// - +void WindowEmperor::_becomeMonarch() +{ + // Add a callback to the window manager so that when the Monarch wants a new + // window made, they come to us + _manager.RequestNewWindow([this](auto&&, const Remoting::WindowRequestedArgs& args) { + _createNewWindowThread(args); + }); + + _createMessageWindow(); + + _setupGlobalHotkeys(); + + // When the settings change, we'll want to update our global hotkeys and our + // notification icon based on the new settings. + _app.Logic().SettingsChanged([this](auto&&, const TerminalApp::SettingsLoadEventArgs& args) { + if (SUCCEEDED(args.Result())) + { + _setupGlobalHotkeys(); + _checkWindowsForNotificationIcon(); + } + }); + + // On startup, immediately check if we need to show the notification icon. + _checkWindowsForNotificationIcon(); + + // Set the number of open windows (so we know if we are the last window) + // and subscribe for updates if there are any changes to that number. + + _revokers.WindowCreated = _manager.WindowCreated(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); + _revokers.WindowClosed = _manager.WindowClosed(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); + + // If the monarch receives a QuitAll event it will signal this event to be + // ran before each peasant is closed. + _revokers.QuitAllRequested = _manager.QuitAllRequested(winrt::auto_revoke, { this, &WindowEmperor::_quitAllRequested }); + + // The monarch should be monitoring if it should save the window layout. + // We want at least some delay to prevent the first save from overwriting + _getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _saveWindowLayoutsRepeat(); })); + _getWindowLayoutThrottler.value()(); +} + +// sender and args are always nullptr +void WindowEmperor::_numberOfWindowsChanged(const winrt::Windows::Foundation::IInspectable&, + const winrt::Windows::Foundation::IInspectable&) +{ + if (_getWindowLayoutThrottler) + { + _getWindowLayoutThrottler.value()(); + } + + // If we closed out the quake window, and don't otherwise need the tray + // icon, let's get rid of it. + _checkWindowsForNotificationIcon(); +} + +// Raised from our windowManager (on behalf of the monarch). We respond by +// giving the monarch an async function that the manager should wait on before +// completing the quit. +void WindowEmperor::_quitAllRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args) +{ + _quitting = true; + + // Make sure that the current timer is destroyed so that it doesn't attempt + // to run while we are in the middle of quitting. + if (_getWindowLayoutThrottler.has_value()) + { + _getWindowLayoutThrottler.reset(); + } + + // Tell the monarch to wait for the window layouts to save before + // everyone quits. + args.BeforeQuitAllAction(_saveWindowLayouts()); +} + +#pragma region LayoutPersistence + +winrt::Windows::Foundation::IAsyncAction WindowEmperor::_saveWindowLayouts() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + if (_app.Logic().ShouldUsePersistedLayout()) + { + try + { + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Collect", + TraceLoggingDescription("Logged when collecting window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + const auto layoutJsons = _manager.GetAllWindowLayouts(); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Save", + TraceLoggingDescription("Logged when writing window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + _app.Logic().SaveWindowLayoutJsons(layoutJsons); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_SaveWindowLayouts_Failed", + TraceLoggingDescription("An error occurred when collecting or writing window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + } + + co_return; +} + +winrt::fire_and_forget WindowEmperor::_saveWindowLayoutsRepeat() +{ + // Make sure we run on a background thread to not block anything. + co_await winrt::resume_background(); + + co_await _saveWindowLayouts(); + + // Don't need to save too frequently. + co_await winrt::resume_after(30s); + + // As long as we are supposed to keep saving, request another save. + // This will be delayed by the throttler so that at most one save happens + // per 10 seconds, if a save is requested by another source simultaneously. + if (_getWindowLayoutThrottler.has_value()) + { + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_requestGetLayout", + TraceLoggingDescription("Logged when triggering a throttled write of the window state"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + _getWindowLayoutThrottler.value()(); + } +} +#pragma endregion + +#pragma region WindowProc + +static WindowEmperor* GetThisFromHandle(HWND const window) noexcept +{ + const auto data = GetWindowLongPtr(window, GWLP_USERDATA); + return reinterpret_cast(data); +} +[[nodiscard]] LRESULT __stdcall WindowEmperor::_wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +{ + WINRT_ASSERT(window); + + if (WM_NCCREATE == message) + { + auto cs = reinterpret_cast(lparam); + WindowEmperor* that = static_cast(cs->lpCreateParams); + WINRT_ASSERT(that); + WINRT_ASSERT(!that->_window); + that->_window = wil::unique_hwnd(window); + SetWindowLongPtr(that->_window.get(), GWLP_USERDATA, reinterpret_cast(that)); + } + else if (WindowEmperor* that = GetThisFromHandle(window)) + { + return that->_messageHandler(message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} +void WindowEmperor::_createMessageWindow() +{ + WNDCLASS wc{}; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hInstance = reinterpret_cast(&__ImageBase); + wc.lpszClassName = TERMINAL_MESSAGE_CLASS_NAME; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WindowEmperor::_wndProc; + wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON)); + RegisterClass(&wc); + WINRT_ASSERT(!_window); + + WINRT_VERIFY(CreateWindow(wc.lpszClassName, + L"Windows Terminal", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + HWND_MESSAGE, + nullptr, + wc.hInstance, + this)); +} + +LRESULT WindowEmperor::_messageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept +{ + // use C++11 magic statics to make sure we only do this once. + // This won't change over the lifetime of the application + static const UINT WM_TASKBARCREATED = []() { return RegisterWindowMessageW(L"TaskbarCreated"); }(); + + switch (message) + { + case WM_HOTKEY: + { + _hotkeyPressed(static_cast(wParam)); + return 0; + } + case CM_NOTIFY_FROM_NOTIFICATION_AREA: + { + switch (LOWORD(lParam)) + { + case NIN_SELECT: + case NIN_KEYSELECT: + { + _notificationIcon->NotificationIconPressed(); + return 0; + } + case WM_CONTEXTMENU: + { + const til::point eventPoint{ GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam) }; + _notificationIcon->ShowContextMenu(eventPoint, _manager.GetPeasantInfos()); + return 0; + } + } + break; + } + case WM_MENUCOMMAND: + { + _notificationIcon->MenuItemSelected((HMENU)lParam, (UINT)wParam); + return 0; + } + default: + { + // We'll want to receive this message when explorer.exe restarts + // so that we can re-add our icon to the notification area. + // This unfortunately isn't a switch case because we register the + // message at runtime. + if (message == WM_TASKBARCREATED) + { + _notificationIcon->ReAddNotificationIcon(); + return 0; + } + } + } + return DefWindowProc(_window.get(), message, wParam, lParam); +} + +// Close the Terminal application. This will exit the main thread for the +// emperor itself. We should probably only ever be called when we have no +// windows left, and we don't want to keep running anymore. This will discard +// all our refrigerated windows. If we try to use XAML on Windows 10 after this, +// we'll undoubtedly crash. +winrt::fire_and_forget WindowEmperor::_close() +{ + { + auto fridge{ _oldThreads.lock() }; + for (auto& window : *fridge) + { + window->ThrowAway(); + } + fridge->clear(); + } + + // Important! Switch back to the main thread for the emperor. That way, the + // quit will go to the emperor's message pump. + co_await wil::resume_foreground(_dispatcher); + PostQuitMessage(0); +} + +#pragma endregion +#pragma region GlobalHotkeys + +// Method Description: +// - Called when the monarch failed to summon a window for a given set of +// SummonWindowSelectionArgs. In this case, we should create the specified +// window ourselves. +// - This is to support the scenario like `globalSummon(Name="_quake")` being +// used to summon the window if it already exists, or create it if it doesn't. +// Arguments: +// - args: Contains information on how we should name the window +// Return Value: +// - +static winrt::fire_and_forget _createNewTerminalWindow(Settings::Model::GlobalSummonArgs args) +{ + // Hop to the BG thread + co_await winrt::resume_background(); + + // This will get us the correct exe for dev/preview/release. If you + // don't stick this in a local, it'll get mangled by ShellExecute. I + // have no idea why. + const auto exePath{ GetWtExePath() }; + + // If we weren't given a name, then just use new to force the window to be + // unnamed. + winrt::hstring cmdline{ + fmt::format(L"-w {}", + args.Name().empty() ? L"new" : + args.Name()) + }; + + SHELLEXECUTEINFOW seInfo{ 0 }; + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_NOASYNC; + seInfo.lpVerb = L"open"; + seInfo.lpFile = exePath.c_str(); + seInfo.lpParameters = cmdline.c_str(); + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + co_return; +} + +void WindowEmperor::_hotkeyPressed(const long hotkeyIndex) +{ + if (hotkeyIndex < 0 || static_cast(hotkeyIndex) > _hotkeys.size()) + { + return; + } + + const auto& summonArgs = til::at(_hotkeys, hotkeyIndex); + Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() }; + + // desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false + // desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false + // desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true + args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); + args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); + args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); + args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); + + switch (summonArgs.Monitor()) + { + case Settings::Model::MonitorBehavior::Any: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + break; + case Settings::Model::MonitorBehavior::ToCurrent: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent); + break; + case Settings::Model::MonitorBehavior::ToMouse: + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse); + break; + } + + _manager.SummonWindow(args); + if (args.FoundMatch()) + { + // Excellent, the window was found. We have nothing else to do here. + } + else + { + // We should make the window ourselves. + _createNewTerminalWindow(summonArgs); + } +} + +bool WindowEmperor::_registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept +{ + const auto vkey = hotkey.Vkey(); + auto hotkeyFlags = MOD_NOREPEAT; + { + const auto modifiers = hotkey.Modifiers(); + WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)); + WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)); + WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)); + WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)); + } + + // TODO GH#8888: We should display a warning of some kind if this fails. + // This can fail if something else already bound this hotkey. + const auto result = ::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey); + LOG_LAST_ERROR_IF(!result); + TraceLoggingWrite(g_hWindowsTerminalProvider, + "RegisterHotKey", + TraceLoggingDescription("Emitted when setting hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingUInt64(vkey, "vkey", "the key"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_WIN), "win", "is WIN in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_ALT), "alt", "is ALT in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_CONTROL), "control", "is CONTROL in the modifiers"), + TraceLoggingUInt64(WI_IsFlagSet(hotkeyFlags, MOD_SHIFT), "shift", "is SHIFT in the modifiers"), + TraceLoggingBool(result, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + return result; +} + +// Method Description: +// - Call UnregisterHotKey once for each previously registered hotkey. +// Return Value: +// - +void WindowEmperor::_unregisterHotKey(const int index) noexcept +{ + TraceLoggingWrite( + g_hWindowsTerminalProvider, + "UnregisterHotKey", + TraceLoggingDescription("Emitted when clearing previously set hotkeys"), + TraceLoggingInt64(index, "index", "the index of the hotkey to remove"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + + LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index)); +} + +winrt::fire_and_forget WindowEmperor::_setupGlobalHotkeys() +{ + // The hotkey MUST be registered on the main thread. It will fail otherwise! + co_await wil::resume_foreground(_dispatcher); + + if (!_window) + { + // MSFT:36797001 There's a surprising number of hits of this callback + // getting triggered during teardown. As a best practice, we really + // should make sure _window exists before accessing it on any coroutine. + // We might be getting called back after the app already began getting + // cleaned up. + co_return; + } + // Unregister all previously registered hotkeys. + // + // RegisterHotKey(), will not unregister hotkeys automatically. + // If a hotkey with a given HWND and ID combination already exists + // then a duplicate one will be added, which we don't want. + // (Additionally we want to remove hotkeys that were removed from the settings.) + for (auto i = 0, count = gsl::narrow_cast(_hotkeys.size()); i < count; ++i) + { + _unregisterHotKey(i); + } + + _hotkeys.clear(); + + // Re-register all current hotkeys. + for (const auto& [keyChord, cmd] : _app.Logic().GlobalHotkeys()) + { + if (auto summonArgs = cmd.ActionAndArgs().Args().try_as()) + { + auto index = gsl::narrow_cast(_hotkeys.size()); + const auto succeeded = _registerHotKey(index, keyChord); + + TraceLoggingWrite(g_hWindowsTerminalProvider, + "AppHost_setupGlobalHotkey", + TraceLoggingDescription("Emitted when setting a single hotkey"), + TraceLoggingInt64(index, "index", "the index of the hotkey to add"), + TraceLoggingWideString(cmd.Name().c_str(), "name", "the name of the command"), + TraceLoggingBoolean(succeeded, "succeeded", "true if we succeeded"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + _hotkeys.emplace_back(summonArgs); + } + } +} + +#pragma endregion + +#pragma region NotificationIcon +// Method Description: +// - Creates a Notification Icon and hooks up its handlers +// Arguments: +// - +// Return Value: +// - +void WindowEmperor::_createNotificationIcon() +{ + _notificationIcon = std::make_unique(_window.get()); + _notificationIcon->SummonWindowRequested([this](auto& args) { _manager.SummonWindow(args); }); +} + +// Method Description: +// - Deletes our notification icon if we have one. +// Arguments: +// - +// Return Value: +// - +void WindowEmperor::_destroyNotificationIcon() +{ + _notificationIcon->RemoveIconFromNotificationArea(); + _notificationIcon = nullptr; +} + +void WindowEmperor::_checkWindowsForNotificationIcon() +{ + // We need to check some conditions to show the notification icon. + // + // * If there's a Quake window somewhere, we'll want to keep the + // notification icon. + // * There's two settings - MinimizeToNotificationArea and + // AlwaysShowNotificationIcon. If either one of them are true, we want to + // make sure there's a notification icon. + // + // If both are false, we want to remove our icon from the notification area. + // When we remove our icon from the notification area, we'll also want to + // re-summon any hidden windows, but right now we're not keeping track of + // who's hidden, so just summon them all. Tracking the work to do a "summon + // all minimized" in GH#10448 + // + // To avoid races between us thinking the settings updated, and the windows + // themselves getting the new settings, only ask the app logic for the + // RequestsTrayIcon setting value, and combine that with the result of each + // window (which won't change during a settings reload). + bool needsIcon = _app.Logic().RequestsTrayIcon(); + { + auto windows{ _windows.lock_shared() }; + for (const auto& _windowThread : *windows) + { + needsIcon |= _windowThread->Logic().IsQuakeWindow(); + } + } + + if (needsIcon) + { + _showNotificationIconRequested(); + } + else + { + _hideNotificationIconRequested(); + } +} + +void WindowEmperor::_showNotificationIconRequested() +{ + if (!_notificationIcon) + { + _createNotificationIcon(); + } +} + +void WindowEmperor::_hideNotificationIconRequested() +{ + // Destroy it only if our settings allow it + if (_notificationIcon) + { + // If we no longer want the tray icon, but we did have one, then quick + // re-summon all our windows, so they don't get lost when the icon + // disappears forever. + _manager.SummonAllWindows(); + + _destroyNotificationIcon(); + } +} +#pragma endregion + +// A callback to the window's logic to let us know when the window's +// quake mode state changes. We'll use this to check if we need to add +// or remove the notification icon. +winrt::fire_and_forget WindowEmperor::_windowIsQuakeWindowChanged(winrt::Windows::Foundation::IInspectable sender, + winrt::Windows::Foundation::IInspectable args) +{ + co_await wil::resume_foreground(this->_dispatcher); + _checkWindowsForNotificationIcon(); +} +winrt::fire_and_forget WindowEmperor::_windowRequestUpdateSettings() +{ + // We MUST be on the main thread to update the settings. We will crash when trying to enumerate fragment extensions otherwise. + co_await wil::resume_foreground(this->_dispatcher); + _app.Logic().ReloadSettings(); +} diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h new file mode 100644 index 0000000000..47e8b35619 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -0,0 +1,95 @@ +/*++ +Copyright (c) Microsoft Corporation Licensed under the MIT license. + +Class Name: +- WindowEmperor.h + +Abstract: +- The WindowEmperor is our class for managing the single Terminal process + with all our windows. It will be responsible for handling the commandline + arguments. It will initially try to find another terminal process to + communicate with. If it does, it'll hand off to the existing process. +- If it determines that it should create a window, it will set up a new thread + for that window, and a message loop on the main thread for handling global + state, such as hotkeys and the notification icon. + +--*/ + +#pragma once +#include "pch.h" + +#include "WindowThread.h" + +class WindowEmperor : public std::enable_shared_from_this +{ +public: + WindowEmperor() noexcept; + ~WindowEmperor(); + void WaitForWindows(); + + bool HandleCommandlineArgs(); + +private: + void _createNewWindowThread(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); + + [[nodiscard]] static LRESULT __stdcall _wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; + LRESULT _messageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept; + wil::unique_hwnd _window; + + winrt::TerminalApp::App _app; + winrt::Windows::System::DispatcherQueue _dispatcher{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowManager _manager; + + til::shared_mutex>> _windows; + std::atomic _windowThreadInstances; + + til::shared_mutex>> _oldThreads; + + std::optional> _getWindowLayoutThrottler; + + winrt::event_token _WindowCreatedToken; + winrt::event_token _WindowClosedToken; + + std::vector _hotkeys; + + std::unique_ptr _notificationIcon; + + bool _quitting{ false }; + + void _windowStartedHandlerPostXAML(const std::shared_ptr& sender); + void _removeWindow(uint64_t senderID); + void _decrementWindowCount(); + + void _becomeMonarch(); + void _numberOfWindowsChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); + void _quitAllRequested(const winrt::Windows::Foundation::IInspectable&, + const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs&); + + winrt::fire_and_forget _windowIsQuakeWindowChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::Foundation::IInspectable args); + winrt::fire_and_forget _windowRequestUpdateSettings(); + + winrt::Windows::Foundation::IAsyncAction _saveWindowLayouts(); + winrt::fire_and_forget _saveWindowLayoutsRepeat(); + + void _createMessageWindow(); + + void _hotkeyPressed(const long hotkeyIndex); + bool _registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; + void _unregisterHotKey(const int index) noexcept; + winrt::fire_and_forget _setupGlobalHotkeys(); + + winrt::fire_and_forget _close(); + + void _createNotificationIcon(); + void _destroyNotificationIcon(); + void _checkWindowsForNotificationIcon(); + void _showNotificationIconRequested(); + void _hideNotificationIconRequested(); + + struct Revokers + { + winrt::Microsoft::Terminal::Remoting::WindowManager::WindowCreated_revoker WindowCreated; + winrt::Microsoft::Terminal::Remoting::WindowManager::WindowClosed_revoker WindowClosed; + winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; + } _revokers{}; +}; diff --git a/src/cascadia/WindowsTerminal/WindowThread.cpp b/src/cascadia/WindowsTerminal/WindowThread.cpp new file mode 100644 index 0000000000..2c70741ad1 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowThread.cpp @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "WindowThread.h" + +WindowThread::WindowThread(winrt::TerminalApp::AppLogic logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::WindowManager manager, + winrt::Microsoft::Terminal::Remoting::Peasant peasant) : + _peasant{ std::move(peasant) }, + _appLogic{ std::move(logic) }, + _args{ std::move(args) }, + _manager{ std::move(manager) } +{ + // DO NOT start the AppHost here in the ctor, as that will start XAML on the wrong thread! +} + +void WindowThread::CreateHost() +{ + // Calling this while refrigerated won't work. + // * We can't re-initialize our winrt apartment. + // * AppHost::Initialize has to be done on the "UI" thread. + assert(_warmWindow == nullptr); + + // Start the AppHost HERE, on the actual thread we want XAML to run on + _host = std::make_shared<::AppHost>(_appLogic, + _args, + _manager, + _peasant); + + _UpdateSettingsRequestedToken = _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); + + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Initialize the xaml content. This must be called AFTER the + // WindowsXamlManager is initialized. + _host->Initialize(); +} + +int WindowThread::RunMessagePump() +{ + // Enter the main window loop. + const auto exitCode = _messagePump(); + // Here, the main window loop has exited. + return exitCode; +} + +void WindowThread::_pumpRemainingXamlMessages() +{ + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } +} + +void WindowThread::RundownForExit() +{ + if (_host) + { + _host->UpdateSettingsRequested(_UpdateSettingsRequestedToken); + _host->Close(); + } + if (_warmWindow) + { + // If we have a _warmWindow, we're a refrigerated thread without a + // AppHost in control of the window. Manually close the window + // ourselves, to free the DesktopWindowXamlSource. + _warmWindow->Close(); + } + + // !! LOAD BEARING !! + // + // Make sure to finish pumping all the messages for our thread here. We + // may think we're all done, but we're not quite. XAML needs more time + // to pump the remaining events through, even at the point we're + // exiting. So do that now. If you don't, then the last tab to close + // will never actually destruct the last tab / TermControl / ControlCore + // / renderer. + _pumpRemainingXamlMessages(); +} + +void WindowThread::ThrowAway() +{ + // raise the signal to unblock KeepWarm. We won't have a host, so we'll drop + // out of the message loop to eventually RundownForExit. + // + // This should only be called when the app is fully quitting. After this is + // called on any thread, on win10, we won't be able to call into XAML + // anymore. + _microwaveBuzzer.notify_one(); +} + +// Method Description: +// - Check if we should keep this window alive, to try it's message loop again. +// If we were refrigerated for later, then this will block the thread on the +// _microwaveBuzzer. We'll sit there like that till the emperor decides if +// they want to re-use this window thread for a new window. +// Return Value: +// - true IFF we should enter this thread's message loop +// INVARIANT: This must be called on our "ui thread", our window thread. +bool WindowThread::KeepWarm() +{ + if (_host != nullptr) + { + // We're currently hot + return true; + } + + // If we're refrigerated, then wait on the microwave signal, which will be + // raised when we get re-heated by another thread to reactivate us. + + if (_warmWindow != nullptr) + { + std::unique_lock lock(_microwave); + _microwaveBuzzer.wait(lock); + + // If ThrowAway() was called, then the buzzer will be signalled without + // setting a new _host. In that case, the app is quitting, for real. We + // just want to exit with false. + const bool reheated = _host != nullptr; + if (reheated) + { + _UpdateSettingsRequestedToken = _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); + // Re-initialize the host here, on the window thread + _host->Initialize(); + } + return reheated; + } + else + { + return false; + } +} + +// Method Description: +// - "Refrigerate" this thread for later reuse. This will refrigerate the window +// itself, and tear down our current app host. We'll save our window for +// later. We'll also pump out the existing message from XAML, before +// returning. After we return, the emperor will add us to the list of threads +// that can be re-used. +void WindowThread::Refrigerate() +{ + _host->UpdateSettingsRequested(_UpdateSettingsRequestedToken); + + // keep a reference to the HWND and DesktopWindowXamlSource alive. + _warmWindow = std::move(_host->Refrigerate()); + + // rundown remaining messages before destructing the app host + _pumpRemainingXamlMessages(); + _host = nullptr; +} + +// Method Description: +// - "Reheat" this thread for reuse. We'll build a new AppHost, and pass in the +// existing window to it. We'll then trigger the _microwaveBuzzer, so KeepWarm +// (which is on the UI thread) will get unblocked, and we can initialize this +// window. +void WindowThread::Microwave( + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::Peasant peasant) +{ + _peasant = std::move(peasant); + _args = std::move(args); + + _host = std::make_shared<::AppHost>(_appLogic, + _args, + _manager, + _peasant, + std::move(_warmWindow)); + + // raise the signal to unblock KeepWarm and start the window message loop again. + _microwaveBuzzer.notify_one(); +} + +winrt::TerminalApp::TerminalWindow WindowThread::Logic() +{ + return _host->Logic(); +} + +static bool _messageIsF7Keypress(const MSG& message) +{ + return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7; +} +static bool _messageIsAltKeyup(const MSG& message) +{ + return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU; +} +static bool _messageIsAltSpaceKeypress(const MSG& message) +{ + return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE; +} + +int WindowThread::_messagePump() +{ + MSG message{}; + + while (GetMessageW(&message, nullptr, 0, 0)) + { + // GH#638 (Pressing F7 brings up both the history AND a caret browsing message) + // The Xaml input stack doesn't allow an application to suppress the "caret browsing" + // dialog experience triggered when you press F7. Official recommendation from the Xaml + // team is to catch F7 before we hand it off. + // AppLogic contains an ad-hoc implementation of event bubbling for a runtime classes + // implementing a custom IF7Listener interface. + // If the recipient of IF7Listener::OnF7Pressed suggests that the F7 press has, in fact, + // been handled we can discard the message before we even translate it. + if (_messageIsF7Keypress(message)) + { + if (_host->OnDirectKeyEvent(VK_F7, LOBYTE(HIWORD(message.lParam)), true)) + { + // The application consumed the F7. Don't let Xaml get it. + continue; + } + } + + // GH#6421 - System XAML will never send an Alt KeyUp event. So, similar + // to how we'll steal the F7 KeyDown above, we'll steal the Alt KeyUp + // here, and plumb it through. + if (_messageIsAltKeyup(message)) + { + // Let's pass to the application + if (_host->OnDirectKeyEvent(VK_MENU, LOBYTE(HIWORD(message.lParam)), false)) + { + // The application consumed the Alt. Don't let Xaml get it. + continue; + } + } + + // GH#7125 = System XAML will show a system dialog on Alt Space. We want to + // explicitly prevent that because we handle that ourselves. So similar to + // above, we steal the event and hand it off to the host. + if (_messageIsAltSpaceKeypress(message)) + { + _host->OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true); + continue; + } + + TranslateMessage(&message); + DispatchMessage(&message); + } + return 0; +} + +uint64_t WindowThread::PeasantID() +{ + return _peasant.GetID(); +} diff --git a/src/cascadia/WindowsTerminal/WindowThread.h b/src/cascadia/WindowsTerminal/WindowThread.h new file mode 100644 index 0000000000..a1af2db650 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowThread.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#pragma once +#include "pch.h" +#include "AppHost.h" + +class WindowThread : public std::enable_shared_from_this +{ +public: + WindowThread(winrt::TerminalApp::AppLogic logic, + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::WindowManager manager, + winrt::Microsoft::Terminal::Remoting::Peasant peasant); + + winrt::TerminalApp::TerminalWindow Logic(); + void CreateHost(); + int RunMessagePump(); + void RundownForExit(); + + bool KeepWarm(); + void Refrigerate(); + void Microwave( + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::Peasant peasant); + void ThrowAway(); + + uint64_t PeasantID(); + + WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); + +private: + winrt::Microsoft::Terminal::Remoting::Peasant _peasant{ nullptr }; + + winrt::TerminalApp::AppLogic _appLogic{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs _args{ nullptr }; + winrt::Microsoft::Terminal::Remoting::WindowManager _manager{ nullptr }; + + // This is a "shared_ptr", but it should be treated as a unique, owning ptr. + // It's shared, because there are edge cases in refrigeration where internal + // co_awaits inside AppHost might resume after we've dtor'd it, and there's + // no other way for us to let the AppHost know it has passed on. + std::shared_ptr<::AppHost> _host{ nullptr }; + winrt::event_token _UpdateSettingsRequestedToken; + + std::unique_ptr<::IslandWindow> _warmWindow{ nullptr }; + std::mutex _microwave; + std::condition_variable _microwaveBuzzer; + + int _messagePump(); + void _pumpRemainingXamlMessages(); +}; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.rc b/src/cascadia/WindowsTerminal/WindowsTerminal.rc index fb03671aaf..d349078a42 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/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 1172c8bab4..62875db2b6 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -18,8 +18,6 @@ true - true - true true true @@ -57,6 +55,8 @@ + + @@ -68,6 +68,8 @@ + + @@ -76,9 +78,6 @@ - - - + + diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index 5911fa2618..c18e70e7cb 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -16,10 +16,11 @@ X(winrt::Microsoft::Terminal::Core::AdjustTextMode, AdjustIndistinguishableColors, winrt::Microsoft::Terminal::Core::AdjustTextMode::Never) // --------------------------- Control Appearance --------------------------- -// All of these settings are defined in IControlSettings. +// All of these settings are defined in IControlAppearance. #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) \ @@ -47,28 +48,31 @@ X(winrt::hstring, StartingTitle) \ X(bool, DetectURLs, true) \ X(bool, VtPassthrough, false) \ - X(bool, AutoMarkPrompts) + X(bool, AutoMarkPrompts) \ + X(bool, RepositionCursorWithMouse, false) // --------------------------- Control Settings --------------------------- // All of these settings are defined in IControlSettings. #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) \ X(winrt::Windows::UI::Text::FontWeight, FontWeight) \ X(IFontFeatureMap, FontFeatures) \ X(IFontAxesMap, FontAxes) \ + X(winrt::hstring, CellWidth) \ + X(winrt::hstring, CellHeight) \ X(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr) \ X(winrt::hstring, Commandline) \ X(winrt::hstring, StartingDirectory) \ - X(winrt::hstring, EnvironmentVariables) \ X(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible) \ X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \ X(bool, ForceFullRepaintRendering, false) \ X(bool, SoftwareRendering, false) \ X(bool, UseAtlasEngine, false) \ X(bool, UseBackgroundImageForWindow, false) \ - X(bool, ShowMarks, false) + X(bool, ShowMarks, false) \ + X(bool, RightClickContextMenu, false) diff --git a/src/cascadia/inc/WindowingBehavior.h b/src/cascadia/inc/WindowingBehavior.h index 0426089319..c00a33e9ce 100644 --- a/src/cascadia/inc/WindowingBehavior.h +++ b/src/cascadia/inc/WindowingBehavior.h @@ -4,10 +4,18 @@ Licensed under the MIT license. --*/ #pragma once -constexpr int32_t WindowingBehaviorUseCurrent{ 0 }; -constexpr int32_t WindowingBehaviorUseNew{ -1 }; -constexpr int32_t WindowingBehaviorUseExisting{ -2 }; -constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; -constexpr int32_t WindowingBehaviorUseName{ -4 }; +inline constexpr int32_t WindowingBehaviorUseCurrent{ 0 }; +inline constexpr int32_t WindowingBehaviorUseNew{ -1 }; +inline constexpr int32_t WindowingBehaviorUseExisting{ -2 }; +inline constexpr int32_t WindowingBehaviorUseAnyExisting{ -3 }; +inline constexpr int32_t WindowingBehaviorUseName{ -4 }; +inline constexpr int32_t WindowingBehaviorUseNone{ -5 }; -static constexpr std::wstring_view QuakeWindowName{ L"_quake" }; +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" }; diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h index 1e9ccb1a0d..17c720614b 100644 --- a/src/cascadia/inc/cppwinrt_utils.h +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -17,45 +17,14 @@ 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); } +template<> +struct fmt::formatter : fmt::formatter +{ + auto format(const winrt::hstring& str, auto& ctx) + { + return fmt::formatter::format({ str.data(), str.size() }, ctx); + } +}; // This is a helper macro for both declaring the signature of an event, and // defining the body. Winrt events need a method for adding a callback to the @@ -63,12 +32,18 @@ private: // signatures and define them both for you, because they don't really vary from // event to event. // Use this in a classes header if you have a Windows.Foundation.TypedEventHandler -#define TYPED_EVENT(name, sender, args) \ -public: \ - winrt::event_token name(const winrt::Windows::Foundation::TypedEventHandler& handler) { return _##name##Handlers.add(handler); } \ - void name(const winrt::event_token& token) { _##name##Handlers.remove(token); } \ - \ -private: \ +#define TYPED_EVENT(name, sender, args) \ +public: \ + winrt::event_token name(const winrt::Windows::Foundation::TypedEventHandler& handler) \ + { \ + return _##name##Handlers.add(handler); \ + } \ + void name(const winrt::event_token& token) \ + { \ + _##name##Handlers.remove(token); \ + } \ + \ +private: \ winrt::event> _##name##Handlers; // This is a helper macro for both declaring the signature of a callback (nee event) and @@ -77,12 +52,18 @@ private: // signatures and define them both for you, because they don't really vary from // event to event. // Use this in a class's header if you have a "delegate" type in your IDL. -#define WINRT_CALLBACK(name, args) \ -public: \ - winrt::event_token name(const args& handler) { return _##name##Handlers.add(handler); } \ - void name(const winrt::event_token& token) { _##name##Handlers.remove(token); } \ - \ -protected: \ +#define WINRT_CALLBACK(name, args) \ +public: \ + winrt::event_token name(const args& handler) \ + { \ + return _##name##Handlers.add(handler); \ + } \ + void name(const winrt::event_token& token) \ + { \ + _##name##Handlers.remove(token); \ + } \ + \ +protected: \ winrt::event _##name##Handlers; // This is a helper macro for both declaring the signature and body of an event @@ -91,26 +72,68 @@ protected: // "proxied" to the handling type. Case in point: many of the events on App are // just forwarded straight to TerminalPage. This macro will both declare the // method signatures and define them both for you. -#define FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ -public: \ - winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) { return handler->handlerName(h); } \ - void name(const winrt::event_token& token) noexcept { handler->handlerName(token); } +#define FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ +public: \ + winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) \ + { \ + return handler->handlerName(h); \ + } \ + void name(const winrt::event_token& token) noexcept \ + { \ + handler->handlerName(token); \ + } // Same thing, but handler is a projected type, not an implementation -#define PROJECTED_FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ -public: \ - winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) { return handler.handlerName(h); } \ - void name(const winrt::event_token& token) noexcept { handler.handlerName(token); } +#define PROJECTED_FORWARDED_TYPED_EVENT(name, sender, args, handler, handlerName) \ +public: \ + winrt::event_token name(const Windows::Foundation::TypedEventHandler& h) \ + { \ + return handler.handlerName(h); \ + } \ + void name(const winrt::event_token& token) noexcept \ + { \ + handler.handlerName(token); \ + } + +// This is a bit like *FORWARDED_TYPED_EVENT. When you use a forwarded event, +// the handler gets added to the object that's raising the event. For example, +// the TerminalPage might be the handler for the TermControl's +// BackgroundColorChanged event, which is actually implemented by the +// ControlCore. So when Core raises an event, it immediately calls the handler +// on the Page. +// +// Instead, the BUBBLED event introduces an indirection layer. In the above +// example, the Core would raise the event, but now the Control would handle it, +// and raise an event with each of its own handlers. +// +// This allows us to detach the core from the control safely, without needing to +// re-wire all the event handlers from page->control again. +// +// Implement like: +// +// _core.TitleChanged({ get_weak(), &TermControl::_bubbleTitleChanged }); +#define BUBBLED_FORWARDED_TYPED_EVENT(name, sender, args) \ + TYPED_EVENT(name, sender, args) \ + void _bubble##name(const sender& s, const args& a) \ + { \ + _##name##Handlers(s, a); \ + } // Use this macro to quick implement both the getter and setter for a property. // This should only be used for simple types where there's no logic in the // getter/setter beyond just accessing/updating the value. -#define WINRT_PROPERTY(type, name, ...) \ -public: \ - type name() const noexcept { return _##name; } \ - void name(const type& value) noexcept { _##name = value; } \ - \ -private: \ +#define WINRT_PROPERTY(type, name, ...) \ +public: \ + type name() const noexcept \ + { \ + return _##name; \ + } \ + void name(const type& value) noexcept \ + { \ + _##name = value; \ + } \ + \ +private: \ type _##name{ __VA_ARGS__ }; // Use this macro to quickly implement both the getter and setter for an @@ -122,7 +145,10 @@ private: \ // (like when the class is being initialized). #define WINRT_OBSERVABLE_PROPERTY(type, name, event, ...) \ public: \ - type name() const noexcept { return _##name; }; \ + type name() const noexcept \ + { \ + return _##name; \ + }; \ void name(const type& value) \ { \ if (_##name != value) \ diff --git a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj index b04be0014b..c6393d7add 100644 --- a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj +++ b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj @@ -53,8 +53,6 @@ ..;$(OpenConsoleDir)src\cascadia\inc;$(OpenConsoleDir)\dep\jsoncpp\json;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalApp\Generated Files";"$(OpenConsoleDir)\src\cascadia\TerminalSettingsModel\Generated Files";%(AdditionalIncludeDirectories) precomp.h - - %(AdditionalOptions) /Zc:twoPhase- 4702;%(DisableSpecificWarnings) @@ -83,13 +81,6 @@ - - - x86 - $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.$(TerminalMUXVersion)\runtimes\win10-$(Native-Platform)\native\" - - + + + + MultiThreadedDebug + + + + %(IgnoreSpecificDefaultLibraries);libucrtd.lib + %(AdditionalOptions) /defaultlib:ucrtd.lib + + + + + + MultiThreaded + + + + %(IgnoreSpecificDefaultLibraries);libucrt.lib + %(AdditionalOptions) /defaultlib:ucrt.lib + + + diff --git a/src/common.build.pre.props b/src/common.build.pre.props index 6cc8877c2e..9404c24c4b 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -81,12 +81,33 @@ + + + false + true + + v143 Unicode false x64 + true + false + + true @@ -103,14 +124,15 @@ Conhost code converts DWORDs to HANDLEs for instance. C4467: usage of ATL attributes is deprecated Conhost code still uses ATL. - C26445: Do not assign gsl::span or std::string_view to a reference. They are cheap to construct and are not owners of the underlying data. (gsl.view). + C26445: Do not assign std::span or std::string_view to a reference. They are cheap to construct and are not owners of the underlying data. (gsl.view). Even for MSVC v19.32 this is actually far from true. Copying (as opposed to referencing) larger than register-sized structures is fairly expensive. Example: https://godbolt.org/z/oPco88PaP - C26813: Use 'bitwise and' to check if a flag is set. - The MSVC v19.31 toolset has a bug where a pointer to an enum is incorrectly flagged with C26813. - It's supposed to be fixed with VS 17.2.1 and 17.3.0 and later respectively. + C26434: Method 'A' hides a non-virtual method 'B' (c.128). + C26456: Operator 'A' hides a non-virtual operator 'B' (c.128) + I think these rules are for when you fully bought into OOP? + We didn't and it breaks WRL and large parts of conhost code. --> - 4201;4312;4467;5105;26445;26813;%(DisableSpecificWarnings) + 4201;4312;4467;5105;26434;26445;26456;%(DisableSpecificWarnings) _WINDOWS;EXTERNAL_BUILD;%(PreprocessorDefinitions) true precomp.h @@ -121,10 +143,10 @@ false true true + true stdcpp20 stdc17 - - %(AdditionalOptions) /utf-8 /Zc:externConstexpr /Zc:lambda /Zc:throwingNew /fp:contract + %(AdditionalOptions) /utf-8 /Zc:__cplusplus /Zc:__STDC__ /Zc:enumTypes /Zc:externConstexpr /Zc:templateScope /Zc:throwingNew Guard diff --git a/src/common.nugetversions.props b/src/common.nugetversions.props index 76c1de8a65..15e2fe774d 100644 --- a/src/common.nugetversions.props +++ b/src/common.nugetversions.props @@ -1,13 +1,10 @@ - + - - - - + @@ -39,4 +36,7 @@ $(MSBuildThisFileDirectory)..\packages\Selenium.Support.3.5.0 + + + diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 08a695e815..2a3d5cc062 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -16,6 +16,8 @@ "$(SolutionDir)dep\nuget\nuget.exe" $(SolutionDir)dep\nuget\packages.config + + $(MSBuildThisFileDirectory)..\packages\Microsoft.Web.WebView2.1.0.1661.34 - + - + + - - - - + - - - - - - - + - + + + @@ -72,34 +72,25 @@ - - + + - - - - - - + + - - - - - - - + - + + diff --git a/src/cppwinrt.build.pre.props b/src/cppwinrt.build.pre.props index c83d1196b3..ad36921e7b 100644 --- a/src/cppwinrt.build.pre.props +++ b/src/cppwinrt.build.pre.props @@ -28,6 +28,7 @@ true true Windows Store + false - + Use pch.h $(IntDir)pch.pch @@ -72,6 +73,8 @@ 5104;28204;%(DisableSpecificWarnings) $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) Console diff --git a/src/features.xml b/src/features.xml index 9d3c36cf7d..4656ef0ffd 100644 --- a/src/features.xml +++ b/src/features.xml @@ -67,6 +67,15 @@ + + Feature_LegacyConhost + conhost should support ForceV2=false, and try to load conhostv1.dll + AlwaysDisabled + + WindowsInbox + + + Feature_AtlasEngine If enabled, AtlasEngine is used by default @@ -77,6 +86,16 @@ + + Feature_AtlasEnginePresentFallback + We don't feel super confident in our usage of the Present1 API, so this settings adds a fallback to Present on error + AlwaysDisabled + + Release + WindowsInbox + + + Feature_NearbyFontLoading Controls whether fonts in the same directory as the binary are used during rendering. Disabled for conhost so that it doesn't iterate the entire system32 directory. @@ -102,6 +121,7 @@ Dev + Canary Preview @@ -116,17 +136,6 @@ - - Feature_IsolatedMonarchMode - Enables a test flag for MSFT:38540483. When enabled, if we ever create a null Monarch, we'll stealthily try to fall back to an in-proc monarch instance. - AlwaysEnabled - - - Dev - Preview - - - Feature_ScrollbarMarks Enables the experimental scrollbar marks feature. @@ -134,8 +143,48 @@ Dev + Canary Preview + + Feature_DynamicSSHProfiles + Enables the dynamic profile generator for OpenSSH config files + 9031 + AlwaysDisabled + + + + Feature_AzureConnectionInProc + Host the AzureConnection inside Terminal rather than via TerminalAzBridge + 4661 + AlwaysDisabled + + Dev + + + + + Feature_ShellCompletions + An experimental escape sequence for client applications to request the Terminal display a list of suggestions. + 3121 + AlwaysDisabled + + Dev + Canary + Preview + + + + + Feature_VtChecksumReport + Enables the DECRQCRA checksum report, which can be used to read the screen contents + 14974 + AlwaysDisabled + + Dev + + + diff --git a/src/host/ApiRoutines.h b/src/host/ApiRoutines.h index 457aaee9f0..a46f1397ec 100644 --- a/src/host/ApiRoutines.h +++ b/src/host/ApiRoutines.h @@ -49,51 +49,25 @@ public: [[nodiscard]] HRESULT GetNumberOfConsoleInputEventsImpl(const InputBuffer& context, ULONG& events) noexcept override; - [[nodiscard]] HRESULT PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; + [[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context, + InputEventQueue& outEvents, + const size_t eventReadCount, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const bool IsPeek, + std::unique_ptr& waiter) noexcept override; - [[nodiscard]] HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; + [[nodiscard]] HRESULT 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, @@ -194,41 +168,41 @@ public: [[nodiscard]] HRESULT ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT WriteConsoleInputAImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept override; [[nodiscard]] HRESULT WriteConsoleInputWImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, - const gsl::span attrs, + const std::span attrs, const til::point target, size_t& used) noexcept override; @@ -243,28 +217,28 @@ public: size_t& used) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept override; - [[nodiscard]] HRESULT GetConsoleTitleAImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleTitleWImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleOriginalTitleAImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleOriginalTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleOriginalTitleWImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleOriginalTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept override; @@ -301,12 +275,12 @@ public: const std::wstring_view exeName) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasAImpl(const std::string_view source, - gsl::span target, + std::span target, size_t& written, const std::string_view exeName) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasWImpl(const std::wstring_view source, - gsl::span target, + std::span target, size_t& written, const std::wstring_view exeName) noexcept override; @@ -321,17 +295,17 @@ public: [[nodiscard]] HRESULT GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasesAImpl(const std::string_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasesWImpl(const std::wstring_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept override; - [[nodiscard]] HRESULT GetConsoleAliasExesAImpl(gsl::span aliasExes, + [[nodiscard]] HRESULT GetConsoleAliasExesAImpl(std::span aliasExes, size_t& written) noexcept override; - [[nodiscard]] HRESULT GetConsoleAliasExesWImpl(gsl::span aliasExes, + [[nodiscard]] HRESULT GetConsoleAliasExesWImpl(std::span aliasExes, size_t& written) noexcept override; #pragma region CMDext Private API @@ -353,11 +327,11 @@ public: size_t& length) noexcept override; [[nodiscard]] HRESULT GetConsoleCommandHistoryAImpl(const std::string_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept override; [[nodiscard]] HRESULT GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept override; #pragma endregion diff --git a/src/host/CommandListPopup.cpp b/src/host/CommandListPopup.cpp deleted file mode 100644 index 2539990d32..0000000000 --- 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.X; - for (size_t i = 0; i < history.GetNumberOfCommands(); ++i) - { - const auto& historyItem = history.GetNth(gsl::narrow(i)); - width = std::max(width, historyItem.size() + padding); - } - if (width > SHRT_MAX) - { - width = SHRT_MAX; - } - - // calculate height, it can range up to 20 rows - auto height = std::clamp(gsl::narrow(history.GetNumberOfCommands()), minSize.Y, 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, static_cast(history.GetNumberOfCommands() - 1)) } -{ - FAIL_FAST_IF(_currentCommand < 0); - _setBottomIndex(); -} - -[[nodiscard]] NTSTATUS CommandListPopup::_handlePopupKeys(COOKED_READ_DATA& cookedReadData, - const wchar_t wch, - const DWORD modifiers) noexcept -{ - try - { - short Index = 0; - const 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((SHORT)(cookedReadData.History().GetNumberOfCommands())); - break; - case VK_HOME: - // Move waaay back, UpdateCommandListPopup() can handle it. - _update(-(SHORT)(cookedReadData.History().GetNumberOfCommands())); - break; - case VK_PRIOR: - _update(-(SHORT)Height()); - break; - case VK_NEXT: - _update((SHORT)Height()); - break; - case VK_DELETE: - return _deleteSelection(cookedReadData); - case VK_LEFT: - case VK_RIGHT: - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, (SHORT)Index); - return CONSOLE_STATUS_WAIT_NO_BLOCK; - default: - break; - } - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_setBottomIndex() -{ - if (_currentCommand < (SHORT)(_history.GetNumberOfCommands() - Height())) - { - _bottomIndex = std::max(_currentCommand, gsl::narrow(Height() - 1)); - } - else - { - _bottomIndex = (SHORT)(_history.GetNumberOfCommands() - 1); - } -} - -[[nodiscard]] NTSTATUS CommandListPopup::_deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - history.Remove(static_cast(_currentCommand)); - _setBottomIndex(); - - if (history.GetNumberOfCommands() == 0) - { - // close the popup - return CONSOLE_STATUS_READ_COMPLETE; - } - else if (_currentCommand >= static_cast(history.GetNumberOfCommands())) - { - _currentCommand = static_cast(history.GetNumberOfCommands() - 1); - _bottomIndex = _currentCommand; - } - - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item up in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapUp(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == 0) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand - 1); - _update(-1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item down in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapDown(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == gsl::narrow(history.GetNumberOfCommands()) - 1) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand + 1); - _update(1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) -{ - short Index = 0; - auto Status = STATUS_SUCCESS; - DWORD LineCount = 1; - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, (SHORT)Index); - cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status); - // complete read - if (cookedReadData.IsEchoInput()) - { - // check for alias - cookedReadData.ProcessAliases(LineCount); - } - - Status = STATUS_SUCCESS; - size_t NumBytes; - if (cookedReadData.BytesRead() > cookedReadData.UserBufferSize() || LineCount > 1) - { - if (LineCount > 1) - { - const wchar_t* Tmp; - for (Tmp = cookedReadData.BufferStartPtr(); *Tmp != UNICODE_LINEFEED; Tmp++) - { - FAIL_FAST_IF(!(Tmp < (cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()))); - } - NumBytes = (Tmp - cookedReadData.BufferStartPtr() + 1) * sizeof(*Tmp); - } - else - { - NumBytes = cookedReadData.UserBufferSize(); - } - - // Copy what we can fit into the user buffer - const 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) -{ - short Index = 0; - if (cookedReadData.History().FindMatchingCommand({ &wch, 1 }, - _currentCommand, - Index, - CommandHistory::MatchOptions::JustLooking)) - { - _update((SHORT)(Index - _currentCommand), true); - } -} - -// Routine Description: -// - This routine handles the command list popup. It returns when we're out of input or the user has selected a command line. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandListPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - - for (;;) - { - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (!NT_SUCCESS(Status)) - { - return Status; - } - - if (popupKeys) - { - Status = _handlePopupKeys(cookedReadData, wch, modifiers); - if (Status != STATUS_SUCCESS) - { - return Status; - } - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - return CONSOLE_STATUS_READ_COMPLETE; - } - else - { - // cycle through commands that start with the letter of the key pressed - _cycleSelectionToMatchingCommands(cookedReadData, wch); - } - } -} - -void CommandListPopup::_DrawContent() -{ - _drawList(); -} - -// Routine Description: -// - Draws a list of commands for the user to choose from -void CommandListPopup::_drawList() -{ - // draw empty popup - 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 = gsl::narrow(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 SHORT originalDelta, const bool wrap) -{ - auto delta = originalDelta; - if (delta == 0) - { - return; - } - const auto Size = Height(); - - auto CurCmdNum = _currentCommand; - SHORT NewCmdNum = CurCmdNum + delta; - - if (wrap) - { - // Modulo the number of commands to "circle" around if we went off the end. - NewCmdNum %= _history.GetNumberOfCommands(); - } - else - { - if (NewCmdNum >= gsl::narrow(_history.GetNumberOfCommands())) - { - NewCmdNum = gsl::narrow(_history.GetNumberOfCommands()) - 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 = gsl::narrow(Size - 1); - } - Scroll = true; - } - else if (NewCmdNum > _bottomIndex) - { - _bottomIndex += delta; - if (_bottomIndex >= gsl::narrow(_history.GetNumberOfCommands())) - { - _bottomIndex = gsl::narrow(_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 SHORT OldCurrentCommand, const SHORT 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 a893b90555..0000000000 --- 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 SHORT delta, const bool wrap = false); - void _updateHighlight(const SHORT oldCommand, const SHORT newCommand); - - void _handleReturn(COOKED_READ_DATA& cookedReadData); - void _cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch); - void _setBottomIndex(); - [[nodiscard]] NTSTATUS _handlePopupKeys(COOKED_READ_DATA& cookedReadData, const wchar_t wch, const DWORD modifiers) noexcept; - [[nodiscard]] NTSTATUS _deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapUp(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapDown(COOKED_READ_DATA& cookedReadData) noexcept; - - SHORT _currentCommand; - SHORT _bottomIndex; // number of command displayed on last line of popup - const CommandHistory& _history; - -#ifdef UNIT_TESTING - friend class CommandListPopupTests; -#endif -}; diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp deleted file mode 100644 index b7911bd222..0000000000 --- a/src/host/CommandNumberPopup.cpp +++ /dev/null @@ -1,196 +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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - 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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - 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(static_cast(_parse()), - cookedReadData.History().GetNumberOfCommands() - 1)); - - CommandLine::Instance().EndAllPopups(); - SetCurrentCommandLine(cookedReadData, commandNumber); -} - -// Routine Description: -// - This routine handles the command number selection popup. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandNumberPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - for (;;) - { - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (!NT_SUCCESS(Status)) - { - return Status; - } - - if (std::iswdigit(wch)) - { - _handleNumber(cookedReadData, wch); - } - else if (wch == UNICODE_BACKSPACE) - { - _handleBackspace(cookedReadData); - } - else if (wch == VK_ESCAPE) - { - _handleEscape(cookedReadData); - break; - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - break; - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CommandNumberPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF9); -} - -// Routine Description: -// - adds single digit number to the popup's number buffer -// Arguments: -// - wch - char of the number to add. must be in the range [L'0', L'9'] -// Note: will throw if wch is out of range -void CommandNumberPopup::_push(const wchar_t wch) -{ - THROW_HR_IF(E_INVALIDARG, !std::iswdigit(wch)); - if (_userInput.size() < COMMAND_NUMBER_LENGTH) - { - _userInput += wch; - } -} - -// Routine Description: -// - removes the last number added to the number buffer -void CommandNumberPopup::_pop() noexcept -{ - if (!_userInput.empty()) - { - _userInput.pop_back(); - } -} - -// Routine Description: -// - get numerical value for the data stored in the number buffer -// Return Value: -// - parsed integer representing the string value found in the number buffer -int CommandNumberPopup::_parse() const noexcept -{ - try - { - return std::stoi(_userInput); - } - catch (...) - { - return 0; - } -} diff --git a/src/host/CommandNumberPopup.hpp b/src/host/CommandNumberPopup.hpp deleted file mode 100644 index 3d0c0c22d8..0000000000 --- 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/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp index 2541cc8ab8..3621bca387 100644 --- a/src/host/ConsoleArguments.cpp +++ b/src/host/ConsoleArguments.cpp @@ -20,7 +20,6 @@ const std::wstring_view ConsoleArguments::WIDTH_ARG = L"--width"; const std::wstring_view ConsoleArguments::HEIGHT_ARG = L"--height"; const std::wstring_view ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor"; const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk"; -const std::wstring_view ConsoleArguments::WIN32_INPUT_MODE = L"--win32input"; const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature"; const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty"; const std::wstring_view ConsoleArguments::COM_SERVER_ARG = L"-Embedding"; @@ -515,12 +514,6 @@ void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector& args, _In s_ConsumeArg(args, i); hr = S_OK; } - else if (arg == WIN32_INPUT_MODE) - { - _win32InputMode = true; - s_ConsumeArg(args, i); - hr = S_OK; - } else if (arg == CLIENT_COMMANDLINE_ARG) { // Everything after this is the explicit commandline @@ -677,10 +670,6 @@ bool ConsoleArguments::IsResizeQuirkEnabled() const { return _resizeQuirk; } -bool ConsoleArguments::IsWin32InputModeEnabled() const -{ - return _win32InputMode; -} #ifdef UNIT_TESTING // Method Description: diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp index bb4a95b21e..8648c92ea6 100644 --- a/src/host/ConsoleArguments.hpp +++ b/src/host/ConsoleArguments.hpp @@ -55,7 +55,6 @@ public: short GetHeight() const; bool GetInheritCursor() const; bool IsResizeQuirkEnabled() const; - bool IsWin32InputModeEnabled() const; #ifdef UNIT_TESTING void EnableConptyModeForTests(); @@ -74,7 +73,6 @@ public: static const std::wstring_view HEIGHT_ARG; static const std::wstring_view INHERIT_CURSOR_ARG; static const std::wstring_view RESIZE_QUIRK; - static const std::wstring_view WIN32_INPUT_MODE; static const std::wstring_view FEATURE_ARG; static const std::wstring_view FEATURE_PTY_ARG; static const std::wstring_view COM_SERVER_ARG; @@ -144,7 +142,6 @@ private: DWORD _signalHandle; bool _inheritCursor; bool _resizeQuirk{ false }; - bool _win32InputMode{ false }; [[nodiscard]] HRESULT _GetClientCommandline(_Inout_ std::vector& args, const size_t index, diff --git a/src/host/CopyFromCharPopup.cpp b/src/host/CopyFromCharPopup.cpp deleted file mode 100644 index cb75980bc9..0000000000 --- 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 (!NT_SUCCESS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (PopupKeys && Char == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - const auto span = cookedReadData.SpanAtPointer(); - const auto foundLocation = std::find(std::next(span.begin()), span.end(), Char); - if (foundLocation == span.end()) - { - // char not found, delete everything to the right of the cursor - CommandLine::Instance().DeletePromptAfterCursor(cookedReadData); - } - else - { - // char was found, delete everything between the cursor and it - const auto difference = std::distance(span.begin(), foundLocation); - for (unsigned int i = 0; i < gsl::narrow(difference); ++i) - { - CommandLine::Instance().DeleteFromRightOfCursor(cookedReadData); - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyFromCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF4); -} diff --git a/src/host/CopyFromCharPopup.hpp b/src/host/CopyFromCharPopup.hpp deleted file mode 100644 index ee40f09be1..0000000000 --- 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 0d503d4f00..0000000000 --- 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 (!NT_SUCCESS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (popupKey && wch == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - // copy up to specified char - const auto lastCommand = cookedReadData.History().GetLastCommand(); - if (!lastCommand.empty()) - { - _copyToChar(cookedReadData, lastCommand, wch); - } - - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyToCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF2); -} diff --git a/src/host/CopyToCharPopup.hpp b/src/host/CopyToCharPopup.hpp deleted file mode 100644 index a1c490d3b3..0000000000 --- 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/CursorBlinker.cpp b/src/host/CursorBlinker.cpp index f9a809fd07..78f072ccc9 100644 --- a/src/host/CursorBlinker.cpp +++ b/src/host/CursorBlinker.cpp @@ -102,10 +102,10 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) const noexcept cursor.SetHasMoved(false); til::rect rc; - rc.left = (position.X - viewport.Left()) * fontSize.X; - rc.top = (position.Y - viewport.Top()) * fontSize.Y; - rc.right = rc.left + fontSize.X; - rc.bottom = rc.top + fontSize.Y; + rc.left = (position.x - viewport.Left()) * fontSize.width; + rc.top = (position.y - viewport.Top()) * fontSize.height; + rc.right = rc.left + fontSize.width; + rc.bottom = rc.top + fontSize.height; pAccessibilityNotifier->NotifyConsoleCaretEvent(rc); @@ -123,7 +123,7 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) const noexcept flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretVisible; } - pAccessibilityNotifier->NotifyConsoleCaretEvent(flags, MAKELONG(position.X, position.Y)); + pAccessibilityNotifier->NotifyConsoleCaretEvent(flags, MAKELONG(position.x, position.y)); } } diff --git a/src/host/PtySignalInputThread.cpp b/src/host/PtySignalInputThread.cpp index 935354ebf6..534a6d3ae8 100644 --- a/src/host/PtySignalInputThread.cpp +++ b/src/host/PtySignalInputThread.cpp @@ -72,7 +72,7 @@ void PtySignalInputThread::ConnectConsole() noexcept } if (_initialShowHide) { - _DoShowHide(_initialShowHide->show); + _DoShowHide(*_initialShowHide); } // We should have successfully used the _earlyReparent message in CreatePseudoWindow. @@ -96,108 +96,67 @@ void PtySignalInputThread::CreatePseudoWindow() // Return Value: // - S_OK if the thread runs to completion. // - Otherwise it may cause an application termination and never return. -[[nodiscard]] HRESULT PtySignalInputThread::_InputThread() +[[nodiscard]] HRESULT PtySignalInputThread::_InputThread() noexcept +try { - PtySignal signalId; - while (_GetData(&signalId, sizeof(signalId))) + const auto shutdown = wil::scope_exit([this]() { + _Shutdown(); + }); + + for (;;) { + PtySignal signalId; + if (!_GetData(&signalId, sizeof(signalId))) + { + return S_OK; + } + switch (signalId) { case PtySignal::ShowHideWindow: { ShowHideData msg = { 0 }; - _GetData(&msg, sizeof(msg)); - - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - - // If the client app hasn't yet connected, stash our initial - // visibility for when we do. We default to not being visible - if a - // terminal wants the ConPTY windows to start "visible", then they - // should send a ShowHidePseudoConsole(..., true) to tell us to - // initially be visible. - // - // Notably, if they don't, then a ShowWindow(SW_HIDE) on the ConPTY - // HWND will initially do _nothing_, because the OS will think that - // the window is already hidden. - if (!_consoleConnected) + if (!_GetData(&msg, sizeof(msg))) { - _initialShowHide = msg; - } - else - { - _DoShowHide(msg.show); + return S_OK; } + + _DoShowHide(msg); break; } case PtySignal::ClearBuffer: { - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - - // If the client app hasn't yet connected, stash the new size in the launchArgs. - // We'll later use the value in launchArgs to set up the console buffer - // We must be under lock here to ensure that someone else doesn't come in - // and set with `ConnectConsole` while we're looking and modifying this. - if (_consoleConnected) - { - _DoClearBuffer(); - } + _DoClearBuffer(); break; } case PtySignal::ResizeWindow: { ResizeWindowData resizeMsg = { 0 }; - _GetData(&resizeMsg, sizeof(resizeMsg)); - - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - - // If the client app hasn't yet connected, stash the new size in the launchArgs. - // We'll later use the value in launchArgs to set up the console buffer - // We must be under lock here to ensure that someone else doesn't come in - // and set with `ConnectConsole` while we're looking and modifying this. - if (!_consoleConnected) + if (!_GetData(&resizeMsg, sizeof(resizeMsg))) { - _earlyResize = resizeMsg; - } - else - { - _DoResizeWindow(resizeMsg); + return S_OK; } + _DoResizeWindow(resizeMsg); break; } case PtySignal::SetParent: { SetParentData reparentMessage = { 0 }; - _GetData(&reparentMessage, sizeof(reparentMessage)); - - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - - // If the client app hasn't yet connected, stash the new owner. - // We'll later (PtySignalInputThread::ConnectConsole) use the value - // to set up the owner of the conpty window. - if (!_consoleConnected) + if (!_GetData(&reparentMessage, sizeof(reparentMessage))) { - _earlyReparent = reparentMessage; - } - else - { - _DoSetWindowParent(reparentMessage); + return S_OK; } + _DoSetWindowParent(reparentMessage); break; } default: - { THROW_HR(E_UNEXPECTED); } - } } - return S_OK; } +CATCH_LOG_RETURN_HR(S_OK) // Method Description: // - Dispatches a resize window message to the rest of the console code @@ -207,6 +166,19 @@ void PtySignalInputThread::CreatePseudoWindow() // - void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data) { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If the client app hasn't yet connected, stash the new size in the launchArgs. + // We'll later use the value in launchArgs to set up the console buffer + // We must be under lock here to ensure that someone else doesn't come in + // and set with `ConnectConsole` while we're looking and modifying this. + if (!_consoleConnected) + { + _earlyResize = data; + return; + } + if (_api.ResizeWindow(data.sx, data.sy)) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -214,15 +186,45 @@ void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data) } } -void PtySignalInputThread::_DoClearBuffer() +void PtySignalInputThread::_DoClearBuffer() const { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If the client app hasn't yet connected, stash the new size in the launchArgs. + // We'll later use the value in launchArgs to set up the console buffer + // We must be under lock here to ensure that someone else doesn't come in + // and set with `ConnectConsole` while we're looking and modifying this. + if (!_consoleConnected) + { + return; + } + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); THROW_IF_FAILED(gci.GetActiveOutputBuffer().ClearBuffer()); } -void PtySignalInputThread::_DoShowHide(const bool show) +void PtySignalInputThread::_DoShowHide(const ShowHideData& data) { - _api.ShowWindow(show); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If the client app hasn't yet connected, stash our initial + // visibility for when we do. We default to not being visible - if a + // terminal wants the ConPTY windows to start "visible", then they + // should send a ShowHidePseudoConsole(..., true) to tell us to + // initially be visible. + // + // Notably, if they don't, then a ShowWindow(SW_HIDE) on the ConPTY + // HWND will initially do _nothing_, because the OS will think that + // the window is already hidden. + if (!_consoleConnected) + { + _initialShowHide = data; + return; + } + + _api.ShowWindow(data.show); } // Method Description: @@ -236,6 +238,18 @@ void PtySignalInputThread::_DoShowHide(const bool show) // - void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data) { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If the client app hasn't yet connected, stash the new owner. + // We'll later (PtySignalInputThread::ConnectConsole) use the value + // to set up the owner of the conpty window. + if (!_consoleConnected) + { + _earlyReparent = data; + return; + } + const auto owner{ reinterpret_cast(data.handle) }; // This will initialize s_interactivityFactory for us. It will also // conveniently return 0 when we're on OneCore. @@ -245,6 +259,12 @@ void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data) // window. if (const auto pseudoHwnd{ ServiceLocator::LocatePseudoWindow(owner) }) { + // SetWindowLongPtrW may call back into the message handler and wait for it to finish, + // similar to SendMessageW(). If the conhost message handler is already processing and + // waiting to acquire the console lock, which we're currently holding, we'd deadlock. + // --> Release the lock now. + Unlock.reset(); + // DO NOT USE SetParent HERE! // // Calling SetParent on a window that is WS_VISIBLE will cause the OS to @@ -257,7 +277,7 @@ void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data) // SetWindowLongPtr seems to do the job of changing who the window owner // is, without all the other side effects of reparenting the window. // See #13066 - ::SetWindowLongPtr(pseudoHwnd, GWLP_HWNDPARENT, reinterpret_cast(owner)); + ::SetWindowLongPtrW(pseudoHwnd, GWLP_HWNDPARENT, reinterpret_cast(owner)); } } @@ -269,29 +289,27 @@ void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data) // - cbBuffer - Count of bytes in the given buffer. // Return Value: // - True if data was retrieved successfully. False otherwise. -bool PtySignalInputThread::_GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, - const DWORD cbBuffer) +[[nodiscard]] bool PtySignalInputThread::_GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer) { + if (!_hFile) + { + return false; + } + DWORD dwRead = 0; - // If we failed to read because the terminal broke our pipe (usually due - // to dying itself), close gracefully with ERROR_BROKEN_PIPE. - // Otherwise throw an exception. ERROR_BROKEN_PIPE is the only case that - // we want to gracefully close in. if (FALSE == ReadFile(_hFile.get(), pBuffer, cbBuffer, &dwRead, nullptr)) { - auto lastError = GetLastError(); - if (lastError == ERROR_BROKEN_PIPE) + if (const auto err = GetLastError(); err != ERROR_BROKEN_PIPE) { - _Shutdown(); - } - else - { - THROW_WIN32(lastError); + LOG_WIN32(err); } + _hFile.reset(); + return false; } - else if (dwRead != cbBuffer) + + if (dwRead != cbBuffer) { - _Shutdown(); + return false; } return true; @@ -326,27 +344,12 @@ bool PtySignalInputThread::_GetData(_Out_writes_bytes_(cbBuffer) void* const pBu // - Perform a shutdown of the console. This happens when the signal pipe is // broken, which means either the parent terminal process has died, or they // called ClosePseudoConsole. -// CloseConsoleProcessState is not enough by itself - it will disconnect -// clients as if the X was pressed, but then we need to actually still die, -// so then call RundownAndExit. // Arguments: // - // Return Value: // - void PtySignalInputThread::_Shutdown() { - // Trigger process shutdown. - CloseConsoleProcessState(); - - // If we haven't terminated by now, that's because there's a client that's still attached. - // Force the handling of the control events by the attached clients. - // As of MSFT:19419231, CloseConsoleProcessState will make sure this - // happens if this method is called outside of lock, but if we're - // currently locked, we want to make sure ctrl events are handled - // _before_ we RundownAndExit. - LockConsole(); - ProcessCtrlEvents(); - - // Make sure we terminate. - ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.GetVtIo()->SendCloseEvent(); } diff --git a/src/host/PtySignalInputThread.hpp b/src/host/PtySignalInputThread.hpp index f9f4268bb9..0534c0600f 100644 --- a/src/host/PtySignalInputThread.hpp +++ b/src/host/PtySignalInputThread.hpp @@ -60,12 +60,12 @@ namespace Microsoft::Console uint64_t handle; }; - [[nodiscard]] HRESULT _InputThread(); - bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer); + [[nodiscard]] HRESULT _InputThread() noexcept; + [[nodiscard]] bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer); void _DoResizeWindow(const ResizeWindowData& data); void _DoSetWindowParent(const SetParentData& data); - void _DoClearBuffer(); - void _DoShowHide(const bool show); + void _DoClearBuffer() const; + void _DoShowHide(const ShowHideData& data); void _Shutdown(); wil::unique_hfile _hFile; diff --git a/src/host/VtApiRoutines.cpp b/src/host/VtApiRoutines.cpp index e01696e384..fe7d0b7060 100644 --- a/src/host/VtApiRoutines.cpp +++ b/src/host/VtApiRoutines.cpp @@ -105,86 +105,33 @@ void VtApiRoutines::_SynchronizeCursor(std::unique_ptr& waiter) no } } -[[nodiscard]] HRESULT VtApiRoutines::PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleInputImpl( + IConsoleInputObject& context, + InputEventQueue& outEvents, + const size_t eventReadCount, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const bool IsPeek, + std::unique_ptr& waiter) noexcept { - const auto hr = m_pUsualRoutines->PeekConsoleInputAImpl(context, outEvents, eventsToRead, readHandleState, waiter); + const auto hr = m_pUsualRoutines->GetConsoleInputImpl(context, outEvents, eventReadCount, readHandleState, IsUnicode, IsPeek, waiter); _SynchronizeCursor(waiter); return hr; } -[[nodiscard]] HRESULT VtApiRoutines::PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept +[[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->PeekConsoleInputWImpl(context, outEvents, eventsToRead, readHandleState, waiter); - _SynchronizeCursor(waiter); - return hr; -} - -[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - const auto hr = m_pUsualRoutines->ReadConsoleInputAImpl(context, outEvents, eventsToRead, readHandleState, waiter); - _SynchronizeCursor(waiter); - return hr; -} - -[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - const auto hr = m_pUsualRoutines->ReadConsoleInputWImpl(context, outEvents, eventsToRead, readHandleState, waiter); - _SynchronizeCursor(waiter); - return hr; -} - -[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - 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, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - const auto hr = m_pUsualRoutines->ReadConsoleWImpl(context, buffer, written, waiter, initialData, exeName, readHandleState, clientHandle, controlWakeupMask, controlKeyState); + 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. @@ -426,14 +373,14 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co const bool isAbsolute, const til::inclusive_rect& windowRect) noexcept { - (void)m_pVtEngine->_ResizeWindow(windowRect.Right - windowRect.Left + 1, windowRect.Bottom - windowRect.Top + 1); + (void)m_pVtEngine->_ResizeWindow(windowRect.right - windowRect.left + 1, windowRect.bottom - windowRect.top + 1); (void)m_pVtEngine->_Flush(); return S_OK; } [[nodiscard]] HRESULT VtApiRoutines::ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { std::fill_n(buffer.data(), buffer.size(), s_readBackUnicode.Attributes); // should be same as the ascii one. @@ -443,7 +390,7 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co [[nodiscard]] HRESULT VtApiRoutines::ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { std::fill_n(buffer.data(), buffer.size(), s_readBackAscii.Char.AsciiChar); @@ -453,7 +400,7 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co [[nodiscard]] HRESULT VtApiRoutines::ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { std::fill_n(buffer.data(), buffer.size(), s_readBackUnicode.Char.UnicodeChar); @@ -462,7 +409,7 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co } [[nodiscard]] HRESULT VtApiRoutines::WriteConsoleInputAImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept { @@ -470,7 +417,7 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co } [[nodiscard]] HRESULT VtApiRoutines::WriteConsoleInputWImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept { @@ -478,11 +425,11 @@ void VtApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& co } extern HRESULT _ConvertCellsToWInplace(const UINT codepage, - gsl::span buffer, + std::span buffer, const Viewport& rectangle) noexcept; [[nodiscard]] HRESULT VtApiRoutines::WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept { @@ -494,7 +441,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, } [[nodiscard]] HRESULT VtApiRoutines::WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept { @@ -516,7 +463,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, (void)m_pVtEngine->WriteTerminalW(std::wstring_view{ &ci.Char.UnicodeChar, 1 }); } - ++cursor.Y; + ++cursor.y; pos += width; } @@ -528,7 +475,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, } [[nodiscard]] HRESULT VtApiRoutines::WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, - const gsl::span attrs, + const std::span attrs, const til::point target, size_t& used) noexcept { @@ -577,7 +524,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, } [[nodiscard]] HRESULT VtApiRoutines::ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept { @@ -587,7 +534,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, } [[nodiscard]] HRESULT VtApiRoutines::ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept { @@ -596,7 +543,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, return S_OK; } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleTitleAImpl(gsl::span title, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -611,7 +558,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, return S_OK; } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleTitleWImpl(gsl::span title, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -626,7 +573,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, return S_OK; } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleOriginalTitleAImpl(gsl::span title, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleOriginalTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -641,7 +588,7 @@ extern HRESULT _ConvertCellsToWInplace(const UINT codepage, return S_OK; } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleOriginalTitleWImpl(gsl::span title, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleOriginalTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -678,8 +625,8 @@ void VtApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept const DWORD index, til::size& size) noexcept { - size.X = 8; - size.Y = 12; + size.width = 8; + size.height = 12; return S_OK; } @@ -719,7 +666,7 @@ void VtApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasAImpl(const std::string_view source, - gsl::span target, + std::span target, size_t& written, const std::string_view exeName) noexcept { @@ -727,7 +674,7 @@ void VtApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasWImpl(const std::wstring_view source, - gsl::span target, + std::span target, size_t& written, const std::wstring_view exeName) noexcept { @@ -757,26 +704,26 @@ void VtApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasesAImpl(const std::string_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleAliasesAImpl(exeName, alias, written); } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasesWImpl(const std::wstring_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleAliasesWImpl(exeName, alias, written); } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasExesAImpl(gsl::span aliasExes, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasExesAImpl(std::span aliasExes, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleAliasExesAImpl(aliasExes, written); } -[[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasExesWImpl(gsl::span aliasExes, +[[nodiscard]] HRESULT VtApiRoutines::GetConsoleAliasExesWImpl(std::span aliasExes, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleAliasExesWImpl(aliasExes, written); @@ -817,14 +764,14 @@ void VtApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleCommandHistoryAImpl(const std::string_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleCommandHistoryAImpl(exeName, commandHistory, written); } [[nodiscard]] HRESULT VtApiRoutines::GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept { return m_pUsualRoutines->GetConsoleCommandHistoryWImpl(exeName, commandHistory, written); diff --git a/src/host/VtApiRoutines.h b/src/host/VtApiRoutines.h index 79962bc989..7dc77f78b0 100644 --- a/src/host/VtApiRoutines.h +++ b/src/host/VtApiRoutines.h @@ -52,51 +52,25 @@ public: [[nodiscard]] HRESULT GetNumberOfConsoleInputEventsImpl(const InputBuffer& context, ULONG& events) noexcept override; - [[nodiscard]] HRESULT PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; + [[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context, + InputEventQueue& outEvents, + const size_t eventReadCount, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const bool IsPeek, + std::unique_ptr& waiter) noexcept override; - [[nodiscard]] HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; - - [[nodiscard]] HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept override; + [[nodiscard]] HRESULT 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, @@ -197,41 +171,41 @@ public: [[nodiscard]] HRESULT ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept override; [[nodiscard]] HRESULT WriteConsoleInputAImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept override; [[nodiscard]] HRESULT WriteConsoleInputWImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; [[nodiscard]] HRESULT WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, - const gsl::span attrs, + const std::span attrs, const til::point target, size_t& used) noexcept override; @@ -246,28 +220,28 @@ public: size_t& used) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept override; [[nodiscard]] HRESULT ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept override; - [[nodiscard]] HRESULT GetConsoleTitleAImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleTitleWImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleOriginalTitleAImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleOriginalTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept override; - [[nodiscard]] HRESULT GetConsoleOriginalTitleWImpl(gsl::span title, + [[nodiscard]] HRESULT GetConsoleOriginalTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept override; @@ -304,12 +278,12 @@ public: const std::wstring_view exeName) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasAImpl(const std::string_view source, - gsl::span target, + std::span target, size_t& written, const std::string_view exeName) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasWImpl(const std::wstring_view source, - gsl::span target, + std::span target, size_t& written, const std::wstring_view exeName) noexcept override; @@ -324,17 +298,17 @@ public: [[nodiscard]] HRESULT GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasesAImpl(const std::string_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept override; [[nodiscard]] HRESULT GetConsoleAliasesWImpl(const std::wstring_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept override; - [[nodiscard]] HRESULT GetConsoleAliasExesAImpl(gsl::span aliasExes, + [[nodiscard]] HRESULT GetConsoleAliasExesAImpl(std::span aliasExes, size_t& written) noexcept override; - [[nodiscard]] HRESULT GetConsoleAliasExesWImpl(gsl::span aliasExes, + [[nodiscard]] HRESULT GetConsoleAliasExesWImpl(std::span aliasExes, size_t& written) noexcept override; #pragma region CMDext Private API @@ -356,11 +330,11 @@ public: size_t& length) noexcept override; [[nodiscard]] HRESULT GetConsoleCommandHistoryAImpl(const std::string_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept override; [[nodiscard]] HRESULT GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept override; #pragma endregion diff --git a/src/host/VtInputThread.cpp b/src/host/VtInputThread.cpp index 9b2c8b3bdf..495a6e51d8 100644 --- a/src/host/VtInputThread.cpp +++ b/src/host/VtInputThread.cpp @@ -94,6 +94,7 @@ DWORD WINAPI VtInputThread::StaticVtInputThreadProc(_In_ LPVOID lpParameter) { const auto pInstance = reinterpret_cast(lpParameter); pInstance->_InputThread(); + return S_OK; } // Method Description: @@ -110,10 +111,6 @@ void VtInputThread::DoReadInput(const bool throwOnFail) DWORD dwRead = 0; auto fSuccess = !!ReadFile(_hFile.get(), buffer, ARRAYSIZE(buffer), &dwRead, nullptr); - // If we failed to read because the terminal broke our pipe (usually due - // to dying itself), close gracefully with ERROR_BROKEN_PIPE. - // Otherwise throw an exception. ERROR_BROKEN_PIPE is the only case that - // we want to gracefully close in. if (!fSuccess) { _exitRequested = true; diff --git a/src/host/VtInputThread.hpp b/src/host/VtInputThread.hpp index 98741ff7e7..7c075b7002 100644 --- a/src/host/VtInputThread.hpp +++ b/src/host/VtInputThread.hpp @@ -30,7 +30,7 @@ namespace Microsoft::Console private: [[nodiscard]] HRESULT _HandleRunInput(const std::string_view u8Str); - [[noreturn]] void _InputThread(); + void _InputThread(); wil::unique_hfile _hFile; wil::unique_handle _hThread; diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index fcb97f4aac..dd8b635efd 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -25,7 +25,6 @@ using namespace Microsoft::Console::Interactivity; VtIo::VtIo() : _initialized(false), - _objectsCreated(false), _lookingForCursorPosition(false), _IoMode(VtIoMode::INVALID) { @@ -72,7 +71,6 @@ VtIo::VtIo() : { _lookingForCursorPosition = pArgs->GetInheritCursor(); _resizeQuirk = pArgs->IsResizeQuirkEnabled(); - _win32InputMode = pArgs->IsWin32InputModeEnabled(); _passthroughMode = pArgs->IsPassthroughMode(); // If we were already given VT handles, set up the VT IO engine to use those. @@ -156,8 +154,8 @@ VtIo::VtIo() : if (IsValidHandle(_hOutput.get())) { auto initialViewport = Viewport::FromDimensions({ 0, 0 }, - gci.GetWindowSize().X, - gci.GetWindowSize().Y); + gci.GetWindowSize().width, + gci.GetWindowSize().height); switch (_IoMode) { case VtIoMode::XTERM_256: @@ -224,13 +222,12 @@ VtIo::VtIo() : } CATCH_RETURN(); - _objectsCreated = true; return S_OK; } bool VtIo::IsUsingVt() const { - return _objectsCreated; + return _initialized; } // Routine Description: @@ -246,7 +243,7 @@ bool VtIo::IsUsingVt() const [[nodiscard]] HRESULT VtIo::StartIfNeeded() { // If we haven't been set up, do nothing (because there's nothing to start) - if (!_objectsCreated) + if (!_initialized) { return S_FALSE; } @@ -258,7 +255,6 @@ bool VtIo::IsUsingVt() const { g.pRender->AddRenderEngine(_pVtRenderEngine.get()); g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get()); - g.getConsoleInformation().GetActiveInputBuffer()->SetTerminalConnection(_pVtRenderEngine.get()); // Force the whole window to be put together first. // We don't really need the handle, we just want to leverage the setup steps. @@ -271,10 +267,7 @@ bool VtIo::IsUsingVt() const // win32-input-mode from them. This will enable the connected terminal to // send us full INPUT_RECORDs as input. If the terminal doesn't understand // this sequence, it'll just ignore it. - if (_win32InputMode) - { - LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input()); - } + LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input()); // MSFT: 15813316 // If the terminal application wants us to inherit the cursor position, @@ -446,7 +439,7 @@ void VtIo::SetWindowVisibility(bool showOrHide) noexcept void VtIo::CloseInput() { _pVtInputThread = nullptr; - _shutdownNow(); + SendCloseEvent(); } void VtIo::CloseOutput() @@ -455,59 +448,29 @@ void VtIo::CloseOutput() g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(nullptr); } -void VtIo::_shutdownNow() +void VtIo::SendCloseEvent() { - // At this point, we no longer have a renderer or inthread. So we've - // effectively been disconnected from the terminal. - - // If we have any remaining attached processes, this will prepare us to send a ctrl+close to them - // if we don't, this will cause us to rundown and exit. - CloseConsoleProcessState(); - - // If we haven't terminated by now, that's because there's a client that's still attached. - // Force the handling of the control events by the attached clients. - // As of MSFT:19419231, CloseConsoleProcessState will make sure this - // happens if this method is called outside of lock, but if we're - // currently locked, we want to make sure ctrl events are handled - // _before_ we RundownAndExit. LockConsole(); - ProcessCtrlEvents(); + const auto unlock = wil::scope_exit([] { UnlockConsole(); }); - // Make sure we terminate. - ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE); -} - -// Method Description: -// - Tell the vt renderer to begin a resize operation. During a resize -// operation, the vt renderer should _not_ request to be repainted during a -// text buffer circling event. Any callers of this method should make sure to -// call EndResize to make sure the renderer returns to normal behavior. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtIo::BeginResize() -{ - if (_pVtRenderEngine) + // This function is called when the ConPTY signal pipe is closed (PtySignalInputThread) and when the input + // pipe is closed (VtIo). Usually these two happen at about the same time. This if condition is a bit of + // a premature optimization and prevents us from sending out a CTRL_CLOSE_EVENT right after another. + if (!std::exchange(_closeEventSent, true)) { - _pVtRenderEngine->BeginResizeRequest(); + CloseConsoleProcessState(); } } -// Method Description: -// - Tell the vt renderer to end a resize operation. -// See BeginResize for more details. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtIo::EndResize() +// The name of this method is an analogy to TCP_CORK. It instructs +// the VT renderer to stop flushing its buffer to the output pipe. +// Don't forget to uncork it! +void VtIo::CorkRenderer(bool corked) const noexcept { - if (_pVtRenderEngine) + _pVtRenderEngine->Cork(corked); + if (!corked) { - _pVtRenderEngine->EndResizeRequest(); + LOG_IF_FAILED(ServiceLocator::LocateGlobals().pRender->PaintFrame()); } } @@ -522,7 +485,7 @@ void VtIo::EndResize() // - void VtIo::EnableConptyModeForTests(std::unique_ptr vtRenderEngine) { - _objectsCreated = true; + _initialized = true; _pVtRenderEngine = std::move(vtRenderEngine); } #endif @@ -531,7 +494,7 @@ void VtIo::EnableConptyModeForTests(std::unique_ptrRequestMouseMode(enable); + } + return S_OK; +} diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index da1099704b..2ecc0d5175 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -32,17 +32,15 @@ namespace Microsoft::Console::VirtualTerminal [[nodiscard]] HRESULT StartIfNeeded(); [[nodiscard]] static HRESULT ParseIoMode(const std::wstring& VtMode, _Out_ VtIoMode& ioMode); - [[nodiscard]] HRESULT SuppressResizeRepaint(); [[nodiscard]] HRESULT SetCursorPosition(const til::point coordCursor); - [[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer); + void SendCloseEvent(); - [[noreturn]] void CloseInput(); + void CloseInput(); void CloseOutput(); - void BeginResize(); - void EndResize(); + void CorkRenderer(bool corked) const noexcept; #ifdef UNIT_TESTING void EnableConptyModeForTests(std::unique_ptr vtRenderEngine); @@ -51,6 +49,7 @@ namespace Microsoft::Console::VirtualTerminal bool IsResizeQuirkEnabled() const; [[nodiscard]] HRESULT ManuallyClearScrollback() const noexcept; + [[nodiscard]] HRESULT RequestMouseMode(bool enable) const noexcept; void CreatePseudoWindow(); void SetWindowVisibility(bool showOrHide) noexcept; @@ -64,13 +63,12 @@ namespace Microsoft::Console::VirtualTerminal VtIoMode _IoMode; bool _initialized; - bool _objectsCreated; bool _lookingForCursorPosition; bool _resizeQuirk{ false }; - bool _win32InputMode{ false }; bool _passthroughMode{ false }; + bool _closeEventSent{ false }; std::unique_ptr _pVtRenderEngine; std::unique_ptr _pVtInputThread; @@ -78,8 +76,6 @@ namespace Microsoft::Console::VirtualTerminal [[nodiscard]] HRESULT _Initialize(const HANDLE InHandle, const HANDLE OutHandle, const std::wstring& VtMode, _In_opt_ const HANDLE SignalHandle); - [[noreturn]] void _shutdownNow(); - #ifdef UNIT_TESTING friend class VtIoTests; #endif diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 38b5a66f65..8ad7117ea0 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -9,12 +9,9 @@ #include "handle.h" #include "misc.h" -#include "../buffer/out/CharRow.hpp" - #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/Viewport.hpp" #include "../types/inc/convert.hpp" -#include "../types/inc/Utf16Parser.hpp" #include #include @@ -69,7 +66,7 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) // Return Value: // - S_OK, E_INVALIDARG or similar HRESULT error. [[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, - const gsl::span attrs, + const std::span attrs, const til::point target, size_t& used) noexcept { @@ -230,7 +227,7 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) // Notify accessibility auto endingCoordinate = startingCoordinate; bufferSize.MoveInBounds(cellsModifiedCoord, endingCoordinate); - screenBuffer.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y); + screenBuffer.NotifyAccessibilityEventing(startingCoordinate.x, startingCoordinate.y, endingCoordinate.x, endingCoordinate.y); } } CATCH_RETURN(); @@ -294,7 +291,7 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) { auto endingCoordinate = startingCoordinate; bufferSize.MoveInBounds(cellsModifiedCoord, endingCoordinate); - screenInfo.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y); + screenInfo.NotifyAccessibilityEventing(startingCoordinate.x, startingCoordinate.y, endingCoordinate.x, endingCoordinate.y); } // GH#3126 - This is a shim for powershell's `Clear-Host` function. In diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 5a6671dd74..bf9e32ed38 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -39,959 +39,266 @@ using Microsoft::Console::VirtualTerminal::StateMachine; // - coordCursor - New location of cursor. // - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge // Return Value: -[[nodiscard]] NTSTATUS AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, - _In_ til::point coordCursor, - const BOOL fKeepCursorVisible, - _Inout_opt_ til::CoordType* psScrollY) +static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const bool interactive, _Inout_opt_ til::CoordType* psScrollY) { - const auto inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); - if (coordCursor.X < 0) + if (coordCursor.x < 0) { - if (coordCursor.Y > 0) + if (coordCursor.y > 0) { - coordCursor.X = bufferSize.X + coordCursor.X; - coordCursor.Y = coordCursor.Y - 1; + coordCursor.x = bufferSize.width + coordCursor.x; + coordCursor.y = coordCursor.y - 1; } else { - coordCursor.X = 0; + coordCursor.x = 0; } } - else if (coordCursor.X >= bufferSize.X) + else if (coordCursor.x >= bufferSize.width) { // at end of line. if wrap mode, wrap cursor. otherwise leave it where it is. if (screenInfo.OutputMode & ENABLE_WRAP_AT_EOL_OUTPUT) { - coordCursor.Y += coordCursor.X / bufferSize.X; - coordCursor.X = coordCursor.X % bufferSize.X; + coordCursor.y += coordCursor.x / bufferSize.width; + coordCursor.x = coordCursor.x % bufferSize.width; } else { - if (inVtMode) + coordCursor.x = screenInfo.GetTextBuffer().GetCursor().GetPosition().x; + } + } + + if (coordCursor.y >= bufferSize.height) + { + 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) { - // In VT mode, the cursor must be left in the last column. - coordCursor.X = bufferSize.X - 1; - } - else - { - // For legacy apps, it is left where it was at the start of the write. - coordCursor.X = screenInfo.GetTextBuffer().GetCursor().GetPosition().X; + 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 (psScrollY) + { + *psScrollY += 1; + } + + coordCursor.y = bufferSize.height - 1; } - // The VT standard requires the lines revealed when scrolling are filled - // with the current background color, but with no meta attributes set. - auto fillAttributes = screenInfo.GetAttributes(); - fillAttributes.SetStandardErase(); + const auto cursorMovedPastViewport = coordCursor.y > screenInfo.GetViewport().BottomInclusive(); - const auto relativeMargins = screenInfo.GetRelativeScrollMargins(); - auto viewport = screenInfo.GetViewport(); - auto srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); - const auto fMarginsSet = srMargins.Bottom > srMargins.Top; - auto currentCursor = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - const auto iCurrentCursorY = currentCursor.Y; - - const auto fCursorInMargins = iCurrentCursorY <= srMargins.Bottom && iCurrentCursorY >= srMargins.Top; - const auto cursorAboveViewport = coordCursor.Y < 0 && inVtMode; - const auto fScrollDown = fMarginsSet && fCursorInMargins && (coordCursor.Y > srMargins.Bottom); - auto fScrollUp = fMarginsSet && fCursorInMargins && (coordCursor.Y < srMargins.Top); - - const auto fScrollUpWithoutMargins = (!fMarginsSet) && cursorAboveViewport; - // if we're in VT mode, AND MARGINS AREN'T SET and a Reverse Line Feed took the cursor up past the top of the viewport, - // VT style scroll the contents of the screen. - // This can happen in applications like `less`, that don't set margins, because they're going to - // scroll the entire screen anyways, so no need for them to ever set the margins. - if (fScrollUpWithoutMargins) + // if at right or bottom edge of window, scroll right or down one char. + if (cursorMovedPastViewport) { - fScrollUp = true; - srMargins.Top = 0; - srMargins.Bottom = screenInfo.GetViewport().BottomInclusive(); + til::point WindowOrigin; + WindowOrigin.x = 0; + WindowOrigin.y = coordCursor.y - screenInfo.GetViewport().BottomInclusive(); + LOG_IF_FAILED(screenInfo.SetViewportOrigin(false, WindowOrigin, true)); } - const auto scrollDownAtTop = fScrollDown && relativeMargins.Top() == 0; - if (scrollDownAtTop) + if (interactive) { - // We're trying to scroll down, and the top margin is at the top of the viewport. - // In this case, we want the lines that are "scrolled off" to appear in - // the scrollback instead of being discarded. - // To do this, we're going to scroll everything starting at the bottom - // margin down, then move the viewport down. - - const auto delta = coordCursor.Y - srMargins.Bottom; - til::inclusive_rect scrollRect; - scrollRect.Left = 0; - scrollRect.Top = srMargins.Bottom + 1; // One below margins - scrollRect.Bottom = bufferSize.Y - 1; // -1, otherwise this would be an exclusive rect. - scrollRect.Right = bufferSize.X - 1; // -1, otherwise this would be an exclusive rect. - - // This is the Y position we're moving the contents below the bottom margin to. - auto moveToYPosition = scrollRect.Top + delta; - - // This is where the viewport will need to be to give the effect of - // scrolling the contents in the margins. - auto newViewTop = viewport.Top() + delta; - - // This is how many new lines need to be added to the buffer to support this operation. - const auto newRows = (viewport.BottomExclusive() + delta) - bufferSize.Y; - - // If we're near the bottom of the buffer, we might need to insert some - // new rows at the bottom. - // If we do this, then the viewport is now one line higher than it used - // to be, so it needs to move down by one less line. - for (auto i = 0; i < newRows; i++) - { - screenInfo.GetTextBuffer().IncrementCircularBuffer(); - moveToYPosition--; - newViewTop--; - scrollRect.Top--; - } - - const til::point newPostMarginsOrigin{ 0, moveToYPosition }; - const til::point newViewOrigin{ 0, newViewTop }; - - try - { - ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes); - } - CATCH_LOG(); - - // Move the viewport down - auto hr = screenInfo.SetViewportOrigin(true, newViewOrigin, true); - if (FAILED(hr)) - { - return NTSTATUS_FROM_HRESULT(hr); - } - // If we didn't actually move the viewport, it's because we're at the - // bottom of the buffer, and the top lines of the viewport have - // changed. Manually invalidate here, to make sure the screen - // displays the correct text. - if (newViewOrigin == viewport.Origin()) - { - // Inside this block, we're shifting down at the bottom. - // This means that we had something like this: - // AAAA - // BBBB - // CCCC - // DDDD - // EEEE - // - // Our margins were set for lines A-D, but not on line E. - // So we circled the whole buffer up by one: - // BBBB - // CCCC - // DDDD - // EEEE - // - // - // Then we scrolled the contents of everything OUTSIDE the margin frame down. - // BBBB - // CCCC - // DDDD - // - // EEEE - // - // And now we need to report that only the bottom line didn't "move" as we put the EEEE - // back where it started, but everything else moved. - // In this case, delta was 1. So the amount that moved is the entire viewport height minus the delta. - auto invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), viewport.Height() - delta }); - screenInfo.GetTextBuffer().TriggerRedraw(invalid); - } - - // reset where our local viewport is, and recalculate the cursor and - // margin positions. - viewport = screenInfo.GetViewport(); - if (newRows > 0) - { - currentCursor.Y -= newRows; - coordCursor.Y -= newRows; - } - srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); + screenInfo.MakeCursorVisible(coordCursor); } - - // If we did the above scrollDownAtTop case, then we've already scrolled - // the margins content, and we can skip this. - if (fScrollUp || (fScrollDown && !scrollDownAtTop)) - { - auto diff = coordCursor.Y - (fScrollUp ? srMargins.Top : srMargins.Bottom); - - til::inclusive_rect scrollRect; - scrollRect.Top = srMargins.Top; - scrollRect.Bottom = srMargins.Bottom; - scrollRect.Left = 0; // NOTE: Left/Right Scroll margins don't do anything currently. - scrollRect.Right = bufferSize.X - 1; // -1, otherwise this would be an exclusive rect. - - til::point dest; - dest.X = scrollRect.Left; - dest.Y = scrollRect.Top - diff; - - try - { - ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes); - } - CATCH_LOG(); - - coordCursor.Y -= diff; - } - - // If the margins are set, then it shouldn't be possible for the cursor to - // move below the bottom of the viewport. Either it should be constrained - // inside the margins by one of the scrollDown cases handled above, or - // we'll need to clamp it inside the viewport here. - if (fMarginsSet && coordCursor.Y > viewport.BottomInclusive()) - { - coordCursor.Y = viewport.BottomInclusive(); - } - - auto Status = STATUS_SUCCESS; - - if (coordCursor.Y >= bufferSize.Y) - { - // At the end of the buffer. Scroll contents of screen buffer so new position is visible. - FAIL_FAST_IF(!(coordCursor.Y == bufferSize.Y)); - if (!StreamScrollRegion(screenInfo)) - { - Status = STATUS_NO_MEMORY; - } - - if (nullptr != psScrollY) - { - *psScrollY += bufferSize.Y - coordCursor.Y - 1; - } - coordCursor.Y += bufferSize.Y - coordCursor.Y - 1; - } - - const auto cursorMovedPastViewport = coordCursor.Y > screenInfo.GetViewport().BottomInclusive(); - const auto cursorMovedPastVirtualViewport = coordCursor.Y > screenInfo.GetVirtualViewport().BottomInclusive(); - if (NT_SUCCESS(Status)) - { - // if at right or bottom edge of window, scroll right or down one char. - if (cursorMovedPastViewport) - { - til::point WindowOrigin; - WindowOrigin.X = 0; - WindowOrigin.Y = coordCursor.Y - screenInfo.GetViewport().BottomInclusive(); - Status = screenInfo.SetViewportOrigin(false, WindowOrigin, true); - } - } - - if (NT_SUCCESS(Status)) - { - if (fKeepCursorVisible) - { - screenInfo.MakeCursorVisible(coordCursor); - } - Status = screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible); - - // MSFT:19989333 - Only re-initialize the cursor row if the cursor moved - // below the terminal section of the buffer (the virtual viewport), - // and the visible part of the buffer (the actual viewport). - // If this is only cursorMovedPastViewport, and you scroll up, then type - // a character, we'll re-initialize the line the cursor is on. - // If this is only cursorMovedPastVirtualViewport and you scroll down, - // (with terminal scrolling disabled) then all lines newly exposed - // will get their attributes constantly cleared out. - // Both cursorMovedPastViewport and cursorMovedPastVirtualViewport works - if (inVtMode && cursorMovedPastViewport && cursorMovedPastVirtualViewport) - { - screenInfo.InitializeCursorRowAttributes(); - } - } - - return Status; + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, interactive)); } -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters. -// WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge -// WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X") -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[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) +// As the name implies, this writes text without processing its control characters. +static void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY) { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); + const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing(); + auto& textBuffer = screenInfo.GetTextBuffer(); + + RowWriteState state{ + .text = text, + .columnLimit = textBuffer.GetSize().RightExclusive(), + }; + + while (!state.text.empty()) + { + auto cursorPosition = textBuffer.GetCursor().GetPosition(); + + state.columnBegin = cursorPosition.x; + textBuffer.Write(cursorPosition.y, textBuffer.GetCurrentAttributes(), state); + cursorPosition.x = state.columnEnd; + + if (wrapAtEOL && state.columnEnd >= state.columnLimit) + { + textBuffer.SetWrapForced(cursorPosition.y, true); + } + + if (hasAccessibilityEventing && state.columnEnd > state.columnBegin) + { + screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y); + } + + AdjustCursorPosition(screenInfo, cursorPosition, interactive, psScrollY); + } +} + +// 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(); - auto CursorPosition = cursor.GetPosition(); - auto Status = STATUS_SUCCESS; - til::CoordType XPosition; - size_t TempNumSpaces = 0; - const auto fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT); - const auto fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); + const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); + auto it = text.begin(); + const auto end = text.end(); - // Must not adjust cursor here. It has to stay on for many write scenarios. Consumers should call for the - // cursor to be turned off if they want that. - - const auto Attributes = screenInfo.GetAttributes(); - const auto BufferSize = *pcb; - *pcb = 0; - - auto lpString = pwchRealUnicode; - - auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); - // In VT mode, the width at which we wrap is determined by the line rendition attribute. - if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + // In VT mode, when you have a 120-column terminal you can write 120 columns without the cursor wrapping. + // Whenever the cursor is in that 120th column IsDelayedEOLWrap() will return true. I'm not sure why the VT parts + // of the code base store this as a boolean. It's also unclear why we handle this here. The intention is likely + // so that when we exit VT mode and receive a write a potentially stored delayed wrap would still be handled. + // The way this code does it however isn't correct since it handles it like the old console APIs would and + // so writing a newline while being delay wrapped will print 2 newlines. + if (cursor.IsDelayedEOLWrap() && wrapAtEOL) { - coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); + auto pos = cursor.GetPosition(); + const auto delayed = cursor.GetDelayedAtPosition(); + cursor.ResetDelayEOLWrap(); + if (delayed == pos) + { + pos.x = 0; + pos.y++; + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); + } } - static constexpr til::CoordType LOCAL_BUFFER_SIZE = 1024; - WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; - - while (*pcb < BufferSize) + // If ENABLE_PROCESSED_OUTPUT is set we search for C0 control characters and handle them like backspace, tab, etc. + // If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed. + if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) { - // correct for delayed EOL - if (cursor.IsDelayedEOLWrap() && fWrapAtEOL) + _writeCharsLegacyUnprocessed(screenInfo, { it, end }, interactive, psScrollY); + it = end; + } + + while (it != end) + { + const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); }); + if (nextControlChar != it) { - const auto coordDelayedAt = cursor.GetDelayedAtPosition(); - cursor.ResetDelayEOLWrap(); - // Only act on a delayed EOL if we didn't move the cursor to a different position from where the EOL was marked. - if (coordDelayedAt.X == CursorPosition.X && coordDelayedAt.Y == CursorPosition.Y) - { - CursorPosition.X = 0; - CursorPosition.Y++; - - Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); - - CursorPosition = cursor.GetPosition(); - // In VT mode, we need to recalculate the width when moving to a new line. - if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) - { - coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); - } - } + _writeCharsLegacyUnprocessed(screenInfo, { it, nextControlChar }, interactive, psScrollY); + it = nextControlChar; } - // As an optimization, collect characters in buffer and print out all at once. - XPosition = cursor.GetPosition().X; - til::CoordType i = 0; - auto LocalBufPtr = LocalBuffer; - while (*pcb < BufferSize && i < LOCAL_BUFFER_SIZE && XPosition < coordScreenBufferSize.X) + for (; it != end && !IS_GLYPH_CHAR(*it); ++it) { -#pragma prefast(suppress : 26019, "Buffer is taken in multiples of 2. Validation is ok.") - const auto Char = *lpString; - // WCL-NOTE: We believe RealUnicodeChar to be identical to Char, because we believe pwchRealUnicode - // WCL-NOTE: to be identical to lpString. They are incremented in lockstep, never separately, and lpString - // WCL-NOTE: is initialized from pwchRealUnicode. - const auto RealUnicodeChar = *pwchRealUnicode; - if (IS_GLYPH_CHAR(RealUnicodeChar) || fUnprocessed) + switch (*it) { - // WCL-NOTE: This operates on a single code unit instead of a whole codepoint. It will mis-measure surrogate pairs. - if (IsGlyphFullWidth(Char)) + case UNICODE_NULL: + if (interactive) { - if (i < (LOCAL_BUFFER_SIZE - 1) && XPosition < (coordScreenBufferSize.X - 1)) - { - *LocalBufPtr++ = Char; - - // cursor adjusted by 2 because the char is double width - XPosition += 2; - i += 1; - pwchBuffer++; - } - else - { - goto EndWhile; - } - } - else - { - *LocalBufPtr = Char; - LocalBufPtr++; - XPosition++; - i++; - pwchBuffer++; - } - } - else - { - FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))); - switch (RealUnicodeChar) - { - case UNICODE_BELL: - if (dwFlags & WC_PRINTABLE_CONTROL_CHARS) - { - goto CtrlChar; - } - else - { - screenInfo.SendNotifyBeep(); - } - break; - case UNICODE_BACKSPACE: - - // automatically go to EndWhile. this is because - // backspace is not destructive, so "aBkSp" prints - // a with the cursor on the "a". we could achieve - // this behavior staying in this loop and figuring out - // the string that needs to be printed, but it would - // be expensive and it's the exceptional case. - - goto EndWhile; - break; - case UNICODE_TAB: - { - const auto TabSize = NUMBER_OF_SPACES_IN_TAB(XPosition); - XPosition = XPosition + TabSize; - if (XPosition >= coordScreenBufferSize.X) - { - goto EndWhile; - } - - for (til::CoordType j = 0; j < TabSize && i < LOCAL_BUFFER_SIZE; j++, i++) - { - *LocalBufPtr = UNICODE_SPACE; - LocalBufPtr++; - } - - pwchBuffer++; break; } - case UNICODE_LINEFEED: - case UNICODE_CARRIAGERETURN: - goto EndWhile; - default: - - // if char is ctrl char, write ^char. - if ((dwFlags & WC_PRINTABLE_CONTROL_CHARS) && (IS_CONTROL_CHAR(RealUnicodeChar))) - { - CtrlChar: - if (i < (LOCAL_BUFFER_SIZE - 1)) - { - // WCL-NOTE: We do not properly measure that there is space for two characters - // WCL-NOTE: left on the screen. - *LocalBufPtr = (WCHAR)'^'; - LocalBufPtr++; - XPosition++; - i++; - - *LocalBufPtr = (WCHAR)(RealUnicodeChar + (WCHAR)'@'); - LocalBufPtr++; - XPosition++; - i++; - - pwchBuffer++; - } - else - { - goto EndWhile; - } - } - else - { - if (Char == UNICODE_NULL) - { - *LocalBufPtr = UNICODE_SPACE; - } - else - { - // As a special favor to incompetent apps that attempt to display control chars, - // convert to corresponding OEM Glyph Chars - WORD CharType; - - GetStringTypeW(CT_CTYPE1, &RealUnicodeChar, 1, &CharType); - if (WI_IsFlagSet(CharType, C1_CNTRL)) - { - ConvertOutputToUnicode(gci.OutputCP, - (LPSTR)&RealUnicodeChar, - 1, - LocalBufPtr, - 1); - } - else - { - // WCL-NOTE: We should never hit this. - // WCL-NOTE: 1. Normal characters are handled via the early check for IS_GLYPH_CHAR - // WCL-NOTE: 2. Control characters are handled via the CtrlChar label (if WC_PRINTABLE_CONTROL_CHARS is on) - // WCL-NOTE: And if they are control characters they will trigger the C1_CNTRL check above. - *LocalBufPtr = Char; - } - } - - LocalBufPtr++; - XPosition++; - i++; - pwchBuffer++; - } - } - } - lpString++; - pwchRealUnicode++; - *pcb += sizeof(WCHAR); - } - EndWhile: - if (i != 0) - { - CursorPosition = cursor.GetPosition(); - - // Make sure we don't write past the end of the buffer. - // WCL-NOTE: This check uses a code unit count instead of a column count. That is incorrect. - if (i > coordScreenBufferSize.X - CursorPosition.X) - { - i = coordScreenBufferSize.X - CursorPosition.X; - } - - // line was wrapped if we're writing up to the end of the current row - OutputCellIterator it(std::wstring_view(LocalBuffer, i), Attributes); - const auto itEnd = screenInfo.Write(it); - - // Notify accessibility - if (screenInfo.HasAccessibilityEventing()) - { - screenInfo.NotifyAccessibilityEventing(CursorPosition.X, CursorPosition.Y, CursorPosition.X + i - 1, CursorPosition.Y); - } - - // The number of "spaces" or "cells" we have consumed needs to be reported and stored for later - // when/if we need to erase the command line. - TempNumSpaces += itEnd.GetCellDistance(it); - // WCL-NOTE: We are using the "estimated" X position delta instead of the actual delta from - // WCL-NOTE: the iterator. It is not clear why. If they differ, the cursor ends up in the - // WCL-NOTE: wrong place (typically inside another character). - CursorPosition.X = XPosition; - - // enforce a delayed newline if we're about to pass the end and the WC_DELAY_EOL_WRAP flag is set. - if (WI_IsFlagSet(dwFlags, WC_DELAY_EOL_WRAP) && CursorPosition.X >= coordScreenBufferSize.X && fWrapAtEOL) - { - // Our cursor position as of this time is going to remain on the last position in this column. - CursorPosition.X = coordScreenBufferSize.X - 1; - - // Update in the structures that we're still pointing to the last character in the row - cursor.SetPosition(CursorPosition); - - // Record for the delay comparison that we're delaying on the last character in the row - cursor.DelayEOLWrap(CursorPosition); - } - else - { - Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); - } - - // WCL-NOTE: If we have processed the entire input string during our "fast one-line print" handler, - // WCL-NOTE: we are done as there is nothing more to do. Neat! - if (*pcb == BufferSize) - { - if (nullptr != pcSpaces) + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], 1 }, interactive, psScrollY); + continue; + case UNICODE_BELL: + if (interactive) { - *pcSpaces = TempNumSpaces; + break; } - return STATUS_SUCCESS; - } - continue; - } - else if (*pcb >= BufferSize) - { - // WCL-NOTE: This case looks like it is never encountered, but it *is* if WC_PRINTABLE_CONTROL_CHARS is off. - // WCL-NOTE: If the string is entirely nonprinting control characters, there will be - // WCL-NOTE: no output in the buffer (LocalBuffer; i == 0) but we will have processed - // WCL-NOTE: "every" character. We can just bail out and report the number of spaces consumed. - FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))); - - // this catches the case where the number of backspaces == the number of characters. - if (nullptr != pcSpaces) + std::ignore = screenInfo.SendNotifyBeep(); + continue; + case UNICODE_BACKSPACE: { - *pcSpaces = TempNumSpaces; - } - return STATUS_SUCCESS; - } - - switch (*lpString) - { - case UNICODE_BACKSPACE: - { - // move cursor backwards one space. overwrite current char with blank. - // we get here because we have to backspace from the beginning of the line - TempNumSpaces -= 1; - if (pwchBuffer == pwchBufferBackupLimit) - { - CursorPosition.X -= 1; - } - else - { - const wchar_t* Tmp; - wchar_t* Tmp2 = nullptr; - WCHAR LastChar; - - const auto bufferSize = pwchBuffer - pwchBufferBackupLimit; - std::unique_ptr buffer; - try - { - buffer = std::make_unique(bufferSize); - std::fill_n(buffer.get(), bufferSize, UNICODE_NULL); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - - for (i = 0, Tmp2 = buffer.get(), Tmp = pwchBufferBackupLimit; - i < bufferSize; - i++, Tmp++) - { - // see 18120085, these two need to be separate if statements - if (*Tmp == UNICODE_BACKSPACE) - { - //it is important we do nothing in the else case for - // this one instead of falling through to the below else. - if (Tmp2 > buffer.get()) - { - Tmp2--; - } - } - else - { - FAIL_FAST_IF(!(Tmp2 >= buffer.get())); - *Tmp2++ = *Tmp; - } - } - if (Tmp2 == buffer.get()) - { - LastChar = UNICODE_SPACE; - } - else - { -#pragma prefast(suppress : 26001, "This is fine. Tmp2 has to have advanced or it would equal pBuffer.") - LastChar = *(Tmp2 - 1); - } - - if (LastChar == UNICODE_TAB) - { - CursorPosition.X -= RetrieveNumberOfSpaces(sOriginalXPosition, - pwchBufferBackupLimit, - pwchBuffer - pwchBufferBackupLimit - 1); - if (CursorPosition.X < 0) - { - CursorPosition.X = (coordScreenBufferSize.X - 1) / TAB_SIZE; - CursorPosition.X *= TAB_SIZE; - CursorPosition.X += 1; - CursorPosition.Y -= 1; - - // since you just backspaced yourself back up into the previous row, unset the wrap - // flag on the prev row if it was set - textBuffer.GetRowByOffset(CursorPosition.Y).SetWrapForced(false); - } - } - else if (IS_CONTROL_CHAR(LastChar)) - { - CursorPosition.X -= 1; - TempNumSpaces -= 1; - - // overwrite second character of ^x sequence. - if (dwFlags & WC_DESTRUCTIVE_BACKSPACE) - { - try - { - screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition); - Status = STATUS_SUCCESS; - } - CATCH_LOG(); - } - - CursorPosition.X -= 1; - } - else if (IsGlyphFullWidth(LastChar)) - { - CursorPosition.X -= 1; - TempNumSpaces -= 1; - - Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY); - if (dwFlags & WC_DESTRUCTIVE_BACKSPACE) - { - try - { - screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition); - Status = STATUS_SUCCESS; - } - CATCH_LOG(); - } - CursorPosition.X -= 1; - } - else - { - CursorPosition.X--; - } - } - if ((dwFlags & WC_LIMIT_BACKSPACE) && (CursorPosition.X < 0)) - { - CursorPosition.X = 0; - OutputDebugStringA(("CONSRV: Ignoring backspace to previous line\n")); - } - Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY); - if (dwFlags & WC_DESTRUCTIVE_BACKSPACE) - { - try - { - screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), cursor.GetPosition()); - } - CATCH_LOG(); - } - if (cursor.GetPosition().X == 0 && fWrapAtEOL && pwchBuffer > pwchBufferBackupLimit) - { - if (CheckBisectProcessW(screenInfo, - pwchBufferBackupLimit, - pwchBuffer + 1 - pwchBufferBackupLimit, - gsl::narrow_cast(coordScreenBufferSize.X) - sOriginalXPosition, - sOriginalXPosition, - dwFlags & WC_PRINTABLE_CONTROL_CHARS)) - { - CursorPosition.X = coordScreenBufferSize.X - 1; - CursorPosition.Y = cursor.GetPosition().Y - 1; - - // since you just backspaced yourself back up into the previous row, unset the wrap flag - // on the prev row if it was set - textBuffer.GetRowByOffset(CursorPosition.Y).SetWrapForced(false); - - Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY); - } - } - // Notify accessibility to read the backspaced character. - // See GH:12735, MSFT:31748387 - if (screenInfo.HasAccessibilityEventing()) - { - if (auto pConsoleWindow = ServiceLocator::LocateConsoleWindow()) - { - LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); - } - } - break; - } - case UNICODE_TAB: - { - const auto TabSize = NUMBER_OF_SPACES_IN_TAB(cursor.GetPosition().X); - CursorPosition.X = cursor.GetPosition().X + TabSize; - - // move cursor forward to next tab stop. fill space with blanks. - // we get here when the tab extends beyond the right edge of the - // window. if the tab goes wraps the line, set the cursor to the first - // position in the next line. - pwchBuffer++; - - TempNumSpaces += TabSize; - size_t NumChars = 0; - if (CursorPosition.X >= coordScreenBufferSize.X) - { - NumChars = gsl::narrow(coordScreenBufferSize.X - cursor.GetPosition().X); - CursorPosition.X = 0; - CursorPosition.Y = cursor.GetPosition().Y + 1; - - // since you just tabbed yourself past the end of the row, set the wrap - textBuffer.GetRowByOffset(cursor.GetPosition().Y).SetWrapForced(true); - } - else - { - NumChars = gsl::narrow(CursorPosition.X - cursor.GetPosition().X); - CursorPosition.Y = cursor.GetPosition().Y; - } - - try - { - const OutputCellIterator it(UNICODE_SPACE, Attributes, NumChars); - const auto done = screenInfo.Write(it, cursor.GetPosition()); - NumChars = done.GetCellDistance(it); - } - CATCH_LOG(); - - Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY); - break; - } - case UNICODE_CARRIAGERETURN: - { - // Carriage return moves the cursor to the beginning of the line. - // We don't need to worry about handling cr or lf for - // backspace because input is sent to the user on cr or lf. - pwchBuffer++; - CursorPosition.X = 0; - CursorPosition.Y = cursor.GetPosition().Y; - Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY); - break; - } - case UNICODE_LINEFEED: - { - // move cursor to the next line. - pwchBuffer++; - - if (gci.IsReturnOnNewlineAutomatic()) - { - // Traditionally, we reset the X position to 0 with a newline automatically. - // Some things might not want this automatic "ONLCR line discipline" (for example, things that are expecting a *NIX behavior.) - // They will turn it off with an output mode flag. - CursorPosition.X = 0; - } - - CursorPosition.Y = cursor.GetPosition().Y + 1; - - { - // since we explicitly just moved down a row, clear the wrap status on the row we just came from - textBuffer.GetRowByOffset(cursor.GetPosition().Y).SetWrapForced(false); - } - - Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY); - break; - } - default: - { - const auto Char = *lpString; - if (Char >= UNICODE_SPACE && - IsGlyphFullWidth(Char) && - XPosition >= (coordScreenBufferSize.X - 1) && - fWrapAtEOL) - { - const auto TargetPoint = cursor.GetPosition(); - auto& Row = textBuffer.GetRowByOffset(TargetPoint.Y); - const auto& charRow = Row.GetCharRow(); - - try - { - // If we're on top of a trailing cell, clear it and the previous cell. - if (charRow.DbcsAttrAt(TargetPoint.X).IsTrailing()) - { - // Space to clear for 2 cells. - OutputCellIterator it(UNICODE_SPACE, 2); - - // Back target point up one. - auto writeTarget = TargetPoint; - writeTarget.X--; - - // Write 2 clear cells. - screenInfo.Write(it, writeTarget); - } - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - - CursorPosition.X = 0; - CursorPosition.Y = TargetPoint.Y + 1; - - // since you just moved yourself down onto the next row with 1 character, that sounds like a - // forced wrap so set the flag - Row.SetWrapForced(true); - - // Additionally, this padding is only called for IsConsoleFullWidth (a.k.a. when a character - // is too wide to fit on the current line). - Row.SetDoubleBytePadded(true); - - Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY); + // 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(); + pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } - break; - } - } - if (!NT_SUCCESS(Status)) - { - return Status; - } - - *pcb += sizeof(WCHAR); - lpString++; - pwchRealUnicode++; - } - - if (nullptr != pcSpaces) - { - *pcSpaces = TempNumSpaces; - } - - return STATUS_SUCCESS; -} - -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters. -// WC_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge -// WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X") -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[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) -{ - if (!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) || - !WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) - { - return WriteCharsLegacy(screenInfo, - pwchBufferBackupLimit, - pwchBuffer, - pwchRealUnicode, - pcb, - pcSpaces, - sOriginalXPosition, - dwFlags, - psScrollY); - } - - auto Status = STATUS_SUCCESS; - - const auto BufferSize = *pcb; - *pcb = 0; - - { - size_t TempNumSpaces = 0; - - { - if (NT_SUCCESS(Status)) + case UNICODE_TAB: { - FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))); - FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING))); + const auto pos = cursor.GetPosition(); + const auto tabCount = gsl::narrow_cast(8 - (pos.x & 7)); + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], tabCount }, interactive, psScrollY); + continue; + } + case UNICODE_LINEFEED: + { + auto pos = cursor.GetPosition(); + if (WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN)) + { + pos.x = 0; + } - // defined down in the WriteBuffer default case hiding on the other end of the state machine. See outputStream.cpp - // This is the only mode used by DoWriteConsole. - FAIL_FAST_IF(!(WI_IsFlagSet(dwFlags, WC_LIMIT_BACKSPACE))); + textBuffer.GetMutableRowByOffset(pos.y).SetWrapForced(false); + pos.y = pos.y + 1; + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); + continue; + } + case UNICODE_CARRIAGERETURN: + { + auto pos = cursor.GetPosition(); + pos.x = 0; + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); + continue; + } + default: + break; + } - auto& machine = screenInfo.GetStateMachine(); - const auto cch = BufferSize / sizeof(WCHAR); - - machine.ProcessString({ pwchRealUnicode, cch }); - *pcb += BufferSize; + // 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'@') }; + _writeCharsLegacyUnprocessed(screenInfo, { &wchs[0], 2 }, interactive, psScrollY); + } + else + { + // As a special favor to incompetent apps that attempt to display control chars, + // convert to corresponding OEM Glyph Chars + const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP; + const auto ch = gsl::narrow_cast(*it); + wchar_t wch = 0; + const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1); + if (result == 1) + { + _writeCharsLegacyUnprocessed(screenInfo, { &wch, 1 }, interactive, psScrollY); + } } } - - if (nullptr != pcSpaces) - { - *pcSpaces = TempNumSpaces; - } } - - return Status; } // Routine Description: @@ -1014,50 +321,53 @@ using Microsoft::Console::VirtualTerminal::StateMachine; 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; } - auto restoreVtQuirk{ - wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); }) - }; - + const auto vtIo = ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo(); + const auto restoreVtQuirk = wil::scope_exit([&]() { + if (requiresVtQuirk) + { + screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); + } + if (vtIo->IsUsingVt()) + { + vtIo->CorkRenderer(false); + } + }); if (requiresVtQuirk) { screenInfo.SetIgnoreLegacyEquivalentVTAttributes(); } - else + if (vtIo->IsUsingVt()) { - restoreVtQuirk.release(); + vtIo->CorkRenderer(true); } - const auto& textBuffer = screenInfo.GetTextBuffer(); - return WriteChars(screenInfo, - pwchBuffer, - pwchBuffer, - pwchBuffer, - pcbBuffer, - nullptr, - textBuffer.GetCursor().GetPosition().X, - WC_LIMIT_BACKSPACE, - 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 149482f1ff..794591a03f 100644 --- a/src/host/_stream.h +++ b/src/host/_stream.h @@ -17,80 +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: ---*/ -[[nodiscard]] NTSTATUS 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_DESTRUCTIVE_BACKSPACE backspace overwrites characters. - WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge - WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X") - -Return Value: - -Note: - This routine does not process tabs and backspace properly. That code - will be implemented as part of the line editing services. ---*/ -[[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 b7c885bc9f..a82fa19876 100644 --- a/src/host/alias.cpp +++ b/src/host/alias.cpp @@ -137,7 +137,7 @@ std::unordered_map> target, + std::optional> target, size_t& writtenOrNeeded, const std::wstring_view exeName) { @@ -195,7 +195,7 @@ std::unordered_map target, + std::span target, size_t& written, const std::string_view exeName) noexcept { @@ -235,7 +235,7 @@ std::unordered_map(targetBuffer.get(), targetNeeded), + std::span(targetBuffer.get(), targetNeeded), targetWritten, exeNameW)); @@ -268,7 +268,7 @@ std::unordered_map target, + std::span target, size_t& written, const std::wstring_view exeName) noexcept { @@ -440,7 +440,7 @@ void Alias::s_ClearCmdExeAliases() // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. [[nodiscard]] HRESULT GetConsoleAliasesWImplHelper(const std::wstring_view exeName, - std::optional> aliasBuffer, + std::optional> aliasBuffer, size_t& writtenOrNeeded) { // Ensure output variables are initialized. @@ -527,7 +527,7 @@ void Alias::s_ClearCmdExeAliases() // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. [[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasesAImpl(const std::string_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -563,7 +563,7 @@ void Alias::s_ClearCmdExeAliases() // Call the Unicode version of this method size_t bufferWritten; - RETURN_IF_FAILED(GetConsoleAliasesWImplHelper(exeNameW, gsl::span(aliasBuffer.get(), bufferNeeded), bufferWritten)); + RETURN_IF_FAILED(GetConsoleAliasesWImplHelper(exeNameW, std::span(aliasBuffer.get(), bufferNeeded), bufferWritten)); // Convert result to A const auto converted = ConvertToA(codepage, { aliasBuffer.get(), bufferWritten }); @@ -591,7 +591,7 @@ void Alias::s_ClearCmdExeAliases() // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. [[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasesWImpl(const std::wstring_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept { LockConsole(); @@ -688,7 +688,7 @@ void Alias::s_ClearCmdExeAliases() // or how many characters would have been needed (if buffer is nullopt). // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. -[[nodiscard]] HRESULT GetConsoleAliasExesWImplHelper(std::optional> aliasExesBuffer, +[[nodiscard]] HRESULT GetConsoleAliasExesWImplHelper(std::optional> aliasExesBuffer, size_t& writtenOrNeeded) { // Ensure output variables are initialized. @@ -745,7 +745,7 @@ void Alias::s_ClearCmdExeAliases() // - written - Specifies how many characters were written // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. -[[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasExesAImpl(gsl::span aliasExes, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasExesAImpl(std::span aliasExes, size_t& written) noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -777,7 +777,7 @@ void Alias::s_ClearCmdExeAliases() // Call the Unicode version of this method size_t bufferWritten; - RETURN_IF_FAILED(GetConsoleAliasExesWImplHelper(gsl::span(targetBuffer.get(), bufferNeeded), bufferWritten)); + RETURN_IF_FAILED(GetConsoleAliasExesWImplHelper(std::span(targetBuffer.get(), bufferNeeded), bufferWritten)); // Convert result to A const auto converted = ConvertToA(codepage, { targetBuffer.get(), bufferWritten }); @@ -804,7 +804,7 @@ void Alias::s_ClearCmdExeAliases() // - pcchAliasExesBufferWrittenOrNeeded - Pointer to space that will specify how many characters were written // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. -[[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasExesWImpl(gsl::span aliasExes, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleAliasExesWImpl(std::span aliasExes, size_t& written) noexcept { LockConsole(); @@ -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 1a745c601f..9c8bc38d53 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 7b5b23c5bd..c523ad9ab3 100644 --- a/src/host/cmdline.cpp +++ b/src/host/cmdline.cpp @@ -2,79 +2,16 @@ // Licensed under the MIT license. #include "precomp.h" - #include "cmdline.h" -#include "popup.h" -#include "CommandNumberPopup.hpp" -#include "CommandListPopup.hpp" -#include "CopyFromCharPopup.hpp" -#include "CopyToCharPopup.hpp" - -#include "_output.h" -#include "output.h" -#include "stream.h" -#include "_stream.h" -#include "dbcs.h" -#include "handle.h" -#include "misc.h" -#include "../types/inc/convert.hpp" -#include "srvinit.h" - -#include "ApiRoutines.h" #include "../interactivity/inc/ServiceLocator.hpp" #pragma hdrstop 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) +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. @@ -86,1221 +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 - { - return false; - } -} - -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.X * coordOriginalCursor.Y; - CharsToWrite += cookedReadData.OriginalCursorPosition().X; // account for prompt - cookedReadData.OriginalCursorPosition().X = 0; - cookedReadData.OriginalCursorPosition().Y = 0; - coordOriginalCursor.X = 0; - coordOriginalCursor.Y = 0; - } - - if (!CheckBisectStringW(cookedReadData.BufferStartPtr(), - CharsToWrite, - coordBufferSize.X - cookedReadData.OriginalCursorPosition().X)) - { - CharsToWrite++; - } - - try - { - cookedReadData.ScreenInfo().Write(OutputCellIterator(UNICODE_SPACE, CharsToWrite), coordOriginalCursor); - } - CATCH_LOG(); - - if (fUpdateFields) - { - cookedReadData.Erase(); - } - - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.OriginalCursorPosition(), true)); -} - -void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.IsEchoInput()) - { - // Draw the command line - cookedReadData.OriginalCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - 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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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++; - } - Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr); - FAIL_FAST_IF_NTSTATUS_FAILED(Status); - } -} - -// Routine Description: -// - This routine copies the commandline specified by Index into the cooked read buffer -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) // index, not command number -{ - DeleteCommandLine(cookedReadData, TRUE); - FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - 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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - 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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - 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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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()) - { - SHORT 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((SHORT)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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &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_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - 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()) - { - Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr); - FAIL_FAST_IF_NTSTATUS_FAILED(Status); - } - - return STATUS_SUCCESS; + if (wch == L' ') + { + return 1; + } + const auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; + if (std::find(delimiters.begin(), delimiters.end(), wch) != delimiters.end()) + { + return 2; + } + return 0; } diff --git a/src/host/cmdline.h b/src/host/cmdline.h index b503ec21b1..89cdf19b5c 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -1,153 +1,11 @@ -/*++ -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 -#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_DESTRUCTIVE_BACKSPACE 0x01 -#define WC_KEEP_CURSOR_VISIBLE 0x02 -#define WC_PRINTABLE_CONTROL_CHARS 0x04 - -// This is no longer necessary. The buffer will always be Unicode. We don't need to perform special work to check if we're in a raster font -// and convert the entire buffer to match (and all insertions). -//#define WC_FALSIFY_UNICODE 0x08 - -#define WC_LIMIT_BACKSPACE 0x10 -//#define WC_NONDESTRUCTIVE_TAB 0x20 - This is not needed anymore, because the VT code handles tabs internally now. -//#define WC_NEWLINE_SAVE_X 0x40 - This has been replaced with an output mode flag instead as it's line discipline behavior that may not necessarily be coupled with VT. -#define WC_DELAY_EOL_WRAP 0x80 // 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_ SHORT 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/conareainfo.cpp b/src/host/conareainfo.cpp index 868e36ede0..0699c7bf15 100644 --- a/src/host/conareainfo.cpp +++ b/src/host/conareainfo.cpp @@ -110,7 +110,7 @@ void ConversionAreaInfo::SetAttributes(const TextAttribute& attr) void ConversionAreaInfo::WriteText(const std::vector& text, const til::CoordType column) { - gsl::span view(text.data(), text.size()); + std::span view(text.data(), text.size()); _screenBuffer->Write(view, { column, 0 }); } @@ -138,21 +138,21 @@ void ConversionAreaInfo::ClearArea() noexcept _caInfo.coordCaBuffer = newSize; // restrict viewport to buffer size. - const til::point restriction = { newSize.X - 1, newSize.Y - 1 }; - _caInfo.rcViewCaWindow.Left = std::min(_caInfo.rcViewCaWindow.Left, restriction.X); - _caInfo.rcViewCaWindow.Right = std::min(_caInfo.rcViewCaWindow.Right, restriction.X); - _caInfo.rcViewCaWindow.Top = std::min(_caInfo.rcViewCaWindow.Top, restriction.Y); - _caInfo.rcViewCaWindow.Bottom = std::min(_caInfo.rcViewCaWindow.Bottom, restriction.Y); + const til::point restriction = { newSize.width - 1, newSize.height - 1 }; + _caInfo.rcViewCaWindow.left = std::min(_caInfo.rcViewCaWindow.left, restriction.x); + _caInfo.rcViewCaWindow.right = std::min(_caInfo.rcViewCaWindow.right, restriction.x); + _caInfo.rcViewCaWindow.top = std::min(_caInfo.rcViewCaWindow.top, restriction.y); + _caInfo.rcViewCaWindow.bottom = std::min(_caInfo.rcViewCaWindow.bottom, restriction.y); return S_OK; } void ConversionAreaInfo::SetWindowInfo(const til::inclusive_rect& view) noexcept { - if (view.Left != _caInfo.rcViewCaWindow.Left || - view.Top != _caInfo.rcViewCaWindow.Top || - view.Right != _caInfo.rcViewCaWindow.Right || - view.Bottom != _caInfo.rcViewCaWindow.Bottom) + if (view.left != _caInfo.rcViewCaWindow.left || + view.top != _caInfo.rcViewCaWindow.top || + view.right != _caInfo.rcViewCaWindow.right || + view.bottom != _caInfo.rcViewCaWindow.bottom) { if (!IsHidden()) { @@ -181,19 +181,19 @@ void ConversionAreaInfo::SetViewPos(const til::point pos) noexcept auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto OldRegion = _caInfo.rcViewCaWindow; - OldRegion.Left += _caInfo.coordConView.X; - OldRegion.Right += _caInfo.coordConView.X; - OldRegion.Top += _caInfo.coordConView.Y; - OldRegion.Bottom += _caInfo.coordConView.Y; + OldRegion.left += _caInfo.coordConView.x; + OldRegion.right += _caInfo.coordConView.x; + OldRegion.top += _caInfo.coordConView.y; + OldRegion.bottom += _caInfo.coordConView.y; WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(OldRegion)); _caInfo.coordConView = pos; auto NewRegion = _caInfo.rcViewCaWindow; - NewRegion.Left += _caInfo.coordConView.X; - NewRegion.Right += _caInfo.coordConView.X; - NewRegion.Top += _caInfo.coordConView.Y; - NewRegion.Bottom += _caInfo.coordConView.Y; + NewRegion.left += _caInfo.coordConView.x; + NewRegion.right += _caInfo.coordConView.x; + NewRegion.top += _caInfo.coordConView.y; + NewRegion.bottom += _caInfo.coordConView.y; WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(NewRegion)); } } @@ -205,10 +205,10 @@ void ConversionAreaInfo::Paint() const noexcept const auto viewport = ScreenInfo.GetViewport(); til::inclusive_rect WriteRegion; - WriteRegion.Left = viewport.Left() + _caInfo.coordConView.X + _caInfo.rcViewCaWindow.Left; - WriteRegion.Right = WriteRegion.Left + (_caInfo.rcViewCaWindow.Right - _caInfo.rcViewCaWindow.Left); - WriteRegion.Top = viewport.Top() + _caInfo.coordConView.Y + _caInfo.rcViewCaWindow.Top; - WriteRegion.Bottom = WriteRegion.Top + (_caInfo.rcViewCaWindow.Bottom - _caInfo.rcViewCaWindow.Top); + WriteRegion.left = viewport.Left() + _caInfo.coordConView.x + _caInfo.rcViewCaWindow.left; + WriteRegion.right = WriteRegion.left + (_caInfo.rcViewCaWindow.right - _caInfo.rcViewCaWindow.left); + WriteRegion.top = viewport.Top() + _caInfo.coordConView.y + _caInfo.rcViewCaWindow.top; + WriteRegion.bottom = WriteRegion.top + (_caInfo.rcViewCaWindow.bottom - _caInfo.rcViewCaWindow.top); if (!IsHidden()) { diff --git a/src/host/conddkrefs.h b/src/host/conddkrefs.h index 1dfc904c53..854d03e808 100644 --- a/src/host/conddkrefs.h +++ b/src/host/conddkrefs.h @@ -15,93 +15,11 @@ and it's easier to include a copy of the infrequently changing defs here. #ifndef _DDK_INCLUDED +#include + extern "C" { #pragma region wdm.h(public DDK) -// -// Define the base asynchronous I/O argument types -// - -// -// ClientId -// - -typedef struct _CLIENT_ID -{ - HANDLE UniqueProcess; - HANDLE UniqueThread; -} CLIENT_ID; -typedef CLIENT_ID* PCLIENT_ID; - -// POBJECT_ATTRIBUTES - -// -// Unicode strings are counted 16-bit character strings. If they are -// NULL terminated, Length does not include trailing NULL. -// - -typedef struct _UNICODE_STRING -{ - USHORT Length; - USHORT MaximumLength; -#ifdef MIDL_PASS - [size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT* Buffer; -#else // MIDL_PASS - _Field_size_bytes_part_(MaximumLength, Length) PWCH Buffer; -#endif // MIDL_PASS -} UNICODE_STRING; -typedef UNICODE_STRING* PUNICODE_STRING; -typedef const UNICODE_STRING* PCUNICODE_STRING; - -// OBJECT_ATTRIBUTES - -// clang-format off -#define OBJ_INHERIT 0x00000002L -#define OBJ_PERMANENT 0x00000010L -#define OBJ_EXCLUSIVE 0x00000020L -#define OBJ_CASE_INSENSITIVE 0x00000040L -#define OBJ_OPENIF 0x00000080L -#define OBJ_OPENLINK 0x00000100L -#define OBJ_KERNEL_HANDLE 0x00000200L -#define OBJ_FORCE_ACCESS_CHECK 0x00000400L -#define OBJ_VALID_ATTRIBUTES 0x000007F2L -// clang-format on - -typedef struct _OBJECT_ATTRIBUTES -{ - ULONG Length; - HANDLE RootDirectory; - PUNICODE_STRING ObjectName; - ULONG Attributes; - PVOID SecurityDescriptor; // Points to type SECURITY_DESCRIPTOR - PVOID SecurityQualityOfService; // Points to type SECURITY_QUALITY_OF_SERVICE -} OBJECT_ATTRIBUTES; -typedef OBJECT_ATTRIBUTES* POBJECT_ATTRIBUTES; -typedef CONST OBJECT_ATTRIBUTES* PCOBJECT_ATTRIBUTES; - -//++ -// -// VOID -// InitializeObjectAttributes( -// _Out_ POBJECT_ATTRIBUTES p, -// _In_ PUNICODE_STRING n, -// _In_ ULONG a, -// _In_ HANDLE r, -// _In_ PSECURITY_DESCRIPTOR s -// ) -// -//-- - -#define InitializeObjectAttributes(p, n, a, r, s) \ - do \ - { \ - (p)->Length = sizeof(OBJECT_ATTRIBUTES); \ - (p)->RootDirectory = r; \ - (p)->Attributes = a; \ - (p)->ObjectName = n; \ - (p)->SecurityDescriptor = s; \ - (p)->SecurityQualityOfService = nullptr; \ - } while (0) // UNICODE_STRING @@ -137,21 +55,6 @@ public: } } -// OBJ_CASE_INSENSITIVE -// OBJ_INHERIT -// InitializeObjectAttributes - -typedef struct _IO_STATUS_BLOCK -{ - union - { - NTSTATUS Status; - PVOID Pointer; - } DUMMYUNIONNAME; - - ULONG_PTR Information; -} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; - // // Define the file system information class values // @@ -236,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/host/conimeinfo.cpp b/src/host/conimeinfo.cpp index fbe226ea25..fc2ce5ae74 100644 --- a/src/host/conimeinfo.cpp +++ b/src/host/conimeinfo.cpp @@ -4,14 +4,14 @@ #include "precomp.h" #include "conimeinfo.h" -#include "conareainfo.h" +#include + +#include "conareainfo.h" #include "_output.h" #include "dbcs.h" - #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/GlyphWidth.hpp" -#include "../types/inc/Utf16Parser.hpp" // Attributes flags: #define COMMON_LVB_GRID_SINGLEFLAG 0x2000 // DBCS: Grid attribute: use for ime cursor. @@ -57,8 +57,8 @@ void ConsoleImeInfo::RedrawCompMessage() // - attributes - Encoded attributes including the cursor position and the color index (to the array) // - colorArray - An array of colors to use for the text void ConsoleImeInfo::WriteCompMessage(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray) + const std::span attributes, + const std::span colorArray) { ClearAllAreas(); @@ -146,7 +146,7 @@ void ConsoleImeInfo::ClearAllAreas() const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); - bufferSize.Y = 1; + bufferSize.height = 1; const auto windowSize = gci.GetActiveOutputBuffer().GetViewport().Dimensions(); @@ -181,8 +181,8 @@ void ConsoleImeInfo::ClearAllAreas() // Return Value: // - TextAttribute object with color and cursor and line drawing data. TextAttribute ConsoleImeInfo::s_RetrieveAttributeAt(const size_t pos, - const gsl::span attributes, - const gsl::span colorArray) + const std::span attributes, + const std::span colorArray) { // Encoded attribute is the shorthand information passed from the IME // that contains a cursor position packed in along with which color in the @@ -218,17 +218,14 @@ TextAttribute ConsoleImeInfo::s_RetrieveAttributeAt(const size_t pos, // Return Value: // - Vector of OutputCells where each one represents one cell of the output buffer. std::vector ConsoleImeInfo::s_ConvertToCells(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray) + const std::span attributes, + const std::span colorArray) { std::vector cells; - // - Convert incoming wchar_t stream into UTF-16 units. - const auto glyphs = Utf16Parser::Parse(text); - // - Walk through all of the grouped up text, match up the correct attribute to it, and make a new cell. size_t attributesUsed = 0; - for (const auto& parsedGlyph : glyphs) + for (const auto& parsedGlyph : til::utf16_iterator{ text }) { const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() }; // Collect up attributes that apply to this glyph range. @@ -256,7 +253,7 @@ std::vector ConsoleImeInfo::s_ConvertToCells(const std::wstring_view // If it's full width, it's two, and we need to make sure we don't draw the cursor // right down the middle of the character. // Otherwise it's one column and we'll push it in with the default empty DbcsAttribute. - DbcsAttribute dbcsAttr; + DbcsAttribute dbcsAttr = DbcsAttribute::Single; if (IsGlyphFullWidth(glyph)) { auto leftHalfAttr = drawingAttr; @@ -269,9 +266,9 @@ std::vector ConsoleImeInfo::s_ConvertToCells(const std::wstring_view leftHalfAttr.SetRightVerticalDisplayed(false); } - dbcsAttr.SetLeading(); + dbcsAttr = DbcsAttribute::Leading; cells.emplace_back(glyph, dbcsAttr, leftHalfAttr); - dbcsAttr.SetTrailing(); + dbcsAttr = DbcsAttribute::Trailing; // If we need a left vertical, don't apply it to the right side of the character if (rightHalfAttr.IsLeftVerticalDisplayed()) @@ -321,14 +318,14 @@ std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(con // Advance the cursor position to set up the next call for success (insert the next conversion area // at the beginning of the following line) - pos.X = view.Left(); - pos.Y++; + pos.x = view.Left(); + pos.y++; // The index of the last column in the viewport. (view is inclusive) const auto finalViewColumn = view.RightInclusive(); // The maximum number of cells we can insert into a line. - const auto lineWidth = finalViewColumn - insertionPos.X + 1; // +1 because view was inclusive + const auto lineWidth = finalViewColumn - insertionPos.x + 1; // +1 because view was inclusive // The iterator to the beginning position to form our line const auto lineBegin = begin; @@ -346,7 +343,7 @@ std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(con // Get the last cell in the run and if it's a leading byte, move the end position back one so we don't // try to insert it. const auto lastCell = lineEnd - 1; - if (lastCell->DbcsAttr().IsLeading()) + if (lastCell->DbcsAttr() == DbcsAttribute::Leading) { lineEnd--; } @@ -368,13 +365,13 @@ std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(con auto& area = ConvAreaCompStr.back(); // Write our text into the conversion area. - area.WriteText(lineVec, insertionPos.X); + area.WriteText(lineVec, insertionPos.x); // Set the viewport and positioning parameters for the conversion area to describe to the renderer // the appropriate location to overlay this conversion area on top of the main screen buffer inside the viewport. - const til::inclusive_rect region{ insertionPos.X, 0, gsl::narrow(insertionPos.X + lineVec.size() - 1), 0 }; + const til::inclusive_rect region{ insertionPos.x, 0, gsl::narrow(insertionPos.x + lineVec.size() - 1), 0 }; area.SetWindowInfo(region); - area.SetViewPos({ 0 - view.Left(), insertionPos.Y - view.Top() }); + area.SetViewPos({ 0 - view.Left(), insertionPos.y - view.Top() }); // Make it visible and paint it. area.SetHidden(false); @@ -383,7 +380,7 @@ std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(con // Notify accessibility that we have updated the text in this display region within the viewport. if (screenInfo.HasAccessibilityEventing()) { - screenInfo.NotifyAccessibilityEventing(region.left, insertionPos.Y, region.right, insertionPos.Y); + screenInfo.NotifyAccessibilityEventing(region.left, insertionPos.y, region.right, insertionPos.y); } // Hand back the iterator representing the end of what we used to be fed into the beginning of the next call. @@ -401,8 +398,8 @@ std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(con // each text character. This view must be the same size as the text view. // - colorArray - 8 colors to be used to format the text for display void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray) + const std::span attributes, + const std::span colorArray) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& screenInfo = gci.GetActiveOutputBuffer(); @@ -465,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/conimeinfo.h b/src/host/conimeinfo.h index 3d84f232ca..81ea359d8a 100644 --- a/src/host/conimeinfo.h +++ b/src/host/conimeinfo.h @@ -48,8 +48,8 @@ public: [[nodiscard]] HRESULT ResizeAllAreas(const til::size newSize); void WriteCompMessage(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray); + const std::span attributes, + const std::span colorArray); void WriteResultMessage(const std::wstring_view text); @@ -64,18 +64,18 @@ private: void _ClearComposition(); void _WriteUndeterminedChars(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray); + const std::span attributes, + const std::span colorArray); void _InsertConvertedString(const std::wstring_view text); static TextAttribute s_RetrieveAttributeAt(const size_t pos, - const gsl::span attributes, - const gsl::span colorArray); + const std::span attributes, + const std::span colorArray); static std::vector s_ConvertToCells(const std::wstring_view text, - const gsl::span attributes, - const gsl::span colorArray); + const std::span attributes, + const std::span colorArray); std::vector::const_iterator _WriteConversionArea(const std::vector::const_iterator begin, const std::vector::const_iterator end, diff --git a/src/host/conserv.h b/src/host/conserv.h index 713e519d3f..f5cef3fd9f 100644 --- a/src/host/conserv.h +++ b/src/host/conserv.h @@ -25,7 +25,6 @@ Revision History: #include "tracing.hpp" #define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) -#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); /* * Used to store some console attributes for the console. This is a means @@ -36,7 +35,7 @@ Revision History: * bytes. */ -#define PACKCOORD(pt) (MAKELONG(((pt).X), ((pt).Y))) +#define PACKCOORD(pt) (MAKELONG(((pt).x), ((pt).y))) typedef struct _CONSOLE_API_CONNECTINFO { diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index fd1fbc803a..4722cebe2b 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -18,37 +18,6 @@ using Microsoft::Console::Interactivity::ServiceLocator; using Microsoft::Console::VirtualTerminal::VtIo; -CONSOLE_INFORMATION::CONSOLE_INFORMATION() : - // ProcessHandleList initializes itself - pInputBuffer(nullptr), - pCurrentScreenBuffer(nullptr), - ScreenBuffers(nullptr), - OutputQueue(), - // ExeAliasList initialized below - _OriginalTitle(), - _Title(), - _Prefix(), - _TitleAndPrefix(), - _LinkTitle(), - Flags(0), - PopupCount(0), - CP(0), - OutputCP(0), - CtrlFlags(0), - LimitingProcessId(0), - // ColorTable initialized below - // CPInfo initialized below - // OutputCPInfo initialized below - _cookedReadData(nullptr), - ConsoleIme{}, - _vtIo(), - _blinker{}, - renderData{} -{ - ZeroMemory((void*)&CPInfo, sizeof(CPInfo)); - ZeroMemory((void*)&OutputCPInfo, sizeof(OutputCPInfo)); -} - bool CONSOLE_INFORMATION::IsConsoleLocked() const noexcept { return _lock.is_locked(); @@ -119,7 +88,7 @@ ULONG CONSOLE_INFORMATION::GetCSRecursionCount() const noexcept } auto Status = DoCreateScreenBuffer(); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { goto ErrorExit2; } @@ -130,7 +99,7 @@ ULONG CONSOLE_INFORMATION::GetCSRecursionCount() const noexcept gci.ConsoleIme.RefreshAreaAttributes(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { return STATUS_SUCCESS; } @@ -161,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; @@ -176,6 +150,16 @@ void CONSOLE_INFORMATION::SetCookedReadData(COOKED_READ_DATA* readData) noexcept _cookedReadData = readData; } +bool CONSOLE_INFORMATION::GetBracketedPasteMode() const noexcept +{ + return _bracketedPasteMode; +} + +void CONSOLE_INFORMATION::SetBracketedPasteMode(const bool enabled) noexcept +{ + _bracketedPasteMode = enabled; +} + // Method Description: // - Return the active screen buffer of the console. // Arguments: @@ -378,6 +362,6 @@ CHAR_INFO CONSOLE_INFORMATION::AsCharInfo(const OutputCellView& cell) const noex // (for mapping RGB values to the nearest table value) const auto& attr = cell.TextAttr(); ci.Attributes = attr.GetLegacyAttributes(); - ci.Attributes |= cell.DbcsAttr().GeneratePublicApiAttributeFormat(); + ci.Attributes |= GeneratePublicApiAttributeFormat(cell.DbcsAttr()); return ci; } diff --git a/src/host/convarea.cpp b/src/host/convarea.cpp index 70f6a3e245..62c9796a56 100644 --- a/src/host/convarea.cpp +++ b/src/host/convarea.cpp @@ -34,22 +34,22 @@ void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, // Do clipping region til::inclusive_rect Region; - Region.Left = currentViewport.Left + areaInfo.rcViewCaWindow.Left + areaInfo.coordConView.X; - Region.Right = Region.Left + (areaInfo.rcViewCaWindow.Right - areaInfo.rcViewCaWindow.Left); - Region.Top = currentViewport.Top + areaInfo.rcViewCaWindow.Top + areaInfo.coordConView.Y; - Region.Bottom = Region.Top + (areaInfo.rcViewCaWindow.Bottom - areaInfo.rcViewCaWindow.Top); + Region.left = currentViewport.left + areaInfo.rcViewCaWindow.left + areaInfo.coordConView.x; + Region.right = Region.left + (areaInfo.rcViewCaWindow.right - areaInfo.rcViewCaWindow.left); + Region.top = currentViewport.top + areaInfo.rcViewCaWindow.top + areaInfo.coordConView.y; + Region.bottom = Region.top + (areaInfo.rcViewCaWindow.bottom - areaInfo.rcViewCaWindow.top); - Region.Left = std::max(Region.Left, currentViewport.Left); - Region.Top = std::max(Region.Top, currentViewport.Top); - Region.Right = std::min(Region.Right, currentViewport.Right); - Region.Bottom = std::min(Region.Bottom, currentViewport.Bottom); + Region.left = std::max(Region.left, currentViewport.left); + Region.top = std::max(Region.top, currentViewport.top); + Region.right = std::min(Region.right, currentViewport.right); + Region.bottom = std::min(Region.bottom, currentViewport.bottom); if (Region) { - Region.Left = std::max(Region.Left, convRegion.Left()); - Region.Top = std::max(Region.Top, convRegion.Top()); - Region.Right = std::min(Region.Right, convRegion.RightInclusive()); - Region.Bottom = std::min(Region.Bottom, convRegion.BottomInclusive()); + Region.left = std::max(Region.left, convRegion.Left()); + Region.top = std::max(Region.top, convRegion.Top()); + Region.right = std::min(Region.right, convRegion.RightInclusive()); + Region.bottom = std::min(Region.bottom, convRegion.BottomInclusive()); if (Region) { // if we have a renderer, we need to update. @@ -116,8 +116,8 @@ void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, } [[nodiscard]] HRESULT ImeComposeData(std::wstring_view text, - gsl::span attributes, - gsl::span colorArray) + std::span attributes, + std::span colorArray) { try { diff --git a/src/host/dbcs.cpp b/src/host/dbcs.cpp index 0d5b51e21a..bed34bfdca 100644 --- a/src/host/dbcs.cpp +++ b/src/host/dbcs.cpp @@ -55,7 +55,7 @@ bool CheckBisectStringA(_In_reads_bytes_(cbBuf) PCHAR pchBuf, _In_ DWORD cbBuf, // - buffer - The buffer to walk and fix // Return Value: // - The length of the final modified buffer. -DWORD UnicodeRasterFontCellMungeOnRead(const gsl::span buffer) +DWORD UnicodeRasterFontCellMungeOnRead(const std::span buffer) { // Walk through the source CHAR_INFO and copy each to the destination. // EXCEPT for trailing bytes (this will de-duplicate the leading/trailing byte double copies of the CHAR_INFOs as stored in the buffer). @@ -177,77 +177,3 @@ BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage) return false; } } - -_Ret_range_(0, cbAnsi) - ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode, - const ULONG cchUnicode, - _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi, - const ULONG cbAnsi, - _Out_ std::unique_ptr& partialEvent) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto TmpUni = new (std::nothrow) WCHAR[cchUnicode]; - if (TmpUni == nullptr) - { - return 0; - } - - memcpy(TmpUni, pwchUnicode, cchUnicode * sizeof(WCHAR)); - - BYTE AsciiDbcs[2]; - AsciiDbcs[1] = 0; - - ULONG i, j; - for (i = 0, j = 0; i < cchUnicode && j < cbAnsi; i++, j++) - { - if (IsGlyphFullWidth(TmpUni[i])) - { - const auto NumBytes = sizeof(AsciiDbcs); - ConvertToOem(gci.CP, &TmpUni[i], 1, (LPSTR)&AsciiDbcs[0], NumBytes); - if (IsDBCSLeadByteConsole(AsciiDbcs[0], &gci.CPInfo)) - { - if (j < cbAnsi - 1) - { // -1 is safe DBCS in buffer - pchAnsi[j] = AsciiDbcs[0]; - j++; - pchAnsi[j] = AsciiDbcs[1]; - AsciiDbcs[1] = 0; - } - else - { - pchAnsi[j] = AsciiDbcs[0]; - break; - } - } - else - { - pchAnsi[j] = AsciiDbcs[0]; - AsciiDbcs[1] = 0; - } - } - else - { - ConvertToOem(gci.CP, &TmpUni[i], 1, &pchAnsi[j], 1); - } - } - - if (AsciiDbcs[1]) - { - try - { - auto keyEvent = std::make_unique(); - if (keyEvent.get()) - { - keyEvent->SetCharData(AsciiDbcs[1]); - partialEvent.reset(static_cast(keyEvent.release())); - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } - } - - delete[] TmpUni; - return j; -} diff --git a/src/host/dbcs.h b/src/host/dbcs.h index 53762d1188..db651eadef 100644 --- a/src/host/dbcs.h +++ b/src/host/dbcs.h @@ -22,17 +22,10 @@ Revision History: bool CheckBisectStringA(_In_reads_bytes_(cbBuf) PCHAR pchBuf, _In_ DWORD cbBuf, const CPINFO* const pCPInfo); -DWORD UnicodeRasterFontCellMungeOnRead(const gsl::span buffer); +DWORD UnicodeRasterFontCellMungeOnRead(const std::span buffer); bool IsDBCSLeadByteConsole(const CHAR ch, const CPINFO* const pCPInfo); BYTE CodePageToCharSet(const UINT uiCodePage); BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage); - -_Ret_range_(0, cbAnsi) - ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode, - const ULONG cchUnicode, - _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi, - const ULONG cbAnsi, - _Out_ std::unique_ptr& partialEvent); diff --git a/src/host/directio.cpp b/src/host/directio.cpp index 38be8e8838..1187bd4faa 100644 --- a/src/host/directio.cpp +++ b/src/host/directio.cpp @@ -25,105 +25,6 @@ using namespace Microsoft::Console::Types; using Microsoft::Console::Interactivity::ServiceLocator; -class CONSOLE_INFORMATION; - -// Routine Description: -// - converts non-unicode InputEvents to unicode InputEvents -// Arguments: -// inEvents - InputEvents to convert -// partialEvent - on output, will contain a partial dbcs byte char -// data if the last event in inEvents is a dbcs lead byte -// Return Value: -// - inEvents will contain unicode InputEvents -// - partialEvent may contain a partial dbcs KeyEvent -void EventsToUnicode(_Inout_ std::deque>& inEvents, - _Out_ std::unique_ptr& partialEvent) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - std::deque> outEvents; - - while (!inEvents.empty()) - { - auto currentEvent = std::move(inEvents.front()); - inEvents.pop_front(); - - if (currentEvent->EventType() != InputEventType::KeyEvent) - { - outEvents.push_back(std::move(currentEvent)); - } - else - { - const auto keyEvent = static_cast(currentEvent.get()); - - std::wstring outWChar; - auto hr = S_OK; - - // convert char data to unicode - if (IsDBCSLeadByteConsole(static_cast(keyEvent->GetCharData()), &gci.CPInfo)) - { - if (inEvents.empty()) - { - // we ran out of data and have a partial byte leftover - partialEvent = std::move(currentEvent); - break; - } - - // get the 2nd byte and convert to unicode - const auto keyEventEndByte = static_cast(inEvents.front().get()); - inEvents.pop_front(); - - char inBytes[] = { - static_cast(keyEvent->GetCharData()), - static_cast(keyEventEndByte->GetCharData()) - }; - try - { - outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); - } - catch (...) - { - hr = wil::ResultFromCaughtException(); - } - } - else - { - char inBytes[] = { - static_cast(keyEvent->GetCharData()) - }; - try - { - outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); - } - catch (...) - { - hr = wil::ResultFromCaughtException(); - } - } - - // push unicode key events back out - if (SUCCEEDED(hr) && outWChar.size() > 0) - { - auto unicodeKeyEvent = *keyEvent; - for (const auto wch : outWChar) - { - try - { - unicodeKeyEvent.SetCharData(wch); - outEvents.push_back(std::make_unique(unicodeKeyEvent)); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } - } - } - } - } - - inEvents.swap(outEvents); - return; -} - // Routine Description: // - This routine reads or peeks input events. In both cases, the events // are copied to the user's buffer. In the read case they are removed @@ -149,13 +50,13 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // - CONSOLE_STATUS_WAIT - If we didn't have enough data or needed to // block, this will be returned along with context in *ppWaiter. // - Or an out of memory/math/string error message in NTSTATUS format. -[[nodiscard]] static NTSTATUS _DoGetConsoleInput(InputBuffer& inputBuffer, - std::deque>& outEvents, - const size_t eventReadCount, - INPUT_READ_HANDLE_DATA& readHandleState, - const bool IsUnicode, - const bool IsPeek, - std::unique_ptr& waiter) noexcept +[[nodiscard]] HRESULT ApiRoutines::GetConsoleInputImpl(IConsoleInputObject& inputBuffer, + InputEventQueue& outEvents, + const size_t eventReadCount, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const bool IsPeek, + std::unique_ptr& waiter) noexcept { try { @@ -169,241 +70,23 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - std::deque> partialEvents; - if (!IsUnicode) - { - if (inputBuffer.IsReadPartialByteSequenceAvailable()) - { - partialEvents.push_back(inputBuffer.FetchReadPartialByteSequence(IsPeek)); - } - } - - size_t amountToRead; - if (FAILED(SizeTSub(eventReadCount, partialEvents.size(), &amountToRead))) - { - return STATUS_INTEGER_OVERFLOW; - } - std::deque> readEvents; - auto Status = inputBuffer.Read(readEvents, - amountToRead, - IsPeek, - true, - IsUnicode, - false); + const auto Status = inputBuffer.Read(outEvents, + eventReadCount, + IsPeek, + true, + IsUnicode, + false); if (CONSOLE_STATUS_WAIT == Status) { - FAIL_FAST_IF(!(readEvents.empty())); // If we're told to wait until later, move all of our context // to the read data object and send it back up to the server. waiter = std::make_unique(&inputBuffer, &readHandleState, - eventReadCount, - std::move(partialEvents)); - } - else if (NT_SUCCESS(Status)) - { - // split key events to oem chars if necessary - if (!IsUnicode) - { - try - { - SplitToOem(readEvents); - } - CATCH_LOG(); - } - - // combine partial and readEvents - while (!partialEvents.empty()) - { - readEvents.push_front(std::move(partialEvents.back())); - partialEvents.pop_back(); - } - - // move events over - for (size_t i = 0; i < eventReadCount; ++i) - { - if (readEvents.empty()) - { - break; - } - outEvents.push_back(std::move(readEvents.front())); - readEvents.pop_front(); - } - - // store partial event if necessary - if (!readEvents.empty()) - { - inputBuffer.StoreReadPartialByteSequence(std::move(readEvents.front())); - readEvents.pop_front(); - FAIL_FAST_IF(!(readEvents.empty())); - } + eventReadCount); } return Status; } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } -} - -// Routine Description: -// - Retrieves input records from the given input object and returns them to the client. -// - The peek version will NOT remove records when it copies them out. -// - The A version will convert to W using the console's current Input codepage (see SetConsoleCP) -// Arguments: -// - context - The input buffer to take records from to return to the client -// - outEvents - storage location for read events -// - eventsToRead - The number of input events to read -// - readHandleState - A structure that will help us maintain -// some input context across various calls on the same input -// handle. Primarily used to restore the "other piece" of partially -// returned strings (because client buffer wasn't big enough) on the -// next call. -// - waiter - If we have to wait (not enough data to fill client -// buffer), this contains context that will allow the server to -// restore this call later. -[[nodiscard]] HRESULT ApiRoutines::PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - try - { - auto Status = _DoGetConsoleInput(context, - outEvents, - eventsToRead, - readHandleState, - false, - true, - waiter); - if (CONSOLE_STATUS_WAIT == Status) - { - return HRESULT_FROM_NT(Status); - } - RETURN_NTSTATUS(Status); - } - CATCH_RETURN(); -} - -// Routine Description: -// - Retrieves input records from the given input object and returns them to the client. -// - The peek version will NOT remove records when it copies them out. -// - The W version accepts UCS-2 formatted characters (wide characters) -// Arguments: -// - context - The input buffer to take records from to return to the client -// - outEvents - storage location for read events -// - eventsToRead - The number of input events to read -// - readHandleState - A structure that will help us maintain -// some input context across various calls on the same input -// handle. Primarily used to restore the "other piece" of partially -// returned strings (because client buffer wasn't big enough) on the -// next call. -// - waiter - If we have to wait (not enough data to fill client -// buffer), this contains context that will allow the server to -// restore this call later. -[[nodiscard]] HRESULT ApiRoutines::PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - try - { - auto Status = _DoGetConsoleInput(context, - outEvents, - eventsToRead, - readHandleState, - true, - true, - waiter); - if (CONSOLE_STATUS_WAIT == Status) - { - return HRESULT_FROM_NT(Status); - } - RETURN_NTSTATUS(Status); - } - CATCH_RETURN(); -} - -// Routine Description: -// - Retrieves input records from the given input object and returns them to the client. -// - The read version WILL remove records when it copies them out. -// - The A version will convert to W using the console's current Input codepage (see SetConsoleCP) -// Arguments: -// - context - The input buffer to take records from to return to the client -// - outEvents - storage location for read events -// - eventsToRead - The number of input events to read -// - readHandleState - A structure that will help us maintain -// some input context across various calls on the same input -// handle. Primarily used to restore the "other piece" of partially -// returned strings (because client buffer wasn't big enough) on the -// next call. -// - waiter - If we have to wait (not enough data to fill client -// buffer), this contains context that will allow the server to -// restore this call later. -[[nodiscard]] HRESULT ApiRoutines::ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - try - { - auto Status = _DoGetConsoleInput(context, - outEvents, - eventsToRead, - readHandleState, - false, - false, - waiter); - if (CONSOLE_STATUS_WAIT == Status) - { - return HRESULT_FROM_NT(Status); - } - RETURN_NTSTATUS(Status); - } - CATCH_RETURN(); -} - -// Routine Description: -// - Retrieves input records from the given input object and returns them to the client. -// - The read version WILL remove records when it copies them out. -// - The W version accepts UCS-2 formatted characters (wide characters) -// Arguments: -// - context - The input buffer to take records from to return to the client -// - outEvents - storage location for read events -// - eventsToRead - The number of input events to read -// - readHandleState - A structure that will help us maintain -// some input context across various calls on the same input -// handle. Primarily used to restore the "other piece" of partially -// returned strings (because client buffer wasn't big enough) on the -// next call. -// - waiter - If we have to wait (not enough data to fill client -// buffer), this contains context that will allow the server to -// restore this call later. -[[nodiscard]] HRESULT ApiRoutines::ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept -{ - try - { - auto Status = _DoGetConsoleInput(context, - outEvents, - eventsToRead, - readHandleState, - true, - false, - waiter); - if (CONSOLE_STATUS_WAIT == Status) - { - return HRESULT_FROM_NT(Status); - } - RETURN_NTSTATUS(Status); - } CATCH_RETURN(); } @@ -418,7 +101,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // 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 { @@ -452,38 +135,99 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Return Value: // - HRESULT indicating success or failure [[nodiscard]] HRESULT ApiRoutines::WriteConsoleInputAImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept +try { written = 0; + if (buffer.empty()) + { + return S_OK; + } + LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - try + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + InputEventQueue events; + + auto it = buffer.begin(); + const auto end = buffer.end(); + + // Check out the loop below. When a previous call ended on a leading DBCS we store it for + // the next call to WriteConsoleInputAImpl to join it with the now available trailing DBCS. + if (context.IsWritePartialByteSequenceAvailable()) { - auto events = IInputEvent::Create(buffer); + auto lead = context.FetchWritePartialByteSequence(); + const auto& trail = *it; - // add partial byte event if necessary - if (context.IsWritePartialByteSequenceAvailable()) + if (trail.EventType == KEY_EVENT) { - events.push_front(context.FetchWritePartialByteSequence(false)); + const char narrow[2]{ + lead.Event.KeyEvent.uChar.AsciiChar, + trail.Event.KeyEvent.uChar.AsciiChar, + }; + wchar_t wide[2]; + const auto length = MultiByteToWideChar(gci.CP, 0, &narrow[0], 2, &wide[0], 2); + + for (int i = 0; i < length; i++) + { + lead.Event.KeyEvent.uChar.UnicodeChar = wide[i]; + events.push_back(lead); + } + + ++it; } - - // convert to unicode if necessary - std::unique_ptr partialEvent; - EventsToUnicode(events, partialEvent); - - if (partialEvent.get()) - { - context.StoreWritePartialByteSequence(std::move(partialEvent)); - } - - return _WriteConsoleInputWImplHelper(context, events, written, append); } - CATCH_RETURN(); + + for (; it != end; ++it) + { + if (it->EventType != KEY_EVENT) + { + events.push_back(*it); + continue; + } + + auto lead = *it; + char narrow[2]{ lead.Event.KeyEvent.uChar.AsciiChar }; + int narrowLength = 1; + + if (IsDBCSLeadByteConsole(lead.Event.KeyEvent.uChar.AsciiChar, &gci.CPInfo)) + { + ++it; + if (it == end) + { + // Missing trailing DBCS -> Store the lead for the next call to WriteConsoleInputAImpl. + context.StoreWritePartialByteSequence(lead); + break; + } + + const auto& trail = *it; + if (trail.EventType != KEY_EVENT) + { + // Invalid input -> Skip. + continue; + } + + narrow[1] = trail.Event.KeyEvent.uChar.AsciiChar; + narrowLength = 2; + } + + wchar_t wide[2]; + const auto length = MultiByteToWideChar(gci.CP, 0, &narrow[0], narrowLength, &wide[0], 2); + + for (int i = 0; i < length; i++) + { + lead.Event.KeyEvent.uChar.UnicodeChar = wide[i]; + events.push_back(lead); + } + } + + return _WriteConsoleInputWImplHelper(context, events, written, append); } +CATCH_RETURN(); // Routine Description: // - Writes events to the input buffer @@ -496,7 +240,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Return Value: // - HRESULT indicating success or failure [[nodiscard]] HRESULT ApiRoutines::WriteConsoleInputWImpl(InputBuffer& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept { @@ -507,9 +251,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, try { - auto events = IInputEvent::Create(buffer); - - return _WriteConsoleInputWImplHelper(context, events, written, append); + return _WriteConsoleInputWImplHelper(context, buffer, written, append); } CATCH_RETURN(); } @@ -524,7 +266,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Return Value: // - Generally S_OK. Could be a memory or math error code. [[nodiscard]] static HRESULT _ConvertCellsToAInplace(const UINT codepage, - const gsl::span buffer, + const std::span buffer, const Viewport rectangle) noexcept { try @@ -532,9 +274,9 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const auto size = rectangle.Dimensions(); auto outIter = buffer.begin(); - for (til::CoordType i = 0; i < size.Y; ++i) + for (til::CoordType i = 0; i < size.height; ++i) { - for (til::CoordType j = 0; j < size.X; ++j, ++outIter) + for (til::CoordType j = 0; j < size.width; ++j, ++outIter) { auto& in1 = *outIter; @@ -549,7 +291,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, if (WI_IsFlagSet(in1.Attributes, COMMON_LVB_LEADING_BYTE)) { // As long as we're not looking at the exact last column of the buffer... - if (j < size.X - 1) + if (j < size.width - 1) { // Walk forward one because we're about to consume two cells. ++j; @@ -601,7 +343,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Return Value: // - Generally S_OK. Could be a memory or math error code. [[nodiscard]] HRESULT _ConvertCellsToWInplace(const UINT codepage, - gsl::span buffer, + std::span buffer, const Viewport& rectangle) noexcept { try @@ -611,9 +353,9 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const auto size = rectangle.Dimensions(); auto outIter = buffer.begin(); - for (til::CoordType i = 0; i < size.Y; ++i) + for (til::CoordType i = 0; i < size.height; ++i) { - for (til::CoordType j = 0; j < size.X; ++j, ++outIter) + for (til::CoordType j = 0; j < size.width; ++j, ++outIter) { // Clear lead/trailing flags. We'll determine it for ourselves versus the given codepage. auto& in1 = *outIter; @@ -623,7 +365,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, if (IsDBCSLeadByteConsole(in1.Char.AsciiChar, &gci.OutputCPInfo)) { // If we're not on the last column, we have two cells to use. - if (j < size.X - 1) + if (j < size.width - 1) { // Mark we're consuming two cells. ++outIter; @@ -671,7 +413,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, CATCH_RETURN(); } -[[nodiscard]] static std::vector _ConvertCellsToMungedW(gsl::span buffer, const Viewport& rectangle) +[[nodiscard]] static std::vector _ConvertCellsToMungedW(std::span buffer, const Viewport& rectangle) { std::vector result; result.reserve(buffer.size()); @@ -679,9 +421,9 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const auto size = rectangle.Dimensions(); auto bufferIter = buffer.begin(); - for (til::CoordType i = 0; i < size.Y; i++) + for (til::CoordType i = 0; i < size.height; i++) { - for (til::CoordType j = 0; j < size.X; j++) + for (til::CoordType j = 0; j < size.width; j++) { // Prepare a candidate charinfo on the output side copying the colors but not the lead/trail information. auto candidate = *bufferIter; @@ -691,7 +433,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, if (IsGlyphFullWidth(candidate.Char.UnicodeChar)) { // If we're not on the final cell of the row... - if (j < size.X - 1) + if (j < size.width - 1) { // Mark that we're consuming two cells. j++; @@ -722,7 +464,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] static HRESULT _ReadConsoleOutputWImplHelper(const SCREEN_INFORMATION& context, - gsl::span targetBuffer, + std::span targetBuffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept { @@ -735,7 +477,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const auto targetSize = requestRectangle.Dimensions(); // If either dimension of the request is too small, return an empty rectangle as read and exit early. - if (targetSize.X <= 0 || targetSize.Y <= 0) + if (targetSize.width <= 0 || targetSize.height <= 0) { readRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 }); return S_OK; @@ -747,18 +489,18 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Clip the request rectangle to the size of the storage buffer auto clip = requestRectangle.ToExclusive(); - clip.Right = std::min(clip.Right, storageSize.X); - clip.Bottom = std::min(clip.Bottom, storageSize.Y); + clip.right = std::min(clip.right, storageSize.width); + clip.bottom = std::min(clip.bottom, storageSize.height); // Find the target point (where to write the user's buffer) // It will either be 0,0 or offset into the buffer by the inverse of the negative values. til::point targetPoint; - targetPoint.X = clip.Left < 0 ? -clip.Left : 0; - targetPoint.Y = clip.Top < 0 ? -clip.Top : 0; + targetPoint.x = clip.left < 0 ? -clip.left : 0; + targetPoint.y = clip.top < 0 ? -clip.top : 0; // The clipped rect must be inside the buffer size, so it has a minimum value of 0. (max of itself and 0) - clip.Left = std::max(clip.Left, 0); - clip.Top = std::max(clip.Top, 0); + clip.left = std::max(clip.left, 0); + clip.top = std::max(clip.top, 0); // The final "request rectangle" or the area inside the buffer we want to read, is the clipped dimensions. const auto clippedRequestRectangle = Viewport::FromExclusive(clip); @@ -795,11 +537,11 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, ++targetIter; // Increment the target - targetPos.X++; - if (targetPos.X >= targetSize.X) + targetPos.x++; + if (targetPos.x >= targetSize.width) { - targetPos.X = 0; - targetPos.Y++; + targetPos.x = 0; + targetPos.y++; } } @@ -812,7 +554,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] HRESULT ApiRoutines::ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept { @@ -834,7 +576,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] HRESULT ApiRoutines::ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept { @@ -858,7 +600,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] static HRESULT _WriteConsoleOutputWImplHelper(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Viewport& requestRectangle, Viewport& writtenRectangle) noexcept { @@ -871,7 +613,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const auto sourceSize = requestRectangle.Dimensions(); // If either dimension of the request is too small, return an empty rectangle as the read and exit early. - if (sourceSize.X <= 0 || sourceSize.Y <= 0) + if (sourceSize.width <= 0 || sourceSize.height <= 0) { writtenRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 }); return S_OK; @@ -879,7 +621,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // If the top and left of the destination we're trying to write it outside the buffer, // give the original request rectangle back and exit early OK. - if (requestRectangle.Left() >= storageSize.X || requestRectangle.Top() >= storageSize.Y) + if (requestRectangle.Left() >= storageSize.width || requestRectangle.Top() >= storageSize.height) { writtenRectangle = requestRectangle; return S_OK; @@ -888,38 +630,38 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // Do clipping according to the legacy patterns. auto writeRegion = requestRectangle.ToInclusive(); til::inclusive_rect sourceRect; - if (writeRegion.Right > storageSize.X - 1) + if (writeRegion.right > storageSize.width - 1) { - writeRegion.Right = storageSize.X - 1; + writeRegion.right = storageSize.width - 1; } - sourceRect.Right = writeRegion.Right - writeRegion.Left; - if (writeRegion.Bottom > storageSize.Y - 1) + sourceRect.right = writeRegion.right - writeRegion.left; + if (writeRegion.bottom > storageSize.height - 1) { - writeRegion.Bottom = storageSize.Y - 1; + writeRegion.bottom = storageSize.height - 1; } - sourceRect.Bottom = writeRegion.Bottom - writeRegion.Top; + sourceRect.bottom = writeRegion.bottom - writeRegion.top; - if (writeRegion.Left < 0) + if (writeRegion.left < 0) { - sourceRect.Left = -writeRegion.Left; - writeRegion.Left = 0; + sourceRect.left = -writeRegion.left; + writeRegion.left = 0; } else { - sourceRect.Left = 0; + sourceRect.left = 0; } - if (writeRegion.Top < 0) + if (writeRegion.top < 0) { - sourceRect.Top = -writeRegion.Top; - writeRegion.Top = 0; + sourceRect.top = -writeRegion.top; + writeRegion.top = 0; } else { - sourceRect.Top = 0; + sourceRect.top = 0; } - if (sourceRect.Left > sourceRect.Right || sourceRect.Top > sourceRect.Bottom) + if (sourceRect.left > sourceRect.right || sourceRect.top > sourceRect.bottom) { return E_INVALIDARG; } @@ -931,18 +673,18 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, // For every row in the request, create a view into the clamped portion of just the one line to write. // This allows us to restrict the width of the call without allocating/copying any memory by just making // a smaller view over the existing big blob of data from the original call. - for (; target.Y < writeRectangle.BottomExclusive(); target.Y++) + for (; target.y < writeRectangle.BottomExclusive(); target.y++) { // We find the offset into the original buffer by the dimensions of the original request rectangle. - const auto rowOffset = (target.Y - requestRectangle.Top()) * requestRectangle.Width(); - const auto colOffset = target.X - requestRectangle.Left(); + const auto rowOffset = (target.y - requestRectangle.Top()) * requestRectangle.Width(); + const auto colOffset = target.x - requestRectangle.Left(); const auto totalOffset = rowOffset + colOffset; // Now we make a subspan starting from that offset for as much of the original request as would fit const auto subspan = buffer.subspan(totalOffset, writeRectangle.Width()); // Convert to a CHAR_INFO view to fit into the iterator - const auto charInfos = gsl::span(subspan.data(), subspan.size()); + const auto charInfos = std::span(subspan.data(), subspan.size()); // Make the iterator and write to the target position. OutputCellIterator it(charInfos); @@ -958,7 +700,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Viewport& requestRectangle, Viewport& writtenRectangle) noexcept { @@ -979,7 +721,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, } [[nodiscard]] HRESULT ApiRoutines::WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, - gsl::span buffer, + std::span buffer, const Viewport& requestRectangle, Viewport& writtenRectangle) noexcept { @@ -1007,7 +749,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, [[nodiscard]] HRESULT ApiRoutines::ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { written = 0; @@ -1028,7 +770,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, [[nodiscard]] HRESULT ApiRoutines::ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { written = 0; @@ -1057,7 +799,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, [[nodiscard]] HRESULT ApiRoutines::ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept { written = 0; @@ -1125,7 +867,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, Cursor::CURSOR_SMALL_SIZE, &ScreenInfo); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { goto Exit; } @@ -1135,7 +877,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, Information->ShareMode, handle)); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { goto Exit; } @@ -1143,7 +885,7 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, SCREEN_INFORMATION::s_InsertScreenBuffer(ScreenInfo); Exit: - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { delete ScreenInfo; } diff --git a/src/host/exe/CConsoleHandoff.h b/src/host/exe/CConsoleHandoff.h index 67ee7bde11..095fb728f0 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/exe/exemain.cpp b/src/host/exe/exemain.cpp index 930942c94d..7aaa45271d 100644 --- a/src/host/exe/exemain.cpp +++ b/src/host/exe/exemain.cpp @@ -52,6 +52,27 @@ class DefaultOutOfProcModuleWithRegistrationFlag : public OutOfProcModuleWithReg // Holds the wwinmain open until COM tells us there are no more server connections wil::unique_event _comServerExitEvent; +[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) +{ + // Make sure this is a console file. + FILE_FS_DEVICE_INFORMATION DeviceInformation; + IO_STATUS_BLOCK IoStatusBlock; + const auto Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); + if (FAILED_NTSTATUS(Status)) + { + RETURN_NTSTATUS(Status); + } + else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) + { + return E_INVALIDARG; + } + else + { + return S_OK; + } +} + +#if TIL_FEATURE_LEGACYCONHOST_ENABLED static bool useV2 = true; static bool ConhostV2ForcedInRegistry() { @@ -66,7 +87,7 @@ static bool ConhostV2ForcedInRegistry() // open HKCU\Console wil::unique_hkey hConsoleSubKey; - LONG lStatus = NTSTATUS_FROM_WIN32(RegOpenKeyExW(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hConsoleSubKey)); + LONG lStatus = RegOpenKeyExW(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hConsoleSubKey); if (ERROR_SUCCESS == lStatus) { // now get the value of the ForceV2 reg value, if it exists @@ -101,26 +122,6 @@ static bool ConhostV2ForcedInRegistry() return fShouldUseConhostV2; } -[[nodiscard]] static HRESULT ValidateServerHandle(const HANDLE handle) -{ - // Make sure this is a console file. - FILE_FS_DEVICE_INFORMATION DeviceInformation; - IO_STATUS_BLOCK IoStatusBlock; - const auto Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); - if (!NT_SUCCESS(Status)) - { - RETURN_NTSTATUS(Status); - } - else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) - { - return E_INVALIDARG; - } - else - { - return S_OK; - } -} - static bool ShouldUseLegacyConhost(const ConsoleArguments& args) { if (args.InConptyMode()) @@ -185,6 +186,7 @@ static bool ShouldUseLegacyConhost(const ConsoleArguments& args) return hr; } +#endif // TIL_FEATURE_LEGACYCONHOST_ENABLED // Routine Description: // - Called back when COM says there is nothing left for our server to do and we can tear down. @@ -303,6 +305,7 @@ int CALLBACK wWinMain( else #endif { +#if TIL_FEATURE_LEGACYCONHOST_ENABLED if (ShouldUseLegacyConhost(args)) { useV2 = false; @@ -321,6 +324,7 @@ int CALLBACK wWinMain( } } if (useV2) +#endif // TIL_FEATURE_LEGACYCONHOST_ENABLED { if (args.ShouldCreateServerHandle()) { diff --git a/src/host/ft_fuzzer/fuzzmain.cpp b/src/host/ft_fuzzer/fuzzmain.cpp index 27763ebf21..cd3708d59e 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 "../../interactivity/inc/ServiceLocator.hpp" -#include "../../server/DeviceHandle.h" -#include "../../server/IoThread.h" #include "../_stream.h" -#include "../getset.h" -#include +#include "../../interactivity/inc/ServiceLocator.hpp" +#include "../../server/IoThread.h" struct NullDeviceComm : public IDeviceComm { @@ -74,7 +72,6 @@ struct NullDeviceComm : public IDeviceComm RETURN_IF_FAILED(gci.ProcessHandleList.AllocProcessData(GetCurrentProcessId(), GetCurrentThreadId(), 0, - nullptr, &pProcessHandle)); pProcessHandle->fRootProcess = true; @@ -133,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_PRINTABLE_CONTROL_CHARS | WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE, - &scrollY); + WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, true, &scrollY); return 0; } diff --git a/src/host/ft_host/API_ModeTests.cpp b/src/host/ft_host/API_ModeTests.cpp index 02a1577f87..9487b1773e 100644 --- a/src/host/ft_host/API_ModeTests.cpp +++ b/src/host/ft_host/API_ModeTests.cpp @@ -21,6 +21,7 @@ class ModeTests TEST_METHOD(TestConsoleModeInputScenario); TEST_METHOD(TestConsoleModeScreenBufferScenario); + TEST_METHOD(TestConsoleModeAcrossMultipleBuffers); TEST_METHOD(TestGetConsoleDisplayMode); @@ -94,6 +95,55 @@ void ModeTests::TestConsoleModeScreenBufferScenario() VERIFY_ARE_EQUAL(dwOutputMode, (DWORD)0, L"Verify able to set zero output flags"); } +void ModeTests::TestConsoleModeAcrossMultipleBuffers() +{ + auto dwInitialMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(Common::_hConsole, &dwInitialMode), + L"Get initial output flags"); + + Log::Comment(L"Verify initial flags match the expected defaults"); + VERIFY_IS_TRUE(WI_IsFlagSet(dwInitialMode, ENABLE_PROCESSED_OUTPUT)); + VERIFY_IS_TRUE(WI_IsFlagSet(dwInitialMode, ENABLE_WRAP_AT_EOL_OUTPUT)); + VERIFY_IS_TRUE(WI_IsFlagClear(dwInitialMode, DISABLE_NEWLINE_AUTO_RETURN)); + VERIFY_IS_TRUE(WI_IsFlagClear(dwInitialMode, ENABLE_LVB_GRID_WORLDWIDE)); + + // The initial VT flag may vary, dependent on the VirtualTerminalLevel registry entry. + const auto defaultVtFlag = WI_IsFlagSet(dwInitialMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + auto dwUpdatedMode = dwInitialMode; + WI_ClearFlag(dwUpdatedMode, ENABLE_PROCESSED_OUTPUT); + WI_ClearFlag(dwUpdatedMode, ENABLE_WRAP_AT_EOL_OUTPUT); + WI_SetFlag(dwUpdatedMode, DISABLE_NEWLINE_AUTO_RETURN); + WI_SetFlag(dwUpdatedMode, ENABLE_LVB_GRID_WORLDWIDE); + WI_UpdateFlag(dwUpdatedMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING, !defaultVtFlag); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(Common::_hConsole, dwUpdatedMode), + L"Update flags to the opposite of their initial values"); + + auto hSecondBuffer = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, + 0 /*dwShareMode*/, + nullptr /*lpSecurityAttributes*/, + CONSOLE_TEXTMODE_BUFFER, + nullptr /*lpReserved*/); + VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hSecondBuffer, L"Create a second screen buffer"); + + auto dwSecondBufferMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(hSecondBuffer, &dwSecondBufferMode), + L"Get initial flags for second buffer"); + + VERIFY_ARE_EQUAL(dwInitialMode, dwSecondBufferMode, L"Verify second buffer initialized with defaults"); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hSecondBuffer, dwSecondBufferMode), + L"Reapply mode to test if it affects the main buffer"); + + VERIFY_WIN32_BOOL_SUCCEEDED(CloseHandle(hSecondBuffer), L"Close the second buffer"); + + auto dwFinalMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(Common::_hConsole, &dwFinalMode), + L"Get flags from the main buffer again"); + + VERIFY_ARE_EQUAL(dwUpdatedMode, dwFinalMode, L"Verify main buffer flags haven't changed"); +} + void ModeTests::TestGetConsoleDisplayMode() { DWORD dwMode = 0; diff --git a/src/host/ft_host/CJK_DbcsTests.cpp b/src/host/ft_host/CJK_DbcsTests.cpp index e3091d17f2..68798b700f 100644 --- a/src/host/ft_host/CJK_DbcsTests.cpp +++ b/src/host/ft_host/CJK_DbcsTests.cpp @@ -2,9 +2,11 @@ // Licensed under the MIT license. #include "precomp.h" + #include #include -#include + +#include "../../types/inc/IInputEvent.hpp" #define JAPANESE_CP 932u @@ -80,8 +82,7 @@ namespace DbcsWriteRead const bool fReadUnicode, CharInfoPattern& rgChars); - void Verify(const CharInfoPattern& rgExpected, - const CharInfoPattern& rgActual); + void Verify(std::span rgExpected, std::span rgActual); void PrepExpected( const WORD wAttrWritten, @@ -116,6 +117,7 @@ class DbcsTests // This test must come before ones that launch another process as launching another process can tamper with the codepage // in ways that this test is not expecting. TEST_METHOD(TestMultibyteInputRetrieval); + TEST_METHOD(TestMultibyteInputCoalescing); BEGIN_TEST_METHOD(TestDbcsWriteRead) TEST_METHOD_PROPERTY(L"Data:fUseTrueTypeFont", L"{true, false}") @@ -154,6 +156,14 @@ class DbcsTests BEGIN_TEST_METHOD(TestDbcsStdCoutScenario) TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsBackupRestore) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestInvalidTrailer) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() }; bool DbcsTests::DbcsTestSetup() @@ -418,7 +428,7 @@ namespace PrepPattern static constexpr WORD leading = COMMON_LVB_LEADING_BYTE; static constexpr WORD trailing = COMMON_LVB_TRAILING_BYTE; - constexpr void replaceColorPlaceholders(CharInfoPattern& pattern, WORD attr) + constexpr void replaceColorPlaceholders(std::span pattern, WORD attr) { for (auto& info : pattern) { @@ -1313,9 +1323,9 @@ void DbcsWriteRead::RetrieveOutput(const HANDLE hOut, } } -void DbcsWriteRead::Verify(const CharInfoPattern& rgExpected, - const CharInfoPattern& rgActual) +void DbcsWriteRead::Verify(std::span rgExpected, std::span rgActual) { + VERIFY_ARE_EQUAL(rgExpected.size(), rgActual.size()); // We will walk through for the number of CHAR_INFOs expected. for (size_t i = 0; i < rgExpected.size(); i++) { @@ -1899,6 +1909,34 @@ void DbcsTests::TestMultibyteInputRetrieval() FlushConsoleInputBuffer(hIn); } +// This test ensures that two separate WriteConsoleInputA with trailing/leading DBCS are joined (coalesced) into a single wide character. +void DbcsTests::TestMultibyteInputCoalescing() +{ + SetConsoleCP(932); + + const auto in = GetStdHandle(STD_INPUT_HANDLE); + FlushConsoleInputBuffer(in); + + DWORD count; + { + const auto record = SynthesizeKeyEvent(true, 1, 123, 456, 0x82, 789); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); + } + { + const auto record = SynthesizeKeyEvent(true, 1, 234, 567, 0xA2, 890); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); + } + + // Asking for 2 records and asserting we only got 1 ensures + // that we receive the exact number of expected records. + INPUT_RECORD actual[2]; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(in, &actual[0], 2, &count)); + VERIFY_ARE_EQUAL(1u, count); + + const auto expected = SynthesizeKeyEvent(true, 1, 123, 456, L'い', 789); + VERIFY_ARE_EQUAL(expected, actual[0]); +} + void DbcsTests::TestDbcsOneByOne() { const auto hOut = GetStdOutputHandle(); @@ -2038,3 +2076,110 @@ void DbcsTests::TestDbcsStdCoutScenario() VERIFY_ARE_EQUAL(cchReadBack, dwRead, L"We should have read as many characters as we expected (length of original printed line.)"); VERIFY_ARE_EQUAL(String(test), String(psReadBack.get()), L"String should match what we wrote."); } + +// Read/WriteConsoleOutput allow a user to implement a restricted form of buffer "backup" and "restore". +// But what if the saved region clips ("bisects") a wide character? This test ensures that we restore proper +// wide characters when given an unpaired trailing/leading CHAR_INFO in the first/last column of the given region. +// In other words, writing a trailing CHAR_INFO will also automatically write a leading CHAR_INFO in the preceding cell. +void DbcsTests::TestDbcsBackupRestore() +{ + static_assert(PrepPattern::DoubledW.size() == 16); + + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + + // We backup/restore 2 lines at once to ensure that it works even then. After all, an incorrect implementation + // might ignore all but the absolutely first CHAR_INFO instead of handling the first CHAR_INFO *on each row*. + std::array expected; + std::ranges::copy(PrepPattern::DoubledW, expected.begin() + 0); + std::ranges::copy(PrepPattern::DoubledW, expected.begin() + 16); + + PrepPattern::replaceColorPlaceholders(expected, FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_GREEN); + + // DoubledW will show up like this in the top/left corner of the terminal: + // +---------------- + // |QいかなZYXWVUTに + // |QいかなZYXWVUTに + // + // Since those 4 Japanese characters probably aren't going to be monospace for you in your editor + // (as they most likely aren't exactly 2 ASCII characters wide), I'll continue referring to them like this: + // +---------------- + // |QaabbccZYXWVUTdd + // |QaabbccZYXWVUTdd + { + SMALL_RECT region{ 0, 0, 15, 1 }; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, expected.data(), { 16, 2 }, {}, ®ion)); + } + + // Make a "backup" of the viewport. The twist is that our backup region only + // copies the trailing/leading half of the first/last glyph respectively like so: + // +---------------- + // | abbccZYXWVUTd + std::array backup{}; + constexpr COORD backupSize{ 13, 2 }; + SMALL_RECT backupRegion{ 2, 0, 14, 1 }; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, backup.data(), backupSize, {}, &backupRegion)); + + // Destroy the text with some narrow ASCII characters, resulting in: + // +---------------- + // |Qxxxxxxxxxxxxxxx + // |Qxxxxxxxxxxxxxxx + { + DWORD ignored; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(out, L'x', 15, { 1, 0 }, &ignored)); + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(out, L'x', 15, { 1, 1 }, &ignored)); + } + + // Restore our "backup". The trailing half of the first wide glyph (indicated as "a" above) + // as well as the leading half of the last wide glyph ("d"), will automatically get a + // matching leading/trailing half respectively. In other words, this: + // +---------------- + // | abbccZYXWVUTd + // | abbccZYXWVUTd + // + // turns into this: + // +---------------- + // | aabbccZYXWVUTdd + // | aabbccZYXWVUTdd + // + // and so we restore this, overwriting all the "x" characters in the process: + // +---------------- + // |QいかなZYXWVUTに + // |QいかなZYXWVUTに + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, backup.data(), backupSize, {}, &backupRegion)); + + std::array infos{}; + { + SMALL_RECT region{ 0, 0, 15, 1 }; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, infos.data(), { 16, 2 }, {}, ®ion)); + } + DbcsWriteRead::Verify(expected, infos); +} + +// As tested by TestDbcsBackupRestore(), we do want to allow users to write trailers into the buffer, to allow +// for an area of the buffer to be backed up and restored via Read/WriteConsoleOutput. But apart from that use +// case, we'd generally do best to avoid trailers whenever possible, as conhost basically ignored them in the +// past and only rendered leaders. Applications might now be relying on us effectively ignoring trailers. +void DbcsTests::TestInvalidTrailer() +{ + auto expected = PrepPattern::DoubledW; + auto input = expected; + decltype(input) output{}; + + for (auto& v : input) + { + if (WI_IsFlagSet(v.Attributes, COMMON_LVB_TRAILING_BYTE)) + { + v.Char.UnicodeChar = 0xfffd; + } + } + + { + static constexpr COORD bufferSize{ 16, 1 }; + SMALL_RECT region{ 0, 0, 15, 0 }; + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, input.data(), bufferSize, {}, ®ion)); + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, output.data(), bufferSize, {}, ®ion)); + } + + DbcsWriteRead::Verify(expected, output); +} diff --git a/src/host/ft_host/InitTests.cpp b/src/host/ft_host/InitTests.cpp index 4c51a04802..50e5c0cf47 100644 --- a/src/host/ft_host/InitTests.cpp +++ b/src/host/ft_host/InitTests.cpp @@ -237,12 +237,12 @@ MODULE_SETUP(ModuleSetup) // to the one that belongs to the CMD.exe in the new OpenConsole.exe window. VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FreeConsole()); - // Wait a moment for the driver to be ready after freeing to attach. - Sleep(1000); VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(AttachConsole(dwFindPid)); - auto tries = 0; - while (tries < 5) + int tries = 0; + DWORD delay; + // This will wait for up to 32s in total (from 10ms to 163840ms) + for (delay = 10; delay < 30000u; delay *= 2) { tries++; Log::Comment(NoThrowString().Format(L"Attempt #%d to confirm we've attached", tries)); @@ -268,17 +268,20 @@ MODULE_SETUP(ModuleSetup) auto succeeded = GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore); if (!succeeded) { - auto gle = GetLastError(); + const auto gle = GetLastError(); VERIFY_ARE_EQUAL(6u, gle, L"If we fail to set up the console, GetLastError should return 6 here."); - Sleep(1000); + + // Sleep with a backoff, to give us longer to try next time. + WaitForSingleObject(GetCurrentThread(), delay); } else { + Log::Comment(NoThrowString().Format(L"Succeeded on try #%d", tries)); break; } }; - VERIFY_IS_LESS_THAN(tries, 5, L"Make sure we set up the new console in time"); + VERIFY_IS_LESS_THAN(delay, 30000u, L"Make sure we set up the new console in time"); return true; } diff --git a/src/host/ft_host/Message_KeyPressTests.cpp b/src/host/ft_host/Message_KeyPressTests.cpp index 53d8b9dade..07ce3ec926 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; @@ -103,6 +102,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 +218,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 +226,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/host/ft_integrity/IntegrityTest.cpp b/src/host/ft_integrity/IntegrityTest.cpp index 0ca45b0b5d..c5cc24d48d 100644 --- a/src/host/ft_integrity/IntegrityTest.cpp +++ b/src/host/ft_integrity/IntegrityTest.cpp @@ -214,14 +214,14 @@ void IntegrityTest::_TestValidationHelper(const bool fIsBlockExpected, GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), &csbiex); - LOG_OUTPUT(L"Buffer Size X:%d Y:%d", csbiex.dwSize.X, csbiex.dwSize.Y); + LOG_OUTPUT(L"Buffer Size X:%d Y:%d", csbiex.dwSize.width, csbiex.dwSize.height); - size_t cch = csbiex.dwSize.X; + size_t cch = csbiex.dwSize.width; wistd::unique_ptr stringData = wil::make_unique_nothrow(cch); THROW_IF_NULL_ALLOC(stringData); COORD coordRead = { 0 }; - for (coordRead.Y = 0; coordRead.Y < 8; coordRead.Y++) + for (coordRead.y = 0; coordRead.y < 8; coordRead.y++) { ZeroMemory(stringData.get(), sizeof(wchar_t) * cch); @@ -237,7 +237,7 @@ void IntegrityTest::_TestValidationHelper(const bool fIsBlockExpected, WEX::Common::String strActual; // At position 0, check the integrity. - if (coordRead.Y == 0) + if (coordRead.y == 0) { strExpected = pwszIntegrityExpected; } @@ -246,11 +246,11 @@ void IntegrityTest::_TestValidationHelper(const bool fIsBlockExpected, // For the rest, check whether the API call worked. if (fIsBlockExpected) { - strExpected = _rgpwszExpectedFail[coordRead.Y - 1]; + strExpected = _rgpwszExpectedFail[coordRead.y - 1]; } else { - strExpected = _rgpwszExpectedSuccess[coordRead.Y - 1]; + strExpected = _rgpwszExpectedSuccess[coordRead.y - 1]; } } stringData[strExpected.GetLength()] = L'\0'; diff --git a/src/host/ft_uia/Common/ShortcutHelper.cs b/src/host/ft_uia/Common/ShortcutHelper.cs index 2b99d53c15..77606070f8 100644 --- a/src/host/ft_uia/Common/ShortcutHelper.cs +++ b/src/host/ft_uia/Common/ShortcutHelper.cs @@ -88,7 +88,7 @@ namespace Conhost.UIA.Tests.Common { sldl.CopyDataBlock(WinConP.NT_CONSOLE_PROPS_SIG, out ppDataBlock); - // The marshaler doesn't like using the existing instance that we made above because it's a value type and + // The marshaller doesn't like using the existing instance that we made above because it's a value type and // there are potential string pointers here. Give it the type instead and it can handle setting everything up. props = (WinConP.NT_CONSOLE_PROPS)Marshal.PtrToStructure(ppDataBlock, typeof(WinConP.NT_CONSOLE_PROPS)); } diff --git a/src/host/ft_uia/Host.Tests.UIA.csproj b/src/host/ft_uia/Host.Tests.UIA.csproj index 0a3c8dd13a..222a3274d2 100644 --- a/src/host/ft_uia/Host.Tests.UIA.csproj +++ b/src/host/ft_uia/Host.Tests.UIA.csproj @@ -149,7 +149,7 @@ - copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\" + mkdir "$(OutDir)\WinAppDriver" 2>nul & copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\WinAppDriver\" - \ No newline at end of file + diff --git a/src/host/ft_uia/Init.cs b/src/host/ft_uia/Init.cs index e6c7fae56c..eb8b76cc92 100644 --- a/src/host/ft_uia/Init.cs +++ b/src/host/ft_uia/Init.cs @@ -28,7 +28,12 @@ namespace Host.Tests.UIA public static void SetupAll(TestContext context) { Log.Comment("Searching for WinAppDriver in the same directory where this test was launched from..."); - string winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver.exe"); + string winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver", "WinAppDriver.exe"); + + if (!File.Exists(winAppDriver)) + { + winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver.exe"); + } Log.Comment($"Attempting to launch WinAppDriver at: {winAppDriver}"); Log.Comment($"Working directory: {Environment.CurrentDirectory}"); diff --git a/src/host/getset.cpp b/src/host/getset.cpp index db84deff8d..eec9f720db 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -139,8 +139,8 @@ void ApiRoutines::GetConsoleScreenBufferInfoExImpl(const SCREEN_INFORMATION& con // to return an inclusive rect. // - For GetConsoleScreenBufferInfo, it will leave these values // untouched, returning an exclusive rect. - srWindow.Right += 1; - srWindow.Bottom += 1; + srWindow.right += 1; + srWindow.bottom += 1; data.dwSize = til::unwrap_coord_size(dwSize); data.dwCursorPosition = til::unwrap_coord(dwCursorPosition); @@ -368,17 +368,19 @@ void ApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept WI_ClearFlag(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS); } - const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) }; - - // Mouse input should be received when mouse mode is on and quick edit mode is off - // (for more information regarding the quirks of mouse mode and why/how it relates - // to quick edit mode, see GH#9970) - const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) }; - const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) }; - - if (oldMouseMode != newMouseMode) + if (gci.IsInVtIoMode()) { - gci.GetActiveInputBuffer()->PassThroughWin32MouseRequest(newMouseMode); + // Mouse input should be received when mouse mode is on and quick edit mode is off + // (for more information regarding the quirks of mouse mode and why/how it relates + // to quick edit mode, see GH#9970) + const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) }; + const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) }; + const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) }; + + if (oldMouseMode != newMouseMode) + { + LOG_IF_FAILED(gci.GetVtIo()->RequestMouseMode(newMouseMode)); + } } context.InputMode = mode; @@ -436,10 +438,6 @@ void ApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept screenInfo.GetStateMachine().ResetState(); } - gci.SetVirtTermLevel(WI_IsFlagSet(dwNewMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) ? 1 : 0); - gci.SetAutomaticReturnOnNewline(WI_IsFlagSet(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN) ? false : true); - gci.SetGridRenderingAllowedWorldwide(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_LVB_GRID_WORLDWIDE)); - // if we changed rendering modes then redraw the output buffer, // but only do this if we're not in conpty mode. if (!gci.IsInVtIoMode() && @@ -530,7 +528,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont // microsoft/terminal#3907 - We shouldn't resize the buffer to be // smaller than the viewport. This was previously erroneously checked // when the host was not in conpty mode. - RETURN_HR_IF(E_INVALIDARG, (size.X < screenInfo.GetViewport().Width() || size.Y < screenInfo.GetViewport().Height())); + RETURN_HR_IF(E_INVALIDARG, (size.width < screenInfo.GetViewport().Width() || size.height < screenInfo.GetViewport().Height())); // see MSFT:17415266 // We only really care about the minimum window size if we have a head. @@ -538,24 +536,24 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont { const auto coordMin = screenInfo.GetMinWindowSizeInCharacters(); // Make sure requested screen buffer size isn't smaller than the window. - RETURN_HR_IF(E_INVALIDARG, (size.Y < coordMin.Y || size.X < coordMin.X)); + RETURN_HR_IF(E_INVALIDARG, (size.height < coordMin.height || size.width < coordMin.width)); } // Ensure the requested size isn't larger than we can handle in our data type. - RETURN_HR_IF(E_INVALIDARG, (size.X == SHORT_MAX || size.Y == SHORT_MAX)); + RETURN_HR_IF(E_INVALIDARG, (size.width == SHORT_MAX || size.height == SHORT_MAX)); // Only do the resize if we're actually changing one of the dimensions const auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); - if (size.X != coordScreenBufferSize.X || size.Y != coordScreenBufferSize.Y) + if (size.width != coordScreenBufferSize.width || size.height != coordScreenBufferSize.height) { RETURN_IF_NTSTATUS_FAILED(screenInfo.ResizeScreenBuffer(size, TRUE)); } // Make sure the viewport doesn't now overflow the buffer dimensions. auto overflow = screenInfo.GetViewport().BottomRightExclusive() - screenInfo.GetBufferSize().Dimensions(); - if (overflow.X > 0 || overflow.Y > 0) + if (overflow.x > 0 || overflow.y > 0) { - overflow = { -std::max(overflow.X, 0), -std::max(overflow.Y, 0) }; + overflow = { -std::max(overflow.x, 0), -std::max(overflow.y, 0) }; RETURN_IF_NTSTATUS_FAILED(screenInfo.SetViewportOrigin(false, overflow, false)); } @@ -599,17 +597,10 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont auto& gci = g.getConsoleInformation(); const auto coordScreenBufferSize = context.GetBufferSize().Dimensions(); - const auto requestedBufferSize = data.dwSize; - if (requestedBufferSize.X != coordScreenBufferSize.X || - requestedBufferSize.Y != coordScreenBufferSize.Y) + const auto requestedBufferSize = til::wrap_coord_size(data.dwSize); + if (requestedBufferSize != coordScreenBufferSize) { - auto& commandLine = CommandLine::Instance(); - - commandLine.Hide(FALSE); - - LOG_IF_FAILED(context.ResizeScreenBuffer(til::wrap_coord_size(data.dwSize), TRUE)); - - commandLine.Show(); + LOG_IF_FAILED(context.ResizeScreenBuffer(requestedBufferSize, TRUE)); } const auto newBufferSize = context.GetBufferSize().Dimensions(); @@ -647,28 +638,20 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont // If we have a window, clamp the requested viewport to the max window size if (!ServiceLocator::LocateGlobals().IsHeadless()) { - NewSize.X = std::min(NewSize.X, data.dwMaximumWindowSize.X); - NewSize.Y = std::min(NewSize.Y, data.dwMaximumWindowSize.Y); + NewSize.width = std::min(NewSize.width, data.dwMaximumWindowSize.X); + NewSize.height = std::min(NewSize.height, data.dwMaximumWindowSize.Y); } // If wrap text is on, then the window width must be the same size as the buffer width if (gci.GetWrapText()) { - NewSize.X = newBufferSize.X; + NewSize.width = newBufferSize.width; } - if (NewSize.X != context.GetViewport().Width() || - NewSize.Y != context.GetViewport().Height()) + 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) @@ -684,9 +667,9 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont // However, we do need to make sure the viewport doesn't now overflow the buffer dimensions. auto overflow = context.GetViewport().BottomRightExclusive() - context.GetBufferSize().Dimensions(); - if (overflow.X > 0 || overflow.Y > 0) + if (overflow.x > 0 || overflow.y > 0) { - overflow = { -std::max(overflow.X, 0), -std::max(overflow.Y, 0) }; + overflow = { -std::max(overflow.x, 0), -std::max(overflow.y, 0) }; RETURN_IF_NTSTATUS_FAILED(context.SetViewportOrigin(false, overflow, false)); } @@ -723,10 +706,10 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont const auto coordScreenBufferSize = buffer.GetBufferSize().Dimensions(); // clang-format off - RETURN_HR_IF(E_INVALIDARG, (position.X >= coordScreenBufferSize.X || - position.Y >= coordScreenBufferSize.Y || - position.X < 0 || - position.Y < 0)); + RETURN_HR_IF(E_INVALIDARG, (position.x >= coordScreenBufferSize.width || + position.y >= coordScreenBufferSize.height || + position.x < 0 || + position.y < 0)); // clang-format on // MSFT: 15813316 - Try to use this SetCursorPosition call to inherit the cursor position. @@ -747,31 +730,31 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont { // When evaluating the X offset, we must convert the buffer position to // equivalent screen coordinates, taking line rendition into account. - const auto lineRendition = buffer.GetTextBuffer().GetLineRendition(position.Y); - const auto screenPosition = BufferToScreenLine({ position.X, position.Y, position.X, position.Y }, lineRendition); + const auto lineRendition = buffer.GetTextBuffer().GetLineRendition(position.y); + const auto screenPosition = BufferToScreenLine({ position.x, position.y, position.x, position.y }, lineRendition); - if (currentViewport.Left > screenPosition.Left) + if (currentViewport.left > screenPosition.left) { - delta.X = screenPosition.Left - currentViewport.Left; + delta.x = screenPosition.left - currentViewport.left; } - else if (currentViewport.Right < screenPosition.Right) + else if (currentViewport.right < screenPosition.right) { - delta.X = screenPosition.Right - currentViewport.Right; + delta.x = screenPosition.right - currentViewport.right; } - if (currentViewport.Top > position.Y) + if (currentViewport.top > position.y) { - delta.Y = position.Y - currentViewport.Top; + delta.y = position.y - currentViewport.top; } - else if (currentViewport.Bottom < position.Y) + else if (currentViewport.bottom < position.y) { - delta.Y = position.Y - currentViewport.Bottom; + delta.y = position.y - currentViewport.bottom; } } til::point newWindowOrigin; - newWindowOrigin.X = currentViewport.Left + delta.X; - newWindowOrigin.Y = currentViewport.Top + delta.Y; + newWindowOrigin.x = currentViewport.left + delta.x; + newWindowOrigin.y = currentViewport.top + delta.y; // SetViewportOrigin will worry about clamping these values to the // buffer for us. RETURN_IF_NTSTATUS_FAILED(buffer.SetViewportOrigin(true, newWindowOrigin, true)); @@ -838,17 +821,17 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont if (!isAbsolute) { auto currentViewport = context.GetViewport().ToInclusive(); - Window.Left += currentViewport.Left; - Window.Right += currentViewport.Right; - Window.Top += currentViewport.Top; - Window.Bottom += currentViewport.Bottom; + Window.left += currentViewport.left; + Window.right += currentViewport.right; + Window.top += currentViewport.top; + Window.bottom += currentViewport.bottom; } - RETURN_HR_IF(E_INVALIDARG, (Window.Right < Window.Left || Window.Bottom < Window.Top)); + RETURN_HR_IF(E_INVALIDARG, (Window.right < Window.left || Window.bottom < Window.top)); - til::point NewWindowSize; - NewWindowSize.X = CalcWindowSizeX(Window); - NewWindowSize.Y = CalcWindowSizeY(Window); + til::size NewWindowSize; + NewWindowSize.width = CalcWindowSizeX(Window); + NewWindowSize.height = CalcWindowSizeY(Window); // see MSFT:17415266 // If we have a actual head, we care about the maximum size the window can be. @@ -863,7 +846,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont if (!g.IsHeadless()) { const auto coordMax = context.GetMaxWindowSizeInCharacters(); - RETURN_HR_IF(E_INVALIDARG, (NewWindowSize.X > coordMax.X || NewWindowSize.Y > coordMax.Y)); + RETURN_HR_IF(E_INVALIDARG, (NewWindowSize.width > coordMax.width || NewWindowSize.height > coordMax.height)); } // Even if it's the same size, we need to post an update in case the scroll bars need to go away. @@ -959,12 +942,12 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont if (enableCmdShim && gci.IsInVtIoMode()) { const auto currentBufferDimensions = buffer.GetBufferSize().Dimensions(); - const auto sourceIsWholeBuffer = (source.Top == 0) && - (source.Left == 0) && - (source.Right == currentBufferDimensions.X) && - (source.Bottom == currentBufferDimensions.Y); - const auto targetIsNegativeBufferHeight = (target.X == 0) && - (target.Y == -currentBufferDimensions.Y); + const auto sourceIsWholeBuffer = (source.top == 0) && + (source.left == 0) && + (source.right == currentBufferDimensions.width) && + (source.bottom == currentBufferDimensions.height); + const auto targetIsNegativeBufferHeight = (target.x == 0) && + (target.y == -currentBufferDimensions.height); const auto noClipProvided = clip == std::nullopt; const auto fillIsBlank = (fillCharacter == UNICODE_SPACE) && (fillAttribute == buffer.GetAttributes().GetLegacyAttributes()); @@ -1286,7 +1269,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // - isOriginal - If true, gets the title when we booted up. If false, gets whatever it is set to right now. // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT GetConsoleTitleWImplHelper(std::optional> title, +[[nodiscard]] HRESULT GetConsoleTitleWImplHelper(std::optional> title, size_t& written, size_t& needed, const bool isOriginal) noexcept @@ -1337,7 +1320,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT GetConsoleTitleAImplHelper(gsl::span title, +[[nodiscard]] HRESULT GetConsoleTitleAImplHelper(std::span title, size_t& written, size_t& needed, const bool isOriginal) noexcept @@ -1367,7 +1350,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept auto unicodeBuffer = std::make_unique(unicodeSize); RETURN_IF_NULL_ALLOC(unicodeBuffer); - const gsl::span unicodeSpan(unicodeBuffer.get(), unicodeSize); + const std::span unicodeSpan(unicodeBuffer.get(), unicodeSize); // Retrieve the title in Unicode. RETURN_IF_FAILED(GetConsoleTitleWImplHelper(unicodeSpan, unicodeWritten, unicodeNeeded, isOriginal)); @@ -1425,7 +1408,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // - needed - The number of characters we would need to completely write out the title. // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT ApiRoutines::GetConsoleTitleAImpl(gsl::span title, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -1448,7 +1431,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // - needed - The number of characters we would need to completely write out the title. // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT ApiRoutines::GetConsoleTitleWImpl(gsl::span title, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -1471,7 +1454,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // - needed - The number of characters we would need to completely write out the title. // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT ApiRoutines::GetConsoleOriginalTitleAImpl(gsl::span title, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleOriginalTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept { @@ -1494,7 +1477,7 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept // - needed - The number of characters we would need to completely write out the title. // Return Value: // - S_OK, E_INVALIDARG, or failure code from thrown exception -[[nodiscard]] HRESULT ApiRoutines::GetConsoleOriginalTitleWImpl(gsl::span title, +[[nodiscard]] HRESULT ApiRoutines::GetConsoleOriginalTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept { diff --git a/src/host/history.cpp b/src/host/history.cpp index 58ae5eba21..245e906695 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,60 +153,23 @@ 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, - gsl::span buffer, - size_t& commandSize) +const std::vector& CommandHistory::GetCommands() const noexcept { - LastDisplayed = index; - - try - { - const auto& cmd = _commands.at(index); - if (cmd.size() > (size_t)buffer.size()) - { - commandSize = buffer.size(); // room for CRLF? - } - else - { - commandSize = cmd.size(); - } - - std::copy_n(cmd.cbegin(), commandSize, buffer.begin()); - - commandSize *= sizeof(wchar_t); - - return S_OK; - } - CATCH_RETURN(); + return _commands; } -[[nodiscard]] HRESULT CommandHistory::Retrieve(const SearchDirection searchDirection, - const gsl::span buffer, - size_t& commandSize) +std::wstring_view CommandHistory::Retrieve(const SearchDirection searchDirection) { - 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 @@ -216,29 +180,32 @@ std::wstring_view CommandHistory::GetNth(const SHORT index) const } else { - _Prev(LastDisplayed); + LastDisplayed--; } } else { - _Next(LastDisplayed); + LastDisplayed++; } - return RetrieveNth(LastDisplayed, buffer, commandSize); + return RetrieveNth(LastDisplayed); +} + +std::wstring_view CommandHistory::RetrieveNth(Index index) +{ + if (_commands.empty()) + { + LastDisplayed = 0; + return {}; + } + + LastDisplayed = std::clamp(index, 0, GetNumberOfCommands() - 1); + return _commands.at(LastDisplayed); } 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 +222,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 +293,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 +321,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 +329,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 +356,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 +396,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 +405,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 +488,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 +618,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. @@ -776,7 +704,7 @@ HRESULT ApiRoutines::GetConsoleCommandHistoryLengthWImpl(const std::wstring_view // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. HRESULT GetConsoleCommandHistoryWImplHelper(const std::wstring_view exeName, - gsl::span historyBuffer, + std::span historyBuffer, size_t& writtenOrNeeded) { // Ensure output variables are initialized @@ -796,10 +724,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; @@ -847,7 +773,7 @@ HRESULT GetConsoleCommandHistoryWImplHelper(const std::wstring_view exeName, // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. HRESULT ApiRoutines::GetConsoleCommandHistoryAImpl(const std::string_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -882,7 +808,7 @@ HRESULT ApiRoutines::GetConsoleCommandHistoryAImpl(const std::string_view exeNam // Call the Unicode version of this method size_t bufferWritten; - RETURN_IF_FAILED(GetConsoleCommandHistoryWImplHelper(exeNameW, gsl::span(buffer.get(), bufferNeeded), bufferWritten)); + RETURN_IF_FAILED(GetConsoleCommandHistoryWImplHelper(exeNameW, std::span(buffer.get(), bufferNeeded), bufferWritten)); // Convert result to A const auto converted = ConvertToA(codepage, { buffer.get(), bufferWritten }); @@ -911,7 +837,7 @@ HRESULT ApiRoutines::GetConsoleCommandHistoryAImpl(const std::string_view exeNam // Return Value: // - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. HRESULT ApiRoutines::GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept { LockConsole(); diff --git a/src/host/history.h b/src/host/history.h index b2c2bb17cf..172be22abf 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,36 +35,32 @@ public: }; 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; [[nodiscard]] HRESULT Add(const std::wstring_view command, const bool suppressDuplicates); - [[nodiscard]] HRESULT Retrieve(const SearchDirection searchDirection, - const gsl::span buffer, - size_t& commandSize); + std::wstring_view Retrieve(const SearchDirection searchDirection); + std::wstring_view RetrieveNth(Index index); - [[nodiscard]] HRESULT RetrieveNth(const SHORT index, - const gsl::span buffer, - size_t& commandSize); + Index GetNumberOfCommands() const; + std::wstring_view GetNth(Index index) const; + const std::vector& GetCommands() const noexcept; - size_t GetNumberOfCommands() const; - std::wstring_view GetNth(const SHORT index) const; - - 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 +68,24 @@ private: // _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/host-common.vcxitems b/src/host/host-common.vcxitems index 3ff022e82c..ee482e31a9 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 2141f9d9fd..ea13a0c0ef 100644 --- a/src/host/input.cpp +++ b/src/host/input.cpp @@ -105,20 +105,21 @@ 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) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlC); } @@ -130,11 +131,11 @@ 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); - if (gci.PopupCount == 0) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlBreak); } @@ -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); } } } @@ -195,8 +188,7 @@ void HandleFocusEvent(const BOOL fSetFocus) try { - const auto EventsWritten = gci.pInputBuffer->Write(std::make_unique(!!fSetFocus)); - FAIL_FAST_IF(EventsWritten != 1); + gci.pInputBuffer->WriteFocusEvent(fSetFocus); } catch (...) { @@ -211,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"); @@ -275,27 +267,21 @@ void ProcessCtrlEvents() const auto LimitingProcessId = gci.LimitingProcessId; gci.LimitingProcessId = 0; - ConsoleProcessTerminationRecord* rgProcessHandleList; - size_t cProcessHandleList; + std::vector termRecords; + const auto hr = gci.ProcessHandleList + .GetTerminationRecordsByGroupId(LimitingProcessId, + WI_IsFlagSet(gci.CtrlFlags, + CONSOLE_CTRL_CLOSE_FLAG), + termRecords); - auto hr = gci.ProcessHandleList - .GetTerminationRecordsByGroupId(LimitingProcessId, - WI_IsFlagSet(gci.CtrlFlags, - CONSOLE_CTRL_CLOSE_FLAG), - &rgProcessHandleList, - &cProcessHandleList); - - if (FAILED(hr) || cProcessHandleList == 0) + if (FAILED(hr) || termRecords.empty()) { gci.UnlockConsole(); return; } // Copy ctrl flags. - auto CtrlFlags = gci.CtrlFlags; - FAIL_FAST_IF(!(!((CtrlFlags & (CONSOLE_CTRL_CLOSE_FLAG | CONSOLE_CTRL_BREAK_FLAG | CONSOLE_CTRL_C_FLAG)) && (CtrlFlags & (CONSOLE_CTRL_LOGOFF_FLAG | CONSOLE_CTRL_SHUTDOWN_FLAG))))); - - gci.CtrlFlags = 0; + const auto CtrlFlags = std::exchange(gci.CtrlFlags, 0); gci.UnlockConsole(); @@ -330,10 +316,13 @@ void ProcessCtrlEvents() case CONSOLE_CTRL_SHUTDOWN_FLAG: EventType = CTRL_SHUTDOWN_EVENT; break; + + default: + return; } auto Status = STATUS_SUCCESS; - for (size_t i = 0; i < cProcessHandleList; i++) + for (const auto& r : termRecords) { /* * Status will be non-successful if a process attached to this console @@ -344,23 +333,16 @@ void ProcessCtrlEvents() * query. In this case, use best effort to send the close event but * ignore any errors. */ - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = ServiceLocator::LocateConsoleControl() - ->EndTask(rgProcessHandleList[i].dwProcessID, + ->EndTask(r.dwProcessID, EventType, CtrlFlags); - if (rgProcessHandleList[i].hProcess == nullptr) + if (!r.hProcess) { Status = STATUS_SUCCESS; } } - - if (rgProcessHandleList[i].hProcess != nullptr) - { - CloseHandle(rgProcessHandleList[i].hProcess); - } } - - delete[] rgProcessHandleList; } diff --git a/src/host/input.h b/src/host/input.h index 4b7667aa06..73f22dff77 100644 --- a/src/host/input.h +++ b/src/host/input.h @@ -58,11 +58,6 @@ private: 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 @@ -78,7 +73,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 2427d4c19e..3a4e556e44 100644 --- a/src/host/inputBuffer.cpp +++ b/src/host/inputBuffer.cpp @@ -3,12 +3,13 @@ #include "precomp.h" #include "inputBuffer.hpp" -#include "dbcs.h" + #include "stream.h" #include "../types/inc/GlyphWidth.hpp" -#include +#include +#include "misc.h" #include "../interactivity/inc/ServiceLocator.hpp" #define INPUT_BUFFER_DEFAULT_INPUT_MODE (ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_MOUSE_INPUT) @@ -24,71 +25,205 @@ using namespace Microsoft::Console; // Return Value: // - A new instance of InputBuffer InputBuffer::InputBuffer() : - InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE }, - WaitQueue{}, - _pTtyConnection(nullptr), - _termInput(std::bind(&InputBuffer::_HandleTerminalInputCallback, this, std::placeholders::_1)) + InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE } { - // The _termInput's constructor takes a reference to this object's _HandleTerminalInputCallback. - // We need to use std::bind to create a reference to that function without a reference to this InputBuffer - // initialize buffer header fInComposition = false; } -// Routine Description: -// - This routine frees the resources associated with an input buffer. -// Arguments: -// - None -// Return Value: -InputBuffer::~InputBuffer() = default; - -// Routine Description: -// - checks if any partial char data is available for reading operation -// Arguments: -// - None -// Return Value: -// - true if partial char data is available, false otherwise -bool InputBuffer::IsReadPartialByteSequenceAvailable() +// Transfer as many `wchar_t`s from source over to the `char`/`wchar_t` buffer `target`. After it returns, +// the start of the `source` and `target` slices will be offset by as many bytes as have been copied +// over, so that if you call this function again it'll continue copying from wherever it left off. +// +// It performs the necessary `WideCharToMultiByte` conversion if `isUnicode` is `false`. +// Since not all converted `char`s might fit into `target` it'll cache the remainder. The next +// time this function is called those cached `char`s will then be the first to be copied over. +void InputBuffer::Consume(bool isUnicode, std::wstring_view& source, std::span& target) { - return _readPartialByteSequence.get() != nullptr; -} + // `_cachedTextReaderA` might still contain target data from a previous invocation. + // `ConsumeCached` calls `_switchReadingMode` for us. + ConsumeCached(isUnicode, target); -// Routine Description: -// - reads any read partial char data available -// Arguments: -// - peek - if true, data will not be removed after being fetched -// Return Value: -// - the partial char data. may be nullptr if no data is available -std::unique_ptr InputBuffer::FetchReadPartialByteSequence(_In_ bool peek) -{ - if (!IsReadPartialByteSequenceAvailable()) + if (source.empty() || target.empty()) { - return std::unique_ptr(); + return; } - if (peek) + if (isUnicode) { - return IInputEvent::Create(_readPartialByteSequence->ToInputRecord()); + // The above block should either leave `target` or `_cachedTextReaderW` empty (or both). + // If we're here, `_cachedTextReaderW` should be empty. + assert(_cachedTextReaderW.empty()); + + til::bytes_transfer(target, source); } else { - std::unique_ptr outEvent; - outEvent.swap(_readPartialByteSequence); - return outEvent; + // The above block should either leave `target` or `_cachedTextReaderA` empty (or both). + // If we're here, `_cachedTextReaderA` should be empty. + assert(_cachedTextReaderA.empty()); + + const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP; + + // Fast path: Batch convert all data in case the user provided buffer is large enough. + { + const auto wideLength = gsl::narrow(source.size()); + const auto narrowLength = gsl::narrow(target.size()); + + const auto length = WideCharToMultiByte(cp, 0, source.data(), wideLength, target.data(), narrowLength, nullptr, nullptr); + if (length > 0) + { + source = {}; + til::bytes_advance(target, gsl::narrow_cast(length)); + return; + } + + const auto error = GetLastError(); + THROW_HR_IF(HRESULT_FROM_WIN32(error), error != ERROR_INSUFFICIENT_BUFFER); + } + + // Slow path: Character-wise conversion otherwise. We do this in order to only + // consume as many characters from `source` as necessary to fill `target`. + { + size_t read = 0; + + for (const auto& wch : source) + { + char buffer[8]; + const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr); + THROW_LAST_ERROR_IF(length <= 0); + + std::string_view slice{ &buffer[0], gsl::narrow_cast(length) }; + til::bytes_transfer(target, slice); + + ++read; + + if (!slice.empty()) + { + _cachedTextA = slice; + _cachedTextReaderA = _cachedTextA; + break; + } + } + + source = source.substr(read); + } } } -// Routine Description: -// - stores partial read char data for a later read. will overwrite -// any previously stored data. -// Arguments: -// - event - The event to store -// Return Value: -// - None -void InputBuffer::StoreReadPartialByteSequence(std::unique_ptr event) +// Same as `Consume`, but without any `source` characters. +void InputBuffer::ConsumeCached(bool isUnicode, std::span& target) { - _readPartialByteSequence.swap(event); + _switchReadingMode(isUnicode ? ReadingMode::StringW : ReadingMode::StringA); + + if (isUnicode) + { + if (!_cachedTextReaderW.empty()) + { + til::bytes_transfer(target, _cachedTextReaderW); + + if (_cachedTextReaderW.empty()) + { + // This is just so that we release memory eagerly. + _cachedTextW = std::wstring{}; + } + } + } + else + { + if (!_cachedTextReaderA.empty()) + { + til::bytes_transfer(target, _cachedTextReaderA); + + if (_cachedTextReaderA.empty()) + { + // This is just so that we release memory eagerly. + _cachedTextA = std::string{}; + } + } + } +} + +void InputBuffer::Cache(std::wstring_view source) +{ + const auto off = _cachedTextW.empty() ? 0 : _cachedTextReaderW.data() - _cachedTextW.data(); + _cachedTextW.append(source); + _cachedTextReaderW = std::wstring_view{ _cachedTextW }.substr(off); +} + +// Moves up to `count`, previously cached events into `target`. +size_t InputBuffer::ConsumeCached(bool isUnicode, size_t count, InputEventQueue& target) +{ + _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA); + + size_t i = 0; + + while (i < count && !_cachedInputEvents.empty()) + { + target.push_back(std::move(_cachedInputEvents.front())); + _cachedInputEvents.pop_front(); + i++; + } + + return i; +} + +// Copies up to `count`, previously cached events into `target`. +size_t InputBuffer::PeekCached(bool isUnicode, size_t count, InputEventQueue& target) +{ + _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA); + + size_t i = 0; + + for (const auto& e : _cachedInputEvents) + { + if (i >= count) + { + break; + } + + target.push_back(e); + i++; + } + + return i; +} + +// Trims `source` to have a size below or equal to `expectedSourceSize` by +// storing any extra events in `_cachedInputEvents` for later retrieval. +void InputBuffer::Cache(bool isUnicode, InputEventQueue& source, size_t expectedSourceSize) +{ + _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA); + + if (source.size() > expectedSourceSize) + { + _cachedInputEvents.insert( + _cachedInputEvents.end(), + std::make_move_iterator(source.begin() + expectedSourceSize), + std::make_move_iterator(source.end())); + source.resize(expectedSourceSize); + } +} + +void InputBuffer::_switchReadingMode(ReadingMode mode) +{ + if (_readingMode != mode) + { + _switchReadingModeSlowPath(mode); + } +} + +void InputBuffer::_switchReadingModeSlowPath(ReadingMode mode) +{ + _cachedTextA = std::string{}; + _cachedTextReaderA = {}; + + _cachedTextW = std::wstring{}; + _cachedTextReaderW = {}; + + _cachedInputEvents = std::deque{}; + + _readingMode = mode; } // Routine Description: @@ -98,9 +233,9 @@ void InputBuffer::StoreReadPartialByteSequence(std::unique_ptr even // - 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: @@ -109,23 +244,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: @@ -135,9 +257,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: @@ -212,32 +335,12 @@ 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()); } -void InputBuffer::SetTerminalConnection(_In_ Render::VtEngine* const pTtyConnection) -{ - this->_pTtyConnection = pTtyConnection; -} - -void InputBuffer::PassThroughWin32MouseRequest(bool enable) -{ - if (_pTtyConnection) - { - if (enable) - { - LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006h")); - } - else - { - LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006l")); - } - } -} - // Routine Description: // - This routine reads from the input buffer. // - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition @@ -255,222 +358,109 @@ 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, const bool Unicode, const bool Stream) +try { - try + assert(OutEvents.empty()); + + const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP; + + if (Peek) { - if (_storage.empty()) + PeekCached(Unicode, AmountToRead, OutEvents); + } + else + { + ConsumeCached(Unicode, AmountToRead, OutEvents); + } + + auto it = _storage.begin(); + const auto end = _storage.end(); + + while (it != end && OutEvents.size() < AmountToRead) + { + if (it->EventType == KEY_EVENT) { - if (!WaitForData) + auto event = *it; + WORD repeat = 1; + + // for stream reads we need to split any key events that have been coalesced + if (Stream) { - return STATUS_SUCCESS; + repeat = std::max(1, event.Event.KeyEvent.wRepeatCount); + event.Event.KeyEvent.wRepeatCount = 1; } - return CONSOLE_STATUS_WAIT; - } - // read from buffer - std::deque> events; - size_t eventsRead; - bool resetWaitEvent; - _ReadBuffer(events, - AmountToRead, - eventsRead, - Peek, - resetWaitEvent, - Unicode, - Stream); - - // copy events to outEvents - while (!events.empty()) - { - OutEvents.push_back(std::move(events.front())); - events.pop_front(); - } - - if (resetWaitEvent) - { - ServiceLocator::LocateGlobals().hInputEvent.ResetEvent(); - } - return STATUS_SUCCESS; - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } -} - -// Routine Description: -// - This routine reads a single event from the input buffer. -// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition -// if there isn't enough data in the buffer, and it can be set to not remove records as it reads them out. -// Note: -// - The console lock must be held when calling this routine. -// Arguments: -// - outEvent - where the read event is stored -// - Peek - If true, copy events to pInputRecord but don't remove them from the input buffer. -// - WaitForData - if true, wait until an event is input (if there aren't enough to fill client buffer). if false, return immediately -// - Unicode - true if the data in key events should be treated as unicode. false if they should be converted by the current input CP. -// - Stream - true if read should unpack KeyEvents that have a >1 repeat count. -// Return Value: -// - STATUS_SUCCESS if records were read into the client buffer and everything is OK. -// - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed) -// - otherwise a suitable memory/math/string error in NTSTATUS form. -[[nodiscard]] NTSTATUS InputBuffer::Read(_Out_ std::unique_ptr& outEvent, - const bool Peek, - const bool WaitForData, - const bool Unicode, - const bool Stream) -{ - NTSTATUS Status; - try - { - std::deque> outEvents; - Status = Read(outEvents, - 1, - Peek, - WaitForData, - Unicode, - Stream); - if (!outEvents.empty()) - { - outEvent.swap(outEvents.front()); - } - } - catch (...) - { - Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - - return Status; -} - -// Routine Description: -// - This routine reads from a buffer. It does the buffer manipulation. -// Arguments: -// - outEvents - where read events are placed -// - readCount - amount of events to read -// - eventsRead - where to store number of events read -// - peek - if true , don't remove data from buffer, just copy it. -// - resetWaitEvent - on exit, true if buffer became empty. -// - unicode - true if read should be done in unicode mode -// - streamRead - true if read should unpack KeyEvents that have a >1 repeat count. readCount must be 1 if streamRead is true. -// Return Value: -// - -// Note: -// - The console lock must be held when calling this routine. -void InputBuffer::_ReadBuffer(_Out_ std::deque>& outEvents, - const size_t readCount, - _Out_ size_t& eventsRead, - const bool peek, - _Out_ bool& resetWaitEvent, - const bool unicode, - const bool streamRead) -{ - // when stream reading, the previous behavior was to only allow reading of a single - // event at a time. - FAIL_FAST_IF(streamRead && readCount != 1); - - resetWaitEvent = false; - - std::deque> readEvents; - // we need another var to keep track of how many we've read - // because dbcs records count for two when we aren't doing a - // unicode read but the eventsRead count should return the number - // of events actually put into outRecords. - size_t virtualReadCount = 0; - - while (!_storage.empty() && virtualReadCount < readCount) - { - auto performNormalRead = true; - // for stream reads we need to split any key events that have been coalesced - if (streamRead) - { - if (_storage.front()->EventType() == InputEventType::KeyEvent) + if (Unicode) { - const auto pKeyEvent = static_cast(_storage.front().get()); - if (pKeyEvent->GetRepeatCount() > 1) + do { - // split the key event - auto streamKeyEvent = std::make_unique(*pKeyEvent); - streamKeyEvent->SetRepeatCount(1); - readEvents.push_back(std::move(streamKeyEvent)); - pKeyEvent->SetRepeatCount(pKeyEvent->GetRepeatCount() - 1); - performNormalRead = false; - } - } - } - - if (performNormalRead) - { - readEvents.push_back(std::move(_storage.front())); - _storage.pop_front(); - } - - ++virtualReadCount; - if (!unicode) - { - if (readEvents.back()->EventType() == InputEventType::KeyEvent) - { - const auto pKeyEvent = static_cast(readEvents.back().get()); - if (IsGlyphFullWidth(pKeyEvent->GetCharData())) - { - ++virtualReadCount; - } - } - } - } - - // the amount of events that were actually read - eventsRead = readEvents.size(); - - // copy the events back if we were supposed to peek - if (peek) - { - if (streamRead) - { - // we need to check and see if the event was split from a coalesced key event - // or if it was unrelated to the current front event in storage - if (!readEvents.empty() && - !_storage.empty() && - readEvents.back()->EventType() == InputEventType::KeyEvent && - _storage.front()->EventType() == InputEventType::KeyEvent && - _CanCoalesce(static_cast(*readEvents.back()), - static_cast(*_storage.front()))) - { - auto& keyEvent = static_cast(*_storage.front()); - keyEvent.SetRepeatCount(keyEvent.GetRepeatCount() + 1); + OutEvents.push_back(event); + repeat--; + } while (repeat > 0 && OutEvents.size() < AmountToRead); } else { - _storage.push_front(IInputEvent::Create(readEvents.back()->ToInputRecord())); + 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); + THROW_LAST_ERROR_IF(length <= 0); + + const std::string_view str{ &buffer[0], gsl::narrow_cast(length) }; + + do + { + for (const auto& ch : str) + { + // 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); + } + + if (repeat && !Peek) + { + it->Event.KeyEvent.wRepeatCount = repeat; + break; } } else { - for (auto it = readEvents.crbegin(); it != readEvents.crend(); ++it) - { - _storage.push_front(IInputEvent::Create((*it)->ToInputRecord())); - } + OutEvents.push_back(*it); } + + ++it; } - // move events read to proper deque - while (!readEvents.empty()) + if (!Peek) { - outEvents.push_back(std::move(readEvents.front())); - readEvents.pop_front(); + _storage.erase(_storage.begin(), it); } - // signal if we emptied the buffer + Cache(Unicode, OutEvents, AmountToRead); + + if (OutEvents.empty()) + { + return WaitForData ? CONSOLE_STATUS_WAIT : STATUS_SUCCESS; + } if (_storage.empty()) { - resetWaitEvent = true; + ServiceLocator::LocateGlobals().hInputEvent.ResetEvent(); } + return STATUS_SUCCESS; +} +catch (...) +{ + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } // Routine Description: @@ -482,23 +472,24 @@ void InputBuffer::_ReadBuffer(_Out_ std::deque>& ou // 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 { - _vtInputShouldSuppress = true; - auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; }); - _HandleConsoleSuspensionEvents(inEvents); if (inEvents.empty()) { return STATUS_SUCCESS; } + + _vtInputShouldSuppress = true; + auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; }); + // read all of the records out of the buffer, then write the // prepend ones, then write the original set. We need to do it // this way to handle any coalescing that might occur. // get all of the existing records, "emptying" the buffer - std::deque> existingStorage; + std::deque existingStorage; existingStorage.swap(_storage); // We will need this variable to pass to _WriteBuffer so it can attempt to determine wait status. @@ -512,10 +503,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. @@ -550,19 +541,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: @@ -574,18 +555,18 @@ 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 { - _vtInputShouldSuppress = true; - auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; }); - _HandleConsoleSuspensionEvents(inEvents); if (inEvents.empty()) { return 0; } + _vtInputShouldSuppress = true; + auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; }); + // Write to buffer. size_t EventsWritten; bool SetWaitEvent; @@ -607,6 +588,76 @@ size_t InputBuffer::Write(_Inout_ std::deque>& inEv } } +// This can be considered a "privileged" variant of Write() which allows FOCUS_EVENTs to generate focus VT sequences. +// If we didn't do this, someone could write a FOCUS_EVENT_RECORD with WriteConsoleInput, exit without flushing the +// input buffer and the next application will suddenly get a "\x1b[I" sequence in their input. See GH#13238. +void InputBuffer::WriteFocusEvent(bool focused) noexcept +{ + if (IsInVirtualTerminalInputMode()) + { + if (const auto out = _termInput.HandleFocus(focused)) + { + _HandleTerminalInputCallback(*out); + } + } + else + { + // This is a mini-version of Write(). + const auto wasEmpty = _storage.empty(); + _storage.push_back(SynthesizeFocusEvent(focused)); + if (wasEmpty) + { + ServiceLocator::LocateGlobals().hInputEvent.SetEvent(); + } + WakeUpReadersWaitingForData(); + } +} + +// Returns true when mouse input started. You should then capture the mouse and produce further events. +bool InputBuffer::WriteMouseEvent(til::point position, const unsigned int button, const short keyState, const short wheelDelta) +{ + if (IsInVirtualTerminalInputMode()) + { + // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx + // "If the high-order bit is 1, the key is down; otherwise, it is up." + static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; + + const TerminalInput::MouseButtonState state{ + WI_IsFlagSet(OneCoreSafeGetKeyState(VK_LBUTTON), KeyPressed), + WI_IsFlagSet(OneCoreSafeGetKeyState(VK_MBUTTON), KeyPressed), + WI_IsFlagSet(OneCoreSafeGetKeyState(VK_RBUTTON), KeyPressed) + }; + + // GH#6401: VT applications should be able to receive mouse events from outside the + // terminal buffer. This is likely to happen when the user drags the cursor offscreen. + // We shouldn't throw away perfectly good events when they're offscreen, so we just + // clamp them to be within the range [(0, 0), (W, H)]. + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.GetActiveOutputBuffer().GetViewport().ToOrigin().Clamp(position); + + if (const auto out = _termInput.HandleMouse(position, button, keyState, wheelDelta, state)) + { + _HandleTerminalInputCallback(*out); + return true; + } + } + + return false; +} + +// Ctrl-S is traditionally considered an alias for the pause key. +// This returns true if it's either of the two. +static bool IsPauseKey(const KEY_EVENT_RECORD& event) +{ + if (event.wVirtualKeyCode == VK_PAUSE) + { + return true; + } + + const auto ctrlButNotAlt = WI_IsAnyFlagSet(event.dwControlKeyState, CTRL_PRESSED) && WI_AreAllFlagsClear(event.dwControlKeyState, ALT_PRESSED); + return ctrlButNotAlt && event.wVirtualKeyCode == L'S'; +} + // Routine Description: // - Coalesces input events and transfers them to storage queue. // Arguments: @@ -619,31 +670,44 @@ size_t InputBuffer::Write(_Inout_ std::deque>& inEv // 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(); + eventsWritten = 0; setWaitEvent = false; const auto initiallyEmptyQueue = _storage.empty(); const auto initialInEventsSize = inEvents.size(); const auto vtInputMode = IsInVirtualTerminalInputMode(); - while (!inEvents.empty()) + for (const auto& inEvent : inEvents) { - // Pop the next event. + 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(inEvent.Event.KeyEvent.wVirtualKeyCode)) + { + UnblockWriteConsole(CONSOLE_OUTPUT_SUSPENDED); + continue; + } + // intercept control-s + if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && IsPauseKey(inEvent.Event.KeyEvent)) + { + WI_SetFlag(gci.Flags, CONSOLE_SUSPENDED); + continue; + } + } + // If we're in vt mode, try and handle it with the vt input module. // If it was handled, do nothing else for it. // If there was one event passed in, try coalescing it with the previous event currently in the buffer. // If it's not coalesced, append it to the buffer. - auto inEvent = std::move(inEvents.front()); - inEvents.pop_front(); if (vtInputMode) { // GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly - const auto handled = _termInput.HandleKey(inEvent.get()); - if (handled) + if (const auto out = _termInput.HandleKey(inEvent)) { + _HandleTerminalInputCallback(*out); eventsWritten++; continue; } @@ -653,42 +717,14 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& // record at a time because this is the original behavior of // the input buffer. Changing this behavior may break stuff // that was depending on it. - if (initialInEventsSize == 1 && !_storage.empty()) + if (initialInEventsSize == 1 && !_storage.empty() && _CoalesceEvent(inEvents[0])) { - // coalescing requires a deque of events, so push it back onto the front. - inEvents.push_front(std::move(inEvent)); - - auto coalesced = false; - // this looks kinda weird but we don't want to coalesce a - // mouse event and then try to coalesce a key event right after. - // - // we also pass the whole deque to the coalescing methods - // even though they only want one event because it should - // be their responsibility to maintain the correct state - // of the deque if they process any records in it. - if (_CoalesceMouseMovedEvents(inEvents)) - { - coalesced = true; - } - else if (_CoalesceRepeatedKeyPressEvents(inEvents)) - { - coalesced = true; - } - if (coalesced) - { - eventsWritten = 1; - return; - } - else - { - // We didn't coalesce the event. pull it from the queue again, - // to keep the state consistent with the start of this block. - inEvent = std::move(inEvents.front()); - inEvents.pop_front(); - } + eventsWritten++; + return; } + // 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()) @@ -697,74 +733,6 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>& } } -// Routine Description: -// - Checks if the last saved event and the first event of inRecords are -// both MOUSE_MOVED events. If they are, the last saved event is -// updated with the new mouse position and the first event of inRecords is -// dropped. -// Arguments: -// - inRecords - The incoming records to process. -// Return Value: -// true if events were coalesced, false if they were not. -// Note: -// - The size of inRecords must be 1. -// - Coalescing here means updating a record that already exists in -// the buffer with updated values from an incoming event, instead of -// storing the incoming event (which would make the original one -// redundant/out of date with the most current state). -bool InputBuffer::_CoalesceMouseMovedEvents(_Inout_ std::deque>& inEvents) -{ - FAIL_FAST_IF(!(inEvents.size() == 1)); - FAIL_FAST_IF(_storage.empty()); - const IInputEvent* const pFirstInEvent = inEvents.front().get(); - const IInputEvent* const pLastStoredEvent = _storage.back().get(); - if (pFirstInEvent->EventType() == InputEventType::MouseEvent && - pLastStoredEvent->EventType() == InputEventType::MouseEvent) - { - const auto pInMouseEvent = static_cast(pFirstInEvent); - const auto pLastMouseEvent = static_cast(pLastStoredEvent); - - if (pInMouseEvent->IsMouseMoveEvent() && - pLastMouseEvent->IsMouseMoveEvent()) - { - // update mouse moved position - const auto pMouseEvent = static_cast(_storage.back().release()); - pMouseEvent->SetPosition(pInMouseEvent->GetPosition()); - std::unique_ptr tempPtr(pMouseEvent); - tempPtr.swap(_storage.back()); - - inEvents.pop_front(); - return true; - } - } - return false; -} - -// Routine Description: -// - checks two KeyEvents to see if they're similar enough to be coalesced -// Arguments: -// - a - the first KeyEvent -// - b - the other KeyEvent -// Return Value: -// - true if the events could be coalesced, false otherwise -bool InputBuffer::_CanCoalesce(const KeyEvent& a, const KeyEvent& b) const noexcept -{ - if (WI_IsFlagSet(a.GetActiveModifierKeys(), NLS_IME_CONVERSION) && - a.GetCharData() == b.GetCharData() && - a.GetActiveModifierKeys() == b.GetActiveModifierKeys()) - { - return true; - } - // other key events check - else if (a.GetVirtualScanCode() == b.GetVirtualScanCode() && - a.GetCharData() == b.GetCharData() && - a.GetActiveModifierKeys() == b.GetActiveModifierKeys()) - { - return true; - } - return false; -} - // Routine Description:: // - If the last input event saved and the first input event in inRecords // are both a keypress down event for the same key, update the repeat @@ -779,76 +747,44 @@ bool InputBuffer::_CanCoalesce(const KeyEvent& a, const KeyEvent& b) const noexc // the buffer with updated values from an incoming event, instead of // storing the incoming event (which would make the original one // redundant/out of date with the most current state). -bool InputBuffer::_CoalesceRepeatedKeyPressEvents(_Inout_ std::deque>& inEvents) +bool InputBuffer::_CoalesceEvent(const INPUT_RECORD& inEvent) noexcept { - FAIL_FAST_IF(!(inEvents.size() == 1)); - FAIL_FAST_IF(_storage.empty()); - const IInputEvent* const pFirstInEvent = inEvents.front().get(); - const IInputEvent* const pLastStoredEvent = _storage.back().get(); - if (pFirstInEvent->EventType() == InputEventType::KeyEvent && - pLastStoredEvent->EventType() == InputEventType::KeyEvent) + auto& lastEvent = _storage.back(); + + if (lastEvent.EventType == MOUSE_EVENT && inEvent.EventType == MOUSE_EVENT) { - const auto pInKeyEvent = static_cast(pFirstInEvent); - const auto pLastKeyEvent = static_cast(pLastStoredEvent); + const auto& inMouse = inEvent.Event.MouseEvent; + auto& lastMouse = lastEvent.Event.MouseEvent; - if (pInKeyEvent->IsKeyDown() && - pLastKeyEvent->IsKeyDown() && - !IsGlyphFullWidth(pInKeyEvent->GetCharData()) && - _CanCoalesce(*pInKeyEvent, *pLastKeyEvent)) + if (lastMouse.dwEventFlags == MOUSE_MOVED && inMouse.dwEventFlags == MOUSE_MOVED) { - // increment repeat count - const auto pKeyEvent = static_cast(_storage.back().release()); - WORD repeatCount = pKeyEvent->GetRepeatCount() + pInKeyEvent->GetRepeatCount(); - pKeyEvent->SetRepeatCount(repeatCount); - std::unique_ptr tempPtr(pKeyEvent); - tempPtr.swap(_storage.back()); - - inEvents.pop_front(); + lastMouse.dwMousePosition = inMouse.dwMousePosition; return true; } } - return false; -} - -// Routine Description: -// - Handles records that suspend/resume the console. -// Arguments: -// - records - records to check for pause/unpause events -// Return Value: -// - None -// Note: -// - The console lock must be held when calling this routine. -// - will throw exception on error -void InputBuffer::_HandleConsoleSuspensionEvents(_Inout_ std::deque>& inEvents) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - std::deque> outEvents; - while (!inEvents.empty()) + else if (lastEvent.EventType == KEY_EVENT && inEvent.EventType == KEY_EVENT) { - auto currEvent = std::move(inEvents.front()); - inEvents.pop_front(); - if (currEvent->EventType() == InputEventType::KeyEvent) + 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.uChar.UnicodeChar)) { - const auto pKeyEvent = static_cast(currEvent.get()); - if (pKeyEvent->IsKeyDown()) - { - if (WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED) && - !IsSystemKey(pKeyEvent->GetVirtualKeyCode())) - { - UnblockWriteConsole(CONSOLE_OUTPUT_SUSPENDED); - continue; - } - else if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && pKeyEvent->IsPauseKey()) - { - WI_SetFlag(gci.Flags, CONSOLE_SUSPENDED); - continue; - } - } + lastKey.wRepeatCount += inKey.wRepeatCount; + return true; } - outEvents.push_back(std::move(currEvent)); } - inEvents.swap(outEvents); + + return false; } // Routine Description: @@ -869,16 +805,18 @@ bool InputBuffer::IsInVirtualTerminalInputMode() const // - inEvents - Series of input records to insert into the buffer // Return Value: // - -void InputBuffer::_HandleTerminalInputCallback(std::deque>& inEvents) +void InputBuffer::_HandleTerminalInputCallback(const TerminalInput::StringType& text) { try { - // add all input events to the storage queue - while (!inEvents.empty()) + if (text.empty()) { - auto inEvent = std::move(inEvents.front()); - inEvents.pop_front(); - _storage.push_back(std::move(inEvent)); + return; + } + + for (const auto& wch : text) + { + _storage.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0)); } if (!_vtInputShouldSuppress) diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp index 5c003faf03..ec7cd75b08 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 @@ -41,17 +26,20 @@ public: bool fInComposition; // specifies if there's an ongoing text composition InputBuffer(); - ~InputBuffer(); - // storage API for partial dbcs bytes being read from the buffer - bool IsReadPartialByteSequenceAvailable(); - std::unique_ptr FetchReadPartialByteSequence(_In_ bool peek); - void StoreReadPartialByteSequence(std::unique_ptr event); + // String oriented APIs + void Consume(bool isUnicode, std::wstring_view& source, std::span& target); + void ConsumeCached(bool isUnicode, std::span& target); + void Cache(std::wstring_view source); + // INPUT_RECORD oriented APIs + size_t ConsumeCached(bool isUnicode, size_t count, InputEventQueue& target); + size_t PeekCached(bool isUnicode, size_t count, InputEventQueue& target); + 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(); @@ -60,35 +48,42 @@ public: 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); bool IsInVirtualTerminalInputMode() const; Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput(); - void SetTerminalConnection(_In_ Microsoft::Console::Render::VtEngine* const pTtyConnection); - void PassThroughWin32MouseRequest(bool enable); private: - std::deque> _storage; - std::unique_ptr _readPartialByteSequence; - std::unique_ptr _writePartialByteSequence; + enum class ReadingMode : uint8_t + { + StringA, + StringW, + InputEventsA, + InputEventsW, + }; + + std::string _cachedTextA; + std::string_view _cachedTextReaderA; + std::wstring _cachedTextW; + std::wstring_view _cachedTextReaderW; + std::deque _cachedInputEvents; + ReadingMode _readingMode = ReadingMode::StringA; + + std::deque _storage; + INPUT_RECORD _writePartialByteSequence{}; + bool _writePartialByteSequenceAvailable = false; Microsoft::Console::VirtualTerminal::TerminalInput _termInput; - Microsoft::Console::Render::VtEngine* _pTtyConnection; // This flag is used in _HandleTerminalInputCallback // If the InputBuffer leads to a _HandleTerminalInputCallback call, @@ -96,24 +91,11 @@ private: // Otherwise, we should be calling them. bool _vtInputShouldSuppress{ false }; - void _ReadBuffer(_Out_ std::deque>& outEvents, - const size_t readCount, - _Out_ size_t& eventsRead, - const bool peek, - _Out_ bool& resetWaitEvent, - const bool unicode, - const bool streamRead); - - void _WriteBuffer(_Inout_ std::deque>& inRecords, - _Out_ size_t& eventsWritten, - _Out_ bool& setWaitEvent); - - bool _CanCoalesce(const KeyEvent& a, const KeyEvent& b) const noexcept; - bool _CoalesceMouseMovedEvents(_Inout_ std::deque>& inEvents); - bool _CoalesceRepeatedKeyPressEvents(_Inout_ std::deque>& inEvents); - void _HandleConsoleSuspensionEvents(_Inout_ std::deque>& inEvents); - - void _HandleTerminalInputCallback(_In_ std::deque>& inEvents); + void _switchReadingMode(ReadingMode mode); + void _switchReadingModeSlowPath(ReadingMode mode); + 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 friend class InputBufferTests; diff --git a/src/host/inputReadHandleData.cpp b/src/host/inputReadHandleData.cpp index e8ab613cb1..8628d13113 100644 --- a/src/host/inputReadHandleData.cpp +++ b/src/host/inputReadHandleData.cpp @@ -20,7 +20,7 @@ bool INPUT_READ_HANDLE_DATA::IsInputPending() const bool INPUT_READ_HANDLE_DATA::IsMultilineInput() const { - FAIL_FAST_IF(!_isInputPending); // we shouldn't have multiline input without a pending input. + assert(_isInputPending); // we shouldn't have multiline input without a pending input. return _isMultilineInput; } diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index fb42149b62..c108f2d141 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 f30fb7d3ac..329328f567 100644 --- a/src/host/misc.cpp +++ b/src/host/misc.cpp @@ -2,13 +2,12 @@ // Licensed under the MIT license. #include "precomp.h" - #include "misc.h" + +#include + #include "dbcs.h" - -#include "../types/inc/convert.hpp" #include "../types/inc/GlyphWidth.hpp" - #include "../interactivity/inc/ServiceLocator.hpp" #pragma hdrstop @@ -48,184 +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 all key events in the deque to the oem char data and adds -// them back to events. -// Arguments: -// - events - on input the IInputEvents to convert. on output, the -// converted input events -// Note: may throw on error -void SplitToOem(std::deque>& events) -{ - const auto codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP; - - // convert events to oem codepage - std::deque> convertedEvents; - while (!events.empty()) - { - auto currentEvent = std::move(events.front()); - events.pop_front(); - if (currentEvent->EventType() == InputEventType::KeyEvent) - { - const auto pKeyEvent = static_cast(currentEvent.get()); - // convert from wchar to char - std::wstring wstr{ pKeyEvent->GetCharData() }; - const auto str = ConvertToA(codepage, wstr); - - for (auto& ch : str) - { - auto tempEvent = std::make_unique(*pKeyEvent); - tempEvent->SetCharData(ch); - convertedEvents.push_back(std::move(tempEvent)); - } - } - else - { - convertedEvents.push_back(std::move(currentEvent)); - } - } - // move all events back - while (!convertedEvents.empty()) - { - events.push_back(std::move(convertedEvents.front())); - convertedEvents.pop_front(); - } -} - // Routine Description: // - Converts unicode characters to ANSI given a destination codepage // Arguments: @@ -250,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, @@ -271,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 6269353f5c..38f9bce3f4 100644 --- a/src/host/misc.h +++ b/src/host/misc.h @@ -28,37 +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; -void SplitToOem(std::deque>& events); - -int ConvertInputToUnicode(const UINT uiCodePage, - _In_reads_(cchSource) const CHAR* const pchSource, - const UINT cchSource, - _Out_writes_(cchTarget) WCHAR* const pwchTarget, - const UINT cchTarget) noexcept; - int ConvertOutputToUnicode(_In_ UINT uiCodePage, _In_reads_(cchSource) const CHAR* const pchSource, _In_ UINT cchSource, _Out_writes_(cchTarget) WCHAR* pwchTarget, _In_ UINT cchTarget) noexcept; - -bool DoBuffersOverlap(const BYTE* const pBufferA, - const UINT cbBufferA, - const BYTE* const pBufferB, - const UINT cbBufferB) noexcept; diff --git a/src/host/ntprivapi.cpp b/src/host/ntprivapi.cpp index d0c1249cb3..0b8793299f 100644 --- a/src/host/ntprivapi.cpp +++ b/src/host/ntprivapi.cpp @@ -5,10 +5,24 @@ #include "NtPrivApi.hpp" +namespace +{ + struct PROCESS_BASIC_INFORMATION_EXPANDED + { + NTSTATUS ExitStatus; + PVOID PebBaseAddress; + ULONG_PTR AffinityMask; + LONG BasePriority; + ULONG_PTR UniqueProcessId; + ULONG_PTR InheritedFromUniqueProcessId; + }; +} + [[nodiscard]] NTSTATUS NtPrivApi::s_GetProcessParentId(_Inout_ PULONG ProcessId) { // TODO: Get Parent current not really available without winternl + NtQueryInformationProcess. http://osgvsowi/8394495 OBJECT_ATTRIBUTES oa; +#pragma warning(suppress : 26477) // This macro contains a bare NULL InitializeObjectAttributes(&oa, nullptr, 0, nullptr, nullptr); CLIENT_ID ClientId; @@ -18,14 +32,14 @@ HANDLE ProcessHandle; auto Status = s_NtOpenProcess(&ProcessHandle, PROCESS_QUERY_LIMITED_INFORMATION, &oa, &ClientId); - PROCESS_BASIC_INFORMATION BasicInfo = { 0 }; - if (NT_SUCCESS(Status)) + PROCESS_BASIC_INFORMATION_EXPANDED BasicInfo = { 0 }; + if (SUCCEEDED_NTSTATUS(Status)) { Status = s_NtQueryInformationProcess(ProcessHandle, ProcessBasicInformation, &BasicInfo, sizeof(BasicInfo), nullptr); LOG_IF_FAILED(s_NtClose(ProcessHandle)); } - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { *ProcessId = 0; return Status; @@ -38,13 +52,13 @@ [[nodiscard]] NTSTATUS NtPrivApi::s_NtOpenProcess(_Out_ PHANDLE ProcessHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, - _In_opt_ PCLIENT_ID ClientId) + _In_opt_ CLIENT_ID* ClientId) { auto hNtDll = _Instance()._hNtDll; if (hNtDll != nullptr) { - typedef NTSTATUS (*PfnNtOpenProcess)(HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId); + typedef NTSTATUS (*PfnNtOpenProcess)(HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, CLIENT_ID * ClientId); static auto pfn = (PfnNtOpenProcess)GetProcAddress(hNtDll, "NtOpenProcess"); diff --git a/src/host/ntprivapi.hpp b/src/host/ntprivapi.hpp index b9f209c6e4..f16c1a9f9b 100644 --- a/src/host/ntprivapi.hpp +++ b/src/host/ntprivapi.hpp @@ -15,30 +15,6 @@ Author(s): #include "conddkrefs.h" -// From winternl.h - -typedef enum _PROCESSINFOCLASS -{ - ProcessBasicInformation = 0, - ProcessDebugPort = 7, - ProcessWow64Information = 26, - ProcessImageFileName = 27, - ProcessBreakOnTermination = 29 -} PROCESSINFOCLASS; - -typedef struct _PROCESS_BASIC_INFORMATION -{ - NTSTATUS ExitStatus; - PVOID PebBaseAddress; - ULONG_PTR AffinityMask; - LONG BasePriority; - ULONG_PTR UniqueProcessId; - ULONG_PTR InheritedFromUniqueProcessId; -} PROCESS_BASIC_INFORMATION; -typedef PROCESS_BASIC_INFORMATION* PPROCESS_BASIC_INFORMATION; - -// end From winternl.h - class NtPrivApi sealed { public: @@ -50,7 +26,7 @@ private: [[nodiscard]] static NTSTATUS s_NtOpenProcess(_Out_ PHANDLE ProcessHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, - _In_opt_ PCLIENT_ID ClientId); + _In_opt_ CLIENT_ID* ClientId); [[nodiscard]] static NTSTATUS s_NtQueryInformationProcess(_In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, diff --git a/src/host/output.cpp b/src/host/output.cpp index 140688fe42..bd168fce9c 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -49,7 +49,7 @@ using namespace Microsoft::Console::Interactivity; // TODO: MSFT 9355013: This needs to be resolved. We increment it once with no handle to ensure it's never cleaned up // and one always exists for the renderer (and potentially other functions.) // It's currently a load-bearing piece of code. http://osgvsowi/9355013 - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { gci.ScreenBuffers[0].IncrementOriginalScreenBuffer(); } @@ -80,11 +80,11 @@ static void _CopyRectangle(SCREEN_INFORMATION& screenInfo, // row locations instead of copying or moving anything. { const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); - const auto sourceFullRows = source.Width() == bufferSize.X; - const auto verticalCopyOnly = source.Left() == 0 && targetOrigin.X == 0; + const auto sourceFullRows = source.Width() == bufferSize.width; + const auto verticalCopyOnly = source.Left() == 0 && targetOrigin.x == 0; if (sourceFullRows && verticalCopyOnly) { - const auto delta = targetOrigin.Y - source.Top(); + const auto delta = targetOrigin.y - source.Top(); screenInfo.GetTextBuffer().ScrollRows(source.Top(), source.Height(), delta); @@ -102,12 +102,16 @@ static void _CopyRectangle(SCREEN_INFORMATION& screenInfo, auto sourcePos = source.GetWalkOrigin(walkDirection); auto targetPos = target.GetWalkOrigin(walkDirection); + // Note that we read two cells from the source before we start writing + // to the target, so a two-cell DBCS character can't accidentally delete + // itself when moving one cell horizontally. + auto next = OutputCell(*screenInfo.GetCellDataAt(sourcePos)); do { - const auto data = OutputCell(*screenInfo.GetCellDataAt(sourcePos)); - screenInfo.Write(OutputCellIterator({ &data, 1 }), targetPos); - + const auto current = next; source.WalkInBounds(sourcePos, walkDirection); + next = OutputCell(*screenInfo.GetCellDataAt(sourcePos)); + screenInfo.GetTextBuffer().WriteLine(OutputCellIterator({ ¤t, 1 }), targetPos); } while (target.WalkInBounds(targetPos, walkDirection)); } } @@ -152,14 +156,14 @@ std::vector ReadOutputAttributes(const SCREEN_INFORMATION& screenInfo, // If the first thing we read is trailing, pad with a space. // OR If the last thing we read is leading, pad with a space. - if ((amountRead == 0 && it->DbcsAttr().IsTrailing()) || - (amountRead == (amountToRead - 1) && it->DbcsAttr().IsLeading())) + if ((amountRead == 0 && it->DbcsAttr() == DbcsAttribute::Trailing) || + (amountRead == (amountToRead - 1) && it->DbcsAttr() == DbcsAttribute::Leading)) { retVal.push_back(legacyAttributes); } else { - retVal.push_back(legacyAttributes | it->DbcsAttr().GeneratePublicApiAttributeFormat()); + retVal.push_back(legacyAttributes | GeneratePublicApiAttributeFormat(it->DbcsAttr())); } amountRead++; @@ -208,15 +212,15 @@ std::wstring ReadOutputStringW(const SCREEN_INFORMATION& screenInfo, { // If the first thing we read is trailing, pad with a space. // OR If the last thing we read is leading, pad with a space. - if ((amountRead == 0 && it->DbcsAttr().IsTrailing()) || - (amountRead == (amountToRead - 1) && it->DbcsAttr().IsLeading())) + if ((amountRead == 0 && it->DbcsAttr() == DbcsAttribute::Trailing) || + (amountRead == (amountToRead - 1) && it->DbcsAttr() == DbcsAttribute::Leading)) { retVal += UNICODE_SPACE; } else { // Otherwise, add anything that isn't a trailing cell. (Trailings are duplicate copies of the leading.) - if (!it->DbcsAttr().IsTrailing()) + if (it->DbcsAttr() != DbcsAttribute::Trailing) { retVal += it->Chars(); } @@ -253,7 +257,7 @@ void ScreenBufferSizeChange(const til::size coordNewSize) try { - gci.pInputBuffer->Write(std::make_unique(coordNewSize)); + gci.pInputBuffer->Write(SynthesizeWindowBufferSizeEvent(coordNewSize)); } catch (...) { @@ -276,7 +280,7 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); if (pNotifier != nullptr) { - pNotifier->NotifyConsoleUpdateScrollEvent(target.Origin().X - source.Left(), target.Origin().Y - source.RightInclusive()); + pNotifier->NotifyConsoleUpdateScrollEvent(target.Origin().x - source.Left(), target.Origin().y - source.RightInclusive()); } } @@ -289,41 +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) -bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo) -{ - // Rotate the circular buffer around and wipe out the previous final line. - const auto inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); - auto fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(inVtMode); - if (fSuccess) - { - // 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); - } - } - } - return fSuccess; -} - // Routine Description: // - This routine copies ScrollRectangle to DestinationOrigin then fills in ScrollRectangle with Fill. // - The scroll region is copied to a third buffer, the scroll region is filled, then the original contents of the scroll region are copied to the destination. @@ -402,8 +371,8 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo, // the target origin. { auto currentSourceOrigin = source.Origin(); - targetOrigin.X += currentSourceOrigin.X - originalSourceOrigin.X; - targetOrigin.Y += currentSourceOrigin.Y - originalSourceOrigin.Y; + targetOrigin.x += currentSourceOrigin.x - originalSourceOrigin.x; + targetOrigin.y += currentSourceOrigin.y - originalSourceOrigin.y; } // And now the target viewport is the same size as the source viewport but at the different position. @@ -420,8 +389,8 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo, { const auto currentTargetOrigin = target.Origin(); auto sourceOrigin = source.Origin(); - sourceOrigin.X += currentTargetOrigin.X - originalTargetOrigin.X; - sourceOrigin.Y += currentTargetOrigin.Y - originalTargetOrigin.Y; + sourceOrigin.x += currentTargetOrigin.x - originalTargetOrigin.x; + sourceOrigin.y += currentTargetOrigin.y - originalTargetOrigin.y; source = Viewport::FromDimensions(sourceOrigin, target.Dimensions()); } @@ -454,7 +423,7 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo, // If we're scrolling an area that encompasses the full buffer width, // then the filled rows should also have their line rendition reset. - if (view.Width() == buffer.Width() && destinationOriginGiven.X == 0) + if (view.Width() == buffer.Width() && destinationOriginGiven.x == 0) { screenInfo.GetTextBuffer().ResetLineRenditionRange(view.Top(), view.BottomExclusive()); } @@ -500,6 +469,7 @@ void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo) void CloseConsoleProcessState() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // If there are no connected processes, sending control events is pointless as there's no one do send them to. In // this case we'll just exit conhost. @@ -511,14 +481,4 @@ void CloseConsoleProcessState() } HandleCtrlEvent(CTRL_CLOSE_EVENT); - - // Jiggle the handle: (see MSFT:19419231) - // When we call this function, we'll only actually close the console once - // we're totally unlocked. If our caller has the console locked, great, - // we'll dispatch the ctrl event once they unlock. However, if they're - // not running under lock (eg PtySignalInputThread::_GetData), then the - // ctrl event will never actually get dispatched. - // So, lock and unlock here, to make sure the ctrl event gets handled. - LockConsole(); - UnlockConsole(); } diff --git a/src/host/output.h b/src/host/output.h index 849ba50559..ffbe75b3ad 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); -bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo); - // For handling process handle state, not the window state itself. void CloseConsoleProcessState(); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 99af19715f..a48e189f00 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -24,39 +24,6 @@ ConhostInternalGetSet::ConhostInternalGetSet(_In_ IIoProvider& io) : { } -// Routine Description: -// - Handles the print action from the state machine -// Arguments: -// - string - The string to be printed. -// Return Value: -// - -void ConhostInternalGetSet::PrintString(const std::wstring_view string) -{ - auto dwNumBytes = string.size() * sizeof(wchar_t); - - auto& cursor = _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - if (!cursor.IsOn()) - { - cursor.SetIsOn(true); - } - - // Defer the cursor drawing while we are iterating the string, for a better performance. - // We can not waste time displaying a cursor event when we know more text is coming right behind it. - cursor.StartDeferDrawing(); - const auto ntstatus = WriteCharsLegacy(_io.GetActiveOutputBuffer(), - string.data(), - string.data(), - string.data(), - &dwNumBytes, - nullptr, - _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition().X, - WC_LIMIT_BACKSPACE | WC_DELAY_EOL_WRAP, - nullptr); - cursor.EndDeferDrawing(); - - THROW_IF_NTSTATUS_FAILED(ntstatus); -} - // - Sends a string response to the input stream of the console. // - Used by various commands where the program attached would like a reply to one of the commands issued. // - This will generate two "key presses" (one down, one up) for every character in the string and place them into the head of the console's input stream. @@ -66,18 +33,17 @@ void ConhostInternalGetSet::PrintString(const std::wstring_view string) // - 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 @@ -129,9 +95,11 @@ til::rect ConhostInternalGetSet::GetViewport() const void ConhostInternalGetSet::SetViewportPosition(const til::point position) { auto& info = _io.GetActiveOutputBuffer(); - const auto dimensions = info.GetViewport().Dimensions(); - const auto windowRect = til::rect{ position, dimensions }.to_inclusive_rect(); - THROW_IF_FAILED(ServiceLocator::LocateGlobals().api->SetConsoleWindowInfoImpl(info, true, windowRect)); + THROW_IF_FAILED(info.SetViewportOrigin(true, position, false)); + // SetViewportOrigin() only updates the virtual bottom (the bottom coordinate of the area + // in the text buffer a VT client writes its output into) when it's moving downwards. + // But this function is meant to truly move the viewport no matter what. Otherwise `tput reset` breaks. + info.UpdateBottom(); } // Method Description: @@ -147,80 +115,49 @@ void ConhostInternalGetSet::SetTextAttributes(const TextAttribute& attrs) } // Routine Description: -// - Sets the ENABLE_WRAP_AT_EOL_OUTPUT mode. This controls whether the cursor moves -// to the beginning of the next row when it reaches the end of the current row. +// - Sets the state of one of the system modes. // Arguments: -// - wrapAtEOL - set to true to wrap, false to overwrite the last character. +// - mode - The mode being updated. +// - enabled - True to enable the mode, false to disable it. // Return Value: // - -void ConhostInternalGetSet::SetAutoWrapMode(const bool wrapAtEOL) +void ConhostInternalGetSet::SetSystemMode(const Mode mode, const bool enabled) { - auto& outputMode = _io.GetActiveOutputBuffer().OutputMode; - WI_UpdateFlag(outputMode, ENABLE_WRAP_AT_EOL_OUTPUT, wrapAtEOL); + switch (mode) + { + case Mode::AutoWrap: + WI_UpdateFlag(_io.GetActiveOutputBuffer().OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT, enabled); + break; + case Mode::LineFeed: + WI_UpdateFlag(_io.GetActiveOutputBuffer().OutputMode, DISABLE_NEWLINE_AUTO_RETURN, !enabled); + break; + case Mode::BracketedPaste: + ServiceLocator::LocateGlobals().getConsoleInformation().SetBracketedPasteMode(enabled); + break; + default: + THROW_HR(E_INVALIDARG); + } } // Routine Description: -// - Sets the top and bottom scrolling margins for the current page. This creates -// a subsection of the screen that scrolls when input reaches the end of the -// region, leaving the rest of the screen untouched. +// - Retrieves the current state of one of the system modes. // Arguments: -// - scrollMargins - A rect who's Top and Bottom members will be used to set -// the new values of the top and bottom margins. If (0,0), then the margins -// will be disabled. NOTE: This is a rect in the case that we'll need the -// left and right margins in the future. +// - mode - The mode being queried. // Return Value: -// - -void ConhostInternalGetSet::SetScrollingRegion(const til::inclusive_rect& scrollMargins) +// - true if the mode is enabled. false otherwise. +bool ConhostInternalGetSet::GetSystemMode(const Mode mode) const { - auto& screenInfo = _io.GetActiveOutputBuffer(); - auto srScrollMargins = screenInfo.GetRelativeScrollMargins().ToInclusive(); - srScrollMargins.Top = scrollMargins.Top; - srScrollMargins.Bottom = scrollMargins.Bottom; - screenInfo.SetScrollMargins(Viewport::FromInclusive(srScrollMargins)); -} - -// Method Description: -// - Retrieves the current Line Feed/New Line (LNM) mode. -// Arguments: -// - None -// Return Value: -// - true if a line feed also produces a carriage return. false otherwise. -bool ConhostInternalGetSet::GetLineFeedMode() const -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - return gci.IsReturnOnNewlineAutomatic(); -} - -// Routine Description: -// - Performs a line feed, possibly preceded by carriage return. -// Arguments: -// - withReturn - Set to true if a carriage return should be performed as well. -// Return Value: -// - -void ConhostInternalGetSet::LineFeed(const bool withReturn) -{ - auto& screenInfo = _io.GetActiveOutputBuffer(); - auto& textBuffer = screenInfo.GetTextBuffer(); - auto cursorPosition = textBuffer.GetCursor().GetPosition(); - - // We turn the cursor on before an operation that might scroll the viewport, otherwise - // that can result in an old copy of the cursor being left behind on the screen. - textBuffer.GetCursor().SetIsOn(true); - - // Since we are explicitly moving down a row, clear the wrap status on the row we're leaving - textBuffer.GetRowByOffset(cursorPosition.Y).SetWrapForced(false); - - cursorPosition.Y += 1; - if (withReturn) + switch (mode) { - cursorPosition.X = 0; + case Mode::AutoWrap: + return WI_IsFlagSet(_io.GetActiveOutputBuffer().OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); + case Mode::LineFeed: + return WI_IsFlagClear(_io.GetActiveOutputBuffer().OutputMode, DISABLE_NEWLINE_AUTO_RETURN); + case Mode::BracketedPaste: + return ServiceLocator::LocateGlobals().getConsoleInformation().GetBracketedPasteMode(); + default: + THROW_HR(E_INVALIDARG); } - else - { - cursorPosition = textBuffer.ClampPositionWithinLine(cursorPosition); - } - - THROW_IF_NTSTATUS_FAILED(AdjustCursorPosition(screenInfo, cursorPosition, FALSE, nullptr)); } // Routine Description: @@ -247,11 +184,13 @@ void ConhostInternalGetSet::SetWindowTitle(std::wstring_view title) // - Swaps to the alternate screen buffer. In virtual terminals, there exists both a "main" // screen buffer and an alternate. This creates a new alternate, and switches to it. // If there is an already existing alternate, it is discarded. +// Arguments: +// - attrs - the attributes the buffer is initialized with. // Return Value: // - -void ConhostInternalGetSet::UseAlternateScreenBuffer() +void ConhostInternalGetSet::UseAlternateScreenBuffer(const TextAttribute& attrs) { - THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer()); + THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer(attrs)); } // Routine Description: @@ -318,18 +257,6 @@ unsigned int ConhostInternalGetSet::GetConsoleOutputCP() const return ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP; } -// Routine Description: -// - Sets the XTerm bracketed paste mode. This controls whether pasted content is -// bracketed with control sequences to differentiate it from typed text. -// Arguments: -// - enable - set to true to enable bracketing, false to disable. -// Return Value: -// - -void ConhostInternalGetSet::EnableXtermBracketedPasteMode(const bool /*enabled*/) -{ - // TODO -} - // Routine Description: // - Copies the given content to the clipboard. // Arguments: @@ -402,6 +329,14 @@ bool ConhostInternalGetSet::ResizeWindow(const til::CoordType sColumns, const ti auto api = ServiceLocator::LocateGlobals().api; auto& screenInfo = _io.GetActiveOutputBuffer(); + // We need to save the attributes separately, since the wAttributes field in + // CONSOLE_SCREEN_BUFFER_INFOEX is not capable of representing the extended + // attribute values, and can end up corrupting that data when restored. + const auto attributes = screenInfo.GetTextBuffer().GetCurrentAttributes(); + const auto restoreAttributes = wil::scope_exit([&] { + screenInfo.GetTextBuffer().SetCurrentAttributes(attributes); + }); + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); api->GetConsoleScreenBufferInfoExImpl(screenInfo, csbiex); @@ -472,7 +407,7 @@ bool ConhostInternalGetSet::IsVtInputEnabled() const void ConhostInternalGetSet::NotifyAccessibilityChange(const til::rect& changedRect) { auto& screenInfo = _io.GetActiveOutputBuffer(); - if (screenInfo.HasAccessibilityEventing()) + if (screenInfo.HasAccessibilityEventing() && changedRect) { screenInfo.NotifyAccessibilityEventing( changedRect.left, @@ -482,7 +417,45 @@ void ConhostInternalGetSet::NotifyAccessibilityChange(const til::rect& changedRe } } -void ConhostInternalGetSet::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) +// Routine Description: +// - Implements conhost-specific behavior when the buffer is rotated. +// Arguments: +// - delta - the number of cycles that the buffer has rotated. +// Return value: +// - +void ConhostInternalGetSet::NotifyBufferRotation(const int delta) +{ + auto& screenInfo = _io.GetActiveOutputBuffer(); + if (screenInfo.IsActiveScreenBuffer()) + { + auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); + if (pNotifier) + { + pNotifier->NotifyConsoleUpdateScrollEvent(0, -delta); + } + } +} + +void ConhostInternalGetSet::MarkPrompt(const ::ScrollMark& /*mark*/) +{ + // Not implemented for conhost. +} + +void ConhostInternalGetSet::MarkCommandStart() +{ + // Not implemented for conhost. +} + +void ConhostInternalGetSet::MarkOutputStart() +{ + // Not implemented for conhost. +} + +void ConhostInternalGetSet::MarkCommandFinish(std::optional /*error*/) +{ + // Not implemented for conhost. +} +void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/) { // Not implemented for conhost. } diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 2acd85ae29..45ea6052e2 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -29,7 +29,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: public: ConhostInternalGetSet(_In_ Microsoft::Console::IIoProvider& io); - void PrintString(const std::wstring_view string) override; void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() override; @@ -39,18 +38,14 @@ public: void SetTextAttributes(const TextAttribute& attrs) override; - void SetAutoWrapMode(const bool wrapAtEOL) override; - - void SetScrollingRegion(const til::inclusive_rect& scrollMargins) override; + void SetSystemMode(const Mode mode, const bool enabled) override; + bool GetSystemMode(const Mode mode) const override; void WarningBell() override; - bool GetLineFeedMode() const override; - void LineFeed(const bool withReturn) override; - void SetWindowTitle(const std::wstring_view title) override; - void UseAlternateScreenBuffer() override; + void UseAlternateScreenBuffer(const TextAttribute& attrs) override; void UseMainScreenBuffer() override; @@ -63,7 +58,6 @@ public: void SetConsoleOutputCP(const unsigned int codepage) override; unsigned int GetConsoleOutputCP() const override; - void EnableXtermBracketedPasteMode(const bool enabled) override; void CopyToClipboard(const std::wstring_view content) override; void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override; void SetWorkingDirectory(const std::wstring_view uri) override; @@ -73,8 +67,14 @@ public: bool IsVtInputEnabled() const override; void NotifyAccessibilityChange(const til::rect& changedRect) override; + void NotifyBufferRotation(const int delta) override; - void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override; + void MarkPrompt(const ScrollMark& mark) override; + void MarkCommandStart() override; + void MarkOutputStart() override; + void MarkCommandFinish(std::optional error) override; + + void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override; private: Microsoft::Console::IIoProvider& _io; diff --git a/src/host/popup.cpp b/src/host/popup.cpp deleted file mode 100644 index 74c2a9c66e..0000000000 --- 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.X - 1; - _region.Bottom = origin.Y + size.Y - 1; - - _oldScreenSize = screenInfo.GetBufferSize().Dimensions(); - - til::inclusive_rect TargetRect; - TargetRect.Left = 0; - TargetRect.Top = _region.Top; - TargetRect.Right = _oldScreenSize.X - 1; - TargetRect.Bottom = _region.Bottom; - - // copy the data into the backup buffer - _oldContents = std::move(screenInfo.ReadRect(Viewport::FromInclusive(TargetRect))); - - 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.X - 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.X += 2; // add borders - size.Y += 2; // add borders - - const auto viewportSize = screenInfo.GetViewport().Dimensions(); - - size.X = std::min(size.X, viewportSize.X); - size.Y = std::min(size.Y, viewportSize.Y); - - // make sure there's enough room for the popup borders - THROW_HR_IF(E_NOT_SUFFICIENT_BUFFER, size.X < 2 || size.Y < 2); - - return size; -} - -// Routine Description: -// - Helper to calculate the origin point (within the screen buffer) for the popup -// Arguments: -// - screenInfo - Screen buffer we will be drawing into -// - size - The size that the popup will consume -// Return Value: -// - Coordinate position of the origin point of the popup -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.X) / 2 + viewport.Left(); - origin.Y = (viewport.Height() - size.Y) / 2 + viewport.Top(); - return origin; -} - -// Routine Description: -// - Helper to return the width of the popup in columns -// Return Value: -// - Width of popup inside attached screen buffer. -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 (!NT_SUCCESS(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 73a91bafe2..0000000000 --- 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/precomp.h b/src/host/precomp.h index f319e291ce..c7f41e06df 100644 --- a/src/host/precomp.h +++ b/src/host/precomp.h @@ -88,7 +88,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hConhostV2EventTraceProvider); #include "../inc/conattrs.hpp" // TODO: MSFT 9355094 Find a better way of doing this. http://osgvsowi/9355094 -[[nodiscard]] inline NTSTATUS NTSTATUS_FROM_HRESULT(HRESULT hr) +[[nodiscard]] constexpr NTSTATUS NTSTATUS_FROM_HRESULT(HRESULT hr) noexcept { return NTSTATUS_FROM_WIN32(HRESULT_CODE(hr)); } diff --git a/src/host/proxy/Host.Proxy.vcxproj b/src/host/proxy/Host.Proxy.vcxproj index 137b1f1a8b..6908c1d75d 100644 --- a/src/host/proxy/Host.Proxy.vcxproj +++ b/src/host/proxy/Host.Proxy.vcxproj @@ -17,6 +17,13 @@ regular builds, while preventing the build failure during fuzzing builds. --> StaticLibrary + + + true @@ -59,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) @@ -77,43 +89,6 @@ - - - - - MultiThreadedDebug - - - - %(IgnoreSpecificDefaultLibraries);libucrtd.lib - %(AdditionalOptions) /defaultlib:ucrtd.lib - - - - - - MultiThreaded - - - - %(IgnoreSpecificDefaultLibraries);libucrt.lib - %(AdditionalOptions) /defaultlib:ucrt.lib - - - diff --git a/src/host/readData.hpp b/src/host/readData.hpp index dce1c3e1fe..b40b03b5de 100644 --- a/src/host/readData.hpp +++ b/src/host/readData.hpp @@ -32,20 +32,9 @@ public: ReadData(_In_ InputBuffer* const pInputBuffer, _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData); - virtual ~ReadData(); + ~ReadData() override; - ReadData(const ReadData&) = delete; ReadData(ReadData&&); - ReadData& operator=(const ReadData&) & = delete; - ReadData& operator=(ReadData&&) & = delete; - - virtual void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) = 0; - virtual bool Notify(const WaitTerminationReason TerminationReason, - const bool fIsUnicode, - _Out_ NTSTATUS* const pReplyStatus, - _Out_ size_t* const pNumBytes, - _Out_ DWORD* const pControlKeyState, - _Out_ void* const pOutputData) = 0; InputBuffer* GetInputBuffer() const; INPUT_READ_HANDLE_DATA* GetInputReadHandleData() const; diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 154e537a69..d05502dc0d 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -3,293 +3,115 @@ #include "precomp.h" #include "readDataCooked.hpp" -#include "dbcs.h" -#include "stream.h" -#include "misc.h" -#include "_stream.h" -#include "inputBuffer.hpp" -#include "cmdline.h" -#include "../types/inc/GlyphWidth.hpp" -#include "../types/inc/convert.hpp" +#include "alias.h" +#include "history.h" +#include "resource.h" +#include "stream.h" +#include "_stream.h" #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, _In_ size_t UserBufferSize, - _In_ PWCHAR UserBuffer, + _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 }, - _bytesRead{ 0 }, - _currentPosition{ 0 }, - _userBufferSize{ UserBufferSize }, - _userBuffer{ UserBuffer }, - _tempHandle{ nullptr }, + _userBuffer{ UserBuffer, UserBufferSize }, _exeName{ exeName }, - _pdwNumBytes{ nullptr }, - - _commandHistory{ CommandHistory::s_Find((HANDLE)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()); + _buffer.assign(initialData); + _bufferCursor = _buffer.size(); + _bufferDirty = !_buffer.empty(); - _bytesRead += initialData.size(); + // 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. - const auto cchInitialData = initialData.size() / sizeof(wchar_t); - 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) + // 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; } + + 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; } - - // TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci -} - -// Routine Description: -// - Destructs a read data class. -// - Decrements count of readers waiting on the given handle. -COOKED_READ_DATA::~COOKED_READ_DATA() -{ - CommandLine::Instance().EndAllPopups(); -} - -gsl::span COOKED_READ_DATA::SpanWholeBuffer() -{ - return gsl::make_span(_backupLimit, (_bufferSize / sizeof(wchar_t))); -} - -gsl::span COOKED_READ_DATA::SpanAtPointer() -{ - auto wholeSpan = SpanWholeBuffer(); - return wholeSpan.subspan(_bufPtr - _backupLimit); -} - -bool COOKED_READ_DATA::HasHistory() const noexcept -{ - return _commandHistory != nullptr; -} - -CommandHistory& COOKED_READ_DATA::History() noexcept -{ - return *_commandHistory; -} - -const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept -{ - return _visibleCharCount; -} - -size_t& COOKED_READ_DATA::VisibleCharCount() noexcept -{ - return _visibleCharCount; -} - -SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept -{ - return _screenInfo; -} - -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; } // 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; - } -} -bool COOKED_READ_DATA::AtEol() const noexcept + return false; +} +NT_CATCH_RETURN() + +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,778 +200,1135 @@ 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); + _readCharInputLoop(); - // if the read was completed (status != wait), free the cooked read - // data. also, close the temporary output handle that was opened to - // echo the characters read. - if (Status != CONSOLE_STATUS_WAIT) - { - Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); - } + // 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(); - return Status; -} - -void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount) -{ - Alias::s_MatchAndCopyAliasLegacy(_backupLimit, - _bytesRead, - _backupLimit, - _bufferSize, - _bytesRead, - _exeName, - lineCount); -} - -// Routine Description: -// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and -// the final ENTER key press that will end the wait and finally return the data. -// Arguments: -// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.) -// - wch - The most recently pressed/retrieved character from the input buffer (keystroke) -// - keyState - Modifier keys/state information with the pressed key/character -// - status - The return code to pass to the client -// Return Value: -// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke. -bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig, - const DWORD keyState, - NTSTATUS& status) -{ - const 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 (_state == State::Accumulating) { return false; } - if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) - { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; - _controlKeyState = keyState; - return true; - } + _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + return true; +} - if (wch == EXTKEY_ERASE_PREV_WORD) - { - wch = UNICODE_BACKSPACE; - } +// 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() +{ + _popupsDone(); - if (AtEol()) + if (_distanceEnd) { - // If at end of line, processing is relatively simple. Just store the character and write it to the screen. - if (wch == UNICODE_BACKSPACE2) + _unwindCursorPosition(_distanceCursor); + _erase(_distanceEnd); + _unwindCursorPosition(_distanceEnd); + _distanceCursor = 0; + _distanceEnd = 0; + } +} + +// 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) + { + --position; + while (position != 0 && chars[position] == L' ') { - wch = UNICODE_BACKSPACE; + --position; } - if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit) + const auto dc = DelimiterClass(chars[position]); + while (position != 0 && DelimiterClass(chars[position - 1]) == dc) { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); - - auto loop = true; - while (loop) - { - loop = false; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - &NumSpaces, - _originalCursorPosition.X, - WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &ScrollY); - if (NT_SUCCESS(status)) - { - _originalCursorPosition.Y += ScrollY; - } - else - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } - - _visibleCharCount += NumSpaces; - if (wch == UNICODE_BACKSPACE && _processedInput) - { - _bytesRead -= sizeof(WCHAR); - // 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 - { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; - } - } + --position; } } - else + return position; +} + +size_t COOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t position) +{ + if (position < chars.size()) { - auto CallWrite = true; - const auto sScreenBufferSizeX = _screenInfo.GetBufferSize().Width(); - - // processing in the middle of the line is more complex: - - // calculate new cursor position - // store new char - // clear the current command line from the screen - // write the new command line to the screen - // update the cursor position - - if (wch == UNICODE_BACKSPACE && _processedInput) + ++position; + const auto dc = DelimiterClass(chars[position - 1]); + while (position != chars.size() && dc == DelimiterClass(chars[position])) { - // 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. + ++position; + } + while (position != chars.size() && chars[position] == L' ') + { + ++position; + } + } + return position; +} - if (_bufPtr != _backupLimit) - { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); +// Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents. +void COOKED_READ_DATA::_readCharInputLoop() +{ + while (_state == State::Accumulating) + { + 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; - 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. + const auto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers); + if (status == CONSOLE_STATUS_WAIT) + { + break; + } + THROW_IF_NTSTATUS_FAILED(status); - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - nullptr, - _originalCursorPosition.X, - WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - nullptr); - if (!NT_SUCCESS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } - _bytesRead -= sizeof(WCHAR); - _bufPtr -= 1; - _currentPosition -= 1; - memmove(_bufPtr, - _bufPtr + 1, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - { - 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; - } - } - } - else - { - CallWrite = false; - } + if (hasPopup) + { + const auto wch = static_cast(popupKeys ? 0 : charOrVkey); + const auto vkey = static_cast(popupKeys ? charOrVkey : 0); + _popupHandleInput(wch, vkey, modifiers); } else { - // store the char - if (wch == UNICODE_CARRIAGERETURN) + if (commandLineEditingKeys) { - _bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); - *_bufPtr = wch; - _bufPtr += 1; - _bytesRead += sizeof(WCHAR); - _currentPosition += 1; + _handleVkey(charOrVkey, modifiers); } else { - auto fBisect = false; - - if (_echoInput) - { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.X, - _originalCursorPosition.X, - TRUE)) - { - fBisect = true; - } - } - - if (_insertMode) - { - memmove(_bufPtr + 1, - _bufPtr, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - _bytesRead += sizeof(WCHAR); - } - *_bufPtr = wch; - _bufPtr += 1; - _currentPosition += 1; - - // calculate new cursor position - if (_echoInput) - { - NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.X, - _backupLimit, - _currentPosition - 1); - if (NumSpaces > 0 && fBisect) - NumSpaces--; - } - } - } - - if (_echoInput && CallWrite) - { - 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_DESTRUCTIVE_BACKSPACE | WC_PRINTABLE_CONTROL_CHARS; - if (wch == UNICODE_CARRIAGERETURN) - { - dwFlags |= WC_KEEP_CURSOR_VISIBLE; - } - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _backupLimit, - _backupLimit, - &NumToWrite, - &_visibleCharCount, - _originalCursorPosition.X, - dwFlags, - &ScrollY); - if (!NT_SUCCESS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - _bytesRead = 0; - return true; - } - - // update cursor position - if (wch != UNICODE_CARRIAGERETURN) - { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.X, - _originalCursorPosition.X, - TRUE)) - { - if (CursorPosition.X == (sScreenBufferSizeX - 1)) - { - CursorPosition.X++; - } - } - - // adjust cursor position for WriteChars - _originalCursorPosition.Y += ScrollY; - CursorPosition.Y += ScrollY; - status = AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr); - if (!NT_SUCCESS(status)) - { - _bytesRead = 0; - return true; - } + _handleChar(charOrVkey, modifiers); } } } - - // in cooked mode, enter (carriage return) is converted to - // carriage return linefeed (0xda). carriage return is always - // stored at the end of the buffer. - if (wch == UNICODE_CARRIAGERETURN) - { - if (_processedInput) - { - if (_bytesRead < _bufferSize) - { - *_bufPtr = UNICODE_LINEFEED; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - _bufPtr, - &NumToWrite, - nullptr, - _originalCursorPosition.X, - WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - nullptr); - if (!NT_SUCCESS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - } - } - _bytesRead += sizeof(WCHAR); - _bufPtr++; - _currentPosition += 1; - } - } - // reset the cursor back to 25% if necessary - if (_lineInput) - { - if (_insertMode != gci.GetInsertMode()) - { - // Make cursor small. - LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0)); - } - - status = STATUS_SUCCESS; - return true; - } - } - - return false; } -// Routine Description: -// - Writes string to current position in prompt line. can overwrite text to the right of the cursor. -// Arguments: -// - wstr - the string to write -// Return Value: -// - The number of chars written -size_t COOKED_READ_DATA::Write(const std::wstring_view wstr) +// Handles character input for _readCharInputLoop() when no popups exist. +void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) { - auto end = wstr.end(); - const auto charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit); - if (wstr.size() > charsRemaining) + // All paths in this function modify the buffer. + + if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) { - end = std::next(wstr.begin(), charsRemaining); + _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; + _transitionState(State::DoneWithWakeupMask); + 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()) + switch (wch) { - size_t NumSpaces = 0; - til::CoordType ScrollY = 0; - - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(), - _backupLimit, - _bufPtr, - _bufPtr, - &bytesInserted, - &NumSpaces, - OriginalCursorPosition().X, - WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, - &ScrollY)); - OriginalCursorPosition().Y += ScrollY; - VisibleCharCount() += NumSpaces; + case UNICODE_CARRIAGERETURN: + { + // NOTE: Don't append newlines to the buffer just yet! See _handlePostCharInputLoop for more information. + _bufferCursor = _buffer.size(); + _markAsDirty(); + _transitionState(State::DoneWithCarriageReturn); + return; } - _bufPtr += charsInserted; + 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) + { + pos = _wordPrev(_buffer, _bufferCursor); + } + else + { + pos = TextBuffer::GraphemePrev(_buffer, _bufferCursor); + } - return charsInserted; + _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()) + { + LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); + } + } + return; + } + // 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 + { + // 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); + } + + _bufferCursor++; + _markAsDirty(); } -// 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) +// Handles non-character input for _readCharInputLoop() when no popups exist. +void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) { - size_t bytesToWrite = 0; - const auto hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite); - if (FAILED(hr)) + const auto ctrlPressed = WI_IsAnyFlagSet(modifiers, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); + const auto altPressed = WI_IsAnyFlagSet(modifiers, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); + + switch (vkey) + { + case VK_ESCAPE: + if (!_buffer.empty()) + { + _buffer.clear(); + _bufferCursor = 0; + _markAsDirty(); + } + break; + case VK_HOME: + if (_bufferCursor > 0) + { + if (ctrlPressed) + { + _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 + { + _bufferCursor = TextBuffer::GraphemePrev(_buffer, _bufferCursor); + } + _markAsDirty(); + } + break; + case VK_F1: + case VK_RIGHT: + if (_bufferCursor != _buffer.size()) + { + if (ctrlPressed && vkey == VK_RIGHT) + { + _bufferCursor = _wordNext(_buffer, _bufferCursor); + } + else + { + _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 (bufferBeg >= bufferSize) + { + _buffer.append(cmd, cmdBeg, cmdEnd - cmdBeg); + _bufferCursor = _buffer.size(); + _markAsDirty(); + break; + } + + bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg); + cmdBeg = cmdEnd; + } + } + break; + case VK_INSERT: + _insertMode = !_insertMode; + _screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode()); + _markAsDirty(); + break; + case VK_DELETE: + if (_bufferCursor < _buffer.size()) + { + _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()) + { + _popupPush(PopupKind::CommandList); + } + } + else if (altPressed) + { + if (_history) + { + _history->Empty(); + _history->Flags |= CommandHistory::CLE_ALLOCATED; + } + } + 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)) + { + _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; + } +} + +// 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 (_state == State::DoneWithCarriageReturn) + { + static constexpr std::wstring_view cr{ L"\r" }; + static constexpr std::wstring_view crlf{ L"\r\n" }; + const auto newlineSuffix = WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr; + std::wstring alias; + + // Here's why we can't easily use _flushBuffer() to handle newlines: + // + // A carriage return (enter key) will increase the _distanceEnd by up to viewport-width many columns, + // since it increases the Y distance between the start and end by 1 (it's a newline after all). + // This will make _flushBuffer() think that the new _buffer is way longer than the old one and so + // _erase() ends up not erasing the tail end of the prompt, even if the new prompt is actually shorter. + // + // If you were to break this (remove this code and then append \r\n in _handleChar()) + // you can reproduce the issue easily if you do this: + // * Run cmd.exe + // * Write "echo hello" and press Enter + // * Write "foobar foo bar" (don't press Enter) + // * Press F7, select "echo hello" and press Enter + // + // It'll print "hello" but the previous prompt will say "echo hello bar" because the _distanceEnd + // ended up being well over 14 leading it to believe that "bar" got overwritten during WriteCharsLegacy(). + + WriteCharsLegacy(_screenInfo, newlineSuffix, true, nullptr); + + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) + { + if (_history) + { + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LOG_IF_FAILED(_history->Add(input, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); + } + + Tracing::s_TraceCookedRead(_processHandle, input); + alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount); + } + + if (!alias.empty()) + { + _buffer = std::move(alias); + } + else + { + _buffer.append(newlineSuffix); + } + + 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)); + } + } + + 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; +} + +void COOKED_READ_DATA::_transitionState(State state) noexcept +{ + assert(_state == State::Accumulating); + _state = state; +} + +// Signals to _flushBuffer() that the contents of _buffer are stale and need to be redrawn. +// ALL _buffer and _bufferCursor changes must be flagged with _markAsDirty(). +// +// 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() +{ + _bufferDirty = true; +} + +// Draws the contents of _buffer onto the screen. +// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information. +void COOKED_READ_DATA::_flushBuffer() +{ + // _flushBuffer() is called often and is a good place to assert() that our _bufferCursor is still in bounds. + assert(_bufferCursor <= _buffer.size()); + _bufferCursor = std::min(_bufferCursor, _buffer.size()); + + if (!_bufferDirty) + { + return; + } + + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) + { + _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; + } + + _bufferDirty = false; +} + +// 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(ptrdiff_t distance) const +{ + if (distance <= 0) + { + 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. +// _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. +ptrdiff_t COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const +{ + if (text.empty()) { return 0; } - memmove(_userBuffer, _backupLimit, bytesToWrite); + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto width = static_cast(textBuffer.GetSize().Width()); + const auto initialCursorPos = cursor.GetPosition(); + til::CoordType scrollY = 0; - if (!IsUnicode()) - { - try - { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto wstr = ConvertToW(gci.CP, { reinterpret_cast(_userBuffer), cch }); - const auto copyAmount = std::min(wstr.size(), _userBufferSize / sizeof(wchar_t)); - std::copy_n(wstr.begin(), copyAmount, _userBuffer); - return copyAmount * sizeof(wchar_t); - } - CATCH_LOG(); - } - return bytesToWrite; + WriteCharsLegacy(_screenInfo, text, true, &scrollY); + + const auto finalCursorPos = cursor.GetPosition(); + const auto distance = (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; + assert(distance >= 0); + return distance; } -// 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) +// 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, ptrdiff_t distance) const { - auto& inputReadHandleData = *GetInputReadHandleData(); - const std::wstring_view pending{ _backupLimit + index, - BytesRead() / sizeof(wchar_t) - index }; - if (multiline) + 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(ptrdiff_t distance) const +{ + if (distance <= 0) { - inputReadHandleData.SaveMultilinePendingInput(pending); + // 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; } - else + + 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) { - inputReadHandleData.SavePendingInput(pending); + 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; + } + + 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. + { + 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) + { + state.text = buffer; + textBuffer.Write(y, attributes, state); + } + } + + 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(); +} + +// 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() +{ + while (!_popups.empty()) + { + 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(); + } + + // Restore cursor blinking. + _screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(false); +} + +void COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) +{ + if (_popups.empty()) + { + assert(false); // Don't call this function. + return; + } + + auto& popup = _popups.back(); + switch (popup.kind) + { + case PopupKind::CopyToChar: + _popupHandleCopyToCharInput(popup, wch, vkey, modifiers); + break; + case PopupKind::CopyFromChar: + _popupHandleCopyFromCharInput(popup, wch, vkey, modifiers); + break; + case PopupKind::CommandNumber: + _popupHandleCommandNumberInput(popup, wch, vkey, modifiers); + break; + case PopupKind::CommandList: + _popupHandleCommandListInput(popup, wch, vkey, modifiers); + break; + default: + break; } } -// 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 (!NT_SUCCESS(Status)) + if (vkey == VK_ESCAPE) { - if (Status != CONSOLE_STATUS_WAIT) - { - _bytesRead = 0; - } - break; - } - - // we should probably set these up in GetChars, but we set them - // up here because the debugger is multi-threaded and calls - // read before outputting the prompt. - - if (_originalCursorPosition.X == -1) - { - _originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); - } - - if (commandLineEditingKeys) - { - // TODO: this is super weird for command line popups only - _unicode = isUnicode; - - _pdwNumBytes = &numBytes; - - Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState); - if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT) - { - break; - } - if (!NT_SUCCESS(Status)) - { - if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK) - { - Status = CONSOLE_STATUS_WAIT; - } - else - { - _bytesRead = 0; - } - break; - } - } - else - { - if (ProcessInput(wch, keyState, Status)) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; - break; - } - } - } - return Status; -} - -// Routine Description: -// - handles any tasks that need to be completed after the read input loop finishes -// Arguments: -// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. -// - numBytes - On in, the number of bytes available in the client -// buffer. On out, the number of bytes consumed in the client buffer. -// - controlKeyState - For some types of reads, this is the modifier key state with the last button press. -// Return Value: -// - Status code that indicates success, out of memory, etc. -[[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept -{ - DWORD LineCount = 1; - - if (_echoInput) - { - // Figure out where real string ends (at carriage return or end of buffer). - auto StringPtr = _backupLimit; - auto StringLength = _bytesRead / sizeof(WCHAR); - auto FoundCR = false; - for (size_t i = 0; i < StringLength; i++) - { - if (*StringPtr++ == UNICODE_CARRIAGERETURN) - { - StringLength = i; - FoundCR = true; - break; - } - } - - if (FoundCR) - { - if (_commandHistory) - { - // add to command line recall list if we have a history list. - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength }, - WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); - } - - Tracing::s_TraceCookedRead(_clientProcess, - _backupLimit, - base::saturated_cast(StringLength)); - - // check for alias - ProcessAliases(LineCount); - } - } - - auto fAddDbcsLead = false; - size_t NumBytes = 0; - // at this point, a->NumBytes contains the number of bytes in - // the UNICODE string read. UserBufferSize contains the converted - // size of the app's buffer. - if (_bytesRead > _userBufferSize || LineCount > 1) - { - if (LineCount > 1) - { - PWSTR Tmp; - if (!isUnicode) - { - if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) - { - fAddDbcsLead = true; - auto event = GetInputBuffer()->FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *_userBuffer = static_cast(pKeyEvent->GetCharData()); - _userBuffer++; - _userBufferSize -= sizeof(wchar_t); - } - - NumBytes = 0; - for (Tmp = _backupLimit; - *Tmp != UNICODE_LINEFEED && _userBufferSize / sizeof(WCHAR) > NumBytes; - Tmp++) - { - NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; - } - } - - // clang-format off -#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "LineCount > 1 means there's a UNICODE_LINEFEED") - // clang-format on - for (Tmp = _backupLimit; *Tmp != UNICODE_LINEFEED; Tmp++) - { - FAIL_FAST_IF(!(Tmp < (_backupLimit + _bytesRead))); - } - - numBytes = (ULONG)(Tmp - _backupLimit + 1) * sizeof(*Tmp); - } - else - { - if (!isUnicode) - { - PWSTR Tmp; - - if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) - { - fAddDbcsLead = true; - auto event = GetInputBuffer()->FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *_userBuffer = static_cast(pKeyEvent->GetCharData()); - _userBuffer++; - _userBufferSize -= sizeof(wchar_t); - } - NumBytes = 0; - auto NumToWrite = _bytesRead; - for (Tmp = _backupLimit; - NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes; - Tmp++, NumToWrite -= sizeof(WCHAR)) - { - NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; - } - } - numBytes = _userBufferSize; - } - - __analysis_assume(numBytes <= _userBufferSize); - memmove(_userBuffer, _backupLimit, numBytes); - - const auto pInputReadHandleData = GetInputReadHandleData(); - const std::wstring_view pending{ _backupLimit + (numBytes / sizeof(wchar_t)), (_bytesRead - numBytes) / sizeof(wchar_t) }; - if (LineCount > 1) - { - pInputReadHandleData->SaveMultilinePendingInput(pending); - } - else - { - pInputReadHandleData->SavePendingInput(pending); + _popupsDone(); } } else { - if (!isUnicode) + // See PopupKind::CopyToChar for more information about this code. + const auto cmd = _history->GetLastCommand(); + const auto idx = cmd.find(wch, _bufferCursor); + + if (idx != decltype(cmd)::npos) { - PWSTR Tmp; - - if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) - { - fAddDbcsLead = true; - auto event = GetInputBuffer()->FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *_userBuffer = static_cast(pKeyEvent->GetCharData()); - _userBuffer++; - _userBufferSize -= sizeof(wchar_t); - - if (_userBufferSize == 0) - { - numBytes = 1; - return STATUS_SUCCESS; - } - } - NumBytes = 0; - auto NumToWrite = _bytesRead; - for (Tmp = _backupLimit; - NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes; - Tmp++, NumToWrite -= sizeof(WCHAR)) - { - NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; - } + // 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(); } - numBytes = _bytesRead; - - if (numBytes > _userBufferSize) - { - return STATUS_BUFFER_OVERFLOW; - } - - memmove(_userBuffer, _backupLimit, numBytes); + _popupsDone(); } - controlKeyState = _controlKeyState; - - if (!isUnicode) - { - // if ansi, translate string. - std::unique_ptr tempBuffer; - try - { - tempBuffer = std::make_unique(NumBytes); - } - catch (...) - { - return STATUS_NO_MEMORY; - } - - std::unique_ptr partialEvent; - numBytes = TranslateUnicodeToOem(_userBuffer, - gsl::narrow(numBytes / sizeof(wchar_t)), - tempBuffer.get(), - gsl::narrow(NumBytes), - partialEvent); - - if (partialEvent.get()) - { - GetInputBuffer()->StoreReadPartialByteSequence(std::move(partialEvent)); - } - - if (numBytes > _userBufferSize) - { - return STATUS_BUFFER_OVERFLOW; - } - - memmove(_userBuffer, tempBuffer.get(), numBytes); - if (fAddDbcsLead) - { - numBytes++; - } - } - return STATUS_SUCCESS; } -void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) +void COOKED_READ_DATA::_popupHandleCopyFromCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) { - // See the comment in WaitBlock.cpp for more information. - if (_userBuffer == reinterpret_cast(oldBuffer)) + if (vkey) { - _userBuffer = reinterpret_cast(newBuffer); + if (vkey == VK_ESCAPE) + { + _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(); } } + +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; + } + + if (wch >= L'0' && wch <= L'9') + { + if (popup.commandNumber.bufferSize < CommandNumberMaxInputLength) + { + popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = wch; + } + } + else if (wch == UNICODE_BACKSPACE) + { + if (popup.commandNumber.bufferSize > 0) + { + popup.commandNumber.buffer[--popup.commandNumber.bufferSize] = L' '; + } + } + else + { + 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); + } +} + +void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers) +{ + auto& cl = popup.commandList; + + if (wch == UNICODE_CARRIAGERETURN) + { + _buffer.assign(_history->RetrieveNth(cl.selected)); + _popupsDone(); + _handleChar(UNICODE_CARRIAGERETURN, modifiers); + return; + } + + switch (vkey) + { + case VK_ESCAPE: + _popupsDone(); + return; + case VK_F9: + _popupPush(PopupKind::CommandNumber); + return; + case VK_DELETE: + _history->Remove(cl.selected); + if (_history->GetNumberOfCommands() <= 0) + { + _popupsDone(); + return; + } + break; + case VK_LEFT: + case VK_RIGHT: + _replaceBuffer(_history->RetrieveNth(cl.selected)); + _popupsDone(); + return; + 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; + } + + _popupDrawCommandList(popup); + return; +} + +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); +} + +void COOKED_READ_DATA::_popupDrawCommandList(Popup& popup) const +{ + assert(popup.kind == PopupKind::CommandList); + + 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); + + { + // 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); + } + + std::wstring buffer; + buffer.reserve(width * 2 + 4); + + 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) + { + 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 a60b8ca87b..803724b6f5 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,153 @@ 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_ PWCHAR UserBuffer, + _In_ char* UserBuffer, _In_ ULONG CtrlWakeupMask, - _In_ const std::wstring_view exeName, - _In_ const std::string_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(); - gsl::span SpanAtPointer(); - gsl::span SpanWholeBuffer(); - - size_t Write(const std::wstring_view wstr); - - void ProcessAliases(DWORD& lineCount); - - [[nodiscard]] HRESULT Read(const bool isUnicode, - size_t& numBytes, - ULONG& controlKeyState) noexcept; - - bool ProcessInput(const wchar_t wch, - const DWORD keyState, - NTSTATUS& status); - - CommandHistory& History() noexcept; - bool HasHistory() const noexcept; - - const size_t& VisibleCharCount() const noexcept; - size_t& VisibleCharCount() noexcept; - - SCREEN_INFORMATION& ScreenInfo() noexcept; - - 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; + static constexpr uint8_t CommandNumberMaxInputLength = 5; - // insertion position into the buffer (where the conceptual prompt cursor is) - size_t _currentPosition; // char position, not byte position + enum class State : uint8_t + { + Accumulating = 0, + DoneWithWakeupMask, + DoneWithCarriageReturn, + }; - wchar_t* _bufPtr; // current position to insert chars at + 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, + }; - // should be const. the first char of the buffer - wchar_t* _backupLimit; + struct Popup + { + PopupKind kind; - size_t _userBufferSize; // doubled size in ansi case - wchar_t* _userBuffer; + // 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; - size_t* _pdwNumBytes; + // 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; - std::unique_ptr _buffer; + // 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); + + void _readCharInputLoop(); + void _handleChar(wchar_t wch, DWORD modifiers); + void _handleVkey(uint16_t vkey, DWORD modifiers); + void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); + void _transitionState(State state) noexcept; + void _markAsDirty(); + void _flushBuffer(); + void _erase(ptrdiff_t distance) const; + 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); + 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); + void _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + void _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); + void _popupDrawPrompt(const Popup& popup, UINT id) const; + void _popupDrawCommandList(Popup& popup) const; + + 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; + std::wstring _buffer; + size_t _bufferCursor = 0; + // _distanceCursor is the distance between the start of the prompt and the + // current cursor location in columns (including wide glyph padding columns). + ptrdiff_t _distanceCursor = 0; + // _distanceEnd is the distance between the start of the prompt and its last + // glyph at the end in columns (including wide glyph padding columns). + ptrdiff_t _distanceEnd = 0; + bool _bufferDirty = false; + bool _insertMode = false; + State _state = State::Accumulating; - 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; - - [[nodiscard]] NTSTATUS _handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept; + std::vector _popups; }; diff --git a/src/host/readDataDirect.cpp b/src/host/readDataDirect.cpp index 21045a9ff4..778b585c4c 100644 --- a/src/host/readDataDirect.cpp +++ b/src/host/readDataDirect.cpp @@ -10,9 +10,7 @@ // Routine Description: // - Constructs direct read data class to hold context across sessions -// generally when there's not enough data to return. Also used a bit -// internally to just pass some information along the stack -// (regardless of wait necessity). +// generally when there's not enough data to return. // Arguments: // - pInputBuffer - Buffer that data will be read from. // - pInputReadHandleData - Context stored across calls from the same @@ -24,20 +22,12 @@ // - THROW: Throws E_INVALIDARG for invalid pointers. DirectReadData::DirectReadData(_In_ InputBuffer* const pInputBuffer, _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, - const size_t eventReadCount, - _In_ std::deque> partialEvents) : + const size_t eventReadCount) : ReadData(pInputBuffer, pInputReadHandleData), - _eventReadCount{ eventReadCount }, - _partialEvents{ std::move(partialEvents) }, - _outEvents{} + _eventReadCount{ eventReadCount } { } -// Routine Description: -// - Destructs a read data class. -// - Decrements count of readers waiting on the given handle. -DirectReadData::~DirectReadData() = default; - // Routine Description: // - This routine is called to complete a direct read that blocked in // ReadInputBuffer. The context of the read was saved in the DirectReadData @@ -57,7 +47,7 @@ DirectReadData::~DirectReadData() = default; // - 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. @@ -68,19 +58,16 @@ bool DirectReadData::Notify(const WaitTerminationReason TerminationReason, _Out_ size_t* const pNumBytes, _Out_ DWORD* const pControlKeyState, _Out_ void* const pOutputData) +try { FAIL_FAST_IF_NULL(pOutputData); - FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); - const auto& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); - FAIL_FAST_IF(!gci.IsConsoleLocked()); + assert(Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); *pReplyStatus = STATUS_SUCCESS; *pControlKeyState = 0; *pNumBytes = 0; - auto retVal = true; - std::deque> readEvents; // If ctrl-c or ctrl-break was seen, ignore it. if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) @@ -88,105 +75,58 @@ bool DirectReadData::Notify(const WaitTerminationReason TerminationReason, return false; } - // check if a partial byte is already stored that we should read - if (!fIsUnicode && - _pInputBuffer->IsReadPartialByteSequenceAvailable() && - _eventReadCount == 1) - { - _partialEvents.push_back(_pInputBuffer->FetchReadPartialByteSequence(false)); - } - // See if called by CsrDestroyProcess or CsrDestroyThread // via ConsoleNotifyWaitBlock. If so, just decrement the ReadCount and return. if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) { *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + return true; } + // We must see if we were woken up because the handle is being // closed. If so, we decrement the read count. If it goes to // zero, we wake up the close thread. Otherwise, we wake up any // other thread waiting for data. - else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) { *pReplyStatus = STATUS_ALERTED; + return true; } - else + + // if we get to here, this routine was called either by the input + // thread or a write routine. both of these callers grab the + // current console lock. + + size_t amountToRead; + if (FAILED(SizeTSub(_eventReadCount, _outEvents.size(), &amountToRead))) { - // if we get to here, this routine was called either by the input - // thread or a write routine. both of these callers grab the - // current console lock. - - // calculate how many events we need to read - size_t amountAlreadyRead; - if (FAILED(SizeTAdd(_partialEvents.size(), _outEvents.size(), &amountAlreadyRead))) - { - *pReplyStatus = STATUS_INTEGER_OVERFLOW; - return retVal; - } - size_t amountToRead; - if (FAILED(SizeTSub(_eventReadCount, amountAlreadyRead, &amountToRead))) - { - *pReplyStatus = STATUS_INTEGER_OVERFLOW; - return retVal; - } - - *pReplyStatus = _pInputBuffer->Read(readEvents, - amountToRead, - false, - false, - fIsUnicode, - false); - - if (*pReplyStatus == CONSOLE_STATUS_WAIT) - { - retVal = false; - } + *pReplyStatus = STATUS_INTEGER_OVERFLOW; + return true; } - if (*pReplyStatus != CONSOLE_STATUS_WAIT) + *pReplyStatus = _pInputBuffer->Read(_outEvents, + amountToRead, + false, + false, + fIsUnicode, + false); + + if (*pReplyStatus == CONSOLE_STATUS_WAIT) { - // split key events to oem chars if necessary - if (*pReplyStatus == STATUS_SUCCESS && !fIsUnicode) - { - try - { - SplitToOem(readEvents); - } - CATCH_LOG(); - } - - // combine partial and whole events - while (!_partialEvents.empty()) - { - readEvents.push_front(std::move(_partialEvents.back())); - _partialEvents.pop_back(); - } - - // move read events to out storage - for (size_t i = 0; i < _eventReadCount; ++i) - { - if (readEvents.empty()) - { - break; - } - _outEvents.push_back(std::move(readEvents.front())); - readEvents.pop_front(); - } - - // store partial event if necessary - if (!readEvents.empty()) - { - _pInputBuffer->StoreReadPartialByteSequence(std::move(readEvents.front())); - readEvents.pop_front(); - FAIL_FAST_IF(!(readEvents.empty())); - } - - // move events to pOutputData - const auto pOutputDeque = reinterpret_cast>* const>(pOutputData); - *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD); - pOutputDeque->swap(_outEvents); + return false; } - return retVal; + + // move events to pOutputData + const auto pOutputDeque = static_cast(pOutputData); + *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD); + *pOutputDeque = std::move(_outEvents); + + return true; +} +catch (...) +{ + *pReplyStatus = wil::StatusFromCaughtException(); + return true; } void DirectReadData::MigrateUserBuffersOnTransitionToBackgroundWait(const void* /*oldBuffer*/, void* /*newBuffer*/) diff --git a/src/host/readDataDirect.hpp b/src/host/readDataDirect.hpp index d1ad2e5033..44768c09d1 100644 --- a/src/host/readDataDirect.hpp +++ b/src/host/readDataDirect.hpp @@ -33,13 +33,10 @@ class DirectReadData final : public ReadData public: DirectReadData(_In_ InputBuffer* const pInputBuffer, _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, - const size_t eventReadCount, - _In_ std::deque> partialEvents); + const size_t eventReadCount); DirectReadData(DirectReadData&&) = default; - ~DirectReadData() override; - void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) override; bool Notify(const WaitTerminationReason TerminationReason, const bool fIsUnicode, @@ -50,6 +47,5 @@ public: private: const size_t _eventReadCount; - std::deque> _partialEvents; - std::deque> _outEvents; + InputEventQueue _outEvents; }; diff --git a/src/host/readDataRaw.cpp b/src/host/readDataRaw.cpp index c1f5be05b8..9351382c07 100644 --- a/src/host/readDataRaw.cpp +++ b/src/host/readDataRaw.cpp @@ -31,7 +31,6 @@ RAW_READ_DATA::RAW_READ_DATA(_In_ InputBuffer* const pInputBuffer, _BufferSize{ BufferSize }, _BufPtr{ THROW_HR_IF_NULL(E_INVALIDARG, BufPtr) } { - THROW_HR_IF(E_INVALIDARG, _BufferSize % sizeof(wchar_t) != 0); THROW_HR_IF(E_INVALIDARG, _BufferSize == 0); } @@ -82,146 +81,50 @@ bool RAW_READ_DATA::Notify(const WaitTerminationReason TerminationReason, *pControlKeyState = 0; *pNumBytes = 0; - size_t NumBytes = 0; - - PWCHAR lpBuffer; - auto RetVal = true; - auto fAddDbcsLead = false; - auto fSkipFinally = false; // If a ctrl-c is seen, don't terminate read. If ctrl-break is seen, terminate read. if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlC)) { return false; } - else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlBreak)) + + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlBreak)) { *pReplyStatus = STATUS_ALERTED; + return true; } + // See if we were called because the thread that owns this wait block is exiting. - else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) { *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + return true; } + // We must see if we were woken up because the handle is being // closed. If so, we decrement the read count. If it goes to zero, // we wake up the close thread. Otherwise, we wake up any other // thread waiting for data. - else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) { *pReplyStatus = STATUS_ALERTED; - } - else - { - // If we get to here, this routine was called either by the input - // thread or a write routine. Both of these callers grab the current - // console lock. - - lpBuffer = _BufPtr; - - if (!fIsUnicode && _pInputBuffer->IsReadPartialByteSequenceAvailable()) - { - auto event = _pInputBuffer->FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *lpBuffer = static_cast(pKeyEvent->GetCharData()); - _BufferSize -= sizeof(wchar_t); - *pReplyStatus = STATUS_SUCCESS; - fAddDbcsLead = true; - - if (_BufferSize == 0) - { - *pNumBytes = 1; - RetVal = false; - fSkipFinally = true; - } - } - else - { - // This call to GetChar may block. - *pReplyStatus = GetChar(_pInputBuffer, - lpBuffer, - true, - nullptr, - nullptr, - nullptr); - } - - if (!NT_SUCCESS(*pReplyStatus) || fSkipFinally) - { - if (*pReplyStatus == CONSOLE_STATUS_WAIT) - { - RetVal = false; - } - } - else - { - NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1; - lpBuffer++; - *pNumBytes += sizeof(WCHAR); - while (*pNumBytes < _BufferSize) - { - // This call to GetChar won't block. - *pReplyStatus = GetChar(_pInputBuffer, - lpBuffer, - false, - nullptr, - nullptr, - nullptr); - if (!NT_SUCCESS(*pReplyStatus)) - { - *pReplyStatus = STATUS_SUCCESS; - break; - } - NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1; - lpBuffer++; - *pNumBytes += sizeof(WCHAR); - } - } + return true; } - // If the read was completed (status != wait), free the raw read data. - if (*pReplyStatus != CONSOLE_STATUS_WAIT && - !fSkipFinally && - !fIsUnicode) - { - // It's ansi, so translate the string. - std::unique_ptr tempBuffer; - try - { - tempBuffer = std::make_unique(NumBytes); - } - catch (...) - { - return true; - } + // If we get to here, this routine was called either by the input + // thread or a write routine. Both of these callers grab the current + // console lock. - lpBuffer = _BufPtr; - std::unique_ptr partialEvent; - - *pNumBytes = TranslateUnicodeToOem(lpBuffer, - gsl::narrow(*pNumBytes / sizeof(wchar_t)), - tempBuffer.get(), - gsl::narrow(NumBytes), - partialEvent); - if (partialEvent.get()) - { - _pInputBuffer->StoreReadPartialByteSequence(std::move(partialEvent)); - } - - memmove(lpBuffer, tempBuffer.get(), *pNumBytes); - if (fAddDbcsLead) - { - (*pNumBytes)++; - } - } - return RetVal; + std::span buffer{ reinterpret_cast(_BufPtr), _BufferSize }; + *pReplyStatus = ReadCharacterInput(*_pInputBuffer, buffer, *pNumBytes, *_pInputReadHandleData, fIsUnicode); + return *pReplyStatus != CONSOLE_STATUS_WAIT; } void RAW_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) { // See the comment in WaitBlock.cpp for more information. - if (_BufPtr == reinterpret_cast(oldBuffer)) + if (_BufPtr == static_cast(oldBuffer)) { - _BufPtr = reinterpret_cast(newBuffer); + _BufPtr = static_cast(newBuffer); } } diff --git a/src/host/registry.cpp b/src/host/registry.cpp index 0529f68021..3c2f62c8c1 100644 --- a/src/host/registry.cpp +++ b/src/host/registry.cpp @@ -37,7 +37,7 @@ void Registry::GetEditKeys(_In_opt_ HKEY hConsoleKey) const if (hConsoleKey == nullptr) { Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return; } @@ -51,7 +51,7 @@ void Registry::GetEditKeys(_In_opt_ HKEY hConsoleKey) const REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status) && dwValue <= 1) + if (SUCCEEDED_NTSTATUS(Status) && dwValue <= 1) { gci.SetAltF4CloseAllowed(!!dwValue); } @@ -81,7 +81,7 @@ void Registry::GetEditKeys(_In_opt_ HKEY hConsoleKey) const REG_DWORD, reinterpret_cast(&dwValue), nullptr); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { // the key isn't a REG_DWORD, try to read it as a REG_SZ const size_t bufferSize = 64; @@ -93,7 +93,7 @@ void Registry::GetEditKeys(_In_opt_ HKEY hConsoleKey) const REG_SZ, reinterpret_cast(awchBuffer), &cbWritten); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // we read something, set it as the word delimiters const std::wstring regWordDelimiters{ awchBuffer, cbWritten / sizeof(wchar_t) }; @@ -151,7 +151,7 @@ void Registry::_LoadMappedProperties(_In_reads_(cPropertyMappings) const Registr } // Don't log "file not found" messages. It's fine to not find a registry key. Log other types. - if (!NT_SUCCESS(Status) && NTSTATUS_FROM_WIN32(ERROR_FILE_NOT_FOUND) != Status) + if (FAILED_NTSTATUS(Status) && NTSTATUS_FROM_WIN32(ERROR_FILE_NOT_FOUND) != Status) { LOG_NTSTATUS(Status); } @@ -170,7 +170,7 @@ void Registry::LoadGlobalsFromRegistry() HKEY hConsoleKey; auto status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { _LoadMappedProperties(RegistrySerialization::s_GlobalPropMappings, RegistrySerialization::s_GlobalPropMappingsSize, hConsoleKey); @@ -201,7 +201,7 @@ void Registry::LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle) HKEY hCurrentUserKey; HKEY hConsoleKey; auto Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return; } @@ -220,7 +220,7 @@ void Registry::LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle) delete[] TranslatedConsoleTitle; TranslatedConsoleTitle = nullptr; - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { TranslatedConsoleTitle = TranslateConsoleTitle(pwszConsoleTitle, TRUE, FALSE); @@ -236,7 +236,7 @@ void Registry::LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle) TranslatedConsoleTitle = nullptr; } - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { RegCloseKey(hConsoleKey); RegCloseKey(hCurrentUserKey); @@ -251,7 +251,7 @@ void Registry::LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle) const auto loadDWORD = [=](const auto valueName) { DWORD value; const auto status = RegistrySerialization::s_QueryValue(hTitleKey, valueName, sizeof(value), REG_DWORD, (PBYTE)&value, nullptr); - return NT_SUCCESS(status) ? std::optional{ value } : std::nullopt; + return SUCCEEDED_NTSTATUS(status) ? std::optional{ value } : std::nullopt; }; // Window Origin Autopositioning Setting diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index 2861db30c3..d327c0b9d6 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -15,7 +15,6 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Interactivity; using Microsoft::Console::Interactivity::ServiceLocator; -#pragma region IBaseData // Routine Description: // - Retrieves the viewport that applies over the data available in the GetTextBuffer() call // Return Value: @@ -98,9 +97,6 @@ void RenderData::UnlockConsole() noexcept ::UnlockConsole(); } -#pragma endregion - -#pragma region IRenderData // Method Description: // - Gets the cursor's position in the buffer, relative to the buffer origin. // Arguments: @@ -265,26 +261,20 @@ bool RenderData::IsCursorDoubleWidth() const const bool RenderData::IsGridLineDrawingAllowed() noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // If virtual terminal output is set, grid line drawing is a must. It is always allowed. - if (WI_IsFlagSet(gci.GetActiveOutputBuffer().OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + const auto outputMode = gci.GetActiveOutputBuffer().OutputMode; + // If virtual terminal output is set, grid line drawing is a must. It is also enabled + // if someone explicitly asked for worldwide line drawing. + if (WI_IsAnyFlagSet(outputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_LVB_GRID_WORLDWIDE)) { return true; } else { - // If someone explicitly asked for worldwide line drawing, enable it. - if (gci.IsGridRenderingAllowedWorldwide()) - { - return true; - } - else - { - // Otherwise, for compatibility reasons with legacy applications that used the additional CHAR_INFO bits by accident or for their own purposes, - // we must enable grid line drawing only in a DBCS output codepage. (Line drawing historically only worked in DBCS codepages.) - // The only known instance of this is Image for Windows by TeraByte, Inc. (TeraByte Unlimited) which used the bits accidentally and for no purpose - // (according to the app developer) in conjunction with the Borland Turbo C cgscrn library. - return !!IsAvailableEastAsianCodePage(gci.OutputCP); - } + // Otherwise, for compatibility reasons with legacy applications that used the additional CHAR_INFO bits by accident or for their own purposes, + // we must enable grid line drawing only in a DBCS output codepage. (Line drawing historically only worked in DBCS codepages.) + // The only known instance of this is Image for Windows by TeraByte, Inc. (TeraByte Unlimited) which used the bits accidentally and for no purpose + // (according to the app developer) in conjunction with the Borland Turbo C cgscrn library. + return !!IsAvailableEastAsianCodePage(gci.OutputCP); } } @@ -327,9 +317,7 @@ const std::vector RenderData::GetPatternId(const til::point /*location*/ { return {}; } -#pragma endregion -#pragma region IUiaData // Routine Description: // - Converts a text attribute into the RGB values that should be presented, applying // relevant table translation information and preferences. @@ -425,22 +413,8 @@ const til::point RenderData::GetSelectionEnd() const noexcept // Then choose the opposite corner. const auto anchor = Selection::Instance().GetSelectionAnchor(); - const auto x_pos = (selectionRect.Left == anchor.X) ? selectionRect.Right : selectionRect.Left; - const auto y_pos = (selectionRect.Top == anchor.Y) ? selectionRect.Bottom : selectionRect.Top; + const auto x_pos = (selectionRect.left == anchor.x) ? selectionRect.right : selectionRect.left; + const auto y_pos = (selectionRect.top == anchor.y) ? selectionRect.bottom : selectionRect.top; 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); -} -#pragma endregion diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 4158d00046..5e649353cd 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -15,15 +15,11 @@ Author(s): #pragma once #include "../renderer/inc/IRenderData.hpp" -#include "../types/inc/colorTable.hpp" -#include "../types/IUiaData.h" class RenderData final : - public Microsoft::Console::Render::IRenderData, - public Microsoft::Console::Types::IUiaData + public Microsoft::Console::Render::IRenderData { public: -#pragma region BaseData Microsoft::Console::Types::Viewport GetViewport() noexcept override; til::point GetTextBufferEndPosition() const noexcept override; const TextBuffer& GetTextBuffer() const noexcept override; @@ -33,9 +29,7 @@ public: void LockConsole() noexcept override; void UnlockConsole() noexcept override; -#pragma endregion -#pragma region IRenderData til::point GetCursorPosition() const noexcept override; bool IsCursorVisible() const noexcept override; bool IsCursorOn() const noexcept override; @@ -54,17 +48,13 @@ public: const std::wstring GetHyperlinkCustomId(uint16_t id) const override; const std::vector GetPatternId(const til::point location) const override; -#pragma endregion -#pragma region IUiaData std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; const bool IsSelectionActive() const override; const bool IsBlockSelection() const noexcept override; void ClearSelection() override; void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; - const til::point GetSelectionAnchor() const noexcept; - const til::point GetSelectionEnd() const noexcept; - void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr); + const til::point GetSelectionAnchor() const noexcept override; + const til::point GetSelectionEnd() const noexcept override; const bool IsUiaDataInitialized() const noexcept override { return true; } -#pragma endregion }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 0260304547..d02bbaf3d6 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -9,7 +9,6 @@ #include "_output.h" #include "misc.h" #include "handle.h" -#include "../buffer/out/CharRow.hpp" #include #include "../interactivity/inc/ServiceLocator.hpp" @@ -48,7 +47,6 @@ SCREEN_INFORMATION::SCREEN_INFORMATION( _pAccessibilityNotifier{ pNotifier }, _api{ *this }, _stateMachine{ nullptr }, - _scrollMargins{ Viewport::Empty() }, _viewport(Viewport::Empty()), _psiAlternateBuffer{ nullptr }, _psiMainBuffer{ nullptr }, @@ -59,10 +57,11 @@ SCREEN_INFORMATION::SCREEN_INFORMATION( _desiredFont{ fontInfo }, _ignoreLegacyEquivalentVTAttributes{ false } { - // Check if VT mode is enabled. Note that this can be true w/o calling - // SetConsoleMode, if VirtualTerminalLevel is set to !=0 in the registry. + // Check if VT mode should be enabled by default. This can be true if + // VirtualTerminalLevel is set to !=0 in the registry, or when conhost + // is started in conpty mode. const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (gci.GetVirtTermLevel() != 0) + if (gci.GetDefaultVirtTermLevel() != 0) { OutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; } @@ -126,7 +125,7 @@ SCREEN_INFORMATION::~SCREEN_INFORMATION() const auto status = pScreen->_InitializeOutputStateMachine(); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { *ppScreen = pScreen; } @@ -379,16 +378,16 @@ void SCREEN_INFORMATION::GetScreenBufferInformation(_Out_ til::size* pcoordSize, // - til::size containing the width and height representing the minimum character grid that can be rendered in the window. til::size SCREEN_INFORMATION::GetMinWindowSizeInCharacters(const til::size coordFontSize /*= { 1, 1 }*/) const { - FAIL_FAST_IF(coordFontSize.X == 0); - FAIL_FAST_IF(coordFontSize.Y == 0); + FAIL_FAST_IF(coordFontSize.width == 0); + FAIL_FAST_IF(coordFontSize.height == 0); // prepare rectangle const auto rcWindowInPixels = _pConsoleWindowMetrics->GetMinClientRectInPixels(); // assign the pixel widths and heights to the final output til::size coordClientAreaSize; - coordClientAreaSize.X = rcWindowInPixels.width(); - coordClientAreaSize.Y = rcWindowInPixels.height(); + coordClientAreaSize.width = rcWindowInPixels.width(); + coordClientAreaSize.height = rcWindowInPixels.height(); // now retrieve the font size and divide the pixel counts into character counts auto coordFont = coordFontSize; // by default, use the size we were given @@ -399,11 +398,11 @@ til::size SCREEN_INFORMATION::GetMinWindowSizeInCharacters(const til::size coord coordFont = GetScreenFontSize(); } - FAIL_FAST_IF(coordFont.X == 0); - FAIL_FAST_IF(coordFont.Y == 0); + FAIL_FAST_IF(coordFont.width == 0); + FAIL_FAST_IF(coordFont.height == 0); - coordClientAreaSize.X /= coordFont.X; - coordClientAreaSize.Y /= coordFont.Y; + coordClientAreaSize.width /= coordFont.width; + coordClientAreaSize.height /= coordFont.height; return coordClientAreaSize; } @@ -418,8 +417,8 @@ til::size SCREEN_INFORMATION::GetMinWindowSizeInCharacters(const til::size coord // grid that can be rendered on the current monitor and/or from the current buffer size. til::size SCREEN_INFORMATION::GetMaxWindowSizeInCharacters(const til::size coordFontSize /*= { 1, 1 }*/) const { - FAIL_FAST_IF(coordFontSize.X == 0); - FAIL_FAST_IF(coordFontSize.Y == 0); + FAIL_FAST_IF(coordFontSize.width == 0); + FAIL_FAST_IF(coordFontSize.height == 0); const auto coordScreenBufferSize = GetBufferSize().Dimensions(); auto coordClientAreaSize = coordScreenBufferSize; @@ -433,8 +432,8 @@ til::size SCREEN_INFORMATION::GetMaxWindowSizeInCharacters(const til::size coord const auto coordWindowRestrictedSize = GetLargestWindowSizeInCharacters(coordFontSize); // If the buffer is smaller than what the max window would allow, then the max client area can only be as big as the // buffer we have. - coordClientAreaSize.X = std::min(coordScreenBufferSize.X, coordWindowRestrictedSize.X); - coordClientAreaSize.Y = std::min(coordScreenBufferSize.Y, coordWindowRestrictedSize.Y); + coordClientAreaSize.width = std::min(coordScreenBufferSize.width, coordWindowRestrictedSize.width); + coordClientAreaSize.height = std::min(coordScreenBufferSize.height, coordWindowRestrictedSize.height); } return coordClientAreaSize; @@ -450,15 +449,15 @@ til::size SCREEN_INFORMATION::GetMaxWindowSizeInCharacters(const til::size coord // grid that can be rendered on the current monitor with the maximum size window. til::size SCREEN_INFORMATION::GetLargestWindowSizeInCharacters(const til::size coordFontSize /*= { 1, 1 }*/) const { - FAIL_FAST_IF(coordFontSize.X == 0); - FAIL_FAST_IF(coordFontSize.Y == 0); + FAIL_FAST_IF(coordFontSize.width == 0); + FAIL_FAST_IF(coordFontSize.height == 0); const auto rcClientInPixels = _pConsoleWindowMetrics->GetMaxClientRectInPixels(); // first assign the pixel widths and heights to the final output til::size coordClientAreaSize; - coordClientAreaSize.X = rcClientInPixels.width(); - coordClientAreaSize.Y = rcClientInPixels.height(); + coordClientAreaSize.width = rcClientInPixels.width(); + coordClientAreaSize.height = rcClientInPixels.height(); // now retrieve the font size and divide the pixel counts into character counts auto coordFont = coordFontSize; // by default, use the size we were given @@ -469,11 +468,11 @@ til::size SCREEN_INFORMATION::GetLargestWindowSizeInCharacters(const til::size c coordFont = GetScreenFontSize(); } - FAIL_FAST_IF(coordFont.X == 0); - FAIL_FAST_IF(coordFont.Y == 0); + FAIL_FAST_IF(coordFont.width == 0); + FAIL_FAST_IF(coordFont.height == 0); - coordClientAreaSize.X /= coordFont.X; - coordClientAreaSize.Y /= coordFont.Y; + coordClientAreaSize.width /= coordFont.width; + coordClientAreaSize.height /= coordFont.height; return coordClientAreaSize; } @@ -486,8 +485,8 @@ til::size SCREEN_INFORMATION::GetScrollBarSizesInCharacters() const auto hScrollSize = ServiceLocator::LocateGlobals().sHorizontalScrollSize; til::size coordBarSizes; - coordBarSizes.X = (vScrollSize / coordFont.X) + ((vScrollSize % coordFont.X) != 0 ? 1 : 0); - coordBarSizes.Y = (hScrollSize / coordFont.Y) + ((hScrollSize % coordFont.Y) != 0 ? 1 : 0); + coordBarSizes.width = (vScrollSize / coordFont.width) + ((vScrollSize % coordFont.width) != 0 ? 1 : 0); + coordBarSizes.height = (hScrollSize / coordFont.height) + ((hScrollSize % coordFont.height) != 0 ? 1 : 0); return coordBarSizes; } @@ -497,8 +496,8 @@ void SCREEN_INFORMATION::GetRequiredConsoleSizeInPixels(_Out_ til::size* const p const auto coordFontSize = GetCurrentFont().GetSize(); // TODO: Assert valid size boundaries - pRequiredSize->cx = GetViewport().Width() * coordFontSize.X; - pRequiredSize->cy = GetViewport().Height() * coordFontSize.Y; + pRequiredSize->width = GetViewport().Width() * coordFontSize.width; + pRequiredSize->height = GetViewport().Height() * coordFontSize.height; } til::size SCREEN_INFORMATION::GetScreenFontSize() const @@ -513,8 +512,8 @@ til::size SCREEN_INFORMATION::GetScreenFontSize() const } // For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations. - coordRet.X = std::max(coordRet.X, 1); - coordRet.Y = std::max(coordRet.Y, 1); + coordRet.width = std::max(coordRet.width, 1); + coordRet.height = std::max(coordRet.height, 1); return coordRet; } @@ -597,7 +596,7 @@ void SCREEN_INFORMATION::NotifyAccessibilityEventing(const til::CoordType sStart if (IsActiveScreenBuffer()) { const auto coordScreenBufferSize = GetBufferSize().Dimensions(); - FAIL_FAST_IF(!(sEndX < coordScreenBufferSize.X)); + FAIL_FAST_IF(!(sEndX < coordScreenBufferSize.width)); if (sStartX == sEndX && sStartY == sEndY) { @@ -757,12 +756,12 @@ void SCREEN_INFORMATION::SetViewportSize(const til::size* const pcoordSize) // if relative coordinates, figure out absolute coords. if (!fAbsolute) { - if (coordWindowOrigin.X == 0 && coordWindowOrigin.Y == 0) + if (coordWindowOrigin.x == 0 && coordWindowOrigin.y == 0) { return STATUS_SUCCESS; } - NewWindow.Left = _viewport.Left() + coordWindowOrigin.X; - NewWindow.Top = _viewport.Top() + coordWindowOrigin.Y; + NewWindow.left = _viewport.Left() + coordWindowOrigin.x; + NewWindow.top = _viewport.Top() + coordWindowOrigin.y; } else { @@ -770,11 +769,11 @@ void SCREEN_INFORMATION::SetViewportSize(const til::size* const pcoordSize) { return STATUS_SUCCESS; } - NewWindow.Left = coordWindowOrigin.X; - NewWindow.Top = coordWindowOrigin.Y; + NewWindow.left = coordWindowOrigin.x; + NewWindow.top = coordWindowOrigin.y; } - NewWindow.Right = NewWindow.Left + WindowSize.X - 1; - NewWindow.Bottom = NewWindow.Top + WindowSize.Y - 1; + NewWindow.right = NewWindow.left + WindowSize.width - 1; + NewWindow.bottom = NewWindow.top + WindowSize.height - 1; const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -782,21 +781,21 @@ void SCREEN_INFORMATION::SetViewportSize(const til::size* const pcoordSize) // below the logical viewport, without updating our virtual bottom // (the logical viewport's position), dont. // Instead move us to the bottom of the logical viewport. - if (gci.IsTerminalScrolling() && !updateBottom && NewWindow.Bottom > _virtualBottom) + if (gci.IsTerminalScrolling() && !updateBottom && NewWindow.bottom > _virtualBottom) { - const auto delta = _virtualBottom - NewWindow.Bottom; - NewWindow.Top += delta; - NewWindow.Bottom += delta; + const auto delta = _virtualBottom - NewWindow.bottom; + NewWindow.top += delta; + NewWindow.bottom += delta; } // see if new window origin would extend window beyond extent of screen buffer const auto coordScreenBufferSize = GetBufferSize().Dimensions(); - if (NewWindow.Left < 0 || - NewWindow.Top < 0 || - NewWindow.Right < 0 || - NewWindow.Bottom < 0 || - NewWindow.Right >= coordScreenBufferSize.X || - NewWindow.Bottom >= coordScreenBufferSize.Y) + if (NewWindow.left < 0 || + NewWindow.top < 0 || + NewWindow.right < 0 || + NewWindow.bottom < 0 || + NewWindow.right >= coordScreenBufferSize.width || + NewWindow.bottom >= coordScreenBufferSize.height) { return STATUS_INVALID_PARAMETER; } @@ -874,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; @@ -889,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(); @@ -928,12 +912,12 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew const auto coordFontSize = GetScreenFontSize(); // We cannot operate if the font size is 0. This shouldn't happen, but stop early if it does. - RETURN_HR_IF(E_NOT_VALID_STATE, 0 == coordFontSize.X || 0 == coordFontSize.Y); + RETURN_HR_IF(E_NOT_VALID_STATE, 0 == coordFontSize.width || 0 == coordFontSize.height); // Find out how much client space we have to work with in the new area. til::size sizeClientNewPixels; - sizeClientNewPixels.cx = prcClientNew->width(); - sizeClientNewPixels.cy = prcClientNew->height(); + sizeClientNewPixels.width = prcClientNew->width(); + sizeClientNewPixels.height = prcClientNew->height(); // Subtract out scroll bar space if scroll bars will be necessary. auto fIsHorizontalVisible = false; @@ -942,20 +926,20 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew if (fIsHorizontalVisible) { - sizeClientNewPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + sizeClientNewPixels.height -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; } if (fIsVerticalVisible) { - sizeClientNewPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + sizeClientNewPixels.width -= ServiceLocator::LocateGlobals().sVerticalScrollSize; } // Now with the scroll bars removed, calculate how many characters could fit into the new window area. *pcoordClientNewCharacters = sizeClientNewPixels / coordFontSize; // If the new client is too tiny, our viewport will be 1x1. - pcoordClientNewCharacters->X = std::max(pcoordClientNewCharacters->X, 1); - pcoordClientNewCharacters->Y = std::max(pcoordClientNewCharacters->Y, 1); + pcoordClientNewCharacters->width = std::max(pcoordClientNewCharacters->width, 1); + pcoordClientNewCharacters->height = std::max(pcoordClientNewCharacters->height, 1); return S_OK; } @@ -984,7 +968,7 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // to fix the buffer and window width together. if (gci.GetWrapText()) { - coordBufferSizeNew.X = coordClientNewCharacters.X; + coordBufferSizeNew.width = coordClientNewCharacters.width; } // Reanalyze scroll bars in case we fixed the edge together for word wrap. @@ -997,18 +981,18 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // The alt buffer always wants to be exactly the size of the screen, never more or less. // This prevents scrollbars when you increase the alt buffer size, then decrease it. // Can't have a buffer dimension of 0 - that'll cause divide by zeros in the future. - coordBufferSizeNew.X = std::max(coordClientNewCharacters.X, 1); - coordBufferSizeNew.Y = std::max(coordClientNewCharacters.Y, 1); + coordBufferSizeNew.width = std::max(coordClientNewCharacters.width, 1); + coordBufferSizeNew.height = std::max(coordClientNewCharacters.height, 1); } else { - if (coordClientNewCharacters.X > coordBufferSizeNew.X) + if (coordClientNewCharacters.width > coordBufferSizeNew.width) { - coordBufferSizeNew.X = coordClientNewCharacters.X; + coordBufferSizeNew.width = coordClientNewCharacters.width; } - if (coordClientNewCharacters.Y > coordBufferSizeNew.Y) + if (coordClientNewCharacters.height > coordBufferSizeNew.height) { - coordBufferSizeNew.Y = coordClientNewCharacters.Y; + coordBufferSizeNew.height = coordClientNewCharacters.height; } } @@ -1017,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; } @@ -1055,8 +1023,8 @@ void SCREEN_INFORMATION::_CalculateViewportSize(const til::rect* const prcClient const auto coordFontSize = GetScreenFontSize(); til::size sizeClientPixels; - sizeClientPixels.cx = prcClientArea->width(); - sizeClientPixels.cy = prcClientArea->height(); + sizeClientPixels.width = prcClientArea->width(); + sizeClientPixels.height = prcClientArea->height(); bool fIsHorizontalVisible; bool fIsVerticalVisible; @@ -1068,16 +1036,16 @@ void SCREEN_INFORMATION::_CalculateViewportSize(const til::rect* const prcClient if (fIsHorizontalVisible) { - sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + sizeClientPixels.height -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; } if (fIsVerticalVisible) { - sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + sizeClientPixels.width -= ServiceLocator::LocateGlobals().sVerticalScrollSize; } - pcoordSize->X = (til::CoordType)(sizeClientPixels.cx / coordFontSize.X); - pcoordSize->Y = (til::CoordType)(sizeClientPixels.cy / coordFontSize.Y); + pcoordSize->width = (til::CoordType)(sizeClientPixels.width / coordFontSize.width); + pcoordSize->height = (til::CoordType)(sizeClientPixels.height / coordFontSize.height); } // Routine Description: @@ -1093,8 +1061,8 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS const bool fResizeFromTop, const bool fResizeFromLeft) { - const auto DeltaX = pcoordSize->X - _viewport.Width(); - const auto DeltaY = pcoordSize->Y - _viewport.Height(); + const auto DeltaX = pcoordSize->width - _viewport.Width(); + const auto DeltaY = pcoordSize->height - _viewport.Height(); const auto coordScreenBufferSize = GetBufferSize().Dimensions(); // do adjustments on a copy that's easily manipulated. @@ -1105,39 +1073,39 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS if (fResizeFromLeft) { // we're being horizontally sized from the left border - const auto sLeftProposed = srNewViewport.Left - DeltaX; + const auto sLeftProposed = srNewViewport.left - DeltaX; if (sLeftProposed >= 0) { // there's enough room in the backlog to just expand left - srNewViewport.Left -= DeltaX; + srNewViewport.left -= DeltaX; } else { // if we're resizing horizontally, we want to show as much // content above as we can, but we can't show more // than the left of the window - srNewViewport.Left = 0; - srNewViewport.Right += abs(sLeftProposed); + srNewViewport.left = 0; + srNewViewport.right += abs(sLeftProposed); } } else { // we're being horizontally sized from the right border - const auto sRightProposed = (srNewViewport.Right + DeltaX); - if (sRightProposed <= (coordScreenBufferSize.X - 1)) + const auto sRightProposed = (srNewViewport.right + DeltaX); + if (sRightProposed <= (coordScreenBufferSize.width - 1)) { - srNewViewport.Right += DeltaX; + srNewViewport.right += DeltaX; } else { - srNewViewport.Right = (coordScreenBufferSize.X - 1); - srNewViewport.Left -= (sRightProposed - (coordScreenBufferSize.X - 1)); + srNewViewport.right = (coordScreenBufferSize.width - 1); + srNewViewport.left -= (sRightProposed - (coordScreenBufferSize.width - 1)); } } if (fResizeFromTop) { - const auto sTopProposed = (srNewViewport.Top - DeltaY); + const auto sTopProposed = (srNewViewport.top - DeltaY); // we're being vertically sized from the top border if (sTopProposed >= 0) { @@ -1147,10 +1115,10 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS // If we're on the 0th row, people expect it to stay stuck // to the top of the window, not to start collapsing down // and hiding the top rows. - if (srNewViewport.Top > 0) + if (srNewViewport.top > 0) { // there's enough room in the backlog to just expand the top - srNewViewport.Top -= DeltaY; + srNewViewport.top -= DeltaY; } else { @@ -1159,7 +1127,7 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS // NOTE: It's += because DeltaY will be negative // already for this circumstance. FAIL_FAST_IF(!(DeltaY <= 0)); - srNewViewport.Bottom += DeltaY; + srNewViewport.bottom += DeltaY; } } else @@ -1167,15 +1135,15 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS // if we're resizing vertically, we want to show as much // content above as we can, but we can't show more // than the top of the window - srNewViewport.Top = 0; - srNewViewport.Bottom += abs(sTopProposed); + srNewViewport.top = 0; + srNewViewport.bottom += abs(sTopProposed); } } else { // we're being vertically sized from the bottom border - const auto sBottomProposed = (srNewViewport.Bottom + DeltaY); - if (sBottomProposed <= (coordScreenBufferSize.Y - 1)) + const auto sBottomProposed = (srNewViewport.bottom + DeltaY); + if (sBottomProposed <= (coordScreenBufferSize.height - 1)) { // If the new bottom is supposed to be before the final line of the buffer // Check to ensure that we don't hide the prompt by collapsing the window. @@ -1188,54 +1156,54 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS // If the bottom of the window when adjusted would be // above the final line of valid text... - if (srNewViewport.Bottom + DeltaY < coordValidEnd.Y) + if (srNewViewport.bottom + DeltaY < coordValidEnd.y) { // Adjust the top of the window instead of the bottom // (so the lines slide upward) - srNewViewport.Top -= DeltaY; + srNewViewport.top -= DeltaY; } else { - srNewViewport.Bottom += DeltaY; + srNewViewport.bottom += DeltaY; } } else { - srNewViewport.Bottom = (coordScreenBufferSize.Y - 1); - srNewViewport.Top -= (sBottomProposed - (coordScreenBufferSize.Y - 1)); + srNewViewport.bottom = (coordScreenBufferSize.height - 1); + srNewViewport.top -= (sBottomProposed - (coordScreenBufferSize.height - 1)); } } // Ensure the viewport is valid. // We can't have a negative left or top. - if (srNewViewport.Left < 0) + if (srNewViewport.left < 0) { - srNewViewport.Right -= srNewViewport.Left; - srNewViewport.Left = 0; + srNewViewport.right -= srNewViewport.left; + srNewViewport.left = 0; } - if (srNewViewport.Top < 0) + if (srNewViewport.top < 0) { - srNewViewport.Bottom -= srNewViewport.Top; - srNewViewport.Top = 0; + srNewViewport.bottom -= srNewViewport.top; + srNewViewport.top = 0; } // Bottom and right cannot pass the final characters in the array. - const auto offRightDelta = srNewViewport.Right - (coordScreenBufferSize.X - 1); + const auto offRightDelta = srNewViewport.right - (coordScreenBufferSize.width - 1); if (offRightDelta > 0) // the viewport was off the right of the buffer... { // ...so slide both left/right back into the buffer. This will prevent us // from having a negative width later. - srNewViewport.Right -= offRightDelta; - srNewViewport.Left = std::max(0, srNewViewport.Left - offRightDelta); + srNewViewport.right -= offRightDelta; + srNewViewport.left = std::max(0, srNewViewport.left - offRightDelta); } - const auto offBottomDelta = srNewViewport.Bottom - (coordScreenBufferSize.Y - 1); + const auto offBottomDelta = srNewViewport.bottom - (coordScreenBufferSize.height - 1); if (offBottomDelta > 0) // the viewport was off the right of the buffer... { // ...so slide both top/bottom back into the buffer. This will prevent us // from having a negative width later. - srNewViewport.Bottom -= offBottomDelta; - srNewViewport.Top = std::max(0, srNewViewport.Top - offBottomDelta); + srNewViewport.bottom -= offBottomDelta; + srNewViewport.top = std::max(0, srNewViewport.top - offBottomDelta); } // In general we want to avoid moving the virtual bottom unless it's aligned with @@ -1249,7 +1217,7 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS (_virtualBottom <= _viewport.BottomInclusive() && _virtualBottom > newViewport.BottomInclusive()) || _virtualBottom < newViewport.Height() - 1) { - _virtualBottom = srNewViewport.Bottom; + _virtualBottom = srNewViewport.bottom; } _viewport = newViewport; @@ -1349,43 +1317,43 @@ void SCREEN_INFORMATION::s_CalculateScrollbarVisibility(const til::rect* const p // Set up the client area in pixels til::size sizeClientPixels; - sizeClientPixels.cx = prcClientArea->width(); - sizeClientPixels.cy = prcClientArea->height(); + sizeClientPixels.width = prcClientArea->width(); + sizeClientPixels.height = prcClientArea->height(); // Set up the buffer area in pixels by multiplying the size by the font size scale factor til::size sizeBufferPixels; - sizeBufferPixels.cx = pcoordBufferSize->X * pcoordFontSize->X; - sizeBufferPixels.cy = pcoordBufferSize->Y * pcoordFontSize->Y; + sizeBufferPixels.width = pcoordBufferSize->width * pcoordFontSize->width; + sizeBufferPixels.height = pcoordBufferSize->height * pcoordFontSize->height; // Now figure out whether we need one or both scroll bars. // Showing a scroll bar in one direction may necessitate showing // the scroll bar in the other (as it will consume client area // space). - if (sizeBufferPixels.cx > sizeClientPixels.cx) + if (sizeBufferPixels.width > sizeClientPixels.width) { *pfIsHorizontalVisible = true; // If we have a horizontal bar, remove it from available // vertical space and check that remaining client area is // enough. - sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + sizeClientPixels.height -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; - if (sizeBufferPixels.cy > sizeClientPixels.cy) + if (sizeBufferPixels.height > sizeClientPixels.height) { *pfIsVerticalVisible = true; } } - else if (sizeBufferPixels.cy > sizeClientPixels.cy) + else if (sizeBufferPixels.height > sizeClientPixels.height) { *pfIsVerticalVisible = true; // If we have a vertical bar, remove it from available // horizontal space and check that remaining client area is // enough. - sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + sizeClientPixels.width -= ServiceLocator::LocateGlobals().sVerticalScrollSize; - if (sizeBufferPixels.cx > sizeClientPixels.cx) + if (sizeBufferPixels.width > sizeClientPixels.width) { *pfIsHorizontalVisible = true; } @@ -1419,38 +1387,27 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeWithReflow(const til::size coordNewScreenSize) +try { - if ((USHORT)coordNewScreenSize.X >= SHORT_MAX || (USHORT)coordNewScreenSize.Y >= SHORT_MAX) + if ((USHORT)coordNewScreenSize.width >= SHORT_MAX || (USHORT)coordNewScreenSize.height >= SHORT_MAX) { - RIPMSG2(RIP_WARNING, "Invalid screen buffer size (0x%x, 0x%x)", coordNewScreenSize.X, coordNewScreenSize.Y); + RIPMSG2(RIP_WARNING, "Invalid screen buffer size (0x%x, 0x%x)", coordNewScreenSize.width, coordNewScreenSize.height); return STATUS_INVALID_PARAMETER; } - // First allocate a new text buffer to take the place of the current one. - std::unique_ptr newTextBuffer; - - // GH#3848 - Stash away the current attributes the old text buffer is using. - // We'll initialize the new buffer with the default attributes, but after - // the resize, we'll want to make sure that the new buffer's current + // GH#3848 - We'll initialize the new buffer with the default attributes, + // but after the resize, we'll want to make sure that the new buffer's current // attributes (the ones used for printing new text) match the old buffer's. - const auto oldPrimaryAttributes = _textBuffer->GetCurrentAttributes(); - try - { - newTextBuffer = std::make_unique(coordNewScreenSize, - TextAttribute{}, - 0, // temporarily set size to 0 so it won't render. - _textBuffer->IsActiveBuffer(), - _textBuffer->GetRenderer()); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } + auto newTextBuffer = std::make_unique(coordNewScreenSize, + TextAttribute{}, + 0, // temporarily set size to 0 so it won't render. + _textBuffer->IsActiveBuffer(), + _textBuffer->GetRenderer()); // Save cursor's relative height versus the viewport - const auto sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top(); + const auto sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().y - _viewport.Top(); // Also save the distance to the virtual bottom so it can be restored after the resize - const auto cursorDistanceFromBottom = _virtualBottom - _textBuffer->GetCursor().GetPosition().Y; + const auto cursorDistanceFromBottom = _virtualBottom - _textBuffer->GetCursor().GetPosition().y; // skip any drawing updates that might occur until we swap _textBuffer with the new buffer or we exit early. newTextBuffer->GetCursor().StartDeferDrawing(); @@ -1458,38 +1415,35 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // we're capturing _textBuffer by reference here because when we exit, we want to EndDefer on the current active buffer. auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - auto hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), std::nullopt, std::nullopt); + TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get()); - if (SUCCEEDED(hr)) - { - // Since the reflow doesn't preserve the virtual bottom, we try and - // estimate where it ought to be by making it the same distance from - // the cursor row as it was before the resize. However, we also need - // to make sure it is far enough down to include the last non-space - // row, and it shouldn't be less than the height of the viewport, - // otherwise the top of the virtual viewport would end up negative. - const auto cursorRow = newTextBuffer->GetCursor().GetPosition().Y; - const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().Y; - const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; - const auto viewportBottom = _viewport.Height() - 1; - _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); + // Since the reflow doesn't preserve the virtual bottom, we try and + // estimate where it ought to be by making it the same distance from + // the cursor row as it was before the resize. However, we also need + // to make sure it is far enough down to include the last non-space + // row, and it shouldn't be less than the height of the viewport, + // otherwise the top of the virtual viewport would end up negative. + const auto cursorRow = newTextBuffer->GetCursor().GetPosition().y; + const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().y; + const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; + const auto viewportBottom = _viewport.Height() - 1; + _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); - // We can't let it extend past the bottom of the buffer either. - _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); + // We can't let it extend past the bottom of the buffer either. + _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); - // Adjust the viewport so the cursor doesn't wildly fly off up or down. - const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); - til::point coordCursorHeightDiff; - coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; - LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); + // Adjust the viewport so the cursor doesn't wildly fly off up or down. + const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); + til::point coordCursorHeightDiff; + coordCursorHeightDiff.y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; + LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); - newTextBuffer->SetCurrentAttributes(oldPrimaryAttributes); + newTextBuffer->SetCurrentAttributes(_textBuffer->GetCurrentAttributes()); - _textBuffer.swap(newTextBuffer); - } - - return NTSTATUS_FROM_HRESULT(hr); + _textBuffer = std::move(newTextBuffer); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: @@ -1499,11 +1453,14 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const til::size coordNewScreenSize) +try { _textBuffer->GetCursor().StartDeferDrawing(); auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - return NTSTATUS_FROM_HRESULT(_textBuffer->ResizeTraditional(coordNewScreenSize)); + _textBuffer->ResizeTraditional(coordNewScreenSize); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: @@ -1525,24 +1482,20 @@ bool SCREEN_INFORMATION::IsMaximizedY() const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto status = STATUS_SUCCESS; - // If we're in conpty mode, suppress any immediate painting we might do - // during the resize. - if (gci.IsInVtIoMode()) - { - gci.GetVtIo()->BeginResize(); - } - auto endResize = wil::scope_exit([&] { - if (gci.IsInVtIoMode()) - { - gci.GetVtIo()->EndResize(); - } - }); - // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions Selection::Instance().ClearSelection(); - // 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. @@ -1562,11 +1515,11 @@ bool SCREEN_INFORMATION::IsMaximizedY() const status = NTSTATUS_FROM_HRESULT(ResizeTraditional(coordNewScreenSize)); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { if (HasAccessibilityEventing()) { - NotifyAccessibilityEventing(0, 0, coordNewScreenSize.X - 1, coordNewScreenSize.Y - 1); + NotifyAccessibilityEventing(0, 0, coordNewScreenSize.width - 1, coordNewScreenSize.height - 1); } if ((!ConvScreenInfo)) @@ -1607,10 +1560,10 @@ void SCREEN_INFORMATION::ClipToScreenBuffer(_Inout_ til::inclusive_rect* const p { const auto bufferSize = GetBufferSize(); - psrClip->Left = std::max(psrClip->Left, bufferSize.Left()); - psrClip->Top = std::max(psrClip->Top, bufferSize.Top()); - psrClip->Right = std::min(psrClip->Right, bufferSize.RightInclusive()); - psrClip->Bottom = std::min(psrClip->Bottom, bufferSize.BottomInclusive()); + psrClip->left = std::max(psrClip->left, bufferSize.Left()); + psrClip->top = std::max(psrClip->top, bufferSize.Top()); + psrClip->right = std::min(psrClip->right, bufferSize.RightInclusive()); + psrClip->bottom = std::min(psrClip->bottom, bufferSize.BottomInclusive()); } void SCREEN_INFORMATION::MakeCurrentCursorVisible() @@ -1719,7 +1672,7 @@ void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) // buffer. // const auto coordScreenBufferSize = GetBufferSize().Dimensions(); - if (Position.X >= coordScreenBufferSize.X || Position.Y >= coordScreenBufferSize.Y || Position.X < 0 || Position.Y < 0) + if (Position.x >= coordScreenBufferSize.width || Position.y >= coordScreenBufferSize.height || Position.x < 0 || Position.y < 0) { return STATUS_INVALID_PARAMETER; } @@ -1740,9 +1693,9 @@ void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) cursor.SetPosition(Position); // If the cursor has moved below the virtual bottom, the bottom should be updated. - if (Position.Y > _virtualBottom) + if (Position.y > _virtualBottom) { - _virtualBottom = Position.Y; + _virtualBottom = Position.y; } // if we have the focus, adjust the cursor state @@ -1767,69 +1720,38 @@ void SCREEN_INFORMATION::MakeCursorVisible(const til::point CursorPosition) { til::point WindowOrigin; - if (CursorPosition.X > _viewport.RightInclusive()) + if (CursorPosition.x > _viewport.RightInclusive()) { - WindowOrigin.X = CursorPosition.X - _viewport.RightInclusive(); + WindowOrigin.x = CursorPosition.x - _viewport.RightInclusive(); } - else if (CursorPosition.X < _viewport.Left()) + else if (CursorPosition.x < _viewport.Left()) { - WindowOrigin.X = CursorPosition.X - _viewport.Left(); + WindowOrigin.x = CursorPosition.x - _viewport.Left(); } else { - WindowOrigin.X = 0; + WindowOrigin.x = 0; } - if (CursorPosition.Y > _viewport.BottomInclusive()) + if (CursorPosition.y > _viewport.BottomInclusive()) { - WindowOrigin.Y = CursorPosition.Y - _viewport.BottomInclusive(); + WindowOrigin.y = CursorPosition.y - _viewport.BottomInclusive(); } - else if (CursorPosition.Y < _viewport.Top()) + else if (CursorPosition.y < _viewport.Top()) { - WindowOrigin.Y = CursorPosition.Y - _viewport.Top(); + WindowOrigin.y = CursorPosition.y - _viewport.Top(); } else { - WindowOrigin.Y = 0; + WindowOrigin.y = 0; } - if (WindowOrigin.X != 0 || WindowOrigin.Y != 0) + if (WindowOrigin.x != 0 || WindowOrigin.y != 0) { LOG_IF_FAILED(SetViewportOrigin(false, WindowOrigin, false)); } } -// Method Description: -// - Sets the scroll margins for this buffer. -// Arguments: -// - margins: The new values of the scroll margins, *relative to the viewport* -void SCREEN_INFORMATION::SetScrollMargins(const Viewport margins) -{ - _scrollMargins = margins; -} - -// Method Description: -// - Returns the scrolling margins boundaries for this screen buffer, relative -// to the origin of the text buffer. Most callers will want the absolute -// positions of the margins, though they are set and stored relative to -// origin of the viewport. -// Arguments: -// - -Viewport SCREEN_INFORMATION::GetAbsoluteScrollMargins() const -{ - return _viewport.ConvertFromOrigin(_scrollMargins); -} - -// Method Description: -// - Returns the scrolling margins boundaries for this screen buffer, relative -// to the current viewport. -// Arguments: -// - -Viewport SCREEN_INFORMATION::GetRelativeScrollMargins() const -{ - return _scrollMargins; -} - // Routine Description: // - Retrieves the active buffer of this buffer. If this buffer has an // alternate buffer, this is the alternate buffer. Otherwise, it is this buffer. @@ -1879,21 +1801,17 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const // machine with the main buffer it belongs to. // TODO: MSFT:19817348 Don't create alt screenbuffer's via an out SCREEN_INFORMATION** // Parameters: +// - initAttributes - the attributes the buffer is initialized with. // - ppsiNewScreenBuffer - a pointer to receive the newly created buffer. // Return value: // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error. -[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer) +[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(const TextAttribute& initAttributes, _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer) { // Create new screen buffer. auto WindowSize = _viewport.Dimensions(); const auto& existingFont = GetCurrentFont(); - // The buffer needs to be initialized with the standard erase attributes, - // i.e. the current background color, but with no meta attributes set. - auto initAttributes = GetAttributes(); - initAttributes.SetStandardErase(); - auto Status = SCREEN_INFORMATION::CreateInstance(WindowSize, existingFont, WindowSize, @@ -1901,7 +1819,7 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const GetPopupAttributes(), Cursor::CURSOR_SMALL_SIZE, ppsiNewScreenBuffer); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Update the alt buffer's cursor style, visibility, and position to match our own. auto& myCursor = GetTextBuffer().GetCursor(); @@ -1912,8 +1830,10 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const altCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); // The new position should match the viewport-relative position of the main buffer. auto altCursorPos = myCursor.GetPosition(); - altCursorPos.Y -= GetVirtualViewport().Top(); + altCursorPos.y -= GetVirtualViewport().Top(); altCursor.SetPosition(altCursorPos); + // The alt buffer's output mode should match the main buffer. + createdBuffer->OutputMode = OutputMode; s_InsertScreenBuffer(createdBuffer); @@ -1958,12 +1878,12 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain) auto newBufferSize = oldScreenBufferSize; // Always resize the width of the console - newBufferSize.X = newViewSize.X; + newBufferSize.width = newViewSize.width; // Only set the new buffer's height if the new size will be TALLER than the current size (e.g., resizing a 80x32 buffer to be 80x124). - if (newViewSize.Y > oldScreenBufferSize.Y) + if (newViewSize.height > oldScreenBufferSize.height) { - newBufferSize.Y = newViewSize.Y; + newBufferSize.height = newViewSize.height; } // From ApiRoutines::SetConsoleScreenBufferInfoExImpl. We don't need @@ -1971,10 +1891,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 @@ -1991,10 +1908,10 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain) // screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already // existing alternate, it is discarded. This allows applications to retain one HANDLE, and switch which buffer it points to seamlessly. // Parameters: -// - None +// - initAttributes - the attributes the buffer is initialized with. // Return value: // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error. -[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer() +[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer(const TextAttribute& initAttributes) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& siMain = GetMainBuffer(); @@ -2002,8 +1919,8 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain) _handleDeferredResize(siMain); SCREEN_INFORMATION* psiNewAltBuffer; - auto Status = _CreateAltBuffer(&psiNewAltBuffer); - if (NT_SUCCESS(Status)) + auto Status = _CreateAltBuffer(initAttributes, &psiNewAltBuffer); + if (SUCCEEDED_NTSTATUS(Status)) { // if this is already an alternate buffer, we want to make the new // buffer the alt on our main buffer, not on ourself, because there @@ -2079,6 +1996,9 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() mainCursor.SetIsVisible(altCursor.IsVisible()); mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + // Copy the alt buffer's output mode back to the main buffer. + psiMain->OutputMode = psiAlt->OutputMode; + s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer // deleting the alt buffer will give the GetSet back to its main @@ -2129,7 +2049,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(); } @@ -2140,7 +2060,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; } @@ -2296,43 +2216,23 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, // - S_OK [[nodiscard]] HRESULT SCREEN_INFORMATION::ClearBuffer() { - const auto oldCursorPos = _textBuffer->GetCursor().GetPosition(); - auto sNewTop = oldCursorPos.Y; - - auto delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height()); - for (auto i = 0; i < delta; i++) + // Rotate the buffer to bring the cursor row to the top of the viewport. + const auto cursorPos = _textBuffer->GetCursor().GetPosition(); + for (auto i = 0; i < cursorPos.y; i++) { _textBuffer->IncrementCircularBuffer(); - sNewTop--; } - const til::point coordNewOrigin{ 0, sNewTop }; - RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true)); + // Erase everything below that point. + RETURN_IF_FAILED(SetCursorPosition({ 0, 1 }, false)); + auto& engine = reinterpret_cast(_stateMachine->Engine()); + engine.Dispatch().EraseInDisplay(DispatchTypes::EraseType::ToEnd); - // SetViewportOrigin will only move the virtual bottom down, but in this - // case we need to reset it to the top, so we have to update it explicitly. - UpdateBottom(); - - // Place the cursor at the same x coord, on the row that's now the top - RETURN_IF_FAILED(SetCursorPosition({ oldCursorPos.X, sNewTop }, false)); - - // Update all the rows in the current viewport with the standard erase attributes, - // i.e. the current background color, but with no meta attributes set. - auto fillAttributes = GetAttributes(); - fillAttributes.SetStandardErase(); - - // +1 on the y coord because we don't want to clear the attributes of the - // cursor row, the one we saved. - til::point fillPosition{ 0, _viewport.Top() + 1 }; - auto fillLength = gsl::narrow(_viewport.Height() * GetBufferSize().Width()); - auto fillData = OutputCellIterator{ fillAttributes, fillLength }; - Write(fillData, fillPosition, false); + // Restore the original cursor x offset, but now on the first row. + RETURN_IF_FAILED(SetCursorPosition({ cursorPos.x, 0 }, false)); _textBuffer->TriggerRedrawAll(); - // Also reset the line rendition for the erased rows. - _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive()); - return S_OK; } @@ -2350,7 +2250,10 @@ void SCREEN_INFORMATION::SetTerminalConnection(_In_ VtEngine* const pTtyConnecti if (pTtyConnection) { engine.SetTerminalConnection(pTtyConnection, - std::bind(&StateMachine::FlushToTerminal, _stateMachine.get())); + [&stateMachine = *_stateMachine]() -> bool { + ServiceLocator::LocateGlobals().pRender->NotifyPaintFrame(); + return stateMachine.FlushToTerminal(); + }); } else { @@ -2359,56 +2262,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().IsTrailing()) - { - *span.begin() = paddingCell; - } - if (span.rbegin()->DbcsAttr().IsLeading()) - { - *span.rbegin() = paddingCell; - } - } - - return result; -} - // Routine Description: // - Writes cells to the output buffer at the cursor position. // Arguments: @@ -2481,8 +2334,8 @@ void SCREEN_INFORMATION::WriteRect(const OutputCellRect& data, const auto iter = data.GetRowIter(i); til::point point; - point.X = location.X; - point.Y = location.Y + i; + point.x = location.x; + point.y = location.y + i; _textBuffer->WriteLine(iter, point); } @@ -2522,7 +2375,7 @@ std::pair SCREEN_INFORMATION::GetWordBoundary(const til: { break; } - --start.X; + --start.x; } // find the end of the word @@ -2534,7 +2387,7 @@ std::pair SCREEN_INFORMATION::GetWordBoundary(const til: break; } ++endIt; - ++end.X; + ++end.x; } // trim leading zeros if we need to @@ -2557,7 +2410,7 @@ std::pair SCREEN_INFORMATION::GetWordBoundary(const til: const auto wch = trimIt->front(); // If the string is long enough to have stuff after the 0x/0n and it doesn't have one... - if (end.X > start.X + 2 && + if (end.x > start.x + 2 && wch != L'x' && wch != L'X' && wch != L'n') @@ -2568,9 +2421,9 @@ std::pair SCREEN_INFORMATION::GetWordBoundary(const til: // we find a single character '0' to Trim off the leading zeroes. while (trimIt->size() == 1 && trimIt->front() == L'0' && - start.X < end.X - 1) + start.x < end.x - 1) { - start.X++; + start.x++; trimIt++; } } @@ -2636,34 +2489,6 @@ void SCREEN_INFORMATION::UpdateBottom() _virtualBottom = _viewport.BottomInclusive(); } -// Method Description: -// - Initialize the row with the cursor on it to the standard erase attributes. -// This is executed when we move the cursor below the current viewport in -// VT mode. When that happens in a real terminal, the line is brand new, -// so it gets initialized for the first time with the current attributes. -// Our rows are usually pre-initialized, so re-initialize it here to -// emulate that behavior. -// See MSFT:17415310. -// Arguments: -// - -// Return Value: -// - -void SCREEN_INFORMATION::InitializeCursorRowAttributes() -{ - if (_textBuffer) - { - const auto& cursor = _textBuffer->GetCursor(); - auto& row = _textBuffer->GetRowByOffset(cursor.GetPosition().Y); - // The VT standard requires that the new row is initialized with - // the current background color, but with no meta attributes set. - auto fillAttributes = GetAttributes(); - fillAttributes.SetStandardErase(); - row.GetAttrRow().SetAttrToEnd(0, fillAttributes); - // The row should also be single width to start with. - row.SetLineRendition(LineRendition::SingleWidth); - } -} - // Method Description: // - Returns the "virtual" Viewport - the viewport with its bottom at // `_virtualBottom`. For VT operations, this is essentially the mutable @@ -2689,8 +2514,7 @@ bool SCREEN_INFORMATION::CursorIsDoubleWidth() const { const auto& buffer = GetTextBuffer(); const auto position = buffer.GetCursor().GetPosition(); - TextBufferTextIterator it(TextBufferCellIterator(buffer, position)); - return IsGlyphFullWidth(*it); + return buffer.GetRowByOffset(position.y).DbcsAttrAt(position.x) != DbcsAttribute::Single; } // Method Description: diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 02043c5884..09fca19809 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -123,8 +123,6 @@ public: 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; @@ -193,11 +191,7 @@ public: void MakeCursorVisible(const til::point CursorPosition); - Microsoft::Console::Types::Viewport GetRelativeScrollMargins() const; - Microsoft::Console::Types::Viewport GetAbsoluteScrollMargins() const; - void SetScrollMargins(const Microsoft::Console::Types::Viewport margins); - - [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(); + [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(const TextAttribute& initAttributes); void UseMainScreenBuffer(); SCREEN_INFORMATION& GetMainBuffer(); @@ -206,8 +200,8 @@ public: 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); @@ -226,8 +220,6 @@ public: FontInfoDesired& GetDesiredFont() noexcept; const FontInfoDesired& GetDesiredFont() const noexcept; - void InitializeCursorRowAttributes(); - void SetIgnoreLegacyEquivalentVTAttributes() noexcept; void ResetIgnoreLegacyEquivalentVTAttributes() noexcept; @@ -260,7 +252,8 @@ private: [[nodiscard]] NTSTATUS _InitializeOutputStateMachine(); void _FreeOutputStateMachine(); - [[nodiscard]] NTSTATUS _CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer); + [[nodiscard]] NTSTATUS _CreateAltBuffer(const TextAttribute& initAttributes, + _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer); bool _IsAltBuffer() const; bool _IsInPtyMode() const; @@ -270,8 +263,6 @@ private: std::shared_ptr _stateMachine; - Microsoft::Console::Types::Viewport _scrollMargins; //The margins of the VT specified scroll region. Left and Right are currently unused, but could be in the future. - // Specifies which coordinates of the screen buffer are visible in the // window client (the "viewport" into the buffer) Microsoft::Console::Types::Viewport _viewport; diff --git a/src/host/scrolling.cpp b/src/host/scrolling.cpp index eba39e45b7..2e42d62076 100644 --- a/src/host/scrolling.cpp +++ b/src/host/scrolling.cpp @@ -84,16 +84,16 @@ void Scrolling::s_ScrollIfNecessary(const SCREEN_INFORMATION& ScreenInfo) pWindow->ConvertScreenToClient(&CursorPos); til::point MousePosition; - MousePosition.X = CursorPos.x; - MousePosition.Y = CursorPos.y; + MousePosition.x = CursorPos.x; + MousePosition.y = CursorPos.y; auto coordFontSize = ScreenInfo.GetScreenFontSize(); - MousePosition.X /= coordFontSize.X; - MousePosition.Y /= coordFontSize.Y; + MousePosition.x /= coordFontSize.width; + MousePosition.y /= coordFontSize.height; - MousePosition.X += ScreenInfo.GetViewport().Left(); - MousePosition.Y += ScreenInfo.GetViewport().Top(); + MousePosition.x += ScreenInfo.GetViewport().Left(); + MousePosition.y += ScreenInfo.GetViewport().Top(); pSelection->ExtendSelection(MousePosition); } @@ -146,15 +146,15 @@ void Scrolling::s_HandleMouseWheel(_In_ bool isMouseWheel, ScreenInfo.WheelDelta %= ulActualDelta; } - NewOrigin.Y -= delta; + NewOrigin.y -= delta; const auto coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); - if (NewOrigin.Y < 0) + if (NewOrigin.y < 0) { - NewOrigin.Y = 0; + NewOrigin.y = 0; } - else if (NewOrigin.Y + ScreenInfo.GetViewport().Height() > coordBufferSize.Y) + else if (NewOrigin.y + ScreenInfo.GetViewport().Height() > coordBufferSize.height) { - NewOrigin.Y = coordBufferSize.Y - ScreenInfo.GetViewport().Height(); + NewOrigin.y = coordBufferSize.height - ScreenInfo.GetViewport().Height(); } LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); } @@ -184,15 +184,15 @@ void Scrolling::s_HandleMouseWheel(_In_ bool isMouseWheel, delta *= (ScreenInfo.HWheelDelta / ulActualDelta); ScreenInfo.HWheelDelta %= ulActualDelta; - NewOrigin.X += delta; + NewOrigin.x += delta; const auto coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); - if (NewOrigin.X < 0) + if (NewOrigin.x < 0) { - NewOrigin.X = 0; + NewOrigin.x = 0; } - else if (NewOrigin.X + ScreenInfo.GetViewport().Width() > coordBufferSize.X) + else if (NewOrigin.x + ScreenInfo.GetViewport().Width() > coordBufferSize.width) { - NewOrigin.X = coordBufferSize.X - ScreenInfo.GetViewport().Width(); + NewOrigin.x = coordBufferSize.width - ScreenInfo.GetViewport().Width(); } LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); @@ -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/selection.cpp b/src/host/selection.cpp index d528d0e49e..97145a6013 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -54,8 +54,8 @@ std::vector Selection::GetSelectionRects() const // _coordSelectionAnchor is at one of the corners of _srSelectionRects // endSelectionAnchor is at the exact opposite corner til::point endSelectionAnchor; - endSelectionAnchor.X = (_coordSelectionAnchor.X == _srSelectionRect.Left) ? _srSelectionRect.Right : _srSelectionRect.Left; - endSelectionAnchor.Y = (_coordSelectionAnchor.Y == _srSelectionRect.Top) ? _srSelectionRect.Bottom : _srSelectionRect.Top; + endSelectionAnchor.x = (_coordSelectionAnchor.x == _srSelectionRect.left) ? _srSelectionRect.right : _srSelectionRect.left; + endSelectionAnchor.y = (_coordSelectionAnchor.y == _srSelectionRect.top) ? _srSelectionRect.bottom : _srSelectionRect.top; const auto blockSelection = !IsLineSelection(); return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection, false); @@ -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: @@ -141,10 +144,10 @@ void Selection::InitializeMouseSelection(const til::point coordBufferPos) _coordSelectionAnchor = coordBufferPos; // since we've started with just a point, the rectangle is 1x1 on the point given - _srSelectionRect.Left = coordBufferPos.X; - _srSelectionRect.Right = coordBufferPos.X; - _srSelectionRect.Top = coordBufferPos.Y; - _srSelectionRect.Bottom = coordBufferPos.Y; + _srSelectionRect.left = coordBufferPos.x; + _srSelectionRect.right = coordBufferPos.x; + _srSelectionRect.top = coordBufferPos.y; + _srSelectionRect.bottom = coordBufferPos.y; // Check for ALT-Mouse Down "use alternate selection" // If in box mode, use line mode. If in line mode, use box mode. @@ -216,8 +219,8 @@ void Selection::ExtendSelection(_In_ til::point coordBufferPos) screenInfo.MakeCursorVisible(coordBufferPos); _dwSelectionFlags |= CONSOLE_SELECTION_NOT_EMPTY; - _srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X; - _srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y; + _srSelectionRect.left = _srSelectionRect.right = _coordSelectionAnchor.x; + _srSelectionRect.top = _srSelectionRect.bottom = _coordSelectionAnchor.y; ShowSelection(); } @@ -233,25 +236,25 @@ void Selection::ExtendSelection(_In_ til::point coordBufferPos) // update selection rect // this adjusts the rectangle dimensions based on which way the move was requested // in respect to the original selection position (the anchor) - if (coordBufferPos.X <= _coordSelectionAnchor.X) + if (coordBufferPos.x <= _coordSelectionAnchor.x) { - srNewSelection.Left = coordBufferPos.X; - srNewSelection.Right = _coordSelectionAnchor.X; + srNewSelection.left = coordBufferPos.x; + srNewSelection.right = _coordSelectionAnchor.x; } - else if (coordBufferPos.X > _coordSelectionAnchor.X) + else if (coordBufferPos.x > _coordSelectionAnchor.x) { - srNewSelection.Right = coordBufferPos.X; - srNewSelection.Left = _coordSelectionAnchor.X; + srNewSelection.right = coordBufferPos.x; + srNewSelection.left = _coordSelectionAnchor.x; } - if (coordBufferPos.Y <= _coordSelectionAnchor.Y) + if (coordBufferPos.y <= _coordSelectionAnchor.y) { - srNewSelection.Top = coordBufferPos.Y; - srNewSelection.Bottom = _coordSelectionAnchor.Y; + srNewSelection.top = coordBufferPos.y; + srNewSelection.bottom = _coordSelectionAnchor.y; } - else if (coordBufferPos.Y > _coordSelectionAnchor.Y) + else if (coordBufferPos.y > _coordSelectionAnchor.y) { - srNewSelection.Bottom = coordBufferPos.Y; - srNewSelection.Top = _coordSelectionAnchor.Y; + srNewSelection.bottom = coordBufferPos.y; + srNewSelection.top = _coordSelectionAnchor.y; } // This function is called on WM_MOUSEMOVE. @@ -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; @@ -396,17 +404,17 @@ void Selection::ColorSelection(const til::inclusive_rect& srRect, const TextAttr auto& screenInfo = gci.GetActiveOutputBuffer(); til::point coordTargetSize; - coordTargetSize.X = CalcWindowSizeX(srRect); - coordTargetSize.Y = CalcWindowSizeY(srRect); + coordTargetSize.x = CalcWindowSizeX(srRect); + coordTargetSize.y = CalcWindowSizeY(srRect); til::point coordTarget; - coordTarget.X = srRect.Left; - coordTarget.Y = srRect.Top; + coordTarget.x = srRect.left; + coordTarget.y = srRect.top; // Now color the selection a line at a time. - for (; (coordTarget.Y < srRect.Top + coordTargetSize.Y); ++coordTarget.Y) + for (; (coordTarget.y < srRect.top + coordTargetSize.y); ++coordTarget.y) { - const auto cchWrite = gsl::narrow(coordTargetSize.X); + const auto cchWrite = gsl::narrow(coordTargetSize.x); try { @@ -602,8 +610,8 @@ void Selection::SelectAll() // or it won't be selecting all the text. if (!IsLineSelection()) { - coordNewSelStart.X = 0; - coordNewSelEnd.X = screenInfo.GetBufferSize().RightInclusive(); + coordNewSelStart.x = 0; + coordNewSelEnd.x = screenInfo.GetBufferSize().RightInclusive(); } SelectNewRegion(coordNewSelStart, coordNewSelEnd); diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp index 895855f70c..f00bbb4b84 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: @@ -174,11 +172,11 @@ til::point Selection::WordByWordSelection(const bool fReverse, // if line boundaries fail, then set them to the buffer corners so they don't restrict anything. if (!fSuccess) { - coordMaxLeft.X = bufferSize.Left(); - coordMaxLeft.Y = bufferSize.Top(); + coordMaxLeft.x = bufferSize.Left(); + coordMaxLeft.y = bufferSize.Top(); - coordMaxRight.X = bufferSize.RightInclusive(); - coordMaxRight.Y = bufferSize.BottomInclusive(); + coordMaxRight.x = bufferSize.RightInclusive(); + coordMaxRight.y = bufferSize.BottomInclusive(); } // track whether we failed to move during an operation @@ -325,8 +323,8 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn // the selection point is the other corner of the rectangle from the anchor that we're about to manipulate til::point coordSelPoint; - coordSelPoint.X = coordAnchor.X == rectSelection.Left ? rectSelection.Right : rectSelection.Left; - coordSelPoint.Y = coordAnchor.Y == rectSelection.Top ? rectSelection.Bottom : rectSelection.Top; + coordSelPoint.x = coordAnchor.x == rectSelection.left ? rectSelection.right : rectSelection.left; + coordSelPoint.y = coordAnchor.y == rectSelection.top ? rectSelection.bottom : rectSelection.top; // this is the maximum size of the buffer const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize(); @@ -360,7 +358,7 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn try { const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr(); - if (attr.IsTrailing()) + if (attr == DbcsAttribute::Trailing) { bufferSize.IncrementInBounds(coordSelPoint); } @@ -372,36 +370,36 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn // shift + up/down extends the selection by one row, stopping at top or bottom of screen case VK_UP: { - if (coordSelPoint.Y > bufferSize.Top()) + if (coordSelPoint.y > bufferSize.Top()) { - coordSelPoint.Y--; + coordSelPoint.y--; } break; } case VK_DOWN: { - if (coordSelPoint.Y < bufferSize.BottomInclusive()) + if (coordSelPoint.y < bufferSize.BottomInclusive()) { - coordSelPoint.Y++; + coordSelPoint.y++; } break; } // shift + pgup/pgdn extends selection up or down one full screen case VK_NEXT: { - coordSelPoint.Y = base::CheckAdd(coordSelPoint.Y, sWindowHeight).ValueOrDefault(bufferSize.BottomInclusive()); - if (coordSelPoint.Y > bufferSize.BottomInclusive()) + coordSelPoint.y = base::CheckAdd(coordSelPoint.y, sWindowHeight).ValueOrDefault(bufferSize.BottomInclusive()); + if (coordSelPoint.y > bufferSize.BottomInclusive()) { - coordSelPoint.Y = bufferSize.BottomInclusive(); + coordSelPoint.y = bufferSize.BottomInclusive(); } break; } case VK_PRIOR: { - coordSelPoint.Y = base::CheckSub(coordSelPoint.Y, sWindowHeight).ValueOrDefault(bufferSize.Top()); - if (coordSelPoint.Y < bufferSize.Top()) + coordSelPoint.y = base::CheckSub(coordSelPoint.y, sWindowHeight).ValueOrDefault(bufferSize.Top()); + if (coordSelPoint.y < bufferSize.Top()) { - coordSelPoint.Y = bufferSize.Top(); + coordSelPoint.y = bufferSize.Top(); } break; } @@ -434,17 +432,17 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) > 0) { // and we're on the same line as the beginning of the input - if (coordInputLineStart.Y == coordSelPoint.Y) + if (coordInputLineStart.y == coordSelPoint.y) { // then only back up to the start of the input - coordSelPoint.X = coordInputLineStart.X; + coordSelPoint.x = coordInputLineStart.x; break; } } } // otherwise, fall through and select to the head of the line. - coordSelPoint.X = 0; + coordSelPoint.x = 0; break; } case VK_END: @@ -482,13 +480,13 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) >= 0) { // and we're on the same line as the end of the input - if (coordInputLineEnd.Y == coordSelPoint.Y) + if (coordInputLineEnd.y == coordSelPoint.y) { // and we're not already on the end of the input... - if (coordSelPoint.X < coordInputLineEnd.X) + if (coordSelPoint.x < coordInputLineEnd.x) { // then only use end to the end of the input - coordSelPoint.X = coordInputLineEnd.X; + coordSelPoint.x = coordInputLineEnd.x; break; } } @@ -496,26 +494,26 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn else { // otherwise if we're outside and on the same line as the start of the input - if (coordInputLineStart.Y == coordSelPoint.Y) + if (coordInputLineStart.y == coordSelPoint.y) { // calculate the end of the outside/output buffer position - const auto sEndOfOutputPos = coordInputLineStart.X - 1; + const auto sEndOfOutputPos = coordInputLineStart.x - 1; // if we're not already on the very last character... - if (coordSelPoint.X < sEndOfOutputPos) + if (coordSelPoint.x < sEndOfOutputPos) { // then only move to just before the beginning of the input - coordSelPoint.X = sEndOfOutputPos; + coordSelPoint.x = sEndOfOutputPos; break; } - else if (coordSelPoint.X == sEndOfOutputPos) + else if (coordSelPoint.x == sEndOfOutputPos) { // if we were on the last character, // then if the end of the input line is also on this current line, // move to that. - if (coordSelPoint.Y == coordInputLineEnd.Y) + if (coordSelPoint.y == coordInputLineEnd.y) { - coordSelPoint.X = coordInputLineEnd.X; + coordSelPoint.x = coordInputLineEnd.x; break; } } @@ -524,7 +522,7 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn } // otherwise, fall through and go to selecting the whole line to the end. - coordSelPoint.X = bufferSize.RightInclusive(); + coordSelPoint.x = bufferSize.RightInclusive(); break; } } @@ -547,17 +545,17 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn // shift + ctrl + up/down does the same thing that shift + up/down does case VK_UP: { - if (coordSelPoint.Y > bufferSize.Top()) + if (coordSelPoint.y > bufferSize.Top()) { - coordSelPoint.Y--; + coordSelPoint.y--; } break; } case VK_DOWN: { - if (coordSelPoint.Y < bufferSize.BottomInclusive()) + if (coordSelPoint.y < bufferSize.BottomInclusive()) { - coordSelPoint.Y++; + coordSelPoint.y++; } break; } @@ -583,7 +581,7 @@ bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pIn try { const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr(); - if (attr.IsTrailing()) + if (attr == DbcsAttribute::Trailing) { // try to move off by highlighting the lead half too. auto fSuccess = bufferSize.DecrementInBounds(coordSelPoint); @@ -638,7 +636,7 @@ bool Selection::_HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo) // If it is line selection, we can assemble that across multiple lines to make a search term. // But if it is block selection and the selected area is > 1 line in height, ignore the shift because we can't search. // Also ignore if there is no current selection. - if ((fShiftPressed) && (!IsAreaSelected() || (!IsLineSelection() && (_srSelectionRect.Top != _srSelectionRect.Bottom)))) + if ((fShiftPressed) && (!IsAreaSelected() || (!IsLineSelection() && (_srSelectionRect.top != _srSelectionRect.bottom)))) { fShiftPressed = false; } @@ -693,9 +691,9 @@ bool Selection::_HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo) std::wstring str; for (const auto& selectRect : selectionRects) { - auto it = screenInfo.GetCellDataAt({ selectRect.Left, selectRect.Top }); + auto it = screenInfo.GetCellDataAt({ selectRect.left, selectRect.top }); - for (til::CoordType i = 0; i < (selectRect.Right - selectRect.Left + 1);) + for (til::CoordType i = 0; i < (selectRect.right - selectRect.left + 1);) { str.append(it->Chars()); i += it->Columns(); @@ -708,10 +706,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); } } } @@ -766,7 +765,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe auto it = ScreenInfo.GetCellLineDataAt(cursorPos); // calculate next right - if (it->DbcsAttr().IsLeading()) + if (it->DbcsAttr() == DbcsAttribute::Leading) { iNextRightX = 2; } @@ -776,19 +775,19 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe } // calculate next left - if (cursorPos.X > 0) + if (cursorPos.x > 0) { it--; - if (it->DbcsAttr().IsTrailing()) + if (it->DbcsAttr() == DbcsAttribute::Trailing) { iNextLeftX = 2; } - else if (it->DbcsAttr().IsLeading()) + else if (it->DbcsAttr() == DbcsAttribute::Leading) { - if (cursorPos.X - 1 > 0) + if (cursorPos.x - 1 > 0) { it--; - if (it->DbcsAttr().IsTrailing()) + if (it->DbcsAttr() == DbcsAttribute::Trailing) { iNextLeftX = 3; } @@ -815,7 +814,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe { case VK_RIGHT: { - if (cursorPos.X + iNextRightX < ScreenInfo.GetBufferSize().Width()) + if (cursorPos.x + iNextRightX < ScreenInfo.GetBufferSize().Width()) { cursor.IncrementXPosition(iNextRightX); } @@ -824,7 +823,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe case VK_LEFT: { - if (cursorPos.X > 0) + if (cursorPos.x > 0) { cursor.DecrementXPosition(iNextLeftX); } @@ -833,7 +832,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe case VK_UP: { - if (cursorPos.Y > 0) + if (cursorPos.y > 0) { cursor.DecrementYPosition(1); } @@ -842,7 +841,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe case VK_DOWN: { - if (cursorPos.Y + 1 < ScreenInfo.GetTerminalBufferSize().Height()) + if (cursorPos.y + 1 < ScreenInfo.GetTerminalBufferSize().Height()) { cursor.IncrementYPosition(1); } @@ -853,9 +852,9 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe { cursor.IncrementYPosition(ScreenInfo.GetViewport().Height() - 1); const auto coordBufferSize = ScreenInfo.GetTerminalBufferSize().Dimensions(); - if (cursor.GetPosition().Y >= coordBufferSize.Y) + if (cursor.GetPosition().y >= coordBufferSize.height) { - cursor.SetYPosition(coordBufferSize.Y - 1); + cursor.SetYPosition(coordBufferSize.height - 1); } break; } @@ -863,7 +862,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe case VK_PRIOR: { cursor.DecrementYPosition(ScreenInfo.GetViewport().Height() - 1); - if (cursor.GetPosition().Y < 0) + if (cursor.GetPosition().y < 0) { cursor.SetYPosition(0); } @@ -881,7 +880,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe GetValidAreaBoundaries(nullptr, &coordValidEnd); // Adjust Y position of cursor to the final line with valid text - cursor.SetYPosition(coordValidEnd.Y); + cursor.SetYPosition(coordValidEnd.y); } break; } @@ -928,8 +927,8 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe cursor.SetHasMoved(true); _coordSelectionAnchor = textBuffer.GetCursor().GetPosition(); ScreenInfo.MakeCursorVisible(_coordSelectionAnchor); - _srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X; - _srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y; + _srSelectionRect.left = _srSelectionRect.right = _coordSelectionAnchor.x; + _srSelectionRect.top = _srSelectionRect.bottom = _coordSelectionAnchor.y; } return true; } @@ -949,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) + if (gci.HasPendingCookedRead()) { - return false; + 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; + } } - const auto& cookedRead = gci.CookedReadData(); - const auto coordStart = cookedRead.OriginalCursorPosition(); - auto coordEnd = cookedRead.OriginalCursorPosition(); - - if (coordEnd.X < 0 && coordEnd.Y < 0) - { - // if the original cursor position from the input line data is invalid, then the buffer cursor position is the final position - coordEnd = textBuffer.GetCursor().GetPosition(); - } - else - { - // otherwise, we need to add the number of characters in the input line to the original cursor position - bufferSize.MoveInBounds(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; - } - - return true; + return false; } // Routine Description: @@ -1006,8 +983,8 @@ void Selection::GetValidAreaBoundaries(_Out_opt_ til::point* const pcoordValidSt { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); til::point coordEnd; - coordEnd.X = 0; - coordEnd.Y = 0; + coordEnd.x = 0; + coordEnd.y = 0; const auto fHaveInput = s_GetInputLineBoundaries(nullptr, &coordEnd); @@ -1026,14 +1003,14 @@ void Selection::GetValidAreaBoundaries(_Out_opt_ til::point* const pcoordValidSt if (pcoordValidStart != nullptr) { // valid area always starts at 0,0 - pcoordValidStart->X = 0; - pcoordValidStart->Y = 0; + pcoordValidStart->x = 0; + pcoordValidStart->y = 0; } if (pcoordValidEnd != nullptr) { - pcoordValidEnd->X = coordEnd.X; - pcoordValidEnd->Y = coordEnd.Y; + pcoordValidEnd->x = coordEnd.x; + pcoordValidEnd->y = coordEnd.y; } } diff --git a/src/host/server.h b/src/host/server.h index 72bcfce4ef..3332cc6b38 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -16,20 +16,17 @@ Revision History: #pragma once -#include "IIoProvider.hpp" - -#include "settings.hpp" - #include "conimeinfo.h" -#include "VtIo.hpp" #include "CursorBlinker.hpp" - +#include "IIoProvider.hpp" +#include "readDataCooked.hpp" +#include "settings.hpp" +#include "VtIo.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 @@ -79,29 +76,27 @@ class CONSOLE_INFORMATION : public Microsoft::Console::IIoProvider { public: - CONSOLE_INFORMATION(); + CONSOLE_INFORMATION() = default; CONSOLE_INFORMATION(const CONSOLE_INFORMATION& c) = delete; CONSOLE_INFORMATION& operator=(const CONSOLE_INFORMATION& c) = delete; ConsoleProcessList ProcessHandleList; - InputBuffer* pInputBuffer; + InputBuffer* pInputBuffer = nullptr; - SCREEN_INFORMATION* ScreenBuffers; // singly linked list + SCREEN_INFORMATION* ScreenBuffers = nullptr; // singly linked list ConsoleWaitQueue OutputQueue; - DWORD Flags; - - std::atomic PopupCount; + DWORD Flags = 0; // the following fields are used for ansi-unicode translation - UINT CP; - UINT OutputCP; + UINT CP = 0; + UINT OutputCP = 0; - ULONG CtrlFlags; // indicates outstanding ctrl requests - ULONG LimitingProcessId; + ULONG CtrlFlags = 0; // indicates outstanding ctrl requests + ULONG LimitingProcessId = 0; - CPINFO CPInfo; - CPINFO OutputCPInfo; + CPINFO CPInfo = {}; + CPINFO OutputCPInfo = {}; ConsoleImeInfo ConsoleIme; @@ -117,14 +112,18 @@ public: void SetActiveOutputBuffer(SCREEN_INFORMATION& screenBuffer); bool HasActiveOutputBuffer() const; - InputBuffer* const GetActiveInputBuffer() const; + InputBuffer* const GetActiveInputBuffer() const override; 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; + bool GetBracketedPasteMode() const noexcept; + void SetBracketedPasteMode(const bool enabled) noexcept; + void SetTitle(const std::wstring_view newTitle); void SetTitlePrefix(const std::wstring_view newTitlePrefix); void SetOriginalTitle(const std::wstring_view originalTitle); @@ -155,16 +154,15 @@ private: std::wstring _TitleAndPrefix; std::wstring _OriginalTitle; std::wstring _LinkTitle; // Path to .lnk file - SCREEN_INFORMATION* pCurrentScreenBuffer; - COOKED_READ_DATA* _cookedReadData; // non-ownership pointer + SCREEN_INFORMATION* pCurrentScreenBuffer = nullptr; + COOKED_READ_DATA* _cookedReadData = nullptr; // non-ownership pointer + bool _bracketedPasteMode = false; Microsoft::Console::VirtualTerminal::VtIo _vtIo; Microsoft::Console::CursorBlinker _blinker; 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/settings.cpp b/src/host/settings.cpp index 863b3786e3..a9673658bb 100644 --- a/src/host/settings.cpp +++ b/src/host/settings.cpp @@ -51,8 +51,6 @@ Settings::Settings() : _fAllowAltF4Close(true), _dwVirtTermLevel(0), _fUseWindowSizePixels(false), - _fAutoReturnOnNewline(true), // the historic Windows behavior defaults this to on. - _fRenderGridWorldwide(false), // historically grid lines were only rendered in DBCS codepages, so this is false by default unless otherwise specified. // window size pixels initialized below _fInterceptCopyPaste(0), _fUseDx(UseDx::Disabled), @@ -79,7 +77,7 @@ Settings::Settings() : } // Routine Description: -// - Applies hardcoded default settings that are in line with what is defined +// - Applies hard-coded default settings that are in line with what is defined // in our Windows edition manifest (living in win32k-settings.man). // - NOTE: This exists in case we cannot access the registry on desktop platforms. // We will use this to provide better defaults than the constructor values which @@ -358,11 +356,11 @@ void Settings::Validate() FAIL_FAST_IF(!(_dwScreenBufferSize.Y > 0)); } -DWORD Settings::GetVirtTermLevel() const +DWORD Settings::GetDefaultVirtTermLevel() const { return _dwVirtTermLevel; } -void Settings::SetVirtTermLevel(const DWORD dwVirtTermLevel) +void Settings::SetDefaultVirtTermLevel(const DWORD dwVirtTermLevel) { _dwVirtTermLevel = dwVirtTermLevel; } @@ -376,33 +374,6 @@ void Settings::SetAltF4CloseAllowed(const bool fAllowAltF4Close) _fAllowAltF4Close = fAllowAltF4Close; } -bool Settings::IsReturnOnNewlineAutomatic() const -{ - return _fAutoReturnOnNewline; -} -void Settings::SetAutomaticReturnOnNewline(const bool fAutoReturnOnNewline) -{ - _fAutoReturnOnNewline = fAutoReturnOnNewline; -} - -bool Settings::IsGridRenderingAllowedWorldwide() const -{ - return _fRenderGridWorldwide; -} -void Settings::SetGridRenderingAllowedWorldwide(const bool fGridRenderingAllowed) -{ - // Only trigger a notification and update the status if something has changed. - if (_fRenderGridWorldwide != fGridRenderingAllowed) - { - _fRenderGridWorldwide = fGridRenderingAllowed; - - if (ServiceLocator::LocateGlobals().pRender != nullptr) - { - ServiceLocator::LocateGlobals().pRender->TriggerRedrawAll(); - } - } -} - bool Settings::GetFilterOnPaste() const { return _fFilterOnPaste; diff --git a/src/host/settings.hpp b/src/host/settings.hpp index bb79bfcf95..03eff33536 100644 --- a/src/host/settings.hpp +++ b/src/host/settings.hpp @@ -50,18 +50,12 @@ public: RenderSettings& GetRenderSettings() noexcept { return _renderSettings; }; const RenderSettings& GetRenderSettings() const noexcept { return _renderSettings; }; - DWORD GetVirtTermLevel() const; - void SetVirtTermLevel(const DWORD dwVirtTermLevel); + DWORD GetDefaultVirtTermLevel() const; + void SetDefaultVirtTermLevel(const DWORD dwVirtTermLevel); bool IsAltF4CloseAllowed() const; void SetAltF4CloseAllowed(const bool fAllowAltF4Close); - bool IsReturnOnNewlineAutomatic() const; - void SetAutomaticReturnOnNewline(const bool fAutoReturnOnNewline); - - bool IsGridRenderingAllowedWorldwide() const; - void SetGridRenderingAllowedWorldwide(const bool fGridRenderingAllowed); - bool GetFilterOnPaste() const; void SetFilterOnPaste(const bool fFilterOnPaste); @@ -225,8 +219,6 @@ private: std::wstring _LaunchFaceName; bool _fAllowAltF4Close; DWORD _dwVirtTermLevel; - bool _fAutoReturnOnNewline; - bool _fRenderGridWorldwide; UseDx _fUseDx; bool _fCopyColor; diff --git a/src/host/sources.inc b/src/host/sources.inc index 173dae000c..bf3b16bcae 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/srvinit.cpp b/src/host/srvinit.cpp index 5b3f5a4bfa..284f448382 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -116,7 +116,7 @@ static bool s_IsOnDesktop() const auto status = Microsoft::Console::Interactivity::ApiDetector::DetectNtUserWindow(&level); LOG_IF_NTSTATUS_FAILED(status); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { switch (level) { @@ -151,7 +151,7 @@ static bool s_IsOnDesktop() // 4. Hardcoded default settings // To establish this hierarchy, we will need to load the settings and apply them in reverse order. - // 4. Initializing Settings will establish hardcoded defaults. + // 4. Initializing Settings will establish hard-coded defaults. // Set to reference of global console information since that's the only place we need to hold the settings. auto& settings = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& launchArgs = ServiceLocator::LocateGlobals().launchArgs; @@ -198,7 +198,7 @@ static bool s_IsOnDesktop() // We want everyone to be using VT by default anyways, so this is a // strong nudge in that direction. If an application _doesn't_ want VT // processing, it's free to disable this setting, even in conpty mode. - settings.SetVirtTermLevel(1); + settings.SetDefaultVirtTermLevel(1); // GH#9458 - In the case of a DefTerm handoff, the OriginalTitle might // be stashed in the lnk. We want to crack that lnk open, so we can get @@ -255,7 +255,7 @@ static bool s_IsOnDesktop() // Allocate console will read the global ServiceLocator::LocateGlobals().getConsoleInformation // for the settings we just set. auto Status = CONSOLE_INFORMATION::AllocateConsole({ Title, TitleLength / sizeof(wchar_t) }); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return Status; } @@ -297,7 +297,7 @@ void ConsoleCheckDebug() wil::unique_hkey hConsole; auto status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUser, &hConsole); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { DWORD dwData = 0; status = RegistrySerialization::s_QueryValue(hConsole.get(), @@ -307,7 +307,7 @@ void ConsoleCheckDebug() (BYTE*)&dwData, nullptr); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { if (dwData != 0) { @@ -603,11 +603,9 @@ try outPipeTheirSide.reset(); signalPipeTheirSide.reset(); - // GH#13211 - Make sure we request win32input mode and that the terminal - // obeys the resizing quirk. Otherwise, defterm connections to the Terminal - // are going to have weird resizing, and aren't going to send full fidelity - // input messages. - const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --resizeQuirk --win32input --signal {:#x}"), + // GH#13211 - Make sure the terminal obeys the resizing quirk. Otherwise, + // defterm connections to the Terminal are going to have weird resizing. + const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --resizeQuirk --signal {:#x}"), (int64_t)signalPipeOurSide.release()); ConsoleArguments consoleArgs(commandLine, inPipeOurSide.release(), outPipeOurSide.release()); @@ -810,7 +808,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, CONSOLE_SERVER_MSG Data = { 0 }; // Try to receive the data sent by the client. auto Status = NTSTATUS_FROM_HRESULT(Message->ReadMessageInput(0, &Data, sizeof(Data))); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return Status; } @@ -904,7 +902,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, // CreateInstance method), and the TextBuffer needs to be constructed with // a reference to the renderer, so the renderer must be created first. auto Status = SetUpConsole(&p->ConsoleInfo, p->TitleLength, p->Title, p->CurDir, p->AppName); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return Status; } @@ -912,7 +910,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, // Allow the renderer to paint once the rest of the console is hooked up. g.pRender->EnablePainting(); - if (NT_SUCCESS(Status) && ConsoleConnectionDeservesVisibleWindow(p)) + if (SUCCEEDED_NTSTATUS(Status) && ConsoleConnectionDeservesVisibleWindow(p)) { HANDLE Thread = nullptr; @@ -940,7 +938,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, CloseHandle(Thread); // This doesn't stop the thread from running. - if (!NT_SUCCESS(g.ntstatusConsoleInputInitStatus)) + if (FAILED_NTSTATUS(g.ntstatusConsoleInputInitStatus)) { Status = g.ntstatusConsoleInputInitStatus; } @@ -973,7 +971,7 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, // Potentially start the VT IO (if needed) // Make sure to do this after the i/o buffers have been created. // We'll need the size of the screen buffer in the vt i/o initialization - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { auto hr = gci.GetVtIo()->CreateIoHandlers(); if (hr == S_FALSE) diff --git a/src/host/stream.cpp b/src/host/stream.cpp index 23dd800271..11a7f6a411 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -6,7 +6,6 @@ #include "_stream.h" #include "stream.h" -#include "dbcs.h" #include "handle.h" #include "misc.h" #include "readDataRaw.hpp" @@ -17,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: @@ -57,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 - - if (!NT_SUCCESS(Status)) + 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); } @@ -116,157 +193,60 @@ 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 zeroKey = OneCoreSafeVkKeyScanW(0); + + 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)) { - 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); - - if (zeroVKey == keyEvent->GetVirtualKeyCode() && - keyEvent->DoActiveModifierKeysMatch(consoleModKeyState)) - { - // This really is the character 0x0000 - *pwchOut = keyEvent->GetCharData(); - return STATUS_SUCCESS; - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } + // This really is the character 0x0000 + *pwchOut = Event.Event.KeyEvent.uChar.UnicodeChar; + return STATUS_SUCCESS; } } } } } -// 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 @@ -281,155 +261,50 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio // - STATUS_NO_MEMORY in low memory situation // - other relevant NTSTATUS codes [[nodiscard]] static NTSTATUS _ReadPendingInput(InputBuffer& inputBuffer, - gsl::span buffer, + std::span buffer, size_t& bytesRead, INPUT_READ_HANDLE_DATA& readHandleState, const bool unicode) +try { - // TODO: MSFT: 18047766 - Correct this method to not play byte counting games. - auto fAddDbcsLead = FALSE; - size_t NumToWrite = 0; - size_t NumToBytes = 0; - auto pBuffer = reinterpret_cast(buffer.data()); - auto bufferRemaining = buffer.size_bytes(); bytesRead = 0; - if (buffer.size_bytes() < sizeof(wchar_t)) - { - return STATUS_BUFFER_TOO_SMALL; - } - const auto pending = readHandleState.GetPendingInput(); - auto pendingBytes = pending.size() * sizeof(wchar_t); - auto Tmp = pending.cbegin(); + auto input = pending; + // This is basically the continuation of COOKED_READ_DATA::_handlePostCharInputLoop. if (readHandleState.IsMultilineInput()) { - if (!unicode) - { - if (inputBuffer.IsReadPartialByteSequenceAvailable()) - { - auto event = inputBuffer.FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *pBuffer = static_cast(pKeyEvent->GetCharData()); - ++pBuffer; - bufferRemaining -= sizeof(wchar_t); - pendingBytes -= sizeof(wchar_t); - fAddDbcsLead = TRUE; - } - - if (pendingBytes == 0 || bufferRemaining == 0) - { - readHandleState.CompletePending(); - bytesRead = 1; - return STATUS_SUCCESS; - } - else - { - for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0; - NumToBytes < pendingBytes && - NumToBytes < bufferRemaining / sizeof(wchar_t) && - *Tmp != UNICODE_LINEFEED; - Tmp++, NumToWrite += sizeof(wchar_t)) - { - NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; - } - } - } - - NumToWrite = 0; - Tmp = pending.cbegin(); - while (NumToWrite < pendingBytes && - *Tmp != UNICODE_LINEFEED) - { - ++Tmp; - NumToWrite += sizeof(wchar_t); - } - - NumToWrite += sizeof(wchar_t); - if (NumToWrite > bufferRemaining) - { - NumToWrite = bufferRemaining; - } + const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1; + input = input.substr(0, std::min(input.size(), firstLineEnd)); } - else + + const auto inputSizeBefore = input.size(); + std::span writer{ buffer }; + inputBuffer.Consume(unicode, input, writer); + + // Since we truncated `input` to only include the first line, + // we need to restore `input` here to the entirety of the remaining input. + if (readHandleState.IsMultilineInput()) { - if (!unicode) - { - if (inputBuffer.IsReadPartialByteSequenceAvailable()) - { - auto event = inputBuffer.FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *pBuffer = static_cast(pKeyEvent->GetCharData()); - ++pBuffer; - bufferRemaining -= sizeof(wchar_t); - pendingBytes -= sizeof(wchar_t); - fAddDbcsLead = TRUE; - } - - if (pendingBytes == 0) - { - readHandleState.CompletePending(); - bytesRead = 1; - return STATUS_SUCCESS; - } - else - { - for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0; - NumToBytes < pendingBytes && NumToBytes < bufferRemaining / sizeof(wchar_t); - Tmp++, NumToWrite += sizeof(wchar_t)) - { - NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; - } - } - } - - NumToWrite = (bufferRemaining < pendingBytes) ? bufferRemaining : pendingBytes; + const auto inputSizeAfter = input.size(); + const auto amountConsumed = inputSizeBefore - inputSizeAfter; + input = pending.substr(std::min(pending.size(), amountConsumed)); } - memmove(pBuffer, pending.data(), NumToWrite); - pendingBytes -= NumToWrite; - if (pendingBytes != 0) - { - std::wstring_view remainingPending{ pending.data() + (NumToWrite / sizeof(wchar_t)), pendingBytes / sizeof(wchar_t) }; - readHandleState.UpdatePending(remainingPending); - } - else + if (input.empty()) { readHandleState.CompletePending(); } - - if (!unicode) + else { - // if ansi, translate string. we allocated the capture buffer - // large enough to handle the translated string. - auto tempBuffer = std::make_unique(NumToBytes); - std::unique_ptr partialEvent; - - NumToWrite = TranslateUnicodeToOem(pBuffer, - gsl::narrow(NumToWrite / sizeof(wchar_t)), - tempBuffer.get(), - gsl::narrow(NumToBytes), - partialEvent); - if (partialEvent.get()) - { - inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent)); - } - - // clang-format off -#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine but prefast can't follow it, evidently") - // clang-format on - memmove(pBuffer, tempBuffer.get(), NumToWrite); - - if (fAddDbcsLead) - { - NumToWrite++; - } + readHandleState.UpdatePending(input); } - bytesRead = NumToWrite; + bytesRead = buffer.size() - writer.size(); return STATUS_SUCCESS; } +NT_CATCH_RETURN() // Routine Description: // - read in characters until the buffer is full or return is read. @@ -457,10 +332,10 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio // - other relevant HRESULT codes [[nodiscard]] static HRESULT _ReadLineInput(InputBuffer& inputBuffer, const HANDLE processData, - gsl::span buffer, + 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, @@ -478,7 +353,7 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio &readHandleState, // pInputReadHandleData screenInfo, // pScreenInfo buffer.size_bytes(), // UserBufferSize - reinterpret_cast(buffer.data()), // UserBuffer + buffer.data(), // UserBuffer ctrlWakeupMask, // CtrlWakeupMask exeName, // exe name initialData, @@ -486,7 +361,7 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio 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()); @@ -521,140 +396,53 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio // populated. // - STATUS_SUCCESS on success // - Other NTSTATUS codes as necessary -[[nodiscard]] static NTSTATUS _ReadCharacterInput(InputBuffer& inputBuffer, - gsl::span buffer, - size_t& bytesRead, - INPUT_READ_HANDLE_DATA& readHandleState, - const bool unicode, - std::unique_ptr& waiter) +[[nodiscard]] NTSTATUS ReadCharacterInput(InputBuffer& inputBuffer, + std::span buffer, + size_t& bytesRead, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool unicode) +try { - size_t NumToWrite = 0; - auto addDbcsLead = false; - auto Status = STATUS_SUCCESS; - auto pBuffer = reinterpret_cast(buffer.data()); - auto bufferRemaining = buffer.size_bytes(); + UNREFERENCED_PARAMETER(readHandleState); + bytesRead = 0; - if (buffer.size() < 1) + const auto charSize = unicode ? sizeof(wchar_t) : sizeof(char); + std::span writer{ buffer }; + + if (writer.size() < charSize) { return STATUS_BUFFER_TOO_SMALL; } - if (bytesRead < bufferRemaining) + inputBuffer.ConsumeCached(unicode, writer); + + auto noDataReadYet = writer.size() == buffer.size(); + auto status = STATUS_SUCCESS; + + while (writer.size() >= charSize) { - auto pwchBufferTmp = pBuffer; - - NumToWrite = 0; - - if (!unicode && inputBuffer.IsReadPartialByteSequenceAvailable()) + wchar_t wch; + // We don't need to wait for input if `ConsumeCached` read something already, which is + // indicated by the writer having been advanced (= it's shorter than the original buffer). + status = GetChar(&inputBuffer, &wch, noDataReadYet, nullptr, nullptr, nullptr); + if (FAILED_NTSTATUS(status)) { - auto event = inputBuffer.FetchReadPartialByteSequence(false); - const auto pKeyEvent = static_cast(event.get()); - *pBuffer = static_cast(pKeyEvent->GetCharData()); - ++pBuffer; - bufferRemaining -= sizeof(wchar_t); - addDbcsLead = true; - - if (bufferRemaining == 0) - { - bytesRead = 1; - return STATUS_SUCCESS; - } - } - else - { - Status = GetChar(&inputBuffer, - pBuffer, - true, - nullptr, - nullptr, - nullptr); + break; } - if (Status == CONSOLE_STATUS_WAIT) - { - waiter = std::make_unique(&inputBuffer, - &readHandleState, - gsl::narrow(buffer.size_bytes()), - reinterpret_cast(buffer.data())); - } + std::wstring_view wchView{ &wch, 1 }; + inputBuffer.Consume(unicode, wchView, writer); - if (!NT_SUCCESS(Status)) - { - bytesRead = 0; - return Status; - } - - if (!addDbcsLead) - { - bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1; - NumToWrite += sizeof(wchar_t); - pBuffer++; - } - - while (NumToWrite < static_cast(bufferRemaining)) - { - Status = GetChar(&inputBuffer, - pBuffer, - false, - nullptr, - nullptr, - nullptr); - if (!NT_SUCCESS(Status)) - { - break; - } - bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1; - NumToWrite += sizeof(wchar_t); - pBuffer++; - } - - // if ansi, translate string. we allocated the capture buffer large enough to handle the translated string. - if (!unicode) - { - std::unique_ptr tempBuffer; - try - { - tempBuffer = std::make_unique(bytesRead); - } - catch (...) - { - return STATUS_NO_MEMORY; - } - - pBuffer = pwchBufferTmp; - std::unique_ptr partialEvent; - - bytesRead = TranslateUnicodeToOem(pBuffer, - gsl::narrow(NumToWrite / sizeof(wchar_t)), - tempBuffer.get(), - gsl::narrow(bytesRead), - partialEvent); - - if (partialEvent.get()) - { - inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent)); - } - -#pragma prefast(suppress : 26053 26015, "PREfast claims read overflow. *pReadByteCount is the exact size of tempBuffer as allocated above.") - memmove(pBuffer, tempBuffer.get(), bytesRead); - - if (addDbcsLead) - { - ++bytesRead; - } - } - else - { - // We always return the byte count for A & W modes, so in - // the Unicode case where we didn't translate back, set - // the return to the byte count that we assembled while - // pulling characters from the internal buffers. - bytesRead = NumToWrite; - } + noDataReadYet = false; } - return STATUS_SUCCESS; + + bytesRead = buffer.size() - writer.size(); + // Once we read some data off the InputBuffer it can't be read again, so we + // need to make sure to return a success status to the client in that case. + return noDataReadYet ? status : STATUS_SUCCESS; } +NT_CATCH_RETURN() // Routine Description: // - This routine reads in characters for stream input and does the @@ -682,10 +470,10 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio // - Other NSTATUS codes as necessary [[nodiscard]] NTSTATUS DoReadConsole(InputBuffer& inputBuffer, const HANDLE processData, - gsl::span buffer, + 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, @@ -730,71 +518,44 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio } else { - return _ReadCharacterInput(inputBuffer, - buffer, - bytesRead, - readHandleState, - unicode, - waiter); + const auto status = ReadCharacterInput(inputBuffer, + buffer, + bytesRead, + readHandleState, + unicode); + if (status == CONSOLE_STATUS_WAIT) + { + waiter = std::make_unique(&inputBuffer, &readHandleState, gsl::narrow(buffer.size()), reinterpret_cast(buffer.data())); + } + return status; } } CATCH_RETURN(); } -[[nodiscard]] HRESULT ApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept +[[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, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept -{ - try - { - return HRESULT_FROM_NT(DoReadConsole(context, - clientHandle, - buffer, - written, - controlKeyState, - initialData, - controlWakeupMask, - readHandleState, - exeName, - true, - waiter)); - } - CATCH_RETURN(); + 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/host/stream.h b/src/host/stream.h index 7ecc0595f2..5a03278fc9 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, @@ -29,16 +27,10 @@ Revision History: _Out_opt_ bool* const pPopupKeys, _Out_opt_ DWORD* const pdwKeyState) noexcept; -// Routine Description: -// - This routine returns the total number of screen spaces the characters up to the specified character take up. -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); +[[nodiscard]] NTSTATUS ReadCharacterInput(InputBuffer& inputBuffer, + std::span buffer, + size_t& bytesRead, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool unicode); VOID UnblockWriteConsole(const DWORD dwReason); diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp index da8a2938e5..21cff15e81 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(), @@ -34,9 +30,6 @@ Telemetry::Telemetry() : _rgiProcessFileNameIndex(), _rguiProcessFileNamesCount(), _rgiAlphabeticalIndex(), - _rguiProcessFileNamesCodesCount(), - _rguiProcessFileNamesFailedCodesCount(), - _rguiProcessFileNamesFailedOutsideCodesCount(), _rguiTimesApiUsed(), _rguiTimesApiUsedAnsi(), _uiNumberProcessFileNames(0), @@ -180,62 +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; -} - -// Total up all the used VT100 codes and assign them to the last process that was attached. -// We originally did this when each process disconnected, but some processes don't -// disconnect when the conhost process exits. So we have to remember the last process that connected. -void Telemetry::TotalCodesForPreviousProcess() -{ - using namespace Microsoft::Console::VirtualTerminal; - // Get the values even if we aren't recording the previously connected process, since we want to reset them to 0. - auto _uiTimesUsedCurrent = TermTelemetry::Instance().GetAndResetTimesUsedCurrent(); - auto _uiTimesFailedCurrent = TermTelemetry::Instance().GetAndResetTimesFailedCurrent(); - auto _uiTimesFailedOutsideRangeCurrent = TermTelemetry::Instance().GetAndResetTimesFailedOutsideRangeCurrent(); - - if (_iProcessConnectedCurrently < c_iMaxProcessesConnected) - { - _rguiProcessFileNamesCodesCount[_iProcessConnectedCurrently] += _uiTimesUsedCurrent; - _rguiProcessFileNamesFailedCodesCount[_iProcessConnectedCurrently] += _uiTimesFailedCurrent; - _rguiProcessFileNamesFailedOutsideCodesCount[_iProcessConnectedCurrently] += _uiTimesFailedOutsideRangeCurrent; - - // Don't total any more process connected telemetry, unless a new processes attaches that we want to gather. - _iProcessConnectedCurrently = SIZE_MAX; - } -} - // Tries to find the process name amongst our previous process names by doing a binary search. // The main difference between this and the standard bsearch library call, is that if this // can't find the string, it returns the position the new string should be inserted at. This saves @@ -284,8 +221,6 @@ void Telemetry::LogProcessConnected(const HANDLE hProcess) // This is a bit of processing, so don't do it for the 95% of machines that aren't being sampled. if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, MICROSOFT_KEYWORD_MEASURES)) { - TotalCodesForPreviousProcess(); - // Don't initialize wszFilePathAndName, QueryFullProcessImageName does that for us. Use QueryFullProcessImageName instead of // GetProcessImageFileName because we need the path to begin with a drive letter and not a device name. WCHAR wszFilePathAndName[MAX_PATH]; @@ -360,15 +295,8 @@ void Telemetry::WriteFinalTraceLog() // This is a bit of processing, so don't do it for the 95% of machines that aren't being sampled. if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, MICROSOFT_KEYWORD_MEASURES)) { - // Normally we would set the activity Id earlier, but since we know the parser only sends - // one final log at the end, setting the activity this late should be fine. - Microsoft::Console::VirtualTerminal::TermTelemetry::Instance().SetActivityId(_activity.Id()); - Microsoft::Console::VirtualTerminal::TermTelemetry::Instance().SetShouldWriteFinalLog(_fUserInteractiveForTelemetry); - if (_fUserInteractiveForTelemetry) { - TotalCodesForPreviousProcess(); - // Send this back using "measures" since we want a good sampling of our entire userbase. time_t tEndedAt; time(&tEndedAt); @@ -395,9 +323,6 @@ void Telemetry::WriteFinalTraceLog() // Casting to UINT should be fine, since our array size is only 2K. TraceLoggingPackedField(_wchProcessFileNames, static_cast(sizeof(WCHAR) * _iProcessFileNamesNext), TlgInUNICODESTRING | TlgInVcount, "ProcessesConnected"), TraceLoggingUInt32Array(_rguiProcessFileNamesCount, _uiNumberProcessFileNames, "ProcessesConnectedCount"), - TraceLoggingUInt32Array(_rguiProcessFileNamesCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedCodesCount"), - TraceLoggingUInt32Array(_rguiProcessFileNamesFailedCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedFailedCodesCount"), - TraceLoggingUInt32Array(_rguiProcessFileNamesFailedOutsideCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedFailedOutsideCount"), // Send back both starting and ending times separately instead just usage time (ending - starting). // This can help us determine if they were using multiple consoles at the same time. TraceLoggingInt32(static_cast(_tStartedAt), "StartedUsingAtSeconds"), @@ -423,17 +348,17 @@ void Telemetry::WriteFinalTraceLog() TraceLoggingUInt32Array((UINT32 const*)renderSettings.GetColorTable().data(), 16, "ColorTable"), TraceLoggingValue(gci.CP, "CodePageInput"), TraceLoggingValue(gci.OutputCP, "CodePageOutput"), - TraceLoggingValue(gci.GetFontSize().X, "FontSizeX"), - TraceLoggingValue(gci.GetFontSize().Y, "FontSizeY"), + TraceLoggingValue(gci.GetFontSize().width, "FontSizeX"), + TraceLoggingValue(gci.GetFontSize().height, "FontSizeY"), TraceLoggingValue(gci.GetHotKey(), "HotKey"), - TraceLoggingValue(gci.GetScreenBufferSize().X, "ScreenBufferSizeX"), - TraceLoggingValue(gci.GetScreenBufferSize().Y, "ScreenBufferSizeY"), + TraceLoggingValue(gci.GetScreenBufferSize().width, "ScreenBufferSizeX"), + TraceLoggingValue(gci.GetScreenBufferSize().height, "ScreenBufferSizeY"), TraceLoggingValue(gci.GetStartupFlags(), "StartupFlags"), - TraceLoggingValue(gci.GetVirtTermLevel(), "VirtualTerminalLevel"), - TraceLoggingValue(gci.GetWindowSize().X, "WindowSizeX"), - TraceLoggingValue(gci.GetWindowSize().Y, "WindowSizeY"), - TraceLoggingValue(gci.GetWindowOrigin().X, "WindowOriginX"), - TraceLoggingValue(gci.GetWindowOrigin().Y, "WindowOriginY"), + TraceLoggingValue(gci.GetDefaultVirtTermLevel(), "VirtualTerminalLevel"), + TraceLoggingValue(gci.GetWindowSize().width, "WindowSizeX"), + TraceLoggingValue(gci.GetWindowSize().height, "WindowSizeY"), + TraceLoggingValue(gci.GetWindowOrigin().width, "WindowOriginX"), + TraceLoggingValue(gci.GetWindowOrigin().height, "WindowOriginY"), TraceLoggingValue(gci.GetFaceName(), "FontName"), TraceLoggingBool(gci.IsAltF4CloseAllowed(), "AllowAltF4Close"), TraceLoggingBool(gci.GetCtrlKeyShortcutsDisabled(), "ControlKeyShortcutsDisabled"), diff --git a/src/host/telemetry.hpp b/src/host/telemetry.hpp index e35659cb3f..4cb2560a3b 100644 --- a/src/host/telemetry.hpp +++ b/src/host/telemetry.hpp @@ -44,9 +44,7 @@ public: 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; @@ -133,16 +131,11 @@ private: void operator=(const Telemetry&); bool FindProcessName(const WCHAR* pszProcessName, _Out_ size_t* iPosition) const; - void TotalCodesForPreviousProcess(); static const int c_iMaxProcessesConnected = 100; TraceLoggingActivity _activity; - float _fpFindStringLengthAverage; - float _fpDirectionDownAverage; - float _fpMatchCaseAverage; - unsigned int _uiFindNextClickedTotal; unsigned int _uiColorSelectionUsed; time_t _tStartedAt; WCHAR const* const c_pwszBashExeName = L"bash.exe"; @@ -159,12 +152,6 @@ private: unsigned int _rguiProcessFileNamesCount[c_iMaxProcessesConnected]; // To speed up searching the Process Names, create an alphabetically sorted index. size_t _rgiAlphabeticalIndex[c_iMaxProcessesConnected]; - // Total of how many codes each process used - unsigned int _rguiProcessFileNamesCodesCount[c_iMaxProcessesConnected]; - // Total of how many failed codes each process used - unsigned int _rguiProcessFileNamesFailedCodesCount[c_iMaxProcessesConnected]; - // Total of how many failed codes each process used outside the valid range. - unsigned int _rguiProcessFileNamesFailedOutsideCodesCount[c_iMaxProcessesConnected]; unsigned int _rguiTimesApiUsed[NUMBER_OF_APIS]; // Most of this array will be empty, and is only used if an API has an ansi specific variant. unsigned int _rguiTimesApiUsedAnsi[NUMBER_OF_APIS]; diff --git a/src/host/tracing.cpp b/src/host/tracing.cpp index c054707507..998410cf24 100644 --- a/src/host/tracing.cpp +++ b/src/host/tracing.cpp @@ -18,7 +18,7 @@ enum TraceKeywords Output = 0x008, // _DBGOUTPUT General = 0x100, Input = 0x200, - API = 0x400, + //API = 0x400, // No longer used UIA = 0x800, CookedRead = 0x1000, ConsoleAttachDetach = 0x2000, @@ -26,241 +26,8 @@ enum TraceKeywords }; DEFINE_ENUM_FLAG_OPERATORS(TraceKeywords); -// Routine Description: -// - Creates a tracing object to assist with automatically firing a stop event -// when this object goes out of scope. -// - Give it back to the caller and they will release it when the event period is over. -// Arguments: -// - onExit - Function to process when the object is destroyed (on exit) -Tracing::Tracing(std::function onExit) : - _onExit(onExit) -{ -} - -// Routine Description: -// - Destructs a tracing object, running any on exit routine, if necessary. -Tracing::~Tracing() -{ - if (_onExit) - { - _onExit(); - } -} - -// Routine Description: -// - Provides generic tracing for all API call types in the form of -// start/stop period events for timing and region-of-interest purposes -// while doing performance analysis. -// Arguments: -// - result - Reference to the area where the result code from the Api call -// will be stored for use in the stop event. -// - traceName - The name of the API call to list in the trace details -// Return Value: -// - An object for the caller to hold until the API call is complete. -// Then destroy it to signal that the call is over so the stop trace can be written. -Tracing Tracing::s_TraceApiCall(const NTSTATUS result, PCSTR traceName) -{ - // clang-format off - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "ApiCall", - TraceLoggingString(traceName, "ApiName"), - TraceLoggingOpcode(WINEVENT_OPCODE_START), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - - return Tracing([traceName, &result] { - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "ApiCall", - TraceLoggingString(traceName, "ApiName"), - TraceLoggingHResult(result, "Result"), - TraceLoggingOpcode(WINEVENT_OPCODE_STOP), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - }); - // clang-format on -} - ULONG Tracing::s_ulDebugFlag = 0x0; -void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_GetLargestWindowSize", - TraceLoggingHexInt32(status, "ResultCode"), - TraceLoggingInt32(a->Size.X, "MaxWindowWidthInChars"), - TraceLoggingInt32(a->Size.Y, "MaxWindowHeightInChars"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - -void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet) -{ - // Duplicate copies required by TraceLogging documentation ("don't get cute" examples) - // Using logic inside these macros can make problems. Do all logic outside macros. - - if (fSet) - { - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_SetConsoleScreenBufferInfo", - TraceLoggingHexInt32(status, "ResultCode"), - TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), - TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), - TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"), - TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"), - TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"), - TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - } - else - { - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_GetConsoleScreenBufferInfo", - TraceLoggingHexInt32(status, "ResultCode"), - TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), - TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), - TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"), - TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"), - TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"), - TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - } -} - -void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_SetConsoleScreenBufferSize", - TraceLoggingHexInt32(status, "ResultCode"), - TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), - TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - -void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_SetConsoleWindowInfo", - TraceLoggingHexInt32(status, "ResultCode"), - TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"), - TraceLoggingInt32(a->Window.Left, "WindowRectLeft"), - TraceLoggingInt32(a->Window.Right, "WindowRectRight"), - TraceLoggingInt32(a->Window.Top, "WindowRectTop"), - TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - -void Tracing::s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a) -{ - // clang-format off - if (a->Unicode) - { - const auto buf = static_cast(buffer); - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_WriteConsole", - TraceLoggingBoolean(a->Unicode, "Unicode"), - TraceLoggingUInt32(a->NumBytes, "NumBytes"), - TraceLoggingCountedWideString(buf, static_cast(a->NumBytes / sizeof(wchar_t)), "input buffer"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - } - else - { - const auto buf = static_cast(buffer); - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_WriteConsole", - TraceLoggingBoolean(a->Unicode, "Unicode"), - TraceLoggingUInt32(a->NumBytes, "NumBytes"), - TraceLoggingCountedString(buf, static_cast(a->NumBytes / sizeof(char)), "input buffer"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - } - // clang-format on -} - -void Tracing::s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_GetConsoleScreenBufferInfo", - TraceLoggingInt16(a->Size.X, "Size.X"), - TraceLoggingInt16(a->Size.Y, "Size.Y"), - TraceLoggingInt16(a->CursorPosition.X, "CursorPosition.X"), - TraceLoggingInt16(a->CursorPosition.Y, "CursorPosition.Y"), - TraceLoggingInt16(a->ScrollPosition.X, "ScrollPosition.X"), - TraceLoggingInt16(a->ScrollPosition.Y, "ScrollPosition.Y"), - TraceLoggingHexUInt16(a->Attributes, "Attributes"), - TraceLoggingInt16(a->CurrentWindowSize.X, "CurrentWindowSize.X"), - TraceLoggingInt16(a->CurrentWindowSize.Y, "CurrentWindowSize.Y"), - TraceLoggingInt16(a->MaximumWindowSize.X, "MaximumWindowSize.X"), - TraceLoggingInt16(a->MaximumWindowSize.Y, "MaximumWindowSize.Y"), - TraceLoggingHexUInt16(a->PopupAttributes, "PopupAttributes"), - TraceLoggingBoolean(a->FullscreenSupported, "FullscreenSupported"), - TraceLoggingHexUInt32FixedArray((UINT32 const*)a->ColorTable, _countof(a->ColorTable), "ColorTable"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); - static_assert(sizeof(UINT32) == sizeof(*a->ColorTable), "a->ColorTable"); -} - -void Tracing::s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring_view handleType) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_GetConsoleMode", - TraceLoggingHexUInt32(a->Mode, "Mode"), - TraceLoggingCountedWideString(handleType.data(), gsl::narrow_cast(handleType.size()), "Handle type"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - -void Tracing::s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_SetConsoleTextAttribute", - TraceLoggingHexUInt16(a->Attributes, "Attributes"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - -void Tracing::s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a) -{ - TraceLoggingWrite( - g_hConhostV2EventTraceProvider, - "API_WriteConsoleOutput", - TraceLoggingInt16(a->WriteCoord.X, "WriteCoord.X"), - TraceLoggingInt16(a->WriteCoord.Y, "WriteCoord.Y"), - TraceLoggingHexUInt32(a->StringType, "StringType"), - TraceLoggingUInt32(a->NumRecords, "NumRecords"), - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingKeyword(TraceKeywords::API)); -} - void Tracing::s_TraceWindowViewport(const Viewport& viewport) { TraceLoggingWrite( @@ -406,17 +173,18 @@ 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", - TraceLoggingCountedWideString(pwchCookedBuffer, cchCookedBufferLength, "ReadBuffer"), - TraceLoggingULong(cchCookedBufferLength, "ReadBufferLength"), - TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), - TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), + TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), + TraceLoggingCountedWideString(text.data(), length, "ReadBuffer"), + TraceLoggingULong(length, "ReadBufferLength"), + TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), TraceLoggingKeyword(TIL_KEYWORD_TRACE), TraceLoggingKeyword(TraceKeywords::CookedRead)); } @@ -431,8 +199,8 @@ void Tracing::s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConso TraceLoggingWrite( g_hConhostV2EventTraceProvider, "ConsoleAttachDetach", - TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), - TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), + TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), + TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), TraceLoggingBool(bIsAttach, "IsAttach"), TraceLoggingBool(bIsUserInteractive, "IsUserInteractive"), TraceLoggingKeyword(TIL_KEYWORD_TRACE), diff --git a/src/host/tracing.hpp b/src/host/tracing.hpp index f1d1e4126a..03cf423f4d 100644 --- a/src/host/tracing.hpp +++ b/src/host/tracing.hpp @@ -35,25 +35,14 @@ Author(s): #define DBGOUTPUT(_params_) #endif +#define TraceLoggingConsoleCoord(value, name) \ + TraceLoggingStruct(2, name), \ + TraceLoggingInt32(value.X, "X"), \ + TraceLoggingInt32(value.Y, "Y") + class Tracing { public: - ~Tracing(); - - static Tracing s_TraceApiCall(const NTSTATUS result, PCSTR traceName); - - static void s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a); - static void s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet); - static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a); - static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a); - - static void s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a); - - static void s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a); - static void s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring_view handleType); - static void s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a); - static void s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a); - static void s_TraceWindowViewport(const Microsoft::Console::Types::Viewport& viewport); static void s_TraceChars(_In_z_ const char* pszMessage, ...); @@ -62,15 +51,11 @@ public: 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; private: static ULONG s_ulDebugFlag; - - Tracing(std::function onExit); - - std::function _onExit; }; diff --git a/src/host/ut_host/AliasTests.cpp b/src/host/ut_host/AliasTests.cpp index 6443dd22f6..cf499bea89 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/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp index d83c9f8ba2..611eb55f14 100644 --- a/src/host/ut_host/ApiRoutinesTests.cpp +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -222,7 +222,7 @@ class ApiRoutinesTests char pszTitle[MAX_PATH]; // most applications use MAX_PATH size_t cchWritten = 0; size_t cchNeeded = 0; - VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(gsl::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(std::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); VERIFY_ARE_NOT_EQUAL(0u, cchWritten); // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). @@ -239,7 +239,7 @@ class ApiRoutinesTests wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH size_t cchWritten = 0; size_t cchNeeded = 0; - VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleWImpl(gsl::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleWImpl(std::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); VERIFY_ARE_NOT_EQUAL(0u, cchWritten); @@ -285,7 +285,7 @@ class ApiRoutinesTests char pszTitle[MAX_PATH]; // most applications use MAX_PATH size_t cchWritten = 0; size_t cchNeeded = 0; - VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleAImpl(gsl::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleAImpl(std::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); VERIFY_ARE_NOT_EQUAL(0u, cchWritten); // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). @@ -302,7 +302,7 @@ class ApiRoutinesTests wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH size_t cchWritten = 0; size_t cchNeeded = 0; - VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleWImpl(gsl::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleWImpl(std::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); VERIFY_ARE_NOT_EQUAL(0u, cchWritten); @@ -519,8 +519,8 @@ class ApiRoutinesTests // Find the delta by comparing the scroll area to the destination point til::point delta; - delta.X = destPoint.X - scrollArea.Left(); - delta.Y = destPoint.Y - scrollArea.Top(); + delta.x = destPoint.x - scrollArea.Left(); + delta.y = destPoint.y - scrollArea.Top(); // Find the area where the scroll text should have gone by taking the scrolled area by the delta auto scrolledDestination = Viewport::Offset(scrollArea, delta); @@ -607,7 +607,7 @@ class ApiRoutinesTests auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever."); + si.GetTextBuffer().ResizeTraditional({ 5, 5 }); // Tests are run both with and without the DECSTBM margins set. This should not alter // the results, since ScrollConsoleScreenBuffer should not be affected by VT margins. @@ -646,7 +646,7 @@ class ApiRoutinesTests { // for scrolling up and down, we're going to clip to only modify the left half of the buffer auto clipRectDimensions = bufferSize.Dimensions(); - clipRectDimensions.X /= 2; + clipRectDimensions.width /= 2; clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); clipRectangle = clipViewport.value().ToInclusive(); @@ -670,7 +670,7 @@ class ApiRoutinesTests { // for scrolling left and right, we're going to clip to only modify the top half of the buffer auto clipRectDimensions = bufferSize.Dimensions(); - clipRectDimensions.Y /= 2; + clipRectDimensions.height /= 2; clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); clipRectangle = clipViewport.value().ToInclusive(); @@ -739,7 +739,7 @@ class ApiRoutinesTests { // for scrolling up and down, we're going to clip to only modify the left half of the buffer auto clipRectDimensions = bufferSize.Dimensions(); - clipRectDimensions.X /= 2; + clipRectDimensions.width /= 2; clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); clipRectangle = clipViewport.value().ToInclusive(); @@ -772,10 +772,10 @@ class ApiRoutinesTests } Log::Comment(L"Scroll a small portion of the screen in an overlapping fashion."); - scroll.Top = 1; - scroll.Bottom = 2; - scroll.Left = 1; - scroll.Right = 2; + scroll.top = 1; + scroll.bottom = 2; + scroll.left = 1; + scroll.right = 2; si.GetActiveBuffer().ClearTextData(); // Clean out screen si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. @@ -801,7 +801,7 @@ class ApiRoutinesTests // ZZZZZ // We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right just one. - destination = { scroll.Left + 1, scroll.Top + 1 }; + destination = { scroll.left + 1, scroll.top + 1 }; // Move rectangle and backfill with red As. VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); @@ -845,7 +845,7 @@ class ApiRoutinesTests // ZZZZZ // We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right by two. - destination = { scroll.Left + 2, scroll.Top + 2 }; + destination = { scroll.left + 2, scroll.top + 2 }; // Move rectangle and backfill with red As. VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); diff --git a/src/host/ut_host/ClipboardTests.cpp b/src/host/ut_host/ClipboardTests.cpp index a8766454ef..2f9e03c88b 100644 --- a/src/host/ut_host/ClipboardTests.cpp +++ b/src/host/ut_host/ClipboardTests.cpp @@ -99,7 +99,7 @@ class ClipboardTests // verify trailing bytes were trimmed // there are 2 double-byte characters in our sample string (see CommonState.hpp for sample) // the width is right - left - VERIFY_ARE_EQUAL((til::CoordType)wcslen(text[0].data()), selection[0].Right - selection[0].Left + 1); + VERIFY_ARE_EQUAL((til::CoordType)wcslen(text[0].data()), selection[0].right - selection[0].left + 1); // since we're not in line selection, the line should be \r\n terminated auto tempPtr = text[0].data(); @@ -109,7 +109,7 @@ class ClipboardTests // since we're not in line selection, spaces should be trimmed from the end tempPtr = text[0].data(); - tempPtr += selection[0].Right - selection[0].Left - 2; + tempPtr += selection[0].right - selection[0].left - 2; tempPtr++; VERIFY_IS_NULL(wcsrchr(tempPtr, L' ')); @@ -153,106 +153,45 @@ 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(); + expectedEvents.push_back(shiftDown); + } - const auto keyState = OneCoreSafeVkKeyScanW(wch); - VERIFY_ARE_NOT_EQUAL(-1, keyState); - const auto virtualScanCode = static_cast(OneCoreSafeMapVirtualKeyW(LOBYTE(keyState), MAPVK_VK_TO_VSC)); + expectedEvents.push_back(event); + event.Event.KeyEvent.bKeyDown = FALSE; + expectedEvents.push_back(event); - 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()); + if (shift) + { + expectedEvents.push_back(shiftUp); } } - } - TEST_METHOD(CanConvertUppercaseText) - { - std::wstring wstr = L"HeLlO WoRlD"; - size_t uppercaseCount = 0; - for (auto wch : wstr) + VERIFY_ARE_EQUAL(expectedEvents.size(), events.size()); + + for (size_t i = 0; i < events.size(); ++i) { - std::isupper(wch) ? ++uppercaseCount : 0; - } - auto events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), - wstr.size()); - - VERIFY_ARE_EQUAL((wstr.size() + uppercaseCount) * 2, events.size()); - for (auto wch : wstr) - { - std::deque keydownPattern{ true, false }; - for (auto isKeyDown : keydownPattern) - { - Log::Comment(NoThrowString().Format(L"testing char: %C; keydown: %d", wch, isKeyDown)); - - VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); - std::unique_ptr keyEvent; - keyEvent.reset(static_cast(events.front().release())); - events.pop_front(); - - const short keyScanError = -1; - const 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); - } - } + VERIFY_ARE_EQUAL(expectedEvents[i], events[i]); } } @@ -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/CodepointWidthDetectorTests.cpp b/src/host/ut_host/CodepointWidthDetectorTests.cpp index fdbc775e13..9f433eb7c3 100644 --- a/src/host/ut_host/CodepointWidthDetectorTests.cpp +++ b/src/host/ut_host/CodepointWidthDetectorTests.cpp @@ -19,7 +19,7 @@ static const std::vector> { 0x7, L"\a", CodepointWidth::Narrow }, // BEL { 0x20, L" ", CodepointWidth::Narrow }, { 0x39, L"9", CodepointWidth::Narrow }, - { 0x414, L"\x414", CodepointWidth::Ambiguous }, // U+0414 cyrillic capital de + { 0x414, L"\x414", CodepointWidth::Narrow }, // U+0414 cyrillic capital de { 0x1104, L"\x1104", CodepointWidth::Wide }, // U+1104 hangul choseong ssangtikeut { 0x306A, L"\x306A", CodepointWidth::Wide }, // U+306A hiragana na { 0x30CA, L"\x30CA", CodepointWidth::Wide }, // U+30CA katakana na @@ -38,18 +38,6 @@ class CodepointWidthDetectorTests VERIFY_IS_TRUE(widthDetector.IsWide(emoji)); } - TEST_METHOD(CanExtractCodepoint) - { - CodepointWidthDetector widthDetector; - for (const auto& data : testData) - { - const auto& expected = std::get<0>(data); - const auto& wstr = std::get<1>(data); - const auto result = widthDetector._extractCodepoint({ wstr.c_str(), wstr.size() }); - VERIFY_ARE_EQUAL(result, expected); - } - } - TEST_METHOD(CanGetWidths) { CodepointWidthDetector widthDetector; @@ -91,8 +79,8 @@ class CodepointWidthDetectorTests // Cached item should match what we expect const auto it = widthDetector._fallbackCache.begin(); - VERIFY_ARE_EQUAL(ambiguous, it->first); - VERIFY_ARE_EQUAL(FallbackMethod(ambiguous), it->second); + VERIFY_ARE_EQUAL(ambiguous[0], it->first); + VERIFY_ARE_EQUAL(FallbackMethod(ambiguous) ? 2u : 1u, it->second); // Cache should empty when font changes. widthDetector.NotifyFontChanged(); diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp deleted file mode 100644 index 511ee2db82..0000000000 --- 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 = 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(), 0u); - } - - TEST_METHOD(CanFillPromptWithPreviousCommandFragment) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - SetPrompt(cookedReadData, L"short and stout"); - - auto& commandLine = CommandLine::Instance(); - commandLine._fillPromptWithPreviousCommandFragment(cookedReadData); - VerifyPromptText(cookedReadData, L"short and stoutapot"); - } - - TEST_METHOD(CanCycleMatchingCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"short and stout", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"inflammable", false)); - 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 d47f3bbfa3..0000000000 --- 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, gsl::narrow(popup._currentCommand)); - } - - TEST_METHOD(HomeMovesSelectionToStart) - { - // function to simulate user pressing home key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static 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 8c4460ec8a..0000000000 --- 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 (unsigned int historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) - { - Popup::UserInputFunction fn = [historyIndex](COOKED_READ_DATA& /*cookedReadData*/, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) { - static 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 ccc54172a1..0000000000 --- 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 320e0b7841..0000000000 --- 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/DbcsTests.cpp b/src/host/ut_host/DbcsTests.cpp index 012f5a9778..3395c4b00d 100644 --- a/src/host/ut_host/DbcsTests.cpp +++ b/src/host/ut_host/DbcsTests.cpp @@ -58,7 +58,7 @@ class DbcsTests wchDouble++; } - const gsl::span buffer(rgci, ARRAYSIZE(rgci)); + const std::span buffer(rgci, ARRAYSIZE(rgci)); // feed it into UnicodeRasterFontCellMungeOnRead to confirm that it is working properly. // do it in-place to confirm that it can operate properly in the common case. diff --git a/src/host/ut_host/HistoryTests.cpp b/src/host/ut_host/HistoryTests.cpp index 4861fb5dd6..dbbe6f1c82 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 @@ private: }; 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/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj index 1a5807f09d..f995dea0a4 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -15,12 +15,7 @@ - - - - - @@ -34,14 +29,11 @@ - - - Create @@ -99,7 +91,6 @@ - diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters index 854d3ee87b..4366c712fb 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj.filters +++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters @@ -54,9 +54,6 @@ Source Files - - Source Files - Source Files @@ -66,15 +63,9 @@ Source Files - - Source Files - Source Files - - Source Files - Source Files @@ -84,27 +75,12 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files Source Files - - Source Files - Source Files @@ -125,9 +101,6 @@ Header Files - - Header Files - diff --git a/src/host/ut_host/InputBufferTests.cpp b/src/host/ut_host/InputBufferTests.cpp index 9fbdb662ed..f81d304bda 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_SUCCESS_NTSTATUS(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,19 +277,19 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - false, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); for (size_t i = 0; i < outEvents.size(); ++i) { - VERIFY_ARE_EQUAL(outEvents[i]->EventType(), InputEventType::KeyEvent); + VERIFY_ARE_EQUAL(outEvents[i].EventType, KEY_EVENT); } } @@ -306,30 +297,30 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - false, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } @@ -339,115 +330,117 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - true, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + true, + false, + false, + false)); VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } TEST_METHOD(EmptyingBufferDuringReadSetsResetWaitEvent) { - Log::Comment(L"ResetWaitEvent should be true if a read to the buffer completely empties it"); + Log::Comment(L"hInputEvent should be reset if a read to the buffer completely empties it"); InputBuffer inputBuffer; // add some events so that we have something to stick in front of INPUT_RECORD records[RECORD_INSERT_COUNT]; - std::deque> inEvents; + 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); - // read one record, make sure ResetWaitEvent isn't set - std::deque> outEvents; - size_t eventsRead = 0; - auto resetWaitEvent = false; - inputBuffer._ReadBuffer(outEvents, - 1, - eventsRead, - false, - resetWaitEvent, - true, - false); - VERIFY_ARE_EQUAL(eventsRead, 1u); - VERIFY_IS_FALSE(!!resetWaitEvent); + auto& waitEvent = ServiceLocator::LocateGlobals().hInputEvent; + waitEvent.SetEvent(); - // read the rest, resetWaitEvent should be set to true + // read one record, hInputEvent should still be signaled + InputEventQueue outEvents; + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + 1, + false, + false, + true, + false)); + VERIFY_ARE_EQUAL(outEvents.size(), 1u); + VERIFY_IS_TRUE(waitEvent.is_signaled()); + + // read the rest, hInputEvent should be reset + waitEvent.SetEvent(); outEvents.clear(); - inputBuffer._ReadBuffer(outEvents, - RECORD_INSERT_COUNT - 1, - eventsRead, - false, - resetWaitEvent, - true, - false); - VERIFY_ARE_EQUAL(eventsRead, RECORD_INSERT_COUNT - 1); - VERIFY_IS_TRUE(!!resetWaitEvent); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + RECORD_INSERT_COUNT - 1, + false, + false, + true, + false)); + VERIFY_ARE_EQUAL(outEvents.size(), RECORD_INSERT_COUNT - 1); + VERIFY_IS_FALSE(waitEvent.is_signaled()); } TEST_METHOD(ReadingDbcsCharsPadsOutputArray) { Log::Comment(L"During a non-unicode read, the input buffer should count twice for each dbcs key event"); + auto& codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP; + const auto restoreCP = wil::scope_exit([&, previous = codepage]() { + codepage = previous; + }); + + codepage = CP_JAPANESE; + // write a mouse event, key event, dbcs key event, mouse event InputBuffer inputBuffer; - const unsigned int recordInsertCount = 4; - INPUT_RECORD inRecords[recordInsertCount]; + + std::array inRecords{}; inRecords[0].EventType = MOUSE_EVENT; inRecords[1] = MakeKeyEvent(TRUE, 1, L'A', 0, L'A', 0); inRecords[2] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0x3042, 0); // U+3042 hiragana A inRecords[3].EventType = MOUSE_EVENT; - std::deque> inEvents; - for (size_t i = 0; i < recordInsertCount; ++i) - { - inEvents.push_back(IInputEvent::Create(inRecords[i])); - } + std::array outRecordsExpected{}; + outRecordsExpected[0].EventType = MOUSE_EVENT; + outRecordsExpected[1] = MakeKeyEvent(TRUE, 1, L'A', 0, L'A', 0); + outRecordsExpected[2] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0x82, 0); + outRecordsExpected[3] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0xa0, 0); + outRecordsExpected[4].EventType = MOUSE_EVENT; 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; - size_t eventsRead = 0; - auto resetWaitEvent = false; - inputBuffer._ReadBuffer(outEvents, - recordInsertCount, - eventsRead, - false, - resetWaitEvent, - false, - false); - // the dbcs record should have counted for two elements in - // the array, making it so that we get less events read - VERIFY_ARE_EQUAL(eventsRead, recordInsertCount - 1); - VERIFY_ARE_EQUAL(eventsRead, outEvents.size()); - for (size_t i = 0; i < eventsRead; ++i) + InputEventQueue outEvents; + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + outRecordsExpected.size(), + false, + false, + false, + false)); + VERIFY_ARE_EQUAL(outEvents.size(), outRecordsExpected.size()); + for (size_t i = 0; i < outEvents.size(); ++i) { - VERIFY_ARE_EQUAL(outEvents[i]->ToInputRecord(), inRecords[i]); + VERIFY_ARE_EQUAL(outEvents[i], outRecordsExpected[i]); } } @@ -457,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); @@ -471,40 +464,40 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - false, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(prependRecords[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(prependRecords[i], outEvents[i]); } outEvents.clear(); // verify the rest of the records - VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - false, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) { - VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + VERIFY_ARE_EQUAL(records[i], outEvents[i]); } } @@ -516,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(); @@ -536,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)); @@ -544,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); @@ -561,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)); @@ -570,39 +563,40 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - amountToRead, - true, - false, - false, - false)); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + amountToRead, + true, + false, + false, + false)); } TEST_METHOD(WritingToEmptyBufferSignalsWaitEvent) { 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(); storage.push_back(std::move(inputEvent2)); inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent); @@ -614,19 +608,19 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - 1, - false, - false, - true, - true)); + VERIFY_ARE_EQUAL(inputBuffer.Write(record), 1u); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + 1, + false, + false, + true, + true)); VERIFY_ARE_EQUAL(outEvents.size(), 1u); VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); - VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount - 1); - VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.front().Event.KeyEvent.wRepeatCount, repeatCount - 1); + VERIFY_ARE_EQUAL(outEvents.front().Event.KeyEvent.wRepeatCount, 1u); } TEST_METHOD(StreamPeekingDeCoalesces) @@ -634,18 +628,18 @@ 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_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, - 1, - true, - false, - true, - true)); + VERIFY_ARE_EQUAL(inputBuffer.Write(record), 1u); + VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents, + 1, + true, + false, + true, + true)); VERIFY_ARE_EQUAL(outEvents.size(), 1u); VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); - VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount); - VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.front().Event.KeyEvent.wRepeatCount, repeatCount); + VERIFY_ARE_EQUAL(outEvents.front().Event.KeyEvent.wRepeatCount, 1u); } }; diff --git a/src/host/ut_host/OutputCellIteratorTests.cpp b/src/host/ut_host/OutputCellIteratorTests.cpp index 88e583b743..e724ae07a8 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 { @@ -29,12 +29,12 @@ class OutputCellIteratorTests OutputCellIterator it(wch, limit); OutputCellView expectedLead({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Leading), + DbcsAttribute::Leading, InvalidTextAttribute, TextAttributeBehavior::Current); OutputCellView expectedTrail({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Trailing), + DbcsAttribute::Trailing, InvalidTextAttribute, TextAttributeBehavior::Current); @@ -284,7 +284,7 @@ class OutputCellIteratorTests for (const auto& wch : testText) { auto expected = OutputCellView({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Leading), + DbcsAttribute::Leading, InvalidTextAttribute, TextAttributeBehavior::Current); @@ -293,7 +293,7 @@ class OutputCellIteratorTests it++; expected = OutputCellView({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Trailing), + DbcsAttribute::Trailing, InvalidTextAttribute, TextAttributeBehavior::Current); @@ -341,7 +341,7 @@ class OutputCellIteratorTests for (const auto& wch : testText) { auto expected = OutputCellView({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Leading), + DbcsAttribute::Leading, color, TextAttributeBehavior::Stored); @@ -350,7 +350,7 @@ class OutputCellIteratorTests it++; expected = OutputCellView({ &wch, 1 }, - DbcsAttribute(DbcsAttribute::Attribute::Trailing), + DbcsAttribute::Trailing, color, TextAttributeBehavior::Stored); @@ -367,7 +367,7 @@ class OutputCellIteratorTests SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const std::vector colors{ FOREGROUND_GREEN, FOREGROUND_RED | BACKGROUND_BLUE, FOREGROUND_BLUE | FOREGROUND_INTENSITY, BACKGROUND_GREEN }; - const gsl::span view{ colors.data(), colors.size() }; + const std::span view{ colors.data(), colors.size() }; OutputCellIterator it(view); @@ -401,7 +401,7 @@ class OutputCellIteratorTests charInfos.push_back(ci); } - const gsl::span view{ charInfos.data(), charInfos.size() }; + const std::span view{ charInfos.data(), charInfos.size() }; OutputCellIterator it(view); @@ -433,7 +433,7 @@ class OutputCellIteratorTests cells.push_back(cell); } - const gsl::span view{ cells.data(), cells.size() }; + const std::span view{ cells.data(), cells.size() }; OutputCellIterator it(view); @@ -496,7 +496,7 @@ class OutputCellIteratorTests const auto value = *it; it++; - if (value.DbcsAttr().IsLeading() || value.DbcsAttr().IsTrailing()) + if (value.DbcsAttr() == DbcsAttribute::Leading || value.DbcsAttr() == DbcsAttribute::Trailing) { VERIFY_IS_TRUE(it); it++; diff --git a/src/host/ut_host/PopupTestHelper.hpp b/src/host/ut_host/PopupTestHelper.hpp deleted file mode 100644 index 146949781e..0000000000 --- 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(), 4u); - } - - 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(), 28u); - } -}; diff --git a/src/host/ut_host/ReadWaitTests.cpp b/src/host/ut_host/ReadWaitTests.cpp deleted file mode 100644 index 44c76579e1..0000000000 --- a/src/host/ut_host/ReadWaitTests.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "misc.h" -#include "dbcs.h" -#include "../../types/inc/IInputEvent.hpp" - -#include "../interactivity/inc/ServiceLocator.hpp" - -#include -#include - -using namespace WEX::Logging; -using Microsoft::Console::Interactivity::ServiceLocator; - -class InputRecordConversionTests -{ - TEST_CLASS(InputRecordConversionTests); - - static const size_t INPUT_RECORD_COUNT = 10; - UINT savedCodepage = 0; - - TEST_CLASS_SETUP(ClassSetup) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - savedCodepage = gci.CP; - gci.CP = CP_JAPANESE; - VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo)); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.CP = savedCodepage; - VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo)); - return true; - } - - TEST_METHOD(SplitToOemLeavesNonKeyEventsAlone) - { - Log::Comment(L"nothing should happen to input events that aren't key events"); - - std::deque> inEvents; - INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 }; - for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) - { - inRecords[i].EventType = MOUSE_EVENT; - inRecords[i].Event.MouseEvent.dwMousePosition.X = static_cast(i); - inRecords[i].Event.MouseEvent.dwMousePosition.Y = static_cast(i * 2); - inEvents.push_back(IInputEvent::Create(inRecords[i])); - } - - SplitToOem(inEvents); - VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size()); - - for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) - { - VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord()); - } - } - - TEST_METHOD(SplitToOemLeavesNonDbcsCharsAlone) - { - Log::Comment(L"non-dbcs chars shouldn't be split"); - - std::deque> inEvents; - INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 }; - for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) - { - inRecords[i].EventType = KEY_EVENT; - inRecords[i].Event.KeyEvent.uChar.UnicodeChar = static_cast(L'a' + i); - inEvents.push_back(IInputEvent::Create(inRecords[i])); - } - - SplitToOem(inEvents); - VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size()); - - for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) - { - VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord()); - } - } - - TEST_METHOD(SplitToOemSplitsDbcsChars) - { - Log::Comment(L"dbcs chars should be split"); - - const auto codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP; - - INPUT_RECORD inRecords[INPUT_RECORD_COUNT * 2] = { 0 }; - std::deque> inEvents; - // U+3042 hiragana letter A - wchar_t hiraganaA = 0x3042; - wchar_t inChars[INPUT_RECORD_COUNT]; - for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) - { - auto currentChar = static_cast(hiraganaA + (i * 2)); - inRecords[i].EventType = KEY_EVENT; - inRecords[i].Event.KeyEvent.uChar.UnicodeChar = currentChar; - inChars[i] = currentChar; - inEvents.push_back(IInputEvent::Create(inRecords[i])); - } - - SplitToOem(inEvents); - VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT * 2, inEvents.size()); - - // create the data to compare the output to - char dbcsChars[INPUT_RECORD_COUNT * 2] = { 0 }; - auto writtenBytes = WideCharToMultiByte(codepage, - 0, - inChars, - INPUT_RECORD_COUNT, - dbcsChars, - INPUT_RECORD_COUNT * 2, - nullptr, - FALSE); - VERIFY_ARE_EQUAL(writtenBytes, static_cast(INPUT_RECORD_COUNT * 2)); - for (size_t i = 0; i < INPUT_RECORD_COUNT * 2; ++i) - { - const auto pKeyEvent = static_cast(inEvents[i].get()); - VERIFY_ARE_EQUAL(static_cast(pKeyEvent->GetCharData()), dbcsChars[i]); - } - } -}; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 186c3e01a5..b7ea13e6dc 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -12,6 +12,7 @@ #include "input.h" #include "getset.h" #include "_stream.h" // For WriteCharsLegacy +#include "output.h" // For ScrollRegion #include "../interactivity/inc/ServiceLocator.hpp" #include "../../inc/conattrs.hpp" @@ -117,11 +118,12 @@ class ScreenBufferTests TEST_METHOD(EraseAllTests); - TEST_METHOD(OutputNULTest); + TEST_METHOD(InactiveControlCharactersTest); TEST_METHOD(VtResize); TEST_METHOD(VtResizeComprehensive); TEST_METHOD(VtResizeDECCOLM); + TEST_METHOD(VtResizePreservingAttributes); TEST_METHOD(VtSoftResetCursorPosition); @@ -181,8 +183,11 @@ class ScreenBufferTests TEST_METHOD(DontResetColorsAboveVirtualBottom); TEST_METHOD(ScrollOperations); + TEST_METHOD(InsertReplaceMode); TEST_METHOD(InsertChars); TEST_METHOD(DeleteChars); + TEST_METHOD(HorizontalScrollOperations); + TEST_METHOD(ScrollingWideCharsHorizontally); TEST_METHOD(EraseScrollbackTests); TEST_METHOD(EraseTests); @@ -198,6 +203,7 @@ class ScreenBufferTests TEST_METHOD(ScrollLines256Colors); + TEST_METHOD(SetLineFeedMode); TEST_METHOD(SetScreenMode); TEST_METHOD(SetOriginMode); TEST_METHOD(SetAutoWrapMode); @@ -217,6 +223,10 @@ class ScreenBufferTests TEST_METHOD(CursorUpDownOutsideMargins); TEST_METHOD(CursorUpDownExactlyAtMargins); + TEST_METHOD(CursorLeftRightAcrossMargins); + TEST_METHOD(CursorLeftRightOutsideMargins); + TEST_METHOD(CursorLeftRightExactlyAtMargins); + TEST_METHOD(CursorNextPreviousLine); TEST_METHOD(CursorPositionRelative); @@ -249,6 +259,11 @@ class ScreenBufferTests TEST_METHOD(TestDeferredMainBufferResize); TEST_METHOD(RectangularAreaOperations); + TEST_METHOD(CopyDoubleWidthRectangularArea); + + TEST_METHOD(DelayedWrapReset); + + TEST_METHOD(EraseColorMode); }; void ScreenBufferTests::SingleAlternateBufferCreationTest() @@ -262,8 +277,8 @@ void ScreenBufferTests::SingleAlternateBufferCreationTest() VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer); VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); - auto Status = psiOriginal->UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + auto Status = psiOriginal->UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"First alternate buffer successfully created"); const auto psiFirstAlternate = &gci.GetActiveOutputBuffer(); @@ -295,8 +310,8 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest() L"main buffer."); const auto psiOriginal = &gci.GetActiveOutputBuffer(); - auto Status = psiOriginal->UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + auto Status = psiOriginal->UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"First alternate buffer successfully created"); const auto psiFirstAlternate = &gci.GetActiveOutputBuffer(); @@ -306,8 +321,8 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest() VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); - Status = psiFirstAlternate->UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + Status = psiFirstAlternate->UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"Second alternate buffer successfully created"); auto psiSecondAlternate = &gci.GetActiveOutputBuffer(); @@ -340,8 +355,8 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() L"Testing creating one alternate buffer, then creating another" L" alternate from the main, before returning to the main buffer."); const auto psiOriginal = &gci.GetActiveOutputBuffer(); - auto Status = psiOriginal->UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + auto Status = psiOriginal->UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"First alternate buffer successfully created"); const auto psiFirstAlternate = &gci.GetActiveOutputBuffer(); @@ -351,8 +366,8 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); - Status = psiOriginal->UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + Status = psiOriginal->UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"Second alternate buffer successfully created"); const auto psiSecondAlternate = &gci.GetActiveOutputBuffer(); @@ -396,7 +411,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest() mainCursor.SetBlinkingAllowed(mainCursorBlinking); Log::Comment(L"Switch to the alternate buffer."); - VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({})); auto& altBuffer = gci.GetActiveOutputBuffer(); auto& altCursor = altBuffer.GetTextBuffer().GetCursor(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); @@ -451,14 +466,14 @@ void ScreenBufferTests::TestReverseLineFeed() Log::Comment(L"Case 1: RI from below top of viewport"); stateMachine.ProcessString(L"foo\nfoo"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 1); VERIFY_ARE_EQUAL(viewport.Top(), 0); stateMachine.ProcessString(reverseLineFeed); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 0); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 0); Log::Comment(NoThrowString().Format( @@ -472,14 +487,14 @@ void ScreenBufferTests::TestReverseLineFeed() Log::Comment(L"Case 2: RI from top of viewport"); cursor.SetPosition({ 0, 0 }); stateMachine.ProcessString(L"123456789"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 0); VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 0); stateMachine.ProcessString(reverseLineFeed); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 0); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 0); Log::Comment(NoThrowString().Format( @@ -489,7 +504,7 @@ void ScreenBufferTests::TestReverseLineFeed() viewport.RightInclusive(), viewport.BottomInclusive())); auto c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); - VERIFY_ARE_EQUAL(c.Y, 2); // This is the coordinates of the second "foo" from before. + VERIFY_ARE_EQUAL(c.y, 2); // This is the coordinates of the second "foo" from before. //////////////////////////////////////////////////////////////////////// Log::Comment(L"Case 3: RI from top of viewport, when viewport is below top of buffer"); @@ -497,14 +512,14 @@ void ScreenBufferTests::TestReverseLineFeed() cursor.SetPosition({ 0, 5 }); VERIFY_SUCCEEDED(screenInfo.SetViewportOrigin(true, { 0, 5 }, true)); stateMachine.ProcessString(L"ABCDEFGH"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 8); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 5); VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 5); stateMachine.ProcessString(reverseLineFeed); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 8); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 8); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 5); viewport = screenInfo.GetViewport(); VERIFY_ARE_EQUAL(viewport.Top(), 5); Log::Comment(NoThrowString().Format( @@ -514,7 +529,7 @@ void ScreenBufferTests::TestReverseLineFeed() viewport.RightInclusive(), viewport.BottomInclusive())); c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); - VERIFY_ARE_EQUAL(c.Y, 6); + VERIFY_ARE_EQUAL(c.y, 6); } void _SetTabStops(SCREEN_INFORMATION& screenInfo, std::list columns, bool replace) @@ -548,7 +563,7 @@ std::list _GetTabStops(SCREEN_INFORMATION& screenInfo) for (;;) { stateMachine.ProcessCharacter(L'\t'); - auto column = cursor.GetPosition().X; + auto column = cursor.GetPosition().x; if (column >= lastColumn) { break; @@ -766,7 +781,7 @@ void ScreenBufferTests::TestGetForwardTab() cursor.SetXPosition(0); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = inputData.front(); + coordCursorExpected.x = inputData.front(); stateMachine.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -780,7 +795,7 @@ void ScreenBufferTests::TestGetForwardTab() cursor.SetXPosition(6); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = *std::next(inputData.begin(), 3); + coordCursorExpected.x = *std::next(inputData.begin(), 3); stateMachine.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -794,7 +809,7 @@ void ScreenBufferTests::TestGetForwardTab() cursor.SetXPosition(30); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = coordScreenBufferSize.X - 1; + coordCursorExpected.x = coordScreenBufferSize.width - 1; stateMachine.ProcessString(nextForwardTab); const auto coordCursorResult = cursor.GetPosition(); @@ -805,7 +820,7 @@ void ScreenBufferTests::TestGetForwardTab() Log::Comment(L"Find next tab from rightmost column."); { - cursor.SetXPosition(coordScreenBufferSize.X - 1); + cursor.SetXPosition(coordScreenBufferSize.width - 1); auto coordCursorExpected = cursor.GetPosition(); @@ -834,7 +849,7 @@ void ScreenBufferTests::TestGetReverseTab() cursor.SetXPosition(1); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = 0; + coordCursorExpected.x = 0; stateMachine.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); @@ -848,7 +863,7 @@ void ScreenBufferTests::TestGetReverseTab() cursor.SetXPosition(6); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = *std::next(inputData.begin()); + coordCursorExpected.x = *std::next(inputData.begin()); stateMachine.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); @@ -862,7 +877,7 @@ void ScreenBufferTests::TestGetReverseTab() cursor.SetXPosition(30); auto coordCursorExpected = cursor.GetPosition(); - coordCursorExpected.X = inputData.back(); + coordCursorExpected.x = inputData.back(); stateMachine.ProcessString(nextReverseTab); const auto coordCursorResult = cursor.GetPosition(); @@ -888,7 +903,7 @@ void ScreenBufferTests::TestAltBufferTabStops() _SetTabStops(mainBuffer, expectedStops, true); VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(mainBuffer)); - VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({})); auto& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); @@ -981,8 +996,8 @@ void ScreenBufferTests::EraseAllTests() VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 220 }, true)); stateMachine.ProcessString(L"bar\nbar\nbar"); viewport = si._viewport; - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 277); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 277); originalRelativePosition = cursor.GetPosition(); viewport.ConvertToOrigin(&originalRelativePosition); @@ -1008,43 +1023,52 @@ void ScreenBufferTests::EraseAllTests() viewport.BottomInclusive())); } -void ScreenBufferTests::OutputNULTest() +void ScreenBufferTests::InactiveControlCharactersTest() { + // These are the control characters that don't write anything to the + // output buffer, and are expected not to move the cursor position. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:ordinal", L"{0, 1, 2, 3, 4, 5, 6, 7, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31}") + END_TEST_METHOD_PROPERTIES() + + unsigned ordinal; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"ordinal", ordinal)); + const auto ch = static_cast(ordinal); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); auto& stateMachine = si.GetStateMachine(); auto& cursor = si._textBuffer->GetCursor(); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing a single NUL")); - stateMachine.ProcessString({ L"\0", 1 }); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + Log::Comment(L"Writing a single control character"); + const auto singleChar = std::wstring(1, ch); + stateMachine.ProcessString(singleChar); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing many NULs")); - stateMachine.ProcessString({ L"\0\0\0\0\0\0\0\0", 8 }); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + Log::Comment(L"Writing many control characters"); + const auto manyChars = std::wstring(8, ch); + stateMachine.ProcessString(manyChars); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Testing a single NUL followed by real text")); - stateMachine.ProcessString({ L"\0foo", 4 }); - VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + Log::Comment(L"Testing a single control character followed by real text"); + stateMachine.ProcessString(singleChar + L"foo"); + VERIFY_ARE_EQUAL(3, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); stateMachine.ProcessString(L"\n"); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); - Log::Comment(NoThrowString().Format( - L"Writing NULs in between other strings")); - stateMachine.ProcessString({ L"\0foo\0bar\0", 9 }); - VERIFY_ARE_EQUAL(6, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + Log::Comment(L"Writing controls in between other strings"); + stateMachine.ProcessString(singleChar + L"foo" + singleChar + L"bar" + singleChar); + VERIFY_ARE_EQUAL(6, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); } void ScreenBufferTests::VtResize() @@ -1223,6 +1247,28 @@ void ScreenBufferTests::VtResizeComprehensive() VERIFY_ARE_EQUAL(expectedViewHeight, newViewHeight); } +til::rect _GetRelativeScrollMargins() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto viewport = si.GetViewport(); + auto& cursor = si.GetTextBuffer().GetCursor(); + const auto savePos = cursor.GetPosition(); + + // We can't access the AdaptDispatch internals where the margins are stored, + // but we calculate their boundaries by using VT sequences to move down and + // up as far as possible, and read the cursor positions at the two limits. + stateMachine.ProcessString(L"\033[H\033[9999B"); + const auto bottom = cursor.GetPosition().y - viewport.Top(); + stateMachine.ProcessString(L"\033[9999A"); + const auto top = cursor.GetPosition().y - viewport.Top(); + + cursor.SetPosition(savePos); + const auto noMargins = (top == 0 && bottom == viewport.Height() - 1); + return noMargins ? til::rect{} : til::rect{ 0, top, 0, bottom }; +} + void ScreenBufferTests::VtResizeDECCOLM() { // Run this test in isolation - for one reason or another, this breaks other tests. @@ -1246,13 +1292,13 @@ void ScreenBufferTests::VtResizeDECCOLM() return si.GetTextBuffer().GetCursor().GetPosition() - si.GetViewport().Origin(); }; auto areMarginsSet = [&]() { - const auto margins = si.GetRelativeScrollMargins(); - return margins.BottomInclusive() > margins.Top(); + const auto margins = _GetRelativeScrollMargins(); + return margins.bottom > margins.top; }; stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); - auto initialMargins = si.GetRelativeScrollMargins(); + auto initialMargins = _GetRelativeScrollMargins(); auto initialCursorPosition = getRelativeCursorPosition(); auto initialSbHeight = si.GetBufferSize().Height(); @@ -1269,7 +1315,7 @@ void ScreenBufferTests::VtResizeDECCOLM() auto newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(areMarginsSet()); - VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); + VERIFY_ARE_EQUAL(initialMargins, _GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); @@ -1305,7 +1351,7 @@ void ScreenBufferTests::VtResizeDECCOLM() stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); - initialMargins = si.GetRelativeScrollMargins(); + initialMargins = _GetRelativeScrollMargins(); initialCursorPosition = getRelativeCursorPosition(); initialSbHeight = newSbHeight; @@ -1323,7 +1369,7 @@ void ScreenBufferTests::VtResizeDECCOLM() newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(areMarginsSet()); - VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); + VERIFY_ARE_EQUAL(initialMargins, _GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); @@ -1358,6 +1404,52 @@ void ScreenBufferTests::VtResizeDECCOLM() VERIFY_ARE_EQUAL(80, newViewWidth); } +void ScreenBufferTests::VtResizePreservingAttributes() +{ + // Run this test in isolation - for one reason or another, this breaks other tests. + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + TEST_METHOD_PROPERTY(L"Data:resizeType", L"{0, 1}") + END_TEST_METHOD_PROPERTIES() + + unsigned resizeType; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"resizeType", resizeType)); + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + 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), RGB(188, 20, 24) }; + testAttr.SetCrossedOut(true); + testAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + testAttr.SetItalic(true); + si.GetTextBuffer().SetCurrentAttributes(testAttr); + + switch (resizeType) + { + case 0: + Log::Comment(L"Resize the screen with CSI 8 t"); + stateMachine.ProcessString(L"\x1b[8;24;132t"); // Set width to 132 + VERIFY_ARE_EQUAL(132, si.GetTextBuffer().GetSize().Width()); + stateMachine.ProcessString(L"\x1b[8;24;80t"); // Set width to 80 + VERIFY_ARE_EQUAL(80, si.GetTextBuffer().GetSize().Width()); + break; + case 1: + Log::Comment(L"Resize the screen with DECCOLM"); + stateMachine.ProcessString(L"\x1b[?40h"); // Allow DECCOLM + stateMachine.ProcessString(L"\x1b[?3h"); // Set width to 132 + VERIFY_ARE_EQUAL(132, si.GetTextBuffer().GetSize().Width()); + stateMachine.ProcessString(L"\x1b[?3l"); // Set width to 80 + VERIFY_ARE_EQUAL(80, si.GetTextBuffer().GetSize().Width()); + break; + } + + Log::Comment(L"Attributes should be preserved after resize"); + VERIFY_ARE_EQUAL(testAttr, si.GetTextBuffer().GetCurrentAttributes()); +} + void ScreenBufferTests::VtSoftResetCursorPosition() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -1464,8 +1556,7 @@ void ScreenBufferTests::VtScrollMarginsNewlineColor() { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -1509,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; @@ -1525,15 +1616,14 @@ void ScreenBufferTests::VtNewlinePastViewport() L"viewport=%s", VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -1542,8 +1632,7 @@ void ScreenBufferTests::VtNewlinePastViewport() } const auto& row = tbi.GetRowByOffset(viewport.BottomInclusive()); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -1586,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; @@ -1602,15 +1691,14 @@ void ScreenBufferTests::VtNewlinePastEndOfBuffer() L"viewport=%s", VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -1619,8 +1707,7 @@ void ScreenBufferTests::VtNewlinePastEndOfBuffer() } const auto& row = tbi.GetRowByOffset(viewport.BottomInclusive()); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -1648,6 +1735,7 @@ void ScreenBufferTests::VtNewlineOutsideMargins() Log::Comment(L"Reset viewport and apply DECSTBM margins"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, true)); + si.UpdateBottom(); stateMachine.ProcessString(L"\x1b[1;5r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); @@ -1981,7 +2069,7 @@ void ScreenBufferTests::ResizeTraditionalDoesNotDoubleFreeAttrRows() gci.SetWrapText(false); auto newBufferSize = si.GetBufferSize().Dimensions(); - newBufferSize.Y--; + newBufferSize.height--; VERIFY_SUCCEEDED(si.ResizeTraditional(newBufferSize)); } @@ -2016,8 +2104,8 @@ void ScreenBufferTests::ResizeCursorUnchanged() gci.SetWrapText(useResizeWithReflow); auto newBufferSize = si.GetBufferSize().Dimensions(); - newBufferSize.X += dx; - newBufferSize.Y += dy; + newBufferSize.width += dx; + newBufferSize.height += dy; VERIFY_SUCCEEDED(si.ResizeScreenBuffer(newBufferSize, false)); @@ -2053,8 +2141,8 @@ void ScreenBufferTests::ResizeAltBuffer() const auto psiAlt = si._psiAlternateBuffer; auto newSize = originalMainSize.Dimensions(); - newSize.X += 2; - newSize.Y += 2; + newSize.width += 2; + newSize.height += 2; Log::Comment(NoThrowString().Format( L"MSFT:15917333 This call shouldn't stack overflow")); @@ -2107,8 +2195,8 @@ void ScreenBufferTests::ResizeAltBufferGetScreenBufferInfo() auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); auto newBufferSize = originalMainSize.Dimensions(); - newBufferSize.X += dx; - newBufferSize.Y += dy; + newBufferSize.width += dx; + newBufferSize.height += dy; const auto originalAltSize = Viewport(altBuffer._viewport); @@ -2219,7 +2307,7 @@ void ScreenBufferTests::GetWordBoundary() // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); @@ -2295,7 +2383,7 @@ void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on) // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); @@ -2363,8 +2451,8 @@ void ScreenBufferTests::TestAltBufferCursorState() VERIFY_IS_NULL(original._psiAlternateBuffer); VERIFY_IS_NULL(original._psiMainBuffer); - auto Status = original.UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + auto Status = original.UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"Alternate buffer successfully created"); auto& alternate = gci.GetActiveOutputBuffer(); @@ -2408,8 +2496,8 @@ void ScreenBufferTests::TestAltBufferVtDispatching() VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer); VERIFY_IS_NULL(mainBuffer._psiMainBuffer); - auto Status = mainBuffer.UseAlternateScreenBuffer(); - if (VERIFY_IS_TRUE(NT_SUCCESS(Status))) + auto Status = mainBuffer.UseAlternateScreenBuffer({}); + if (VERIFY_NT_SUCCESS(Status)) { Log::Comment(L"Alternate buffer successfully created"); auto& alternate = gci.GetActiveOutputBuffer(); @@ -2476,10 +2564,9 @@ void ScreenBufferTests::TestAltBufferVtDispatching() // Recall we didn't print an 'X' to the main buffer, so there's no // char to inspect the attributes of. - const auto& altRow = alternate.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); - const auto altAttrRow = &altRow.GetAttrRow(); - const std::vector altAttrs{ altAttrRow->begin(), altAttrRow->end() }; - const auto altAttrA = altAttrs[altCursor.GetPosition().X - 1]; + const auto& altRow = alternate.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().y); + const std::vector altAttrs{ altRow.AttrBegin(), altRow.AttrEnd() }; + const auto altAttrA = altAttrs[altCursor.GetPosition().x - 1]; VERIFY_ARE_EQUAL(expectedRgb, altAttrA); } } @@ -2572,8 +2659,7 @@ void ScreenBufferTests::SetDefaultsIndividuallyBothDefault() VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; @@ -2663,8 +2749,7 @@ void ScreenBufferTests::SetDefaultsTogether() VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; @@ -2727,8 +2812,7 @@ void ScreenBufferTests::ReverseResetWithDefaultBackground() VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[2]; @@ -2790,8 +2874,7 @@ void ScreenBufferTests::BackspaceDefaultAttrs() VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; @@ -2812,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, 3, 4, 5, 6, 7}") END_TEST_METHOD_PROPERTIES(); bool writeSingly; VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false"); - DWORD writeCharsLegacyMode; - VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L""); - // Created for MSFT:19735050. // Kinda the same as above, but with WriteCharsLegacy instead. // The variable that really breaks this scenario @@ -2848,18 +2927,13 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() if (writeSingly) { - auto str = L"X"; - size_t seqCb = 2; - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); - str = L"\x08"; - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + 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_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + WriteCharsLegacy(si, L"XX\x08", false, nullptr); } TextAttribute expectedDefaults{}; @@ -2869,8 +2943,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; @@ -2922,8 +2995,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsInPrompt() stateMachine.ProcessString(L"\x1b[2J"); const auto viewport = si.GetViewport(); - const auto& row = tbi.GetRowByOffset(cursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); + const auto& row = tbi.GetRowByOffset(cursor.GetPosition().y); { SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); @@ -2931,7 +3003,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsInPrompt() L"Make sure the row contains what we're expecting before we start." L"It should entirely be filled with defaults")); - const std::vector initialAttrs{ attrRow->begin(), attrRow->end() }; + const std::vector initialAttrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x <= viewport.RightInclusive(); x++) { const auto& attr = initialAttrs[x]; @@ -2948,7 +3020,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsInPrompt() til::point expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; for (auto x = 0; x <= viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; @@ -2991,9 +3063,8 @@ void ScreenBufferTests::SetGlobalColorTable() til::point expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); { - const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, renderSettings.GetAttributeColors(attrA).second); @@ -3001,7 +3072,7 @@ void ScreenBufferTests::SetGlobalColorTable() Log::Comment(NoThrowString().Format(L"Create an alt buffer")); - VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({})); auto& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); @@ -3017,9 +3088,8 @@ void ScreenBufferTests::SetGlobalColorTable() stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); { - const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, renderSettings.GetAttributeColors(attrA).second); @@ -3032,9 +3102,8 @@ void ScreenBufferTests::SetGlobalColorTable() stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(til::point(2, 0), altCursor.GetPosition()); { - const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); @@ -3055,9 +3124,8 @@ void ScreenBufferTests::SetGlobalColorTable() stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(til::point(2, 0), mainCursor.GetPosition()); { - const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); @@ -3098,9 +3166,8 @@ void ScreenBufferTests::SetColorTableThreeDigits() til::point expectedCursor{ 1, 0 }; VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); { - const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, renderSettings.GetAttributeColors(attrA).second); @@ -3108,7 +3175,7 @@ void ScreenBufferTests::SetColorTableThreeDigits() Log::Comment(NoThrowString().Format(L"Create an alt buffer")); - VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({})); auto& altBuffer = gci.GetActiveOutputBuffer(); auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); @@ -3124,9 +3191,8 @@ void ScreenBufferTests::SetColorTableThreeDigits() stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); { - const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; LOG_ATTR(attrA); VERIFY_ARE_EQUAL(originalRed, renderSettings.GetAttributeColors(attrA).second); @@ -3142,9 +3208,8 @@ void ScreenBufferTests::SetColorTableThreeDigits() stateMachine.ProcessString(L"X"); VERIFY_ARE_EQUAL(til::point(2, 0), altCursor.GetPosition()); { - const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrB = attrs[1]; // TODO MSFT:20105972 - attrA and attrB should both be the same color now LOG_ATTR(attrB); @@ -3286,15 +3351,15 @@ void ScreenBufferTests::DeleteCharsNearEndOfLine() // If you are deleting N chars, // and there are N+X chars left in the row after the cursor, such that X v_w - 1 - c_x - d) && (v_w - 1 - c_x - d >= 0)` // where: // - `d`: num chars to delete // - `v_w`: viewport.Width() - // - `c_x`: cursor.X + // - `c_x`: cursor.x // // Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase) // start with the following buffer contents, and the cursor on the "D" @@ -3346,19 +3411,12 @@ void ScreenBufferTests::DeleteCharsNearEndOfLine() VERIFY_ARE_EQUAL(til::point(mainView.Width() - 1, 0), mainCursor.GetPosition()); - Log::Comment(NoThrowString().Format( - L"row_i=[%s]", - tbi.GetRowByOffset(0).GetText().c_str())); - - mainCursor.SetPosition({ mainView.Width() - static_cast(dx), 0 }); + mainCursor.SetPosition({ mainView.Width() - dx, 0 }); std::wstringstream ss; ss << L"\x1b[" << numCharsToDelete << L"P"; // Delete N chars stateMachine.ProcessString(ss.str()); - Log::Comment(NoThrowString().Format( - L"row_f=[%s]", - tbi.GetRowByOffset(0).GetText().c_str())); - VERIFY_ARE_EQUAL(til::point(mainView.Width() - static_cast(dx), 0), mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(til::point(mainView.Width() - dx, 0), mainCursor.GetPosition()); auto iter = tbi.GetCellDataAt({ 0, 0 }); auto expectedNumSpaces = std::min(dx, numCharsToDelete); for (auto x = 0; x < mainView.Width() - expectedNumSpaces; x++) @@ -3414,14 +3472,11 @@ void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleFirstCase() // Place the cursor on the 'D' mainCursor.SetPosition({ 3, 0 }); - Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); // Delete 3 chars - [D, E, F] std::wstringstream ss; ss << L"\x1b[" << 3 << L"P"; stateMachine.ProcessString(ss.str()); - Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); - // Cursor shouldn't have moved VERIFY_ARE_EQUAL(til::point(3, 0), mainCursor.GetPosition()); @@ -3475,15 +3530,11 @@ void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleSecondCase() // Place the cursor on the 'C' mainCursor.SetPosition({ 2, 0 }); - Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); - // Delete 4 chars - [C, D, E, F] std::wstringstream ss; ss << L"\x1b[" << 4 << L"P"; stateMachine.ProcessString(ss.str()); - Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); - VERIFY_ARE_EQUAL(til::point(2, 0), mainCursor.GetPosition()); auto iter = tbi.GetCellDataAt({ 0, 0 }); @@ -3518,7 +3569,7 @@ void ScreenBufferTests::DontResetColorsAboveVirtualBottom() auto& cursor = si.GetTextBuffer().GetCursor(); const auto& renderSettings = gci.GetRenderSettings(); - VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 1 }, true)); + VERIFY_NT_SUCCESS(si.SetViewportOrigin(true, { 0, 1 }, true)); cursor.SetPosition({ 0, si.GetViewport().BottomInclusive() }); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); @@ -3537,11 +3588,10 @@ void ScreenBufferTests::DontResetColorsAboveVirtualBottom() L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(2, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().x); { - const auto& row = tbi.GetRowByOffset(cursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = tbi.GetRowByOffset(cursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; LOG_ATTR(attrA); @@ -3552,14 +3602,14 @@ void ScreenBufferTests::DontResetColorsAboveVirtualBottom() } Log::Comment(NoThrowString().Format(L"Emulate scrolling up with the mouse")); - VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_NT_SUCCESS(si.SetViewportOrigin(true, { 0, 0 }, false)); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, si.GetViewport().BottomInclusive()); + VERIFY_IS_GREATER_THAN(cursor.GetPosition().y, si.GetViewport().BottomInclusive()); stateMachine.ProcessString(L"X"); @@ -3568,11 +3618,10 @@ void ScreenBufferTests::DontResetColorsAboveVirtualBottom() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(3, cursor.GetPosition().x); { - const auto& row = tbi.GetRowByOffset(cursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto& row = tbi.GetRowByOffset(cursor.GetPosition().y); + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; const auto attrC = attrs[1]; @@ -3592,8 +3641,8 @@ 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); - row.WriteCells({ fillContent, fillAttr }, position.X, false); + auto& row = si.GetTextBuffer().GetMutableRowByOffset(position.y); + row.WriteCells({ fillContent, fillAttr }, position.x, false); } template @@ -3748,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; @@ -3762,7 +3811,7 @@ void ScreenBufferTests::ScrollOperations() // Unless this is reverse index, which has to be be at the top of the viewport. if (scrollType == ReverseIndex) { - cursorPos.Y = viewportStart; + cursorPos.y = viewportStart; } Log::Comment(L"Set the cursor position and perform the operation."); @@ -3774,7 +3823,7 @@ void ScreenBufferTests::ScrollOperations() // Unless this is an IL or DL control, which moves the cursor to the left margin. if (scrollType == InsertLine || scrollType == DeleteLine) { - expectedCursorPos.X = 0; + expectedCursorPos.x = 0; } Log::Comment(L"Verify expected cursor position."); @@ -3789,7 +3838,7 @@ void ScreenBufferTests::ScrollOperations() const auto insertedLines = scrollDirection == Down ? scrollMagnitude : 0; // Insert and delete operations only scroll the viewport below the cursor position. - const auto scrollStart = (scrollType == InsertLine || scrollType == DeleteLine) ? cursorPos.Y : viewportStart; + const auto scrollStart = (scrollType == InsertLine || scrollType == DeleteLine) ? cursorPos.y : viewportStart; // Reset the viewport character and line number for the verification loop. viewportChar = L'A'; @@ -3815,14 +3864,90 @@ void ScreenBufferTests::ScrollOperations() VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr)); } +void ScreenBufferTests::InsertReplaceMode() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto bufferHeight = si.GetBufferSize().Height(); + const auto viewport = si.GetViewport(); + const auto targetRow = viewport.Top() + 5; + const auto targetCol = til::CoordType{ 10 }; + + // Fill the entire buffer with Zs. Blue on Green. + const auto bufferChar = L'Z'; + const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; + _FillLines(0, bufferHeight, bufferChar, bufferAttr); + + // Fill the target row with asterisks and a range of letters at the start. Red on Blue. + const auto initialChars = L"ABCDEFGHIJKLMNOPQRST"; + const auto initialAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLine(targetRow, L'*', initialAttr); + _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), RGB(18, 29, 55) }; + newAttr.SetCrossedOut(true); + newAttr.SetReverseVideo(true); + newAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + si.SetAttributes(newAttr); + + Log::Comment(L"Write additional content into a line of text with IRM mode enabled."); + + // Set the cursor position partway through the target row. + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + // Enable Insert/Replace mode. + stateMachine.ProcessString(L"\033[4h"); + // Write out some new content. + const auto newChars = L"12345"; + stateMachine.ProcessString(newChars); + + VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr), + L"First half of the line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr), + L"New content should be inserted at the cursor position with active attributes."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"KLMNOPQRST", initialAttr), + L"Second half of the line should have moved 5 columns across."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr), + L"With the remainder of the line filled with asterisks."); + VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr), + L"The following line should be unaffected."); + + // Fill the target row with the initial content again. + _FillLine(targetRow, L'*', initialAttr); + _FillLine(targetRow, initialChars, initialAttr); + + Log::Comment(L"Write additional content into a line of text with IRM mode disabled."); + + // Set the cursor position partway through the target row. + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + // Disable Insert/Replace mode. + stateMachine.ProcessString(L"\033[4l"); + // Write out some new content. + stateMachine.ProcessString(newChars); + + VERIFY_IS_TRUE(_ValidateLineContains({ 0, targetRow }, L"ABCDEFGHIJ", initialAttr), + L"First half of the line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol, targetRow }, newChars, newAttr), + L"New content should be added at the cursor position with active attributes."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 5, targetRow }, L"PQRST", initialAttr), + L"Second half of the line should have been partially overwritten."); + VERIFY_IS_TRUE(_ValidateLineContains({ targetCol + 25, targetRow }, L'*', initialAttr), + L"With the remainder of the line filled with asterisks."); + VERIFY_IS_TRUE(_ValidateLineContains(targetRow + 1, bufferChar, bufferAttr), + L"The following line should be unaffected."); +} + void ScreenBufferTests::InsertChars() { BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:setVerticalMargins", L"{false, true}") END_TEST_METHOD_PROPERTIES(); - bool setMargins; - VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins)); + bool setVerticalMargins; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"setVerticalMargins", setVerticalMargins)); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); @@ -3837,16 +3962,24 @@ void ScreenBufferTests::InsertChars() VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); - // Tests are run both with and without the DECSTBM margins set. This should not alter - // the results, since the ICH operation is not affected by vertical margins. - stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r"); + // Tests are run both with and without the DECSTBM margins set. And while they + // don't affect the ICH operation directly, when we're outside the vertical + // margins, the horizontal margins won't apply, and that does affect ICH. + const auto horizontalMarginsActive = !setVerticalMargins; + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[11;30s"); + stateMachine.ProcessString(setVerticalMargins ? L"\x1b[15;20r" : L"\x1b[r"); // Make sure we clear the margins on exit so they can't break other tests. - auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); + auto clearMargins = wil::scope_exit([&] { + stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); + }); Log::Comment( L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " L"Then insert 5 spaces at the cursor. Watch spaces get inserted, text slides right " - L"out of the viewport, pushing some of the Qs out of the buffer."); + L"up to the right margin or off the edge of the buffer."); const auto insertLine = til::CoordType{ 10 }; auto insertPos = til::CoordType{ 20 }; @@ -3869,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; @@ -3880,13 +4013,12 @@ void ScreenBufferTests::InsertChars() // Insert 5 spaces at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: QQQQQQQQQQABCDEFGHIJ KLMNOPQRSTQQQQQ + // After: QQQQQQQQQQABCDEFGHIJ KLMNOQQQQQQQQQQ (horizontal margins active) + // After: QQQQQQQQQQABCDEFGHIJ KLMNOPQRSTQQQQQ (horizontal margins inactive) Log::Comment(L"Inserting 5 spaces in the middle of the line."); auto before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[5@"); auto after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); @@ -3898,18 +4030,29 @@ void ScreenBufferTests::InsertChars() L"First half of the alphabet should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), L"Spaces should be inserted with standard erase attributes at the cursor position."); - VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr), - L"Second half of the alphabet should have moved to the right by the number of spaces inserted."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr), - L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer."); + + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNO", textAttr), + L"Second half of the alphabet should have moved to the right but only up to right margin."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQ", bufferAttr), + L"Field of Qs right of the margin area should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr), + L"Second half of the alphabet should have moved to the right by the number of spaces inserted."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr), + L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer."); + } Log::Comment( - L"Test 2: Inserting at the exact end of the line. Same line structure. " - L"Move cursor to right edge of window and insert > 1 space. " + L"Test 2: Inserting at the exact end of the scroll area. Same line structure. " + L"Move cursor to right edge of window or right margin and insert > 1 space. " L"Only 1 should be inserted, everything else unchanged."); // Move cursor to right edge. - insertPos = bufferWidth - 1; + insertPos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1; VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); expectedCursor = cursor.GetPosition(); @@ -3921,13 +4064,12 @@ void ScreenBufferTests::InsertChars() // Insert 5 spaces at the right edge. Only 1 should be inserted. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ - Log::Comment(L"Inserting 5 spaces at the right edge of the buffer."); + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRS QQQQQQQQQQ (horizontal margins active) + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ (horizontal margins inactive) + Log::Comment(L"Inserting 5 spaces at the right edge."); before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[5@"); after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); @@ -3935,20 +4077,32 @@ void ScreenBufferTests::InsertChars() // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), - L"Entire viewport range should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr), - L"Field of Qs right of the viewport should remain unchanged except for the last spot."); VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), L"One space should be inserted with standard erase attributes at the cursor position."); + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRS", textAttr), + L"Viewport range should remain unchanged except for the last spot."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), + L"Entire viewport range should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged except for the last spot."); + } + Log::Comment( - L"Test 3: Inserting at the exact beginning of the line. Same line structure. " - L"Move cursor to left edge of buffer and insert > buffer width of space. " - L"The whole row should be replaced with spaces."); + L"Test 3: Inserting at the beginning of the scroll area. Same line structure. " + L"Move cursor to left edge of buffer or left margin and insert > buffer width of space. " + L"The whole scroll area should be replaced with spaces."); // Move cursor to left edge. - VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, insertLine }, true)); + insertPos = horizontalMarginsActive ? viewportStart : 0; + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -3957,32 +4111,43 @@ void ScreenBufferTests::InsertChars() // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, insertLine }, textChars, textAttr); - // Insert greater than the buffer width at the left edge. The entire line should be erased. + // Insert greater than the buffer width at the left edge. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: - Log::Comment(L"Inserting 100 spaces at the left edge of the buffer."); + // After: QQQQQQQQQQ QQQQQQQQQQ (horizontal margins active) + // After: (horizontal margins inactive) + Log::Comment(L"Inserting 100 spaces at the left edge."); before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); stateMachine.ProcessString(L"\x1b[100@"); after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); // Verify the updated structure of the line. - VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr), - L"A whole line of spaces was inserted at the start, erasing the line."); + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), + L"Entire viewport range should be erased with spaces."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr), + L"A whole line of spaces was inserted at the start, erasing the line."); + } } void ScreenBufferTests::DeleteChars() { BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:setVerticalMargins", L"{false, true}") END_TEST_METHOD_PROPERTIES(); - bool setMargins; - VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins)); + bool setVerticalMargins; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"setVerticalMargins", setVerticalMargins)); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); @@ -3997,16 +4162,24 @@ void ScreenBufferTests::DeleteChars() VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); - // Tests are run both with and without the DECSTBM margins set. This should not alter - // the results, since the DCH operation is not affected by vertical margins. - stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r"); + // Tests are run both with and without the DECSTBM margins set. And while they + // don't affect the DCH operation directly, when we're outside the vertical + // margins, the horizontal margins won't apply, and that does affect DCH. + const auto horizontalMarginsActive = !setVerticalMargins; + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[11;30s"); + stateMachine.ProcessString(setVerticalMargins ? L"\x1b[15;20r" : L"\x1b[r"); // Make sure we clear the margins on exit so they can't break other tests. - auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); + auto clearMargins = wil::scope_exit([&] { + stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); + }); Log::Comment( L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " L"Then delete 5 characters at the cursor. Watch the rest of the line slide left, " - L"replacing the deleted characters, with spaces inserted at the end of the line."); + L"with spaces inserted at the end of the line or the right margin."); const auto deleteLine = til::CoordType{ 10 }; auto deletePos = til::CoordType{ 20 }; @@ -4029,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; @@ -4040,13 +4213,12 @@ void ScreenBufferTests::DeleteChars() // Delete 5 characters at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ + // After: QQQQQQQQQQABCDEFGHIJPQRST QQQQQQQQQQ (horizontal margins active) + // After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ (horizontal margins inactive) Log::Comment(L"Deleting 5 characters in the middle of the line."); auto before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[5P"); auto after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); @@ -4058,18 +4230,29 @@ void ScreenBufferTests::DeleteChars() L"First half of the alphabet should remain unchanged."); VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"PQRST", textAttr), L"Only half of the second part of the alphabet remains."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr), - L"Field of Qs right of the viewport should be moved left."); - VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", expectedFillAttr), - L"The rest of the line should be replaced with spaces with standard erase attributes."); + + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L" ", expectedFillAttr), + L"The rest of the margin area should be replaced with spaces with standard erase attributes."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should be moved left."); + VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", expectedFillAttr), + L"The rest of the line should be replaced with spaces with standard erase attributes."); + } Log::Comment( - L"Test 2: Deleting at the exact end of the line. Same line structure. " - L"Move cursor to right edge of window and delete > 1 character. " + L"Test 2: Deleting at the exact end of the scroll area. Same line structure. " + L"Move cursor to right edge of window or right margin and delete > 1 character. " L"Only 1 should be deleted, everything else unchanged."); // Move cursor to right edge. - deletePos = bufferWidth - 1; + deletePos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1; VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); expectedCursor = cursor.GetPosition(); @@ -4081,13 +4264,12 @@ void ScreenBufferTests::DeleteChars() // Delete 5 characters at the right edge. Only 1 should be deleted. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ - Log::Comment(L"Deleting 5 characters at the right edge of the buffer."); + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRS QQQQQQQQQQ (horizontal margins active) + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ (horizontal margins inactive) + Log::Comment(L"Deleting 5 characters at the right edge."); before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[5P"); after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); @@ -4095,20 +4277,32 @@ void ScreenBufferTests::DeleteChars() // Verify the updated structure of the line. VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), L"Field of Qs left of the viewport should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), - L"Entire viewport range should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr), - L"Field of Qs right of the viewport should remain unchanged except for the last spot."); VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr), L"One character should be erased with standard erase attributes at the cursor position."); + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRS", textAttr), + L"Viewport range should remain unchanged except for the last spot."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), + L"Entire viewport range should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged except for the last spot."); + } + Log::Comment( - L"Test 3: Deleting at the exact beginning of the line. Same line structure. " - L"Move cursor to left edge of buffer and delete > buffer width of characters. " - L"The whole row should be replaced with spaces."); + L"Test 3: Deleting at the beginning of the scroll area. Same line structure. " + L"Move cursor to left edge of buffer or left margin and delete > buffer width of characters. " + L"The whole scroll area should be replaced with spaces."); // Move cursor to left edge. - VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, deleteLine }, true)); + deletePos = horizontalMarginsActive ? viewportStart : 0; + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -4117,22 +4311,192 @@ void ScreenBufferTests::DeleteChars() // Fill the viewport range with text. Red on Blue. _FillLine({ viewportStart, deleteLine }, textChars, textAttr); - // Delete greater than the buffer width at the left edge. The entire line should be erased. + // Delete greater than the buffer width at the left edge. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ - // After: - Log::Comment(L"Deleting 100 characters at the left edge of the buffer."); + // After: QQQQQQQQQQ QQQQQQQQQQ (horizontal margins active) + // After: (horizontal margins inactive) + Log::Comment(L"Deleting 100 characters at the left edge."); before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); stateMachine.ProcessString(L"\x1b[100P"); after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); - Log::Comment(before.c_str(), L"Before"); - Log::Comment(after.c_str(), L" After"); // Verify cursor didn't move. VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); // Verify the updated structure of the line. - VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr), - L"A whole line of spaces was inserted from the right, erasing the line."); + if (horizontalMarginsActive) + { + VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr), + L"Entire viewport range should be erased with spaces."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged."); + } + else + { + VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr), + L"A whole line of spaces was inserted from the right, erasing the line."); + } +} + +void ScreenBufferTests::HorizontalScrollOperations() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:op", L"{0, 1, 2, 3}") + END_TEST_METHOD_PROPERTIES(); + + enum Op : int + { + DECIC, + DECDC, + DECFI, + DECBI + } op; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", (int&)op)); + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + // Set the buffer width to 40, with a centered viewport of 20. + const auto bufferWidth = 40; + const auto bufferHeight = si.GetBufferSize().Height(); + const auto viewportStart = 10; + const auto viewportEnd = viewportStart + 20; + VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); + si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); + + // Set the margin area to columns 10 to 29 and rows 14 to 19 (zero based). + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[11;30s"); + stateMachine.ProcessString(L"\x1b[15;20r"); + // Make sure we clear the margins on exit so they can't break other tests. + auto clearMargins = wil::scope_exit([&] { + stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); + }); + + // Fill the buffer with text. Red on Blue. + const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn"; + const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _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), RGB(18, 29, 55) }; + fillAttr.SetCrossedOut(true); + fillAttr.SetReverseVideo(true); + fillAttr.SetUnderlineStyle(UnderlineStyle::CurlyUnderlined); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); + + auto& cursor = si.GetTextBuffer().GetCursor(); + switch (op) + { + case DECIC: + Log::Comment(L"Insert 4 columns in the middle of the margin area."); + cursor.SetPosition({ 20, 17 }); + stateMachine.ProcessString(L"\x1b[4'}"); + VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move."); + VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRST", bufferAttr), + L"The margin area left of the cursor position should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(20, 14, 20, L" ", expectedFillAttr), + L"4 blank columns should be inserted at the cursor position."); + VERIFY_IS_TRUE(_ValidateLinesContain(24, 14, 20, L"UVWXYZ", bufferAttr), + L"The area right of that should be scrolled right by 4 columns."); + break; + case DECDC: + Log::Comment(L"Delete 4 columns in the middle of the margin area."); + cursor.SetPosition({ 20, 17 }); + stateMachine.ProcessString(L"\x1b[4'~"); + VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move."); + VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRSTYZabcd", bufferAttr), + L"The area right of the cursor position should be scrolled left by 4 columns."); + VERIFY_IS_TRUE(_ValidateLinesContain(26, 14, 20, L" ", expectedFillAttr), + L"4 blank columns should be inserted at the right of the margin area."); + break; + case DECFI: + Log::Comment(L"Forward index 4 times, 2 columns before the right margin."); + cursor.SetPosition({ 27, 17 }); + stateMachine.ProcessString(L"\x1b\x39\x1b\x39\x1b\x39\x1b\x39"); + VERIFY_ARE_EQUAL(til::point(29, 17), cursor.GetPosition(), L"The cursor should not pass the right margin."); + VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"MNOPQRSTUVWXYZabcd", bufferAttr), + L"The margin area should scroll left by 2 columns."); + VERIFY_IS_TRUE(_ValidateLinesContain(28, 14, 20, L" ", expectedFillAttr), + L"2 blank columns should be inserted at the right of the margin area."); + break; + case DECBI: + Log::Comment(L"Back index 4 times, 2 columns before the left margin."); + cursor.SetPosition({ 12, 17 }); + stateMachine.ProcessString(L"\x1b\x36\x1b\x36\x1b\x36\x1b\x36"); + VERIFY_ARE_EQUAL(til::point(10, 17), cursor.GetPosition(), L"The cursor should not pass the left margin."); + VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L" ", expectedFillAttr), + L"2 blank columns should be inserted at the left of the margin area."); + VERIFY_IS_TRUE(_ValidateLinesContain(12, 14, 20, L"KLMNOPQRSTUVWXYZab", bufferAttr), + L"The rest of the margin area should scroll right by 2 columns."); + break; + } + + VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, bufferChars, bufferAttr), + L"Content above the top margin should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(20, 25, bufferChars, bufferAttr), + L"Content below the bottom margin should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, 20, L"ABCDEFGHIJ", bufferAttr), + L"Content before the left margin should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(30, 14, 20, L"efghijklmn", bufferAttr), + L"Content beyond the right margin should remain unchanged."); +} + +void ScreenBufferTests::ScrollingWideCharsHorizontally() +{ + // The point of this test is to make sure wide characters can be + // moved horizontally by one cell without erasing themselves. + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto viewport = si.GetViewport(); + const auto testRow = viewport.Top(); + + Log::Comment(L"Fill the test row with content containing wide chars"); + const auto testChars = L"こんにちは World"; + const auto testAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLine(testRow, testChars, testAttr); + + Log::Comment(L"Position the cursor at the start of the test row"); + VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, testRow }, true)); + + Log::Comment(L"Insert 1 cell at the start of the test row"); + stateMachine.ProcessString(L"\033[@"); + VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr)); + + Log::Comment(L"Delete 1 cell from the start of the test row"); + stateMachine.ProcessString(L"\033[P"); + VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr)); + + Log::Comment(L"Copy the test row 1 cell to the right"); + stateMachine.ProcessString(L"\033[1;1;1;;;1;2$v"); + VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr)); + + Log::Comment(L"Copy the test row 1 cell to the left"); + stateMachine.ProcessString(L"\033[1;2;1;;;1;1$v"); + VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr)); + + Log::Comment(L"Scroll the test row 1 cell to the right"); + const auto testRect = til::inclusive_rect{ 0, testRow, viewport.Width() - 2, testRow }; + ScrollRegion(si, testRect, std::nullopt, { 1, testRow }, L' ', testAttr); + VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr)); + + Log::Comment(L"Scroll the test row 1 cell to the left"); + const auto testRect2 = til::inclusive_rect{ 1, testRow, viewport.Width() - 1, testRow }; + ScrollRegion(si, testRect2, std::nullopt, { 0, testRow }, L' ', testAttr); + VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr)); } void ScreenBufferTests::EraseScrollbackTests() @@ -4180,7 +4544,7 @@ void ScreenBufferTests::EraseScrollbackTests() // maintains the same relative position. const auto expectedOffset = til::point{ 0, -viewport.Top() }; const auto expectedViewport = Viewport::Offset(viewport, expectedOffset); - const auto expectedCursorPos = til::point{ cursorPos.X, cursorPos.Y + expectedOffset.Y }; + const auto expectedCursorPos = til::point{ cursorPos.x, cursorPos.y + expectedOffset.y }; Log::Comment(L"Verify expected viewport."); VERIFY_ARE_EQUAL(expectedViewport, si.GetViewport()); @@ -4284,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; @@ -4323,22 +4687,22 @@ void ScreenBufferTests::EraseTests() { // For eraseScreen, if we're not erasing to the end, these rows will be cleared. Log::Comment(L"Lines before the cursor line should be erased."); - VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.Y, L' ', expectedFillAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.y, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines before the cursor line should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.Y, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, viewportStart, cursorPos.y, bufferChar, bufferAttr)); } // 2. Cursor Line - auto prefixPos = til::point{ protectedOffset, cursorPos.Y }; + auto prefixPos = til::point{ protectedOffset, cursorPos.y }; auto suffixPos = cursorPos; // When erasing from the beginning, the cursor column is included in the range. - suffixPos.X += (eraseType == DispatchTypes::EraseType::FromBeginning); - size_t prefixWidth = suffixPos.X - prefixPos.X; - size_t suffixWidth = bufferWidth - suffixPos.X; + suffixPos.x += (eraseType == DispatchTypes::EraseType::FromBeginning); + size_t prefixWidth = suffixPos.x - prefixPos.x; + size_t suffixWidth = bufferWidth - suffixPos.x; if (eraseType == DispatchTypes::EraseType::ToEnd) { Log::Comment(L"The start of the cursor line should remain unchanged."); @@ -4364,13 +4728,13 @@ void ScreenBufferTests::EraseTests() { // For eraseScreen, if we're not erasing from the beginning, these rows will be cleared. Log::Comment(L"Lines after the cursor line should be erased."); - VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.y + 1, viewportEnd, L' ', expectedFillAttr)); } else { // Otherwise we'll be left with the original buffer content. Log::Comment(L"Lines after the cursor line should remain unchanged."); - VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(protectedOffset, cursorPos.y + 1, viewportEnd, bufferChar, bufferAttr)); } // 4. Protected columns @@ -4446,24 +4810,24 @@ void ScreenBufferTests::ProtectedAttributeTests() void _CommonScrollingSetup() { // Used for testing MSFT:20204600 - // Place an A on the first line, and a B on the 6th line (index 5). + // Place As on the first line, and Bs on the 6th line (index 5). // Set the scrolling region in between those lines (so scrolling won't affect them.) - // First write "1\n2\n3\n4", to put 1-4 on the lines in between the A and B. + // First write "1111\n2222\n3333\n4444", to put 1-4 on the lines in between the A and B. // the viewport will look like: - // A - // 1 - // 2 - // 3 - // 4 - // B - // then write "\n5\n6\n7\n", which will cycle around the scroll region a bit. + // AAAAAA... + // 111111... + // 222222... + // 333333... + // 444444... + // BBBBBB... + // then write "\n5555\n6666\n7777\n", which will cycle around the scroll region a bit. // the viewport will look like: - // A - // 5 - // 6 - // 7 + // AAAAAA... + // 555555... + // 666666... + // 777777... // - // B + // BBBBBB... auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); @@ -4472,61 +4836,61 @@ void _CommonScrollingSetup() auto& cursor = si.GetTextBuffer().GetCursor(); const auto oldView = si.GetViewport(); const auto view = Viewport::FromDimensions({ 0, 0 }, { oldView.Width(), 6 }); + const auto bufferWidth = tbi.GetSize().Width(); + const auto attr = tbi.GetCurrentAttributes(); + si.SetViewport(view, true); cursor.SetPosition({ 0, 0 }); - stateMachine.ProcessString(L"A"); + stateMachine.ProcessString(std::wstring(bufferWidth, L'A')); cursor.SetPosition({ 0, 5 }); - stateMachine.ProcessString(L"B"); + stateMachine.ProcessString(std::wstring(bufferWidth, L'B')); stateMachine.ProcessString(L"\x1b[2;5r"); stateMachine.ProcessString(L"\x1b[2;1H"); - stateMachine.ProcessString(L"1\n2\n3\n4"); + stateMachine.ProcessString(std::wstring(bufferWidth, L'1')); + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'2')); + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'3')); + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'4')); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"1", iter1->Chars()); - VERIFY_ARE_EQUAL(L"2", iter2->Chars()); - VERIFY_ARE_EQUAL(L"3", iter3->Chars()); - VERIFY_ARE_EQUAL(L"4", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(bufferWidth - 1, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().y); - stateMachine.ProcessString(L"\n5\n6\n7\n"); + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'1', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'2', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'3', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'4', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); + + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'5')); + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'6')); + stateMachine.ProcessCharacter(L'\n'); + stateMachine.ProcessString(std::wstring(bufferWidth, L'7')); + stateMachine.ProcessCharacter(L'\n'); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"5", iter1->Chars()); - VERIFY_ARE_EQUAL(L"6", iter2->Chars()); - VERIFY_ARE_EQUAL(L"7", iter3->Chars()); - // Chars() will return a single space for an empty row. - VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::ScrollUpInMargins() @@ -4540,7 +4904,8 @@ void ScreenBufferTests::ScrollUpInMargins() auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); - auto& cursor = si.GetTextBuffer().GetCursor(); + auto& cursor = tbi.GetCursor(); + const auto attr = tbi.GetCurrentAttributes(); // Execute a Scroll Up command stateMachine.ProcessString(L"\x1b[S"); @@ -4550,22 +4915,37 @@ void ScreenBufferTests::ScrollUpInMargins() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"6", iter1->Chars()); - VERIFY_ARE_EQUAL(L"7", iter2->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); + + _CommonScrollingSetup(); + // Set horizontal margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); }); + + // Execute a Scroll Up command + stateMachine.ProcessString(L"\x1b[S"); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L"6666", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"7777", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::ScrollDownInMargins() @@ -4579,7 +4959,8 @@ void ScreenBufferTests::ScrollDownInMargins() auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); - auto& cursor = si.GetTextBuffer().GetCursor(); + auto& cursor = tbi.GetCursor(); + const auto attr = tbi.GetCurrentAttributes(); // Execute a Scroll Down command stateMachine.ProcessString(L"\x1b[T"); @@ -4589,22 +4970,39 @@ void ScreenBufferTests::ScrollDownInMargins() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); - VERIFY_ARE_EQUAL(L"5", iter2->Chars()); - VERIFY_ARE_EQUAL(L"6", iter3->Chars()); - VERIFY_ARE_EQUAL(L"7", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); + + _CommonScrollingSetup(); + // Set horizontal margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); }); + + // Execute a Scroll Down command + stateMachine.ProcessString(L"\x1b[T"); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"5555", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"6666", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"7777", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::InsertLinesInMargins() @@ -4618,7 +5016,8 @@ void ScreenBufferTests::InsertLinesInMargins() auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); - auto& cursor = si.GetTextBuffer().GetCursor(); + auto& cursor = tbi.GetCursor(); + const auto attr = tbi.GetCurrentAttributes(); // Move to column 5 of line 3 stateMachine.ProcessString(L"\x1b[3;5H"); @@ -4631,22 +5030,15 @@ void ScreenBufferTests::InsertLinesInMargins() L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"5", iter1->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter2->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); - VERIFY_ARE_EQUAL(L"6", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); Log::Comment( L"Does the common scrolling setup, then inserts one line with no " @@ -4666,22 +5058,46 @@ void ScreenBufferTests::InsertLinesInMargins() L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); - VERIFY_ARE_EQUAL(L"5", iter2->Chars()); - VERIFY_ARE_EQUAL(L"6", iter3->Chars()); - VERIFY_ARE_EQUAL(L"7", iter4->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L' ', attr)); + + Log::Comment( + L"Does the common scrolling setup, then inserts two lines inside the " + L"margin boundaries, and verifies the rows have what we'd expect."); + + _CommonScrollingSetup(); + // Set horizontal margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); }); + // Move to column 5 of line 3 + stateMachine.ProcessString(L"\x1b[3;5H"); + // Insert 2 lines + stateMachine.ProcessString(L"\x1b[2L"); + + // Verify cursor moved to left margin. + VERIFY_ARE_EQUAL(2, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"6666", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::DeleteLinesInMargins() @@ -4695,7 +5111,8 @@ void ScreenBufferTests::DeleteLinesInMargins() auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); - auto& cursor = si.GetTextBuffer().GetCursor(); + auto& cursor = tbi.GetCursor(); + const auto attr = tbi.GetCurrentAttributes(); // Move to column 5 of line 3 stateMachine.ProcessString(L"\x1b[3;5H"); @@ -4708,22 +5125,15 @@ void ScreenBufferTests::DeleteLinesInMargins() L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(2, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"5", iter1->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter2->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); Log::Comment( L"Does the common scrolling setup, then deletes one line with no " @@ -4743,22 +5153,44 @@ void ScreenBufferTests::DeleteLinesInMargins() L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); // Verify cursor moved to left margin. - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"6", iter1->Chars()); - VERIFY_ARE_EQUAL(L"7", iter2->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter3->Chars()); - VERIFY_ARE_EQUAL(L"B", iter4->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter5->Chars()); - } + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'B', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L' ', attr)); + + Log::Comment( + L"Does the common scrolling setup, then deletes two lines inside the " + L"margin boundaries, and verifies the rows have what we'd expect."); + + _CommonScrollingSetup(); + // Set horizontal margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); }); + // Move to column 5 of line 3 + stateMachine.ProcessString(L"\x1b[3;5H"); + // Delete 2 lines + stateMachine.ProcessString(L"\x1b[2M"); + + // Verify cursor moved to left margin. + VERIFY_ARE_EQUAL(2, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::ReverseLineFeedInMargins() @@ -4772,7 +5204,8 @@ void ScreenBufferTests::ReverseLineFeedInMargins() auto& si = gci.GetActiveOutputBuffer(); auto& tbi = si.GetTextBuffer(); auto& stateMachine = si.GetStateMachine(); - auto& cursor = si.GetTextBuffer().GetCursor(); + auto& cursor = tbi.GetCursor(); + const auto attr = tbi.GetCurrentAttributes(); // Move to column 5 of line 2, the top margin stateMachine.ProcessString(L"\x1b[2;5H"); @@ -4784,22 +5217,15 @@ void ScreenBufferTests::ReverseLineFeedInMargins() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"A", iter0->Chars()); - VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); - VERIFY_ARE_EQUAL(L"5", iter2->Chars()); - VERIFY_ARE_EQUAL(L"6", iter3->Chars()); - VERIFY_ARE_EQUAL(L"7", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(4, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); Log::Comment( L"Does the common scrolling setup, then executes a reverse line feed " @@ -4821,22 +5247,47 @@ void ScreenBufferTests::ReverseLineFeedInMargins() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(4, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); - { - auto iter0 = tbi.GetCellDataAt({ 0, 0 }); - auto iter1 = tbi.GetCellDataAt({ 0, 1 }); - auto iter2 = tbi.GetCellDataAt({ 0, 2 }); - auto iter3 = tbi.GetCellDataAt({ 0, 3 }); - auto iter4 = tbi.GetCellDataAt({ 0, 4 }); - auto iter5 = tbi.GetCellDataAt({ 0, 5 }); - VERIFY_ARE_EQUAL(L"\x20", iter0->Chars()); - VERIFY_ARE_EQUAL(L"A", iter1->Chars()); - VERIFY_ARE_EQUAL(L"5", iter2->Chars()); - VERIFY_ARE_EQUAL(L"6", iter3->Chars()); - VERIFY_ARE_EQUAL(L"7", iter4->Chars()); - VERIFY_ARE_EQUAL(L"B", iter5->Chars()); - } + VERIFY_ARE_EQUAL(4, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(1, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); + + Log::Comment( + L"Does the common scrolling setup, then executes a reverse line feed " + L"below the top margin, and verifies the rows have what we'd expect."); + + _CommonScrollingSetup(); + // Set horizontal margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); }); + // Move to column 5 of line 2, the top margin + stateMachine.ProcessString(L"\x1b[2;5H"); + // Execute a reverse line feed (RI) + stateMachine.ProcessString(L"\x1bM"); + + VERIFY_ARE_EQUAL(4, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); + + VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"5555", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"6666", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L" ", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"7777", attr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr)); + VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr)); } void ScreenBufferTests::LineFeedEscapeSequences() @@ -4883,8 +5334,8 @@ void ScreenBufferTests::LineFeedEscapeSequences() cursor.SetPosition({ initialX, initialY }); stateMachine.ProcessString(escapeSequence); - VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); } @@ -4896,8 +5347,8 @@ void ScreenBufferTests::LineFeedEscapeSequences() cursor.SetPosition({ initialX, initialY }); stateMachine.ProcessString(escapeSequence); - VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); } @@ -4915,13 +5366,49 @@ void ScreenBufferTests::LineFeedEscapeSequences() cursor.SetPosition({ initialX, initialY }); stateMachine.ProcessString(escapeSequence); - VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(expectedX, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().y); VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); // Verify the line of Qs has been scrolled up. VERIFY_IS_TRUE(_ValidateLineContains(initialY - 1, L'Q', {})); VERIFY_IS_TRUE(_ValidateLineContains(initialY, L' ', si.GetAttributes())); } + + { + Log::Comment(L"Starting at the bottom of the scroll margins with horizontal margins"); + // Set the margins to rows 5 to 10. + stateMachine.ProcessString(L"\x1b[5;10r"); + // Set the margins to columns 3 to 6. + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[3;6s"); + // Make sure we clear the margins on exit so they can't break other tests. + auto clearMargins = wil::scope_exit([&] { + stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); + }); + + const auto initialY = si.GetViewport().Top() + 9; + const auto expectedY = initialY; + const auto initialXInMargins = 5; + const auto expectedXInMargins = withReturn ? 2 : initialXInMargins; + const auto expectedViewportTop = si.GetViewport().Top(); + _FillLine(initialY, L'R', {}); + cursor.SetPosition({ initialXInMargins, initialY }); + stateMachine.ProcessString(escapeSequence); + + VERIFY_ARE_EQUAL(expectedXInMargins, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().y); + VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top()); + // Verify the line of Rs has been scrolled up only within the margins. + const auto defaultAttr = TextAttribute{}; + VERIFY_IS_TRUE(_ValidateLineContains({ 0, initialY - 1 }, L"QQ", defaultAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, initialY - 1 }, L"RRRR", defaultAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, initialY - 1 }, L'Q', defaultAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 0, initialY }, L"RR", defaultAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 2, initialY }, L" ", si.GetAttributes())); + VERIFY_IS_TRUE(_ValidateLineContains({ 6, initialY }, L'R', defaultAttr)); + } } void ScreenBufferTests::ScrollLines256Colors() @@ -5010,14 +5497,14 @@ void ScreenBufferTests::ScrollLines256Colors() Log::Comment(NoThrowString().Format( L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer())); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); stateMachine.ProcessString(L"foo"); Log::Comment(NoThrowString().Format( L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer())); - VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(3, cursor.GetPosition().x); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); { auto iter00 = tbi.GetCellDataAt({ 0, 0 }); auto iter10 = tbi.GetCellDataAt({ 1, 0 }); @@ -5041,6 +5528,28 @@ void ScreenBufferTests::ScrollLines256Colors() } } +void ScreenBufferTests::SetLineFeedMode() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto& terminalInput = gci.GetActiveInputBuffer()->GetTerminalInput(); + + // We need to start with newline auto return disabled for LNM to be active. + WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); + auto restoreMode = wil::scope_exit([&] { WI_ClearFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); }); + + Log::Comment(L"When LNM is set, newline auto return and line feed mode are enabled."); + stateMachine.ProcessString(L"\x1B[20h"); + VERIFY_IS_TRUE(WI_IsFlagClear(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN)); + VERIFY_IS_TRUE(terminalInput.GetInputMode(TerminalInput::Mode::LineFeed)); + + Log::Comment(L"When LNM is reset, newline auto return and line feed mode are disabled."); + stateMachine.ProcessString(L"\x1B[20l"); + VERIFY_IS_FALSE(WI_IsFlagClear(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN)); + VERIFY_IS_FALSE(terminalInput.GetInputMode(TerminalInput::Mode::LineFeed)); +} + void ScreenBufferTests::SetScreenMode() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -5082,28 +5591,35 @@ void ScreenBufferTests::SetOriginMode() cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); + stateMachine.ProcessString(L"\x1B[?69h"); + cursor.SetPosition({ 40, 12 }); + stateMachine.ProcessString(L"\x1B[31;50s"); + VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition()); - Log::Comment(L"The cursor can be moved below the bottom margin."); - stateMachine.ProcessString(L"\x1B[23;41H"); - VERIFY_ARE_EQUAL(til::point(40, 22), cursor.GetPosition()); + Log::Comment(L"The cursor can be moved past the bottom right margins."); + stateMachine.ProcessString(L"\x1B[23;61H"); + VERIFY_ARE_EQUAL(til::point(60, 22), cursor.GetPosition()); // Testing the effects of DECOM being set (relative cursor addressing) Log::Comment(L"Setting DECOM moves the cursor to the top-left of the margin area."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[?6h"); - VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition()); + VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition()); Log::Comment(L"Setting a margin moves the cursor to the top-left of the margin area."); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); - VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition()); + VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition()); + cursor.SetPosition({ 40, 12 }); + stateMachine.ProcessString(L"\x1B[31;50s"); + VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the margin area."); - stateMachine.ProcessString(L"\x1B[8;41H"); + stateMachine.ProcessString(L"\x1B[8;11H"); VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition()); - Log::Comment(L"The cursor cannot be moved below the bottom margin."); - stateMachine.ProcessString(L"\x1B[100;41H"); - VERIFY_ARE_EQUAL(til::point(40, 19), cursor.GetPosition()); + Log::Comment(L"The cursor cannot be moved past the bottom right margins."); + stateMachine.ProcessString(L"\x1B[100;100H"); + VERIFY_ARE_EQUAL(til::point(49, 19), cursor.GetPosition()); // Testing the effects of DECOM being reset (absolute cursor addressing) Log::Comment(L"Resetting DECOM moves the cursor to the top-left of the screen."); @@ -5114,16 +5630,20 @@ void ScreenBufferTests::SetOriginMode() cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[6;20r"); VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); + cursor.SetPosition({ 40, 12 }); + stateMachine.ProcessString(L"\x1B[31;50s"); + VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); Log::Comment(L"Cursor addressing is relative to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition()); - Log::Comment(L"The cursor can be moved below the bottom margin."); - stateMachine.ProcessString(L"\x1B[23;41H"); - VERIFY_ARE_EQUAL(til::point(40, 22), cursor.GetPosition()); + Log::Comment(L"The cursor can be moved past the bottom right margins."); + stateMachine.ProcessString(L"\x1B[23;61H"); + VERIFY_ARE_EQUAL(til::point(60, 22), cursor.GetPosition()); // Testing the effects of DECOM being set with no margins Log::Comment(L"With no margins, setting DECOM moves the cursor to the top-left of the screen."); stateMachine.ProcessString(L"\x1B[r"); + stateMachine.ProcessString(L"\x1B[s"); cursor.SetPosition({ 40, 12 }); stateMachine.ProcessString(L"\x1B[?6h"); VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); @@ -5131,8 +5651,8 @@ void ScreenBufferTests::SetOriginMode() stateMachine.ProcessString(L"\x1B[13;41H"); VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition()); - // Reset DECOM so we don't affect future tests - stateMachine.ProcessString(L"\x1B[?6l"); + // Reset DECOM and DECLRMM so we don't affect future tests + stateMachine.ProcessString(L"\x1B[?6;69l"); } void ScreenBufferTests::SetAutoWrapMode() @@ -5166,6 +5686,16 @@ void ScreenBufferTests::SetAutoWrapMode() // Content should be clamped to the line width, overwriting the last char. VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"abf", attributes)); VERIFY_ARE_EQUAL(til::point(79, startLine), cursor.GetPosition()); + // Writing a wide glyph into the last 2 columns and overwriting it with a narrow one. + cursor.SetPosition({ 80 - 3, startLine }); + stateMachine.ProcessString(L"a\U0001F604b"); + VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"a b", attributes)); + VERIFY_ARE_EQUAL(til::point(79, startLine), cursor.GetPosition()); + // Writing a wide glyph into the last column and overwriting it with a narrow one. + cursor.SetPosition({ 80 - 3, startLine }); + stateMachine.ProcessString(L"ab\U0001F604c"); + VERIFY_IS_TRUE(_ValidateLineContains({ 80 - 3, startLine }, L"abc", attributes)); + VERIFY_ARE_EQUAL(til::point(79, startLine), cursor.GetPosition()); Log::Comment(L"When DECAWM is set, output is wrapped again."); stateMachine.ProcessString(L"\x1b[?7h"); @@ -5233,7 +5763,7 @@ void ScreenBufferTests::HardResetBuffer() } VERIFY_IS_FALSE(isBufferClear()); VERIFY_IS_GREATER_THAN(viewport.Top(), viewport.Height()); - VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, viewport.Height()); + VERIFY_IS_GREATER_THAN(cursor.GetPosition().y, viewport.Height()); Log::Comment(L"After a reset, buffer should be clear, with viewport and cursor at 0,0"); stateMachine.ProcessString(resetToInitialState); @@ -5263,7 +5793,7 @@ void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling() VERIFY_IS_NULL(siMain._psiAlternateBuffer); Log::Comment(L"Create an alternate buffer"); - if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer()))) + if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({}))) { VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer); auto& altBuffer = *siMain._psiAlternateBuffer; @@ -5273,18 +5803,18 @@ void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling() auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); const auto originalSize = originalView.Dimensions(); - const til::size doubledSize{ originalSize.X * 2, originalSize.Y * 2 }; + const til::size doubledSize{ originalSize.width * 2, originalSize.height * 2 }; // Create some RECTs, which are dimensions in pixels, because // ProcessResizeWindow needs to work on rects in screen _pixel_ // dimensions, not character sizes. til::rect originalClientRect, maximizedClientRect; - originalClientRect.right = originalSize.X * coordFontSize.X; - originalClientRect.bottom = originalSize.Y * coordFontSize.Y; + originalClientRect.right = originalSize.width * coordFontSize.width; + originalClientRect.bottom = originalSize.height * coordFontSize.height; - maximizedClientRect.right = doubledSize.X * coordFontSize.X; - maximizedClientRect.bottom = doubledSize.Y * coordFontSize.Y; + maximizedClientRect.right = doubledSize.width * coordFontSize.width; + maximizedClientRect.bottom = doubledSize.height * coordFontSize.height; Log::Comment(NoThrowString().Format( L"Emulate a maximize")); @@ -5400,8 +5930,8 @@ void ScreenBufferTests::ClearAlternateBuffer() auto& stateMachine = siMain.GetStateMachine(); auto& cursor = tbi.GetCursor(); stateMachine.ProcessString(L"foo\nfoo"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 1); }; auto VerifyText = [&](TextBuffer& tbi) { @@ -5428,7 +5958,7 @@ void ScreenBufferTests::ClearAlternateBuffer() VerifyText(siMain.GetTextBuffer()); Log::Comment(L"Create an alternate buffer"); - if (VERIFY_IS_TRUE(NT_SUCCESS(siMain.UseAlternateScreenBuffer()))) + if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({}))) { VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer); auto& altBuffer = *siMain._psiAlternateBuffer; @@ -5462,15 +5992,15 @@ void ScreenBufferTests::ClearAlternateBuffer() // Verify state of alt buffer auto& altBufferCursor = altBuffer.GetTextBuffer().GetCursor(); - VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().X, 0); - VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().x, 0); + VERIFY_ARE_EQUAL(altBufferCursor.GetPosition().y, 0); #pragma endregion } // Verify state of main buffer is untouched auto& cursor = siMain.GetTextBuffer().GetCursor(); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 1); VerifyText(siMain.GetTextBuffer()); } @@ -5513,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(); @@ -5585,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"); @@ -5707,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); @@ -5830,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); } @@ -5881,20 +6417,20 @@ void ScreenBufferTests::CursorUpDownAcrossMargins() // Set some scrolling margins stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[24H"); - VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(23, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[99A"); - VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(5, cursor.GetPosition().y); stateMachine.ProcessString(L"X"); { auto iter = tbi.GetCellDataAt({ 0, 5 }); VERIFY_ARE_EQUAL(L"X", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1H"); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[99B"); - VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(18, cursor.GetPosition().y); stateMachine.ProcessString(L"Y"); { auto iter = tbi.GetCellDataAt({ 0, 18 }); @@ -5931,20 +6467,20 @@ void ScreenBufferTests::CursorUpDownOutsideMargins() // Set some scrolling margins stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[24H"); - VERIFY_ARE_EQUAL(23, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(23, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[1A"); - VERIFY_ARE_EQUAL(22, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(22, cursor.GetPosition().y); stateMachine.ProcessString(L"X"); { auto iter = tbi.GetCellDataAt({ 0, 22 }); VERIFY_ARE_EQUAL(L"X", iter->Chars()); } stateMachine.ProcessString(L"\x1b[1H"); - VERIFY_ARE_EQUAL(0, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[1B"); - VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().y); stateMachine.ProcessString(L"Y"); { auto iter = tbi.GetCellDataAt({ 0, 1 }); @@ -5985,9 +6521,9 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins() stateMachine.ProcessString(L"\x1b[6;19r"); stateMachine.ProcessString(L"\x1b[19;1H"); - VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(18, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[1B"); - VERIFY_ARE_EQUAL(18, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(18, cursor.GetPosition().y); stateMachine.ProcessString(L"1"); { auto iter = tbi.GetCellDataAt({ 0, 18 }); @@ -5995,7 +6531,7 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins() } stateMachine.ProcessString(L"\x1b[1A"); - VERIFY_ARE_EQUAL(17, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(17, cursor.GetPosition().y); stateMachine.ProcessString(L"2"); { auto iter = tbi.GetCellDataAt({ 1, 17 }); @@ -6003,10 +6539,10 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins() } stateMachine.ProcessString(L"\x1b[6;1H"); - VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(5, cursor.GetPosition().y); stateMachine.ProcessString(L"\x1b[1A"); - VERIFY_ARE_EQUAL(5, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(5, cursor.GetPosition().y); stateMachine.ProcessString(L"3"); { auto iter = tbi.GetCellDataAt({ 0, 5 }); @@ -6014,7 +6550,7 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins() } stateMachine.ProcessString(L"\x1b[1B"); - VERIFY_ARE_EQUAL(6, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(6, cursor.GetPosition().y); stateMachine.ProcessString(L"4"); { auto iter = tbi.GetCellDataAt({ 1, 6 }); @@ -6024,6 +6560,136 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins() stateMachine.ProcessString(L"\x1b[r"); } +void ScreenBufferTests::CursorLeftRightAcrossMargins() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); + + // Set some scrolling margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[31;50s"); + + stateMachine.ProcessString(L"\x1b[12;40H"); + VERIFY_ARE_EQUAL(39, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[99C"); + VERIFY_ARE_EQUAL(49, cursor.GetPosition().x); + stateMachine.ProcessString(L"X"); + { + auto iter = tbi.GetCellDataAt({ 49, 11 }); + VERIFY_ARE_EQUAL(L"X", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[12;40H"); + VERIFY_ARE_EQUAL(39, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[99D"); + VERIFY_ARE_EQUAL(30, cursor.GetPosition().x); + stateMachine.ProcessString(L"Y"); + { + auto iter = tbi.GetCellDataAt({ 30, 11 }); + VERIFY_ARE_EQUAL(L"Y", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); +} + +void ScreenBufferTests::CursorLeftRightOutsideMargins() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); + + // Set some scrolling margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[31;50s"); + + stateMachine.ProcessString(L"\x1b[12;1H"); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[1C"); + VERIFY_ARE_EQUAL(1, cursor.GetPosition().x); + stateMachine.ProcessString(L"Y"); + { + auto iter = tbi.GetCellDataAt({ 1, 11 }); + VERIFY_ARE_EQUAL(L"Y", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[12;80H"); + VERIFY_ARE_EQUAL(79, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[1D"); + VERIFY_ARE_EQUAL(78, cursor.GetPosition().x); + stateMachine.ProcessString(L"X"); + { + auto iter = tbi.GetCellDataAt({ 78, 11 }); + VERIFY_ARE_EQUAL(L"X", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); +} + +void ScreenBufferTests::CursorLeftRightExactlyAtMargins() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24); + + // Set some scrolling margins + stateMachine.ProcessString(L"\x1b[?69h"); + stateMachine.ProcessString(L"\x1b[31;50s"); + + stateMachine.ProcessString(L"\x1b[12;50H"); + VERIFY_ARE_EQUAL(49, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[1C"); + VERIFY_ARE_EQUAL(49, cursor.GetPosition().x); + stateMachine.ProcessString(L"1"); + { + auto iter = tbi.GetCellDataAt({ 49, 11 }); + VERIFY_ARE_EQUAL(L"1", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[1D"); + VERIFY_ARE_EQUAL(48, cursor.GetPosition().x); + stateMachine.ProcessString(L"2"); + { + auto iter = tbi.GetCellDataAt({ 48, 11 }); + VERIFY_ARE_EQUAL(L"2", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[12;31H"); + VERIFY_ARE_EQUAL(30, cursor.GetPosition().x); + stateMachine.ProcessString(L"\x1b[1D"); + VERIFY_ARE_EQUAL(30, cursor.GetPosition().x); + stateMachine.ProcessString(L"3"); + { + auto iter = tbi.GetCellDataAt({ 30, 11 }); + VERIFY_ARE_EQUAL(L"3", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[1C"); + VERIFY_ARE_EQUAL(32, cursor.GetPosition().x); + stateMachine.ProcessString(L"4"); + { + auto iter = tbi.GetCellDataAt({ 32, 11 }); + VERIFY_ARE_EQUAL(L"4", iter->Chars()); + } + + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); +} + void ScreenBufferTests::CursorNextPreviousLine() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -6050,26 +6716,34 @@ void ScreenBufferTests::CursorNextPreviousLine() // We should end up in column 0 of line 5. VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition()); - // Set the margins to 8:12 (9:13 in VT coordinates). + // Enable DECLRMM margin mode + stateMachine.ProcessString(L"\x1b[?69h"); + // Set horizontal margins to 10:29 (11:30 in VT coordinates). + stateMachine.ProcessString(L"\x1b[11;30s"); + // Set vertical margins to 8:12 (9:13 in VT coordinates). stateMachine.ProcessString(L"\x1b[9;13r"); // Make sure we clear the margins on exit so they can't break other tests. - auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); + auto clearMargins = wil::scope_exit([&] { + stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); + }); Log::Comment(L"CNL inside margins"); // Starting from column 20 of line 10. cursor.SetPosition({ 20, 10 }); // Move down 5 lines (CNL). stateMachine.ProcessString(L"\x1b[5E"); - // We should stop on line 12, the bottom margin. - VERIFY_ARE_EQUAL(til::point(0, 12), cursor.GetPosition()); + // We should stop on column 10, line 12, the bottom left margins. + VERIFY_ARE_EQUAL(til::point(10, 12), cursor.GetPosition()); Log::Comment(L"CPL inside margins"); // Starting from column 20 of line 10. cursor.SetPosition({ 20, 10 }); // Move up 5 lines (CPL). stateMachine.ProcessString(L"\x1b[5F"); - // We should stop on line 8, the top margin. - VERIFY_ARE_EQUAL(til::point(0, 8), cursor.GetPosition()); + // We should stop on column 10, line 8, the top left margins. + VERIFY_ARE_EQUAL(til::point(10, 8), cursor.GetPosition()); Log::Comment(L"CNL below bottom"); // Starting from column 20 of line 13 (1 below bottom margin). @@ -6177,8 +6851,8 @@ void ScreenBufferTests::CursorSaveRestore() const auto selectAsciiChars = L"\x1b(B"; const auto selectGraphicsChars = L"\x1b(0"; - const auto saveCursor = L"\x1b[s"; - const auto restoreCursor = L"\x1b[u"; + const auto saveCursor = L"\x1b\x37"; + const auto restoreCursor = L"\x1b\x38"; const auto setDECOM = L"\x1b[?6h"; const auto resetDECOM = L"\x1b[?6l"; @@ -6186,33 +6860,38 @@ void ScreenBufferTests::CursorSaveRestore() VERIFY_SUCCEEDED(si.SetViewportOrigin(true, til::point(0, 0), true)); Log::Comment(L"Restore after save."); - // Set the cursor position, attributes, and character set. + // Set the cursor position, delayed wrap, attributes, and character set. cursor.SetPosition({ 20, 10 }); + cursor.DelayEOLWrap(); si.SetAttributes(colorAttrs); stateMachine.ProcessString(selectGraphicsChars); // Save state. stateMachine.ProcessString(saveCursor); - // Reset the cursor position, attributes, and character set. + // Reset the cursor position, delayed wrap, attributes, and character set. cursor.SetPosition({ 0, 0 }); si.SetAttributes(defaultAttrs); stateMachine.ProcessString(selectAsciiChars); // Restore state. stateMachine.ProcessString(restoreCursor); - // Verify initial position, colors, and graphic character set. + // Verify initial position, delayed wrap, colors, and graphic character set. VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition()); + VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + cursor.ResetDelayEOLWrap(); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains({ 20, 10 }, graphicText, colorAttrs)); Log::Comment(L"Restore again without save."); - // Reset the cursor position, attributes, and character set. + // Reset the cursor position, delayed wrap, attributes, and character set. cursor.SetPosition({ 0, 0 }); si.SetAttributes(defaultAttrs); stateMachine.ProcessString(selectAsciiChars); // Restore state. stateMachine.ProcessString(restoreCursor); - // Verify initial saved position, colors, and graphic character set. + // Verify initial saved position, delayed wrap, colors, and graphic character set. VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition()); + VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + cursor.ResetDelayEOLWrap(); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains({ 20, 10 }, graphicText, colorAttrs)); @@ -6220,24 +6899,28 @@ void ScreenBufferTests::CursorSaveRestore() Log::Comment(L"Restore after reset."); // Soft reset. stateMachine.ProcessString(L"\x1b[!p"); - // Set the cursor position, attributes, and character set. + // Set the cursor position, delayed wrap, attributes, and character set. cursor.SetPosition({ 20, 10 }); + cursor.DelayEOLWrap(); si.SetAttributes(colorAttrs); stateMachine.ProcessString(selectGraphicsChars); // Restore state. stateMachine.ProcessString(restoreCursor); - // Verify home position, default attributes, and ascii character set. + // Verify home position, no delayed wrap, default attributes, and ascii character set. VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); + VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap()); VERIFY_ARE_EQUAL(defaultAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains(til::point(0, 0), asciiText, defaultAttrs)); Log::Comment(L"Restore origin mode."); // Set margins and origin mode to relative. + stateMachine.ProcessString(L"\x1b[?69h"); stateMachine.ProcessString(L"\x1b[10;20r"); + stateMachine.ProcessString(L"\x1b[31;50s"); stateMachine.ProcessString(setDECOM); // Verify home position inside margins. - VERIFY_ARE_EQUAL(til::point(0, 9), cursor.GetPosition()); + VERIFY_ARE_EQUAL(til::point(30, 9), cursor.GetPosition()); // Save state and reset origin mode to absolute. stateMachine.ProcessString(saveCursor); stateMachine.ProcessString(resetDECOM); @@ -6247,37 +6930,43 @@ void ScreenBufferTests::CursorSaveRestore() stateMachine.ProcessString(restoreCursor); stateMachine.ProcessString(L"\x1b[H"); // Verify home position inside margins, i.e. relative origin mode restored. - VERIFY_ARE_EQUAL(til::point(0, 9), cursor.GetPosition()); + VERIFY_ARE_EQUAL(til::point(30, 9), cursor.GetPosition()); - Log::Comment(L"Clamp inside top margin."); + Log::Comment(L"Restore relative to new origin."); // Reset margins, with absolute origin, and set cursor position. stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); stateMachine.ProcessString(setDECOM); - cursor.SetPosition({ 5, 15 }); + cursor.SetPosition({ 5, 5 }); // Save state. stateMachine.ProcessString(saveCursor); // Set margins and restore state. - stateMachine.ProcessString(L"\x1b[20;25r"); + stateMachine.ProcessString(L"\x1b[15;25r"); + stateMachine.ProcessString(L"\x1b[31;50s"); stateMachine.ProcessString(restoreCursor); - // Verify Y position is clamped inside the top margin - VERIFY_ARE_EQUAL(til::point(5, 19), cursor.GetPosition()); + // Verify position is now relative to new top left margins + VERIFY_ARE_EQUAL(til::point(35, 19), cursor.GetPosition()); Log::Comment(L"Clamp inside bottom margin."); // Reset margins, with absolute origin, and set cursor position. stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); stateMachine.ProcessString(setDECOM); - cursor.SetPosition({ 5, 15 }); + cursor.SetPosition({ 15, 15 }); // Save state. stateMachine.ProcessString(saveCursor); // Set margins and restore state. stateMachine.ProcessString(L"\x1b[1;10r"); + stateMachine.ProcessString(L"\x1b[1;10s"); stateMachine.ProcessString(restoreCursor); - // Verify Y position is clamped inside the top margin - VERIFY_ARE_EQUAL(til::point(5, 9), cursor.GetPosition()); + // Verify position is clamped inside the bottom right margins + VERIFY_ARE_EQUAL(til::point(9, 9), cursor.GetPosition()); // Reset origin mode and margins. stateMachine.ProcessString(resetDECOM); stateMachine.ProcessString(L"\x1b[r"); + stateMachine.ProcessString(L"\x1b[s"); + stateMachine.ProcessString(L"\x1b[?69l"); } void ScreenBufferTests::ScreenAlignmentPattern() @@ -6289,8 +6978,8 @@ void ScreenBufferTests::ScreenAlignmentPattern() WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); auto areMarginsSet = [&]() { - const auto margins = si.GetRelativeScrollMargins(); - return margins.BottomInclusive() > margins.Top(); + const auto margins = _GetRelativeScrollMargins(); + return margins.bottom > margins.top; }; Log::Comment(L"Set the initial buffer state."); @@ -6310,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. @@ -6499,15 +7188,14 @@ 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_SUCCESS_NTSTATUS(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 }; + const auto newCursorPos = til::point{ initialCursorPos.x, initialCursorPos.y + 10 }; VERIFY_ARE_EQUAL(newCursorPos, cursor.GetPosition()); Log::Comment(L"Confirm that the virtual bottom matches that new cursor position"); - const auto newVirtualBottom = newCursorPos.Y; + const auto newVirtualBottom = newCursorPos.y; VERIFY_ARE_EQUAL(newVirtualBottom, si._virtualBottom); Log::Comment(L"The viewport itself should not have changed at this point"); @@ -6549,12 +7237,12 @@ void ScreenBufferTests::UpdateVirtualBottomWithSetConsoleCursorPosition() VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); Log::Comment(L"Pan further down so the viewport is below the cursor"); - const auto belowCursor = til::point{ 0, cursor.GetPosition().Y + 10 }; + const auto belowCursor = til::point{ 0, cursor.GetPosition().y + 10 }; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, belowCursor, false)); VERIFY_ARE_EQUAL(belowCursor, si.GetViewport().Origin()); Log::Comment(L"Set the cursor position one line down, still inside the virtual viewport"); - const auto oneLineDown = til::point{ 0, cursor.GetPosition().Y + 1 }; + const auto oneLineDown = til::point{ 0, cursor.GetPosition().y + 1 }; VERIFY_SUCCEEDED(g.api->SetConsoleCursorPositionImpl(si, oneLineDown)); VERIFY_ARE_EQUAL(oneLineDown, cursor.GetPosition()); @@ -6595,7 +7283,7 @@ void ScreenBufferTests::UpdateVirtualBottomAfterInternalSetViewportSize() Log::Comment(L"Set the cursor to the bottom of the current page"); stateMachine.ProcessString(L"\033[9999H"); - VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().y); Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); @@ -6603,7 +7291,7 @@ void ScreenBufferTests::UpdateVirtualBottomAfterInternalSetViewportSize() Log::Comment(L"Shrink the viewport height by two lines"); auto viewportSize = si.GetViewport().Dimensions(); - viewportSize.Y -= 2; + viewportSize.height -= 2; si._InternalSetViewportSize(&viewportSize, false, false); VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); @@ -6611,12 +7299,12 @@ void ScreenBufferTests::UpdateVirtualBottomAfterInternalSetViewportSize() VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); Log::Comment(L"Position the viewport just above the virtual bottom"); - auto viewportTop = si._virtualBottom - viewportSize.Y; + auto viewportTop = si._virtualBottom - viewportSize.height; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, false)); VERIFY_ARE_EQUAL(si._virtualBottom - 1, si.GetViewport().BottomInclusive()); Log::Comment(L"Expand the viewport height so it 'passes through' the virtual bottom"); - viewportSize.Y += 2; + viewportSize.height += 2; si._InternalSetViewportSize(&viewportSize, false, false); VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); @@ -6624,12 +7312,12 @@ void ScreenBufferTests::UpdateVirtualBottomAfterInternalSetViewportSize() VERIFY_ARE_EQUAL(si._virtualBottom, si.GetViewport().BottomInclusive()); Log::Comment(L"Position the viewport bottom just below the virtual bottom"); - viewportTop = si._virtualBottom - viewportSize.Y + 2; + viewportTop = si._virtualBottom - viewportSize.height + 2; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, false)); VERIFY_ARE_EQUAL(si._virtualBottom + 1, si.GetViewport().BottomInclusive()); Log::Comment(L"Shrink the viewport height so it 'passes through' the virtual bottom"); - viewportSize.Y -= 2; + viewportSize.height -= 2; si._InternalSetViewportSize(&viewportSize, false, false); VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); @@ -6648,8 +7336,8 @@ void ScreenBufferTests::UpdateVirtualBottomAfterResizeWithReflow() Log::Comment(L"Output a couple of pages of content"); auto bufferSize = si.GetTextBuffer().GetSize().Dimensions(); const auto viewportSize = si.GetViewport().Dimensions(); - const auto line = std::wstring(bufferSize.X - 1, L'X') + L'\n'; - for (auto i = 0; i < viewportSize.Y * 2; i++) + const auto line = std::wstring(bufferSize.width - 1, L'X') + L'\n'; + for (auto i = 0; i < viewportSize.height * 2; i++) { stateMachine.ProcessString(line); } @@ -6662,24 +7350,24 @@ void ScreenBufferTests::UpdateVirtualBottomAfterResizeWithReflow() VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); Log::Comment(L"Shrink the viewport width by a half"); - bufferSize.X /= 2; + bufferSize.width /= 2; VERIFY_NT_SUCCESS(si.ResizeWithReflow(bufferSize)); Log::Comment(L"Confirm that the virtual viewport includes the last non-space row"); - const auto lastNonSpaceRow = si.GetTextBuffer().GetLastNonSpaceCharacter().Y; + const auto lastNonSpaceRow = si.GetTextBuffer().GetLastNonSpaceCharacter().y; VERIFY_IS_GREATER_THAN_OR_EQUAL(si._virtualBottom, lastNonSpaceRow); Log::Comment(L"Clear the screen and note the cursor distance to the virtual bottom"); stateMachine.ProcessString(L"\033[H\033[2J"); - const auto cursorDistanceFromBottom = si._virtualBottom - si.GetTextBuffer().GetCursor().GetPosition().Y; + const auto cursorDistanceFromBottom = si._virtualBottom - si.GetTextBuffer().GetCursor().GetPosition().y; VERIFY_ARE_EQUAL(si.GetViewport().Height() - 1, cursorDistanceFromBottom); Log::Comment(L"Stretch the viewport back to full width"); - bufferSize.X *= 2; + bufferSize.width *= 2; VERIFY_NT_SUCCESS(si.ResizeWithReflow(bufferSize)); Log::Comment(L"Confirm cursor distance to the virtual bottom is unchanged"); - VERIFY_ARE_EQUAL(cursorDistanceFromBottom, si._virtualBottom - si.GetTextBuffer().GetCursor().GetPosition().Y); + VERIFY_ARE_EQUAL(cursorDistanceFromBottom, si._virtualBottom - si.GetTextBuffer().GetCursor().GetPosition().y); } void ScreenBufferTests::DontShrinkVirtualBottomDuringResizeWithReflowAtTop() @@ -6704,7 +7392,7 @@ void ScreenBufferTests::DontShrinkVirtualBottomDuringResizeWithReflowAtTop() Log::Comment(L"Shrink the viewport width by a half"); auto bufferSize = si.GetTextBuffer().GetSize().Dimensions(); - bufferSize.X /= 2; + bufferSize.width /= 2; VERIFY_NT_SUCCESS(si.ResizeWithReflow(bufferSize)); Log::Comment(L"Confirm that the virtual bottom is still at the bottom of the viewport"); @@ -6762,7 +7450,7 @@ void ScreenBufferTests::DontChangeVirtualBottomAfterResizeWindow() Log::Comment(L"Set the cursor to the bottom of the current page"); stateMachine.ProcessString(L"\033[9999H"); - VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().y); Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); @@ -6807,19 +7495,19 @@ void ScreenBufferTests::DontChangeVirtualBottomWithMakeCursorVisible() Log::Comment(L"Make the cursor visible"); si.MakeCurrentCursorVisible(); - VERIFY_ARE_EQUAL(si.GetViewport().BottomInclusive(), cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(si.GetViewport().BottomInclusive(), cursor.GetPosition().y); Log::Comment(L"Confirm that the virtual bottom has not changed"); VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); Log::Comment(L"Pan further down so the viewport is below the cursor"); - const auto belowCursor = til::point{ 0, cursor.GetPosition().Y + 10 }; + const auto belowCursor = til::point{ 0, cursor.GetPosition().y + 10 }; VERIFY_SUCCEEDED(si.SetViewportOrigin(true, belowCursor, false)); VERIFY_ARE_EQUAL(belowCursor, si.GetViewport().Origin()); Log::Comment(L"Make the cursor visible"); si.MakeCurrentCursorVisible(); - VERIFY_ARE_EQUAL(si.GetViewport().Top(), cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(si.GetViewport().Top(), cursor.GetPosition().y); Log::Comment(L"Confirm that the virtual bottom has not changed"); VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); @@ -6852,15 +7540,15 @@ void ScreenBufferTests::RetainHorizontalOffsetWhenMovingToBottom() VERIFY_SUCCEEDED(si.SetViewportOrigin(false, { 0, -10 }, false)); Log::Comment(L"Verify Y offset has moved up and X is unchanged"); - VERIFY_ARE_EQUAL(initialOrigin.Y - 10, si.GetViewport().Top()); - VERIFY_ARE_EQUAL(initialOrigin.X, si.GetViewport().Left()); + VERIFY_ARE_EQUAL(initialOrigin.y - 10, si.GetViewport().Top()); + VERIFY_ARE_EQUAL(initialOrigin.x, si.GetViewport().Left()); Log::Comment(L"Move the viewport back to the virtual bottom"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, si.GetVirtualViewport().Origin(), true)); Log::Comment(L"Verify Y offset has moved back and X is unchanged"); - VERIFY_ARE_EQUAL(initialOrigin.Y, si.GetViewport().Top()); - VERIFY_ARE_EQUAL(initialOrigin.X, si.GetViewport().Left()); + VERIFY_ARE_EQUAL(initialOrigin.y, si.GetViewport().Top()); + VERIFY_ARE_EQUAL(initialOrigin.x, si.GetViewport().Left()); } void ScreenBufferTests::TestWriteConsoleVTQuirkMode() @@ -6887,10 +7575,9 @@ void ScreenBufferTests::TestWriteConsoleVTQuirkMode() mainBuffer.SetAttributes(defaultAttribute); const auto verifyLastAttribute = [&](const TextAttribute& expected) { - const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(cursor.GetPosition().Y); - const auto attrRow = &row.GetAttrRow(); - auto iter{ attrRow->begin() }; - iter += cursor.GetPosition().X - 1; + const auto& row = mainBuffer.GetTextBuffer().GetRowByOffset(cursor.GetPosition().y); + auto iter{ row.AttrBegin() }; + iter += cursor.GetPosition().x - 1; VERIFY_ARE_EQUAL(expected, *iter); }; @@ -7274,8 +7961,8 @@ void ScreenBufferTests::TestDeferredMainBufferResize() const til::size oldSize{ siMain->GetBufferSize().Dimensions() }; const til::size oldView{ siMain->GetViewport().Dimensions() }; - Log::Comment(NoThrowString().Format(L"Original buffer size: %dx%d", oldSize.X, oldSize.Y)); - Log::Comment(NoThrowString().Format(L"Original viewport: %dx%d", oldView.X, oldView.Y)); + Log::Comment(NoThrowString().Format(L"Original buffer size: %dx%d", oldSize.width, oldSize.height)); + Log::Comment(NoThrowString().Format(L"Original viewport: %dx%d", oldView.width, oldView.height)); VERIFY_ARE_NOT_EQUAL(oldSize, oldView); // printf "\x1b[?1049h" ; sleep 1 ; printf "\x1b[8;24;60t" ; sleep 1 ; printf "\x1b[?1049l" ; sleep 1 ; printf "\n" @@ -7367,18 +8054,19 @@ 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); - // The area we're targetting in all the operations below is 27;3 to 54;6. + // The area we're targeting in all the operations below is 27;3 to 54;6. // But VT coordinates use origin 1;1 so we need to subtract 1, and til::rect // expects exclusive coordinates, so the bottom/right also need to add 1. const auto targetArea = til::rect{ 27 - 1, viewport.Top() + 3 - 1, 54, viewport.Top() + 6 }; @@ -7420,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"); @@ -7440,7 +8130,7 @@ void ScreenBufferTests::RectangularAreaOperations() _FillLines(viewport.Top() + 10, viewport.BottomExclusive(), expectedChar, expectedAttr); // Copy a rectangle from that lower part up to the top with DECCRA. stateMachine.ProcessString(L"\033[11;27;14;54;1;3;27;1;4$v"); - // Reset the lower part back to it's original content. + // Reset the lower part back to its original content. _FillLines(viewport.Top() + 10, viewport.BottomExclusive(), bufferChar, bufferAttr); break; default: @@ -7459,3 +8149,247 @@ void ScreenBufferTests::RectangularAreaOperations() VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.top, targetArea.bottom, bufferChars, bufferAttr)); VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.right, targetArea.top, targetArea.bottom, bufferChar, bufferAttr)); } + +void ScreenBufferTests::CopyDoubleWidthRectangularArea() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& textBuffer = si.GetTextBuffer(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + // Fill the entire buffer with Zs. Blue on Green and Underlined. + const auto bufferChar = L'Z'; + const auto bufferHeight = si.GetBufferSize().Height(); + auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; + bufferAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + _FillLines(0, bufferHeight, bufferChar, bufferAttr); + + // Fill the first three lines with Cs. Green on Red and Intense. + const auto copyChar = L'C'; + auto copyAttr = TextAttribute{ FOREGROUND_GREEN | BACKGROUND_RED }; + copyAttr.SetIntense(true); + _FillLines(0, 3, copyChar, copyAttr); + + // Set the active attributes to Red on Blue; + auto activeAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + si.SetAttributes(activeAttr); + + // Make the second line (offset 1) double width. + textBuffer.GetCursor().SetPosition({ 0, 1 }); + textBuffer.SetCurrentLineRendition(LineRendition::DoubleWidth, activeAttr); + + // Copy a segment of the top three lines with DECCRA. + stateMachine.ProcessString(L"\033[1;31;3;50;1;4;31;1$v"); + + Log::Comment(L"The first 30 columns of the target area should remain unchanged"); + const auto thirtyBufferChars = std::wstring(30, bufferChar); + VERIFY_IS_TRUE(_ValidateLinesContain(3, 6, thirtyBufferChars, bufferAttr)); + + Log::Comment(L"The next 20 columns should contain the copied content"); + const auto twentyCopyChars = std::wstring(20, copyChar); + VERIFY_IS_TRUE(_ValidateLineContains({ 30, 3 }, twentyCopyChars, copyAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 30, 5 }, twentyCopyChars, copyAttr)); + + Log::Comment(L"But the second target row ends after 10, because of the double-width source"); + const auto tenCopyChars = std::wstring(10, copyChar); + VERIFY_IS_TRUE(_ValidateLineContains({ 30, 4 }, tenCopyChars, copyAttr)); + + Log::Comment(L"The subsequent columns in each row should remain unchanged"); + VERIFY_IS_TRUE(_ValidateLineContains({ 50, 3 }, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 40, 4 }, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLineContains({ 50, 5 }, bufferChar, bufferAttr)); +} + +void ScreenBufferTests::DelayedWrapReset() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + TEST_METHOD_PROPERTY(L"Data:op", L"{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35}") + END_TEST_METHOD_PROPERTIES(); + + int opIndex; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", opIndex)); + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto width = si.GetTextBuffer().GetSize().Width(); + const auto halfWidth = width / 2; + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); + + const auto startRow = 5; + const auto startCol = width - 1; + + // The operations below are all those that the DEC STD 070 reference has + // documented as needing to reset the Last Column Flag (see page D-13). The + // only controls that we haven't included are HT and SUB, because most DEC + // terminals did *not* trigger a reset after executing those sequences, and + // most modern terminals also seem to have agreed that that is the correct + // approach to take. + const struct + { + std::wstring_view name; + std::wstring_view sequence; + til::point expectedPos = {}; + bool absolutePos = false; + } ops[] = { + { L"DECSTBM", L"\033[1;10r", { 0, 0 }, true }, + { L"DECSLRM", L"\033[1;10s", { 0, 0 }, true }, + { L"DECSWL", L"\033#5" }, + { L"DECDWL", L"\033#6", { halfWidth - 1, startRow }, true }, + { L"DECDHL (top)", L"\033#3", { halfWidth - 1, startRow }, true }, + { L"DECDHL (bottom)", L"\033#4", { halfWidth - 1, startRow }, true }, + { L"DECCOLM set", L"\033[?3h", { 0, 0 }, true }, + { L"DECOM set", L"\033[?6h", { 0, 0 }, true }, + { L"DECCOLM reset", L"\033[?3l", { 0, 0 }, true }, + { L"DECOM reset", L"\033[?6l", { 0, 0 }, true }, + { L"DECAWM reset", L"\033[?7l" }, + { L"CUU", L"\033[A", { 0, -1 } }, + { L"CUD", L"\033[B", { 0, 1 } }, + { L"CUF", L"\033[C" }, + { L"CUB", L"\033[D", { -1, 0 } }, + { L"CUP", L"\033[3;7H", { 6, 2 }, true }, + { L"HVP", L"\033[3;7f", { 6, 2 }, true }, + { L"BS", L"\b", { -1, 0 } }, + { L"LF", L"\n", { 0, 1 } }, + { L"VT", L"\v", { 0, 1 } }, + { L"FF", L"\f", { 0, 1 } }, + { L"CR", L"\r", { 0, startRow }, true }, + { L"IND", L"\033D", { 0, 1 } }, + { L"RI", L"\033M", { 0, -1 } }, + { L"NEL", L"\033E", { 0, startRow + 1 }, true }, + { L"ECH", L"\033[X" }, + { L"DCH", L"\033[P" }, + { L"ICH", L"\033[@" }, + { L"EL", L"\033[K" }, + { L"DECSEL", L"\033[?K" }, + { L"DL", L"\033[M", { 0, startRow }, true }, + { L"IL", L"\033[L", { 0, startRow }, true }, + { L"ED", L"\033[J" }, + { L"ED (all)", L"\033[2J" }, + { L"ED (scrollback)", L"\033[3J" }, + { L"DECSED", L"\033[?J" }, + }; + const auto& op = gsl::at(ops, opIndex); + + if (op.name == L"DECSLRM") + { + // Private mode 69 makes sure DECSLRM is allowed + stateMachine.ProcessString(L"\033[?69h"); + } + if (op.name.starts_with(L"DECCOLM")) + { + // Private mode 40 makes sure DECCOLM is allowed + stateMachine.ProcessString(L"\033[?40h"); + } + + Log::Comment(L"Writing a character at the end of the line should set delayed EOL wrap"); + const auto startPos = til::point{ startCol, startRow }; + si.GetTextBuffer().GetCursor().SetPosition(startPos); + stateMachine.ProcessCharacter(L'X'); + { + auto& cursor = si.GetTextBuffer().GetCursor(); + VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + VERIFY_ARE_EQUAL(startPos, cursor.GetPosition()); + } + + Log::Comment(NoThrowString().Format( + L"Executing a %s control should reset delayed EOL wrap", + op.name.data())); + const auto expectedPos = op.absolutePos ? op.expectedPos : startPos + op.expectedPos; + stateMachine.ProcessString(op.sequence); + { + auto& cursor = si.GetTextBuffer().GetCursor(); + const auto actualPos = cursor.GetPosition() - si.GetViewport().Origin(); + VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap()); + VERIFY_ARE_EQUAL(expectedPos, actualPos); + } +} + +void ScreenBufferTests::EraseColorMode() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:op", L"{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}") + TEST_METHOD_PROPERTY(L"Data:eraseMode", L"{false,true}") + END_TEST_METHOD_PROPERTIES(); + + bool eraseMode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseMode", eraseMode)); + int opIndex; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", opIndex)); + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); + + const auto bufferWidth = si.GetBufferSize().Width(); + const auto bufferHeight = si.GetBufferSize().Height(); + + // Set the viewport to 24 lines. + si.SetViewport(Viewport::FromDimensions({ 0, 0 }, { bufferWidth, 24 }), true); + const auto viewport = si.GetViewport(); + + // Fill the buffer with text. Red on Blue. + const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn"; + const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLines(0, bufferHeight, bufferChars, bufferAttr); + + // Set the active attributes with a mix of color and meta attributes. + auto activeAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + activeAttr.SetCrossedOut(true); + activeAttr.SetReverseVideo(true); + activeAttr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + si.SetAttributes(activeAttr); + + // By default, the meta attributes are expected to be cleared when erasing. + auto standardEraseAttr = activeAttr; + standardEraseAttr.SetStandardErase(); + + const struct + { + std::wstring_view name; + std::wstring_view sequence; + til::point startPos = {}; + til::point erasePos = {}; + } ops[] = { + { L"LF", L"\n", { 0, 23 }, { 0, 24 } }, // pans down on last row + { L"VT", L"\v", { 0, 23 }, { 0, 24 } }, // pans down on last row + { L"FF", L"\f", { 0, 23 }, { 0, 24 } }, // pans down on last row + { L"NEL", L"\033E", { 0, 23 }, { 0, 24 } }, // pans down on last row + { L"IND", L"\033D", { 0, 23 }, { 0, 24 } }, // pans down on last row + { L"RI", L"\033M" }, + { L"DECBI", L"\033\066" }, + { L"DECFI", L"\033\071", { 79, 0 }, { 79, 0 } }, // scrolls in last column + { L"DECIC", L"\033['}" }, + { L"DECDC", L"\033['~", {}, { 79, 0 } }, // last column erased + { L"ICH", L"\033[@" }, + { L"DCH", L"\033[P", {}, { 79, 0 } }, // last column erased + { L"IL", L"\033[L" }, + { L"DL", L"\033[M", {}, { 0, 23 } }, // last row erased + { L"ECH", L"\033[X" }, + { L"EL", L"\033[K" }, + { L"ED", L"\033[J" }, + { L"DECERA", L"\033[$z" }, + { L"SU", L"\033[S", {}, { 0, 23 } }, // last row erased + { L"SD", L"\033[T" }, + }; + const auto& op = gsl::at(ops, opIndex); + + si.GetTextBuffer().GetCursor().SetPosition(op.startPos); + + Log::Comment(eraseMode ? L"Enable DECECM" : L"Disable DECECM"); + stateMachine.ProcessString(eraseMode ? L"\033[?117h" : L"\033[?117l"); + + Log::Comment(NoThrowString().Format(L"Execute %s", op.name.data())); + stateMachine.ProcessString(op.sequence); + + Log::Comment(L"Verify expected erase attributes"); + const auto expectedEraseAttr = eraseMode ? TextAttribute{} : standardEraseAttr; + const auto cellData = si.GetCellDataAt(op.erasePos); + VERIFY_ARE_EQUAL(expectedEraseAttr, cellData->TextAttr()); + VERIFY_ARE_EQUAL(L" ", cellData->Chars()); +} diff --git a/src/host/ut_host/SearchTests.cpp b/src/host/ut_host/SearchTests.cpp index 4e784349c4..5a2e4b9bd6 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; + 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); + coordStartExpected.y += lineDelta; + coordEndExpected.y += lineDelta; + s.FindNext(); - coordStartExpected.Y += lineDelta; - coordEndExpected.Y += lineDelta; - VERIFY_IS_TRUE(s.FindNext()); - VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); - VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + VERIFY_IS_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); + coordStartExpected.y += lineDelta; + coordEndExpected.y += lineDelta; + 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()); + + coordStartExpected.y += lineDelta; + coordEndExpected.y += lineDelta; + 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/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp index d8d7a6aaf8..f33889992b 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" @@ -62,7 +61,7 @@ class SelectionTests void VerifyGetSelectionRects_BoxMode() { const auto selectionRects = m_pSelection->GetSelectionRects(); - const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1; + const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.bottom - m_pSelection->_srSelectionRect.top + 1; if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size())) { @@ -71,13 +70,13 @@ class SelectionTests // ensure each rectangle is exactly the width requested (block selection) const auto psrRect = &selectionRects[iRect]; - const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_srSelectionRect.Top; + const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_srSelectionRect.top; - VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber); - VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->top, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->bottom, sRectangleLineNumber); - VERIFY_ARE_EQUAL(psrRect->Left, m_pSelection->_srSelectionRect.Left); - VERIFY_ARE_EQUAL(psrRect->Right, m_pSelection->_srSelectionRect.Right); + VERIFY_ARE_EQUAL(psrRect->left, m_pSelection->_srSelectionRect.left); + VERIFY_ARE_EQUAL(psrRect->right, m_pSelection->_srSelectionRect.right); } } } @@ -87,14 +86,14 @@ class SelectionTests m_pSelection->_fSelectionVisible = true; // set selection region - m_pSelection->_srSelectionRect.Top = 0; - m_pSelection->_srSelectionRect.Bottom = 3; - m_pSelection->_srSelectionRect.Left = 1; - m_pSelection->_srSelectionRect.Right = 10; + m_pSelection->_srSelectionRect.top = 0; + m_pSelection->_srSelectionRect.bottom = 3; + m_pSelection->_srSelectionRect.left = 1; + m_pSelection->_srSelectionRect.right = 10; // #1 top-left to bottom right selection first - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.left; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.top; // A. false/false for the selection modes should mean box selection m_pSelection->_fLineSelection = false; @@ -110,20 +109,20 @@ class SelectionTests // now try the other 3 configurations of box region. // #2 top-right to bottom-left selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.right; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.top; VerifyGetSelectionRects_BoxMode(); // #3 bottom-left to top-right selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.left; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.bottom; VerifyGetSelectionRects_BoxMode(); // #4 bottom-right to top-left selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.right; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.bottom; VerifyGetSelectionRects_BoxMode(); } @@ -133,7 +132,7 @@ class SelectionTests const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto selectionRects = m_pSelection->GetSelectionRects(); - const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1; + const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.bottom - m_pSelection->_srSelectionRect.top + 1; if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size())) { @@ -152,15 +151,15 @@ class SelectionTests if (fHaveOneLine) { auto srSelectionRect = m_pSelection->_srSelectionRect; - VERIFY_ARE_EQUAL(srSelectionRect.Top, srSelectionRect.Bottom); + VERIFY_ARE_EQUAL(srSelectionRect.top, srSelectionRect.bottom); const auto psrRect = &selectionRects[0]; - VERIFY_ARE_EQUAL(psrRect->Top, srSelectionRect.Top); - VERIFY_ARE_EQUAL(psrRect->Bottom, srSelectionRect.Bottom); + VERIFY_ARE_EQUAL(psrRect->top, srSelectionRect.top); + VERIFY_ARE_EQUAL(psrRect->bottom, srSelectionRect.bottom); - VERIFY_ARE_EQUAL(psrRect->Left, srSelectionRect.Left); - VERIFY_ARE_EQUAL(psrRect->Right, srSelectionRect.Right); + VERIFY_ARE_EQUAL(psrRect->left, srSelectionRect.left); + VERIFY_ARE_EQUAL(psrRect->right, srSelectionRect.right); } else { @@ -170,10 +169,10 @@ class SelectionTests // ensure each rectangle is exactly the width requested (block selection) const auto psrRect = &selectionRects[iRect]; - const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_srSelectionRect.Top; + const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_srSelectionRect.top; - VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber); - VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->top, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->bottom, sRectangleLineNumber); auto fIsFirstLine = iRect == 0; auto fIsLastLine = iRect == selectionRects.size() - 1; @@ -182,13 +181,13 @@ class SelectionTests if (!fIsLastLine) { // buffer size = 80, then selection goes 0 to 79. Thus X - 1. - VERIFY_ARE_EQUAL(psrRect->Right, gci.GetActiveOutputBuffer().GetTextBuffer().GetSize().RightInclusive()); + VERIFY_ARE_EQUAL(psrRect->right, gci.GetActiveOutputBuffer().GetTextBuffer().GetSize().RightInclusive()); } // for all lines except the first, the line should reach the left edge of the buffer if (!fIsFirstLine) { - VERIFY_ARE_EQUAL(psrRect->Left, 0); + VERIFY_ARE_EQUAL(psrRect->left, 0); } } @@ -205,11 +204,11 @@ class SelectionTests auto coordAnchor = m_pSelection->_coordSelectionAnchor; // if the anchor is in the top right or bottom left corner, we must have removed a region. otherwise, it stays as is. - if (coordAnchor.Y == srSelectionRect.Top && coordAnchor.X == srSelectionRect.Right) + if (coordAnchor.y == srSelectionRect.top && coordAnchor.x == srSelectionRect.right) { fRemoveRegion = true; } - else if (coordAnchor.Y == srSelectionRect.Bottom && coordAnchor.X == srSelectionRect.Left) + else if (coordAnchor.y == srSelectionRect.bottom && coordAnchor.x == srSelectionRect.left) { fRemoveRegion = true; } @@ -217,21 +216,21 @@ class SelectionTests // now check the first row's left based on removal if (!fRemoveRegion) { - VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Left); + VERIFY_ARE_EQUAL(psrFirst->left, srSelectionRect.left); } else { - VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Right); + VERIFY_ARE_EQUAL(psrFirst->left, srSelectionRect.right); } // and the last row's right based on removal if (!fRemoveRegion) { - VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Right); + VERIFY_ARE_EQUAL(psrLast->right, srSelectionRect.right); } else { - VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Left); + VERIFY_ARE_EQUAL(psrLast->right, srSelectionRect.left); } } } @@ -243,14 +242,14 @@ class SelectionTests // Part I: Multiple line selection // set selection region - m_pSelection->_srSelectionRect.Top = 0; - m_pSelection->_srSelectionRect.Bottom = 3; - m_pSelection->_srSelectionRect.Left = 1; - m_pSelection->_srSelectionRect.Right = 10; + m_pSelection->_srSelectionRect.top = 0; + m_pSelection->_srSelectionRect.bottom = 3; + m_pSelection->_srSelectionRect.left = 1; + m_pSelection->_srSelectionRect.right = 10; // #1 top-left to bottom right selection first - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.left; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.top; // A. true/false for the selection modes should mean line selection m_pSelection->_fLineSelection = true; @@ -266,40 +265,40 @@ class SelectionTests // now try the other 3 configurations of box region. // #2 top-right to bottom-left selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.right; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.top; VerifyGetSelectionRects_LineMode(); // #3 bottom-left to top-right selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.left; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.bottom; VerifyGetSelectionRects_LineMode(); // #4 bottom-right to top-left selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.right; + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.bottom; VerifyGetSelectionRects_LineMode(); // Part II: Single line selection - m_pSelection->_srSelectionRect.Top = 2; - m_pSelection->_srSelectionRect.Bottom = 2; - m_pSelection->_srSelectionRect.Left = 1; - m_pSelection->_srSelectionRect.Right = 10; + m_pSelection->_srSelectionRect.top = 2; + m_pSelection->_srSelectionRect.bottom = 2; + m_pSelection->_srSelectionRect.left = 1; + m_pSelection->_srSelectionRect.right = 10; // #1: left to right selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; - VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top); - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.left; + VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.bottom == m_pSelection->_srSelectionRect.top); + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.bottom; VerifyGetSelectionRects_LineMode(); // #2: right to left selection - m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; - VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top); - m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + m_pSelection->_coordSelectionAnchor.x = m_pSelection->_srSelectionRect.right; + VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.bottom == m_pSelection->_srSelectionRect.top); + m_pSelection->_coordSelectionAnchor.y = m_pSelection->_srSelectionRect.top; VerifyGetSelectionRects_LineMode(); } @@ -315,21 +314,21 @@ class SelectionTests til::inclusive_rect srOriginal; sStringLength = sLength; - coordTargetPoint.X = sTargetX; - coordTargetPoint.Y = sTargetY; + coordTargetPoint.x = sTargetX; + coordTargetPoint.y = sTargetY; // selection area is always one row at a time so top/bottom = Y = row position - srSelection.Top = srSelection.Bottom = coordTargetPoint.Y; + srSelection.top = srSelection.bottom = coordTargetPoint.y; // selection rectangle starts from the target and goes for the length requested - srSelection.Left = coordTargetPoint.X; - srSelection.Right = coordTargetPoint.X + sStringLength; + srSelection.left = coordTargetPoint.x; + srSelection.right = coordTargetPoint.x + sStringLength; // save original for comparison - srOriginal.Top = srSelection.Top; - srOriginal.Bottom = srSelection.Bottom; - srOriginal.Left = srSelection.Left; - srOriginal.Right = srSelection.Right; + srOriginal.top = srSelection.top; + srOriginal.bottom = srSelection.bottom; + srOriginal.left = srSelection.left; + srOriginal.right = srSelection.right; til::point startPos{ sTargetX, sTargetY }; til::point endPos{ sTargetX + sLength, sTargetY }; @@ -338,72 +337,10 @@ class SelectionTests VERIFY_ARE_EQUAL(static_cast(1), selectionRects.size()); srSelection = selectionRects.at(0); - VERIFY_ARE_EQUAL(srOriginal.Top, srSelection.Top); - VERIFY_ARE_EQUAL(srOriginal.Bottom, srSelection.Bottom); - VERIFY_ARE_EQUAL(srOriginal.Left + sDeltaLeft, srSelection.Left); - VERIFY_ARE_EQUAL(srOriginal.Right + sDeltaRight, srSelection.Right); - } - - TEST_METHOD(TestBisectSelection) - { - m_state->FillTextBufferBisect(); - - // From CommonState, this is what rows look like: - // positions of き are at 0, 27-28, 39-40, 67-68, 79 - // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き - // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き - // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き - // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き - - // 1a. Start position is trailing half and is at beginning of row - - // start from position Column 0, Row 2 - // selection is 5 characters long - // the left edge should move one to the right (+1) to not select the trailing byte - // right edge shouldn't move - TestBisectSelectionDelta(0, 2, 5, 1, 0); - - // 1b. Start position is trailing half and is elsewhere in the row - - // start from position Column 28, Row 2, which is the position of a trailing き in the mid row - // selection is 5 characters long - // the left edge should move one to the left (-1) to select the leading byte also - // right edge shouldn't move - TestBisectSelectionDelta(28, 2, 5, -1, 0); - - // 1c. Start position is trailing half and is beginning of buffer - - // start from position Column 0, Row 0 which is a trailing byte - // selection is 5 characters long - // the left edge should move one to the right (+1) to not select the trailing byte - // right edge shouldn't move - TestBisectSelectionDelta(0, 0, 5, 1, 0); - - // 2a. End position is leading half and is at end of row - - // start from position 10 before end of row (80 length row) - // row is 2 - // selection is 9 characters long - // the left edge shouldn't move - // the right edge should move one to the left (-1) to not select the leading byte - TestBisectSelectionDelta(70, 2, 9, 0, -1); - - // 2b. End position is leading half and is elsewhere in the row - - // start from 10 before trailing き in the mid row (pos 68 - 10 = 58) - // row is 2 - // selection is 10 characters long - // the left edge shouldn't move - // the right edge should not move, because it is already on the trailing byte - TestBisectSelectionDelta(58, 2, 10, 0, 0); - - // 2c. End position is leading half and is at end of buffer - // start from position 10 before end of row (80 length row) - // row is 300 (or 299 for the index) - // selection is 9 characters long - // the left edge shouldn't move - // the right edge should move one to the left (-1) to not select the leading byte - TestBisectSelectionDelta(70, 299, 9, 0, -1); + VERIFY_ARE_EQUAL(srOriginal.top, srSelection.top); + VERIFY_ARE_EQUAL(srOriginal.bottom, srSelection.bottom); + VERIFY_ARE_EQUAL(srOriginal.left + sDeltaLeft, srSelection.left); + VERIFY_ARE_EQUAL(srOriginal.right + sDeltaRight, srSelection.right); } }; @@ -444,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() @@ -564,14 +418,14 @@ class SelectionInputTests do { - resultExpected.X--; - } while (resultExpected.X > 0 && text.at(resultExpected.X - 1) != UNICODE_SPACE); + resultExpected.x--; + } while (resultExpected.x > 0 && text.at(resultExpected.x - 1) != UNICODE_SPACE); point = sel.WordByWordSelection(true, bufferSize, anchor, point); VERIFY_ARE_EQUAL(resultExpected, point); - } while (point.X > left.X); + } while (point.x > left.x); } TEST_METHOD(TestWordByWordNext) @@ -611,22 +465,22 @@ class SelectionInputTests do { - resultExpected.X++; - } while (resultExpected.X + 1 < right.X && text.at(resultExpected.X + 1) != UNICODE_SPACE); - resultExpected.X++; + resultExpected.x++; + } while (resultExpected.x + 1 < right.x && text.at(resultExpected.x + 1) != UNICODE_SPACE); + resultExpected.x++; // when we reach the end, word by word selection will seek forward to the end of the buffer, so update // the expected to the end in that circumstance - if (resultExpected.X >= right.X) + if (resultExpected.x >= right.x) { - resultExpected.X = bufferSize.RightInclusive(); - resultExpected.Y = bufferSize.BottomInclusive(); + resultExpected.x = bufferSize.RightInclusive(); + resultExpected.y = bufferSize.BottomInclusive(); } point = sel.WordByWordSelection(false, bufferSize, anchor, point); VERIFY_ARE_EQUAL(resultExpected, point); - } while (point.Y < bufferSize.BottomInclusive()); // stop once we've advanced to a point on the bottom row of the buffer. + } while (point.y < bufferSize.BottomInclusive()); // stop once we've advanced to a point on the bottom row of the buffer. } }; diff --git a/src/host/ut_host/TextBufferIteratorTests.cpp b/src/host/ut_host/TextBufferIteratorTests.cpp index e7e9a149d2..52026b71e0 100644 --- a/src/host/ut_host/TextBufferIteratorTests.cpp +++ b/src/host/ut_host/TextBufferIteratorTests.cpp @@ -11,7 +11,6 @@ #include "../buffer/out/textBuffer.hpp" #include "../buffer/out/textBufferCellIterator.hpp" #include "../buffer/out/textBufferTextIterator.hpp" -#include "../buffer/out/CharRow.hpp" #include "input.h" @@ -145,7 +144,7 @@ class TextBufferIteratorTests const auto it = GetIterator(); auto oneOff = it._pos; - oneOff.X++; + oneOff.x++; const auto it2 = GetIteratorAt(oneOff); VERIFY_ARE_NOT_EQUAL(it, it2); @@ -161,7 +160,7 @@ class TextBufferIteratorTests ptrdiff_t diffUnit = 3; auto expectedPos = it._pos; - expectedPos.X += gsl::narrow(diffUnit); + expectedPos.x += gsl::narrow(diffUnit); const auto itExpected = GetIteratorAt(expectedPos); it += diffUnit; @@ -179,7 +178,7 @@ class TextBufferIteratorTests ptrdiff_t diffUnit = 3; til::point pos = itExpected._pos; - pos.X += gsl::narrow(diffUnit); + pos.x += gsl::narrow(diffUnit); auto itOffset = GetIteratorAt(pos); itOffset -= diffUnit; @@ -196,7 +195,7 @@ class TextBufferIteratorTests auto itActual = GetIterator(); til::point expectedPos = itActual._pos; - expectedPos.X++; + expectedPos.x++; const auto itExpected = GetIteratorAt(expectedPos); ++itActual; @@ -213,7 +212,7 @@ class TextBufferIteratorTests const auto itExpected = GetIteratorWithAdvance(); til::point pos = itExpected._pos; - pos.X++; + pos.x++; auto itActual = GetIteratorAt(pos); --itActual; @@ -230,7 +229,7 @@ class TextBufferIteratorTests auto it = GetIterator(); auto expectedPos = it._pos; - expectedPos.X++; + expectedPos.x++; const auto itExpected = GetIteratorAt(expectedPos); ++it; @@ -247,7 +246,7 @@ class TextBufferIteratorTests const auto itExpected = GetIteratorWithAdvance(); til::point pos = itExpected._pos; - pos.X++; + pos.x++; auto itActual = GetIteratorAt(pos); itActual--; @@ -265,7 +264,7 @@ class TextBufferIteratorTests ptrdiff_t diffUnit = 3; auto expectedPos = it._pos; - expectedPos.X += gsl::narrow(diffUnit); + expectedPos.x += gsl::narrow(diffUnit); const auto itExpected = GetIteratorAt(expectedPos); const auto itActual = it + diffUnit; @@ -283,7 +282,7 @@ class TextBufferIteratorTests ptrdiff_t diffUnit = 3; til::point pos = itExpected._pos; - pos.X += gsl::narrow(diffUnit); + pos.x += gsl::narrow(diffUnit); auto itOffset = GetIteratorAt(pos); const auto itActual = itOffset - diffUnit; @@ -329,7 +328,7 @@ void TextBufferIteratorTests::BoolOperatorCell() Log::Comment(L"For cells, also check incrementing past the end."); const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); const auto size = outputBuffer.GetBufferSize().Dimensions(); - TextBufferCellIterator it(outputBuffer.GetTextBuffer(), { size.X - 1, size.Y - 1 }); + TextBufferCellIterator it(outputBuffer.GetTextBuffer(), { size.width - 1, size.height - 1 }); VERIFY_IS_TRUE(it); it++; VERIFY_IS_FALSE(it); @@ -453,10 +452,10 @@ void TextBufferIteratorTests::AsCharInfoCell() const auto& outputBuffer = gci.GetActiveOutputBuffer(); - const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.y); - const auto wcharExpected = *row.GetCharRow().GlyphAt(it._pos.X).begin(); - const auto attrExpected = row.GetAttrRow().GetAttrByColumn(it._pos.X); + const auto wcharExpected = *row.GlyphAt(it._pos.x).begin(); + const auto attrExpected = row.GetAttrByColumn(it._pos.x); const auto cellActual = gci.AsCharInfo(*it); const auto wcharActual = cellActual.Char.UnicodeChar; @@ -473,9 +472,9 @@ void TextBufferIteratorTests::DereferenceOperatorText() const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); - const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.y); - const auto wcharExpected = row.GetCharRow().GlyphAt(it._pos.X); + const auto wcharExpected = row.GlyphAt(it._pos.x); const auto wcharActual = *it; VERIFY_ARE_EQUAL(*wcharExpected.begin(), *wcharActual.begin()); @@ -488,11 +487,11 @@ void TextBufferIteratorTests::DereferenceOperatorCell() const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); - const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.y); - const auto textExpected = (std::wstring_view)row.GetCharRow().GlyphAt(it._pos.X); - const auto dbcsExpected = row.GetCharRow().DbcsAttrAt(it._pos.X); - const auto attrExpected = row.GetAttrRow().GetAttrByColumn(it._pos.X); + const auto textExpected = (std::wstring_view)row.GlyphAt(it._pos.x); + const auto dbcsExpected = row.DbcsAttrAt(it._pos.x); + const auto attrExpected = row.GetAttrByColumn(it._pos.x); const auto cellActual = *it; const auto textActual = cellActual.Chars(); @@ -541,15 +540,15 @@ void TextBufferIteratorTests::ConstructedLimits() const auto& textBuffer = outputBuffer.GetTextBuffer(); til::inclusive_rect limits; - limits.Top = 1; - limits.Bottom = 1; - limits.Left = 3; - limits.Right = 5; + limits.top = 1; + limits.bottom = 1; + limits.left = 3; + limits.right = 5; const auto viewport = Microsoft::Console::Types::Viewport::FromInclusive(limits); til::point pos; - pos.X = limits.Left; - pos.Y = limits.Top; + pos.x = limits.left; + pos.y = limits.top; TextBufferCellIterator it(textBuffer, pos, viewport); diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index eae3b06965..d1c7395635 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -2,6 +2,9 @@ // Licensed under the MIT license. #include "precomp.h" + +#include + #include "WexTestClass.h" #include "../inc/consoletaeftemplates.hpp" @@ -9,7 +12,6 @@ #include "globals.h" #include "../buffer/out/textBuffer.hpp" -#include "../buffer/out/CharRow.hpp" #include "input.h" #include "_stream.h" @@ -73,8 +75,6 @@ class TextBufferTests til::CoordType GetBufferHeight(); - TEST_METHOD(TestBufferRowByOffset); - TEST_METHOD(TestWrapFlag); TEST_METHOD(TestWrapThroughWriteLine); @@ -87,8 +87,9 @@ class TextBufferTests const til::CoordType cLeft, const til::CoordType cRight); + TEST_METHOD(TestBoundaryMeasuresEmptyString); + TEST_METHOD(TestBoundaryMeasuresFullString); TEST_METHOD(TestBoundaryMeasuresRegularString); - TEST_METHOD(TestBoundaryMeasuresFloatingString); TEST_METHOD(TestCopyProperties); @@ -145,6 +146,8 @@ class TextBufferTests TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval); TEST_METHOD(TestBurrito); + TEST_METHOD(TestOverwriteChars); + TEST_METHOD(TestRowReplaceText); TEST_METHOD(TestAppendRTFText); @@ -181,24 +184,11 @@ til::CoordType TextBufferTests::GetBufferHeight() return GetTbi().GetSize().Height(); } -void TextBufferTests::TestBufferRowByOffset() -{ - auto& textBuffer = GetTbi(); - auto csBufferHeight = GetBufferHeight(); - - VERIFY_IS_TRUE(csBufferHeight > 20); - - auto sId = csBufferHeight / 2 - 5; - - const auto& row = textBuffer.GetRowByOffset(sId); - VERIFY_ARE_EQUAL(row.GetId(), sId); -} - void TextBufferTests::TestWrapFlag() { auto& textBuffer = GetTbi(); - auto& Row = textBuffer._GetFirstRow(); + auto& Row = textBuffer.GetMutableRowByOffset(0); // no wrap by default VERIFY_IS_FALSE(Row.WasWrapForced()); @@ -217,7 +207,7 @@ void TextBufferTests::TestWrapThroughWriteLine() auto& textBuffer = GetTbi(); auto VerifyWrap = [&](bool expected) { - auto& Row = textBuffer._GetFirstRow(); + auto& Row = textBuffer.GetRowByOffset(0); if (expected) { @@ -288,7 +278,7 @@ void TextBufferTests::TestDoubleBytePadFlag() { auto& textBuffer = GetTbi(); - auto& Row = textBuffer._GetFirstRow(); + auto& Row = textBuffer.GetMutableRowByOffset(0); // no padding by default VERIFY_IS_FALSE(Row.WasDoubleBytePadded()); @@ -310,12 +300,12 @@ void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString, { auto& textBuffer = GetTbi(); - auto& charRow = textBuffer._GetFirstRow().GetCharRow(); + auto& row = textBuffer.GetMutableRowByOffset(0); // copy string into buffer for (til::CoordType i = 0; i < cLength; ++i) { - charRow.GlyphAt(i) = { &pwszInputString[i], 1 }; + row.ReplaceCharacters(i, 1, { &pwszInputString[i], 1 }); } // space pad the rest of the string @@ -323,19 +313,37 @@ void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString, { for (auto cStart = cLength; cStart < cMax; cStart++) { - charRow.ClearGlyph(cStart); + row.ClearCell(cStart); } } // left edge should be 0 since there are no leading spaces - VERIFY_ARE_EQUAL(charRow.MeasureLeft(), cLeft); + VERIFY_ARE_EQUAL(row.MeasureLeft(), cLeft); // right edge should be one past the index of the last character or the string length - VERIFY_ARE_EQUAL(charRow.MeasureRight(), cRight); + VERIFY_ARE_EQUAL(row.MeasureRight(), cRight); +} + +void TextBufferTests::TestBoundaryMeasuresEmptyString() +{ + const auto csBufferWidth = GetBufferWidth(); + + // length 0, left 80, right 0 + const auto pwszLazyDog = L""; + DoBoundaryTest(pwszLazyDog, 0, csBufferWidth, 80, 0); +} + +void TextBufferTests::TestBoundaryMeasuresFullString() +{ + const auto csBufferWidth = GetBufferWidth(); + + // length 0, left 80, right 0 + const std::wstring str(csBufferWidth, L'X'); + DoBoundaryTest(str.data(), csBufferWidth, csBufferWidth, 0, 80); } void TextBufferTests::TestBoundaryMeasuresRegularString() { - auto csBufferWidth = GetBufferWidth(); + const auto csBufferWidth = GetBufferWidth(); // length 44, left 0, right 44 const auto pwszLazyDog = L"The quick brown fox jumps over the lazy dog."; @@ -344,7 +352,7 @@ void TextBufferTests::TestBoundaryMeasuresRegularString() void TextBufferTests::TestBoundaryMeasuresFloatingString() { - auto csBufferWidth = GetBufferWidth(); + const auto csBufferWidth = GetBufferWidth(); // length 5 spaces + 4 chars + 5 spaces = 14, left 5, right 9 const auto pwszOffsets = L" C:\\> "; @@ -397,23 +405,20 @@ void TextBufferTests::TestInsertCharacter() const auto coordCursorBefore = textBuffer.GetCursor().GetPosition(); // Get current row from the buffer - auto& Row = textBuffer.GetRowByOffset(coordCursorBefore.Y); + auto& Row = textBuffer.GetRowByOffset(coordCursorBefore.y); // create some sample test data const auto wch = L'Z'; const std::wstring_view wchTest(&wch, 1); - DbcsAttribute dbcsAttribute; - dbcsAttribute.SetTrailing(); + const auto dbcsAttribute = DbcsAttribute::Leading; const auto wAttrTest = BACKGROUND_INTENSITY | FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; auto TestAttributes = TextAttribute(wAttrTest); - auto& charRow = Row.GetCharRow(); - charRow.DbcsAttrAt(coordCursorBefore.X).SetLeading(); // ensure that the buffer didn't start with these fields - VERIFY_ARE_NOT_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); - VERIFY_ARE_NOT_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); + VERIFY_ARE_NOT_EQUAL(Row.GlyphAt(coordCursorBefore.x), wchTest); + VERIFY_ARE_NOT_EQUAL(Row.DbcsAttrAt(coordCursorBefore.x), dbcsAttribute); - auto attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); + auto attr = Row.GetAttrByColumn(coordCursorBefore.x); VERIFY_ARE_NOT_EQUAL(attr, TestAttributes); @@ -421,15 +426,15 @@ void TextBufferTests::TestInsertCharacter() textBuffer.InsertCharacter(wchTest, dbcsAttribute, TestAttributes); // ensure that the buffer position where the cursor WAS contains the test items - VERIFY_ARE_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); - VERIFY_ARE_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); + VERIFY_ARE_EQUAL(Row.GlyphAt(coordCursorBefore.x), wchTest); + VERIFY_ARE_EQUAL(Row.DbcsAttrAt(coordCursorBefore.x), dbcsAttribute); - attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); + attr = Row.GetAttrByColumn(coordCursorBefore.x); VERIFY_ARE_EQUAL(attr, TestAttributes); // ensure that the cursor moved to a new position (X or Y or both have changed) - VERIFY_IS_TRUE((coordCursorBefore.X != textBuffer.GetCursor().GetPosition().X) || - (coordCursorBefore.Y != textBuffer.GetCursor().GetPosition().Y)); + VERIFY_IS_TRUE((coordCursorBefore.x != textBuffer.GetCursor().GetPosition().x) || + (coordCursorBefore.y != textBuffer.GetCursor().GetPosition().y)); // the proper advancement of the cursor (e.g. which position it goes to) is validated in other tests } @@ -453,8 +458,8 @@ void TextBufferTests::TestIncrementCursor() textBuffer.IncrementCursor(); - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 1); // X should advance by 1 - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // Y shouldn't have moved + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 1); // X should advance by 1 + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y); // Y shouldn't have moved Log::Comment(L"Test line wrap case where cursor is on the right edge of the line"); textBuffer.GetCursor().SetXPosition(sBufferWidth - 1); @@ -464,8 +469,8 @@ void TextBufferTests::TestIncrementCursor() textBuffer.IncrementCursor(); - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // position should be reset to the left edge when passing right edge - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y - 1, coordCursorBefore.Y); // the cursor should be moved one row down from where it used to be + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // position should be reset to the left edge when passing right edge + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y - 1, coordCursorBefore.y); // the cursor should be moved one row down from where it used to be } void TextBufferTests::TestNewlineCursor() @@ -492,8 +497,8 @@ void TextBufferTests::TestNewlineCursor() textBuffer.NewlineCursor(); // verify - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge of buffer - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y + 1); // move down one row + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // move to left edge of buffer + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y + 1); // move down one row Log::Comment(L"Verify increment when already on last row of buffer"); @@ -509,8 +514,8 @@ void TextBufferTests::TestNewlineCursor() textBuffer.NewlineCursor(); // verify - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // cursor Y position should not have moved. stays on same logical final line of buffer + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // move to left edge + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y); // cursor Y position should not have moved. stays on same logical final line of buffer // This is okay because the backing circular buffer changes, not the logical screen position (final visible line of the buffer) } @@ -523,7 +528,7 @@ void TextBufferTests::TestLastNonSpace(const til::CoordType cursorPosY) auto coordLastNonSpace = textBuffer.GetLastNonSpaceCharacter(); // We expect the last non space character to be the last printable character in the row. - // The .Right property on a row is 1 past the last printable character in the row. + // The .right property on a row is 1 past the last printable character in the row. // If there is one character in the row, the last character would be 0. // If there are no characters in the row, the last character would be -1 and we need to seek backwards to find the previous row with a character. @@ -531,19 +536,19 @@ void TextBufferTests::TestLastNonSpace(const til::CoordType cursorPosY) auto coordExpected = textBuffer.GetCursor().GetPosition(); // Try to get the X position from the current cursor position. - coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; + coordExpected.x = textBuffer.GetRowByOffset(coordExpected.y).MeasureRight() - 1; // If we went negative, this row was empty and we need to continue seeking upward... // - As long as X is negative (empty rows) // - As long as we have space before the top of the buffer (Y isn't the 0th/top row). - while (coordExpected.X < 0 && coordExpected.Y > 0) + while (coordExpected.x < 0 && coordExpected.y > 0) { - coordExpected.Y--; - coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; + coordExpected.y--; + coordExpected.x = textBuffer.GetRowByOffset(coordExpected.y).MeasureRight() - 1; } - VERIFY_ARE_EQUAL(coordLastNonSpace.X, coordExpected.X); - VERIFY_ARE_EQUAL(coordLastNonSpace.Y, coordExpected.Y); + VERIFY_ARE_EQUAL(coordLastNonSpace.x, coordExpected.x); + VERIFY_ARE_EQUAL(coordLastNonSpace.y, coordExpected.y); } void TextBufferTests::TestGetLastNonSpaceCharacter() @@ -564,9 +569,9 @@ void TextBufferTests::TestSetWrapOnCurrentRow() { auto& textBuffer = GetTbi(); - auto sCurrentRow = textBuffer.GetCursor().GetPosition().Y; + auto sCurrentRow = textBuffer.GetCursor().GetPosition().y; - auto& Row = textBuffer.GetRowByOffset(sCurrentRow); + auto& Row = textBuffer.GetMutableRowByOffset(sCurrentRow); Log::Comment(L"Testing off to on"); @@ -617,23 +622,21 @@ void TextBufferTests::TestIncrementCircularBuffer() textBuffer._firstRow = iRowToTestIndex; // fill first row with some stuff - auto& FirstRow = textBuffer._GetFirstRow(); - auto& charRow = FirstRow.GetCharRow(); - const auto stuff = L'A'; - charRow.GlyphAt(0) = { &stuff, 1 }; + auto& FirstRow = textBuffer.GetMutableRowByOffset(0); + FirstRow.ReplaceCharacters(0, 1, { L"A" }); // ensure it does say that it contains text - VERIFY_IS_TRUE(FirstRow.GetCharRow().ContainsText()); + VERIFY_IS_TRUE(FirstRow.ContainsText()); // try increment textBuffer.IncrementCircularBuffer(); // validate that first row has moved VERIFY_ARE_EQUAL(textBuffer._firstRow, iNextRowIndex); // first row has incremented - VERIFY_ARE_NOT_EQUAL(textBuffer._GetFirstRow(), FirstRow); // the old first row is no longer the first + VERIFY_ARE_NOT_EQUAL(textBuffer.GetRowByOffset(0), FirstRow); // the old first row is no longer the first // ensure old first row has been emptied - VERIFY_IS_FALSE(FirstRow.GetCharRow().ContainsText()); + VERIFY_IS_FALSE(FirstRow.ContainsText()); } } @@ -655,11 +658,10 @@ void TextBufferTests::TestMixedRgbAndLegacyForeground() const auto sequence = L"\x1b[m\x1b[38;2;64;128;255mX\x1b[49mX\x1b[m"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( @@ -700,11 +702,10 @@ void TextBufferTests::TestMixedRgbAndLegacyBackground() const auto sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[39mX\x1b[m"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( @@ -743,11 +744,10 @@ void TextBufferTests::TestMixedRgbAndLegacyUnderline() Log::Comment(L"Case 3 \"\\E[m\\E[48;2;64;128;255mX\\E[4mX\\E[m\""); const auto sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[4mX\x1b[m"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( @@ -793,11 +793,10 @@ void TextBufferTests::TestMixedRgbAndLegacyBrightness() const auto sequence = L"\x1b[m\x1b[32mX\x1b[1mX"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( @@ -845,8 +844,8 @@ void TextBufferTests::TestRgbEraseLine() sequence = L"X"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", @@ -856,9 +855,8 @@ void TextBufferTests::TestRgbEraseLine() VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); const auto len = tbi.GetSize().Width(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attr0 = attrs[0]; @@ -895,8 +893,8 @@ void TextBufferTests::TestUnintense() std::wstring sequence = L"\x1b[1;32mX\x1b[22mX"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); @@ -908,8 +906,7 @@ void TextBufferTests::TestUnintense() VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; @@ -947,8 +944,8 @@ void TextBufferTests::TestUnintenseRgb() std::wstring sequence = L"\x1b[1;32m\x1b[48;2;1;2;3mX\x1b[22mX"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); @@ -960,8 +957,7 @@ void TextBufferTests::TestUnintenseRgb() VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; @@ -1007,8 +1003,8 @@ void TextBufferTests::TestComplexUnintense() Log::Comment(NoThrowString().Format(sequence.c_str())); stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); @@ -1020,8 +1016,7 @@ void TextBufferTests::TestComplexUnintense() VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 6]; const auto attrB = attrs[x - 5]; const auto attrC = attrs[x - 4]; @@ -1090,8 +1085,8 @@ void TextBufferTests::CopyAttrs() std::wstring sequence = L"\x1b[32mX\x1b[33mX\n\x1b[34mX\x1b[35mX\x1b[H\x1b[M"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto dark_blue = gci.GetColorTableEntry(TextColor::DARK_BLUE); const auto dark_magenta = gci.GetColorTableEntry(TextColor::DARK_MAGENTA); @@ -1103,8 +1098,7 @@ void TextBufferTests::CopyAttrs() VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(0); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; @@ -1145,8 +1139,8 @@ void TextBufferTests::EmptySgrTest() std::wstring sequence = L"\x1b[0mX\x1b[31mX\x1b[31;mX"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto darkRed = gci.GetColorTableEntry(TextColor::DARK_RED); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", @@ -1155,8 +1149,7 @@ void TextBufferTests::EmptySgrTest() VERIFY_IS_TRUE(x >= 3); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 3]; const auto attrB = attrs[x - 2]; const auto attrC = attrs[x - 1]; @@ -1205,8 +1198,8 @@ void TextBufferTests::TestReverseReset() std::wstring sequence = L"\x1b[42m\x1b[38;2;128;5;255mX\x1b[7mX\x1b[27mX"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto rgbColor = RGB(128, 5, 255); @@ -1217,8 +1210,7 @@ void TextBufferTests::TestReverseReset() VERIFY_IS_TRUE(x >= 3); const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 3]; const auto attrB = attrs[x - 2]; const auto attrC = attrs[x - 1]; @@ -1309,8 +1301,8 @@ void TextBufferTests::CopyLastAttr() std::wstring insertLineAtHome = L"\x1b[H\x1b[L"; stateMachine.ProcessString(insertLineAtHome); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", @@ -1321,9 +1313,9 @@ void TextBufferTests::CopyLastAttr() const auto& row2 = tbi.GetRowByOffset(y + 2); const auto& row3 = tbi.GetRowByOffset(y + 3); - const std::vector attrs1{ row1.GetAttrRow().begin(), row1.GetAttrRow().end() }; - const std::vector attrs2{ row2.GetAttrRow().begin(), row2.GetAttrRow().end() }; - const std::vector attrs3{ row3.GetAttrRow().begin(), row3.GetAttrRow().end() }; + const std::vector attrs1{ row1.AttrBegin(), row1.AttrEnd() }; + const std::vector attrs2{ row2.AttrBegin(), row2.AttrEnd() }; + const std::vector attrs3{ row3.AttrBegin(), row3.AttrEnd() }; const auto attr1A = attrs1[0]; @@ -1379,11 +1371,10 @@ void TextBufferTests::TestRgbThenIntense() const auto sequence = L"\x1b[38;2;40;40;40m\x1b[48;2;168;153;132mX\x1b[1mX\x1b[m"; stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( @@ -1417,7 +1408,7 @@ void TextBufferTests::TestResetClearsIntensity() Log::Comment(NoThrowString().Format( L"Test that resetting intense attributes clears the intensity.")); - const auto x0 = cursor.GetPosition().X; + const auto x0 = cursor.GetPosition().x; // Test assumes that the background/foreground were default attribute when it starts up, // so set that here. @@ -1432,11 +1423,10 @@ void TextBufferTests::TestResetClearsIntensity() Log::Comment(NoThrowString().Format(sequence)); stateMachine.ProcessString(sequence); - const auto x = cursor.GetPosition().X; - const auto y = cursor.GetPosition().Y; + const auto x = cursor.GetPosition().x; + const auto y = cursor.GetPosition().y; const auto& row = tbi.GetRowByOffset(y); - const auto attrRow = &row.GetAttrRow(); - const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const std::vector attrs{ row.AttrBegin(), row.AttrEnd() }; const auto attrA = attrs[x0]; const auto attrB = attrs[x0 + 1]; const auto attrC = attrs[x0 + 2]; @@ -1486,11 +1476,11 @@ void TextBufferTests::TestBackspaceRightSideVt() const auto postCursorPosition = cursor.GetPosition(); // make sure newline was handled correctly - VERIFY_ARE_EQUAL(0, postCursorPosition.X); - VERIFY_ARE_EQUAL(preCursorPosition.Y, postCursorPosition.Y - 1); + VERIFY_ARE_EQUAL(0, postCursorPosition.x); + VERIFY_ARE_EQUAL(preCursorPosition.y, postCursorPosition.y - 1); // make sure "yx" was written to the end of the line the cursor started on - const auto& row = tbi.GetRowByOffset(preCursorPosition.Y); + const auto& row = tbi.GetRowByOffset(preCursorPosition.y); const auto rowText = row.GetText(); auto it = rowText.crbegin(); VERIFY_ARE_EQUAL(*it, L'x'); @@ -1506,8 +1496,8 @@ void TextBufferTests::TestBackspaceStrings() auto& stateMachine = si.GetStateMachine(); const auto& cursor = tbi.GetCursor(); - const auto x0 = cursor.GetPosition().X; - const auto y0 = cursor.GetPosition().Y; + const auto x0 = cursor.GetPosition().x; + const auto y0 = cursor.GetPosition().y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", @@ -1516,8 +1506,8 @@ void TextBufferTests::TestBackspaceStrings() std::wstring seq = L"a\b \b"; stateMachine.ProcessString(seq); - const auto x1 = cursor.GetPosition().X; - const auto y1 = cursor.GetPosition().Y; + const auto x1 = cursor.GetPosition().x; + const auto y1 = cursor.GetPosition().y; VERIFY_ARE_EQUAL(x1, x0); VERIFY_ARE_EQUAL(y1, y0); @@ -1531,8 +1521,8 @@ void TextBufferTests::TestBackspaceStrings() seq = L"\b"; stateMachine.ProcessString(seq); - const auto x2 = cursor.GetPosition().X; - const auto y2 = cursor.GetPosition().Y; + const auto x2 = cursor.GetPosition().x; + const auto y2 = cursor.GetPosition().y; VERIFY_ARE_EQUAL(x2, x0); VERIFY_ARE_EQUAL(y2, y0); @@ -1548,11 +1538,10 @@ void TextBufferTests::TestBackspaceStringsAPI() const auto& tbi = si.GetTextBuffer(); const auto& cursor = tbi.GetCursor(); - gci.SetVirtTermLevel(0); WI_ClearFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); - const auto x0 = cursor.GetPosition().X; - const auto y0 = cursor.GetPosition().Y; + const auto x0 = cursor.GetPosition().x; + const auto y0 = cursor.GetPosition().y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", @@ -1565,62 +1554,22 @@ 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_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); - - VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); - - Log::Comment(NoThrowString().Format( - L"Using DoWriteConsole, write \\b \\b as a single string.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, 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_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); - } - { - const auto str = L" "; - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); - } - - VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); } void TextBufferTests::TestRepeatCharacter() @@ -1645,8 +1594,8 @@ void TextBufferTests::TestRepeatCharacter() sequence = L"\x1b[b"; stateMachine.ProcessString(sequence); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 0); { const auto& row0 = tbi.GetRowByOffset(0); @@ -1665,8 +1614,8 @@ void TextBufferTests::TestRepeatCharacter() stateMachine.ProcessString(L"\x1b[A"); stateMachine.ProcessString(L"\x1b[b"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 0); { const auto& row0 = tbi.GetRowByOffset(0); @@ -1688,8 +1637,8 @@ void TextBufferTests::TestRepeatCharacter() stateMachine.ProcessString(L"C"); stateMachine.ProcessString(L"\x1b[5b"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 6); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 6); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 2); { const auto& row2 = tbi.GetRowByOffset(2); @@ -1707,26 +1656,26 @@ void TextBufferTests::TestRepeatCharacter() L"Test 3: try repeating a non-graphical character. It should do nothing."); stateMachine.ProcessString(L"\r\n"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 3); stateMachine.ProcessString(L"D\n"); stateMachine.ProcessString(L"\x1b[b"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 4); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 4); Log::Comment( L"Test 4: try repeating multiple times. It should do nothing."); stateMachine.ProcessString(L"\r\n"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); - VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, 5); stateMachine.ProcessString(L"E"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 1); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 1); stateMachine.ProcessString(L"\x1b[b"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 2); stateMachine.ProcessString(L"\x1b[b"); - VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, 2); { const auto& row5 = tbi.GetRowByOffset(5); @@ -1762,7 +1711,7 @@ void TextBufferTests::ResizeTraditional() TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(expectedChar, expectedAttr); const auto finalIt = buffer.Write(it); - VERIFY_ARE_EQUAL(smallSize.X * smallSize.Y, finalIt.GetCellDistance(it), L"Verify we said we filled every cell."); + VERIFY_ARE_EQUAL(smallSize.width * smallSize.height, finalIt.GetCellDistance(it), L"Verify we said we filled every cell."); const auto writtenView = Viewport::FromDimensions({ 0, 0 }, smallSize); @@ -1782,38 +1731,38 @@ void TextBufferTests::ResizeTraditional() if (shrinkX) { - newSize.X -= 2; + newSize.width -= 2; } else { - newSize.X += 2; + newSize.width += 2; } if (shrinkY) { - newSize.Y -= 2; + newSize.height -= 2; } else { - newSize.Y += 2; + newSize.height += 2; } // When we grow, we extend the last color. Therefore, this region covers the area colored the same as the letters but filled with a blank. - const auto widthAdjustedView = Viewport::FromDimensions(writtenView.Origin(), { newSize.X, smallSize.Y }); + const auto widthAdjustedView = Viewport::FromDimensions(writtenView.Origin(), { newSize.width, smallSize.height }); // When we resize, we expect the attributes to be unchanged, but the new cells // to be filled with spaces auto expectedSpace = UNICODE_SPACE; std::wstring_view expectedSpaceView(&expectedSpace, 1); - VERIFY_SUCCEEDED(buffer.ResizeTraditional(newSize)); + buffer.ResizeTraditional(newSize); Log::Comment(L"Verify every cell in the X dimension is still the same as when filled and the new Y row is just empty default cells."); { TextBufferCellIterator viewIt(buffer, { 0, 0 }); while (viewIt) { - Log::Comment(NoThrowString().Format(L"Checking cell (Y=%d, X=%d)", viewIt._pos.Y, viewIt._pos.X)); + Log::Comment(NoThrowString().Format(L"Checking cell (Y=%d, X=%d)", viewIt._pos.y, viewIt._pos.x)); if (writtenView.IsInBounds(viewIt._pos)) { Log::Comment(L"This position is inside our original write area. It should have the original character and color."); @@ -1853,13 +1802,12 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() // Get a position inside the buffer const til::point pos{ 2, 1 }; - auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the negative squared latin capital letter B emoji: 🅱 // It's encoded in UTF-16, as needed by the buffer. const auto bButton = L"\xD83C\xDD71"; - position = bButton; + _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); @@ -1867,13 +1815,13 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() VERIFY_ARE_EQUAL(String(bButton), String(readBackText.data(), gsl::narrow(readBackText.size()))); // Make it the first row in the buffer so it will rotate around when we resize and cause renumbering - const auto delta = _buffer->GetFirstRowIndex() - pos.Y; - const til::point newPos{ pos.X, pos.Y + delta }; + const auto delta = _buffer->GetFirstRowIndex() - pos.y; + const til::point newPos{ pos.x, pos.y + delta }; - _buffer->_SetFirstRowIndex(pos.Y); + _buffer->_SetFirstRowIndex(pos.y); // Perform resize to rotate the rows around - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(bufferSize)); + _buffer->ResizeTraditional(bufferSize); // Retrieve the text at the old and new positions. const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); @@ -1895,13 +1843,12 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() // Get a position inside the buffer const til::point pos{ 2, 1 }; - auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the fire emoji: 🔥 // It's encoded in UTF-16, as needed by the buffer. const auto fire = L"\xD83D\xDD25"; - position = fire; + _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); @@ -1910,16 +1857,12 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() // Prepare a delta and the new position we expect the symbol to be moved into. const auto delta = 5; - const til::point newPos{ pos.X, pos.Y + delta }; + const til::point newPos{ pos.x, pos.y + delta }; // Scroll the row with our data by delta. - _buffer->ScrollRows(pos.Y, 1, delta); + _buffer->ScrollRows(pos.y, 1, delta); - // Retrieve the text at the old and new positions. - const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); const auto shouldBeFireText = *_buffer->GetTextDataAt(newPos); - - VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow(shouldBeEmptyText.size()))); VERIFY_ARE_EQUAL(String(fire), String(shouldBeFireText.data(), gsl::narrow(shouldBeFireText.size()))); } @@ -1934,28 +1877,23 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); // Get a position inside the buffer in the bottom row - const til::point pos{ 0, bufferSize.Y - 1 }; - auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + const til::point pos{ 0, bufferSize.height - 1 }; // Fill it up with a sequence that will have to hit the high unicode storage. // This is the eggplant emoji: 🍆 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF46"; - position = emoji; + _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); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); - VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); - // Perform resize to trim off the row of the buffer that included the emoji - til::size trimmedBufferSize{ bufferSize.X, bufferSize.Y - 1 }; + til::size trimmedBufferSize{ bufferSize.width, bufferSize.height - 1 }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); - - VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); + _buffer->ResizeTraditional(trimmedBufferSize); } // This tests that columns removed from the buffer while resizing traditionally will also drop the high unicode @@ -1968,29 +1906,24 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); - // Get a position inside the buffer in the last column - const til::point pos{ bufferSize.X - 1, 0 }; - auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + // Get a position inside the buffer in the last column (-2 as the inserted character is 2 columns wide). + const til::point pos{ bufferSize.width - 2, 0 }; // Fill it up with a sequence that will have to hit the high unicode storage. // This is the peach emoji: 🍑 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF51"; - position = emoji; + _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); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); - VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); - // Perform resize to trim off the column of the buffer that included the emoji - til::size trimmedBufferSize{ bufferSize.X - 1, bufferSize.Y }; + til::size trimmedBufferSize{ bufferSize.width - 1, bufferSize.height }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); - - VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); + _buffer->ResizeTraditional(trimmedBufferSize); } void TextBufferTests::TestBurrito() @@ -2014,6 +1947,143 @@ void TextBufferTests::TestBurrito() VERIFY_IS_FALSE(afterBurritoIter); } +void TextBufferTests::TestOverwriteChars() +{ + til::size bufferSize{ 10, 3 }; + UINT cursorSize = 12; + TextAttribute attr{ 0x7f }; + TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; + auto& row = buffer.GetMutableRowByOffset(0); + +// scientist emoji U+1F9D1 U+200D U+1F52C +#define complex1 L"\U0001F9D1\U0000200D\U0001F52C" +// technologist emoji U+1F9D1 U+200D U+1F4BB +#define complex2 L"\U0001F9D1\U0000200D\U0001F4BB" +#define simple L"X" + + // Test overwriting narrow chars with wide chars at the begin/end of a row. + row.ReplaceCharacters(0, 2, complex1); + row.ReplaceCharacters(8, 2, complex1); + VERIFY_ARE_EQUAL(complex1 L" " complex1, row.GetText()); + + // 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, 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, 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, row.GetText()); + + // Test clearing narrow/wide chars. + row.ReplaceCharacters(0, 1, simple); + row.ReplaceCharacters(1, 2, complex2); + row.ReplaceCharacters(3, 1, simple); + row.ReplaceCharacters(6, 1, simple); + row.ReplaceCharacters(7, 2, complex2); + row.ReplaceCharacters(9, 1, simple); + VERIFY_ARE_EQUAL(simple complex2 simple L" " simple complex2 simple, row.GetText()); + + row.ClearCell(0); + row.ClearCell(1); + row.ClearCell(3); + row.ClearCell(6); + row.ClearCell(8); + row.ClearCell(9); + VERIFY_ARE_EQUAL(L" ", row.GetText()); + +#undef simple +#undef complex2 +#undef complex1 +} + +void TextBufferTests::TestRowReplaceText() +{ + static constexpr til::size bufferSize{ 10, 3 }; + static constexpr UINT cursorSize = 12; + const TextAttribute attr{ 0x7f }; + TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; + auto& row = buffer.GetMutableRowByOffset(0); + +#define complex L"\U0001F41B" + + struct Test + { + const wchar_t* description; + struct + { + std::wstring_view text; + til::CoordType columnBegin = 0; + til::CoordType columnLimit = 0; + } input; + struct + { + std::wstring_view text; + til::CoordType columnEnd = 0; + til::CoordType columnBeginDirty = 0; + til::CoordType columnEndDirty = 0; + } expected; + std::wstring_view expectedRow; + }; + + static constexpr std::array tests{ + Test{ + L"Not enough space -> early exit", + { complex, 2, 2 }, + { complex, 2, 2, 2 }, + L" ", + }, + Test{ + L"Exact right amount of space", + { complex, 2, 4 }, + { L"", 4, 2, 4 }, + L" " complex L" ", + }, + Test{ + L"Not enough space -> columnEnd = columnLimit", + { complex complex, 0, 3 }, + { complex, 3, 0, 4 }, + complex L" ", + }, + Test{ + L"Too much to fit into the row", + { complex L"b" complex L"c" complex L"abcd", 0, til::CoordTypeMax }, + { L"cd", 10, 0, 10 }, + complex L"b" complex L"c" complex L"ab", + }, + Test{ + L"Overwriting wide glyphs dirties both cells, but leaves columnEnd at the end of the text", + { L"efg", 1, til::CoordTypeMax }, + { L"", 4, 0, 5 }, + L" efg c" complex L"ab", + }, + }; + + for (const auto& t : tests) + { + Log::Comment(t.description); + RowWriteState actual{ + .text = t.input.text, + .columnBegin = t.input.columnBegin, + .columnLimit = t.input.columnLimit, + }; + row.ReplaceText(actual); + VERIFY_ARE_EQUAL(t.expected.text, actual.text); + VERIFY_ARE_EQUAL(t.expected.columnEnd, actual.columnEnd); + VERIFY_ARE_EQUAL(t.expected.columnBeginDirty, actual.columnBeginDirty); + VERIFY_ARE_EQUAL(t.expected.columnEndDirty, actual.columnEndDirty); + VERIFY_ARE_EQUAL(t.expectedRow, row.GetText()); + } + +#undef complex +} + void TextBufferTests::TestAppendRTFText() { { @@ -2134,7 +2204,7 @@ void TextBufferTests::GetWordBoundaries() const std::wstring_view delimiters = L" "; for (const auto& test : testData) { - Log::Comment(NoThrowString().Format(L"til::point (%hd, %hd)", test.startPos.X, test.startPos.Y)); + Log::Comment(NoThrowString().Format(L"til::point (%hd, %hd)", test.startPos.x, test.startPos.y)); const auto result = _buffer->GetWordStart(test.startPos, delimiters, accessibilityMode); const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled; VERIFY_ARE_EQUAL(expected, result); @@ -2170,7 +2240,7 @@ void TextBufferTests::GetWordBoundaries() for (const auto& test : testData) { - Log::Comment(NoThrowString().Format(L"til::point (%hd, %hd)", test.startPos.X, test.startPos.Y)); + Log::Comment(NoThrowString().Format(L"til::point (%hd, %hd)", test.startPos.x, test.startPos.y)); auto result = _buffer->GetWordEnd(test.startPos, delimiters, accessibilityMode); const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled; VERIFY_ARE_EQUAL(expected, result); @@ -2244,7 +2314,7 @@ void TextBufferTests::MoveByWord() const auto lastCharPos = _buffer->GetLastNonSpaceCharacter(); for (const auto& test : testData) { - Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y)); + Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.x, test.startPos.y)); auto pos{ test.startPos }; const auto result = movingForwards ? _buffer->MoveToNextWord(pos, delimiters, lastCharPos) : @@ -2270,11 +2340,12 @@ void TextBufferTests::GetGlyphBoundaries() // clang-format off const std::vector expected = { - { L"Buffer Start", { 0, 0 }, { 2, 0 }, { 1, 0 } }, - { L"Line Start", { 0, 1 }, { 2, 1 }, { 1, 1 } }, - { L"General Case", { 1, 1 }, { 3, 1 }, { 2, 1 } }, - { L"Line End", { 9, 1 }, { 0, 2 }, { 0, 2 } }, - { L"Buffer End", { 9, 9 }, { 0, 10 }, { 0, 10 } }, + { L"Buffer Start", { 0, 0 }, { 2, 0 }, { 1, 0 } }, + { L"Line Start", { 0, 1 }, { 2, 1 }, { 1, 1 } }, + { L"General Case 1", { 1, 1 }, { 3, 1 }, { 2, 1 } }, + { L"Line End", { 8, 1 }, { 0, 2 }, { 9, 1 } }, + { L"General Case 2", { 7, 1 }, { 9, 1 }, { 8, 1 } }, + { L"Buffer End", { 9, 9 }, { 0, 10 }, { 0, 10 } }, }; // clang-format on @@ -2644,14 +2715,14 @@ void TextBufferTests::HyperlinkTrim() const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); - _buffer->GetRowByOffset(pos.Y).GetAttrRow().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).GetAttrRow().SetAttrToEnd(otherPos.X, newAttr); + _buffer->GetMutableRowByOffset(otherPos.y).SetAttrToEnd(otherPos.x, newAttr); _buffer->AddHyperlinkToMap(otherUrl, otherId); // Increment the circular buffer @@ -2688,12 +2759,12 @@ void TextBufferTests::NoHyperlinkTrim() const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); - _buffer->GetRowByOffset(pos.Y).GetAttrRow().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).GetAttrRow().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/Utf16ParserTests.cpp b/src/host/ut_host/Utf16ParserTests.cpp deleted file mode 100644 index 0215077086..0000000000 --- a/src/host/ut_host/Utf16ParserTests.cpp +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "../../types/inc/Utf16Parser.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; - -static const std::vector CyrillicChar = { 0x0431 }; // lowercase be -static const std::vector LatinChar = { 0x0061 }; // uppercase A -static const std::vector FullWidthChar = { 0xFF2D }; // fullwidth latin small letter m -static const std::vector GaelicChar = { 0x1E41 }; // latin small letter m with dot above -static const std::vector HiraganaChar = { 0x3059 }; // hiragana su -static const std::vector SunglassesEmoji = { 0xD83D, 0xDE0E }; // smiling face with sunglasses emoji - -class Utf16ParserTests -{ - TEST_CLASS(Utf16ParserTests); - - TEST_METHOD(CanParseNonSurrogateText) - { - const std::vector> expected = { CyrillicChar, LatinChar, FullWidthChar, GaelicChar, HiraganaChar }; - - std::wstring wstr; - for (const auto& charData : expected) - { - wstr.push_back(charData.at(0)); - } - - const auto result = Utf16Parser::Parse(wstr); - - VERIFY_ARE_EQUAL(expected.size(), result.size()); - for (size_t i = 0; i < result.size(); ++i) - { - const auto& sequence = result.at(i); - VERIFY_ARE_EQUAL(sequence, expected.at(i)); - } - } - - TEST_METHOD(CanParseSurrogatePairs) - { - const std::wstring wstr{ SunglassesEmoji.begin(), SunglassesEmoji.end() }; - const auto result = Utf16Parser::Parse(wstr); - - VERIFY_ARE_EQUAL(result.size(), 1u); - VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); - for (size_t i = 0; i < SunglassesEmoji.size(); ++i) - { - VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); - } - } - - TEST_METHOD(WillDropBadSurrogateCombinations) - { - // test dropping of invalid leading surrogates - std::wstring wstr{ SunglassesEmoji.begin(), SunglassesEmoji.end() }; - wstr += wstr; - wstr.at(1) = SunglassesEmoji.at(0); // wstr contains 3 leading, 1 trailing surrogate sequence - - auto result = Utf16Parser::Parse(wstr); - - VERIFY_ARE_EQUAL(result.size(), 1u); - VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); - for (size_t i = 0; i < SunglassesEmoji.size(); ++i) - { - VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); - } - - // test dropping of invalid trailing surrogates - wstr = { SunglassesEmoji.begin(), SunglassesEmoji.end() }; - wstr += wstr; - wstr.at(0) = SunglassesEmoji.at(1); // wstr contains 2 trailing, 1 leading, 1 trailing surrogate sequence - - result = Utf16Parser::Parse(wstr); - - VERIFY_ARE_EQUAL(result.size(), 1u); - VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); - for (size_t i = 0; i < SunglassesEmoji.size(); ++i) - { - VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); - } - } - - const std::wstring_view Replacement{ &UNICODE_REPLACEMENT, 1 }; - - TEST_METHOD(ParseNextLeadOnly) - { - std::wstring wstr{ SunglassesEmoji.at(0) }; - - const auto expected = Replacement; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextTrailOnly) - { - std::wstring wstr{ SunglassesEmoji.at(1) }; - - const auto expected = Replacement; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextSingleOnly) - { - std::wstring wstr{ CyrillicChar.at(0) }; - - const auto expected = std::wstring_view{ CyrillicChar.data(), CyrillicChar.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextLeadLead) - { - std::wstring wstr{ SunglassesEmoji.at(0) }; - wstr += SunglassesEmoji.at(0); - - const auto expected = Replacement; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextLeadTrail) - { - std::wstring wstr{ SunglassesEmoji.at(0) }; - wstr += SunglassesEmoji.at(1); - - const auto expected = std::wstring_view{ SunglassesEmoji.data(), SunglassesEmoji.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextTrailTrail) - { - std::wstring wstr{ SunglassesEmoji.at(1) }; - wstr += SunglassesEmoji.at(1); - - const auto expected = Replacement; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextLeadSingle) - { - std::wstring wstr{ SunglassesEmoji.at(0) }; - wstr += LatinChar.at(0); - - const auto expected = std::wstring_view{ LatinChar.data(), LatinChar.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextTrailSingle) - { - std::wstring wstr{ SunglassesEmoji.at(1) }; - wstr += LatinChar.at(0); - - const auto expected = std::wstring_view{ LatinChar.data(), LatinChar.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextLeadLeadTrail) - { - std::wstring wstr{ SunglassesEmoji.at(0) }; - wstr += SunglassesEmoji.at(0); - wstr += SunglassesEmoji.at(1); - - const auto expected = std::wstring_view{ SunglassesEmoji.data(), SunglassesEmoji.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextTrailLeadTrail) - { - std::wstring wstr{ SunglassesEmoji.at(1) }; - wstr += SunglassesEmoji.at(0); - wstr += SunglassesEmoji.at(1); - - const auto expected = std::wstring_view{ SunglassesEmoji.data(), SunglassesEmoji.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } - - TEST_METHOD(ParseNextSingleLeadTrail) - { - std::wstring wstr{ GaelicChar.at(0) }; - wstr += SunglassesEmoji.at(0); - wstr += SunglassesEmoji.at(1); - - const auto expected = std::wstring_view{ GaelicChar.data(), GaelicChar.size() }; - const auto actual = Utf16Parser::ParseNext(wstr); - - VERIFY_ARE_EQUAL(expected, actual); - } -}; diff --git a/src/host/ut_host/UtilsTests.cpp b/src/host/ut_host/UtilsTests.cpp index 0ed977a6c1..ad889a291c 100644 --- a/src/host/ut_host/UtilsTests.cpp +++ b/src/host/ut_host/UtilsTests.cpp @@ -63,13 +63,13 @@ class UtilsTests void FillBothCoordsSameRandom(til::point* pcoordA, til::point* pcoordB) { - pcoordA->X = pcoordB->X = RandomCoord(); - pcoordA->Y = pcoordB->Y = RandomCoord(); + pcoordA->x = pcoordB->x = RandomCoord(); + pcoordA->y = pcoordB->y = RandomCoord(); } void LogCoordinates(const til::point coordA, const til::point coordB) { - Log::Comment(String().Format(L"Coordinates - A: (%d, %d) B: (%d, %d)", coordA.X, coordA.Y, coordB.X, coordB.Y)); + Log::Comment(String().Format(L"Coordinates - A: (%d, %d) B: (%d, %d)", coordA.x, coordA.y, coordB.x, coordB.y)); } void SubtractRandom(til::CoordType& psValue) @@ -86,8 +86,8 @@ class UtilsTests // Set the buffer size to be able to accommodate large values. til::size coordMaxBuffer; - coordMaxBuffer.X = SHORT_MAX; - coordMaxBuffer.Y = SHORT_MAX; + coordMaxBuffer.width = SHORT_MAX; + coordMaxBuffer.height = SHORT_MAX; Log::Comment(L"#1: 0 case. Coords equal"); FillBothCoordsSameRandom(&coordA, &coordB); @@ -98,30 +98,30 @@ class UtilsTests Log::Comment(L"#2: -1 case. A comes before B"); Log::Comment(L"A. A left of B, same line"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordA.X); + SubtractRandom(coordA.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_LESS_THAN(result, 0); Log::Comment(L"B. A above B, same column"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordA.Y); + SubtractRandom(coordA.y); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_LESS_THAN(result, 0); Log::Comment(L"C. A up and to the left of B."); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordA.Y); - SubtractRandom(coordA.X); + SubtractRandom(coordA.y); + SubtractRandom(coordA.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_LESS_THAN(result, 0); Log::Comment(L"D. A up and to the right of B."); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordA.Y); - SubtractRandom(coordB.X); + SubtractRandom(coordA.y); + SubtractRandom(coordB.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_LESS_THAN(result, 0); @@ -129,30 +129,30 @@ class UtilsTests Log::Comment(L"#3: 1 case. A comes after B"); Log::Comment(L"A. A right of B, same line"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordB.X); + SubtractRandom(coordB.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_GREATER_THAN(result, 0); Log::Comment(L"B. A below B, same column"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordB.Y); + SubtractRandom(coordB.y); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_GREATER_THAN(result, 0); Log::Comment(L"C. A down and to the left of B"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordB.Y); - SubtractRandom(coordA.X); + SubtractRandom(coordB.y); + SubtractRandom(coordA.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_GREATER_THAN(result, 0); Log::Comment(L"D. A down and to the right of B"); FillBothCoordsSameRandom(&coordA, &coordB); - SubtractRandom(coordB.Y); - SubtractRandom(coordB.X); + SubtractRandom(coordB.y); + SubtractRandom(coordB.x); LogCoordinates(coordA, coordB); result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); VERIFY_IS_GREATER_THAN(result, 0); diff --git a/src/host/ut_host/ViewportTests.cpp b/src/host/ut_host/ViewportTests.cpp index 9e8905d649..edc9b70a2d 100644 --- a/src/host/ut_host/ViewportTests.cpp +++ b/src/host/ut_host/ViewportTests.cpp @@ -36,29 +36,29 @@ class ViewportTests TEST_METHOD(CreateFromInclusive) { til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; til::point origin; - origin.X = rect.Left; - origin.Y = rect.Top; + origin.x = rect.left; + origin.y = rect.top; til::size dimensions; - dimensions.X = rect.Right - rect.Left + 1; - dimensions.Y = rect.Bottom - rect.Top + 1; + dimensions.width = rect.right - rect.left + 1; + dimensions.height = rect.bottom - rect.top + 1; const auto v = Viewport::FromInclusive(rect); - VERIFY_ARE_EQUAL(rect.Left, v.Left()); - VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); - VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); - VERIFY_ARE_EQUAL(rect.Top, v.Top()); - VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); - VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); - VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); - VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(rect.left, v.Left()); + VERIFY_ARE_EQUAL(rect.right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.top, v.Top()); + VERIFY_ARE_EQUAL(rect.bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.height, v.Height()); + VERIFY_ARE_EQUAL(dimensions.width, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); } @@ -66,29 +66,29 @@ class ViewportTests TEST_METHOD(CreateFromExclusive) { til::rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; til::point origin; - origin.X = rect.Left; - origin.Y = rect.Top; + origin.x = rect.left; + origin.y = rect.top; til::size dimensions; - dimensions.X = rect.Right - rect.Left; - dimensions.Y = rect.Bottom - rect.Top; + dimensions.width = rect.right - rect.left; + dimensions.height = rect.bottom - rect.top; const auto v = Viewport::FromExclusive(rect); - VERIFY_ARE_EQUAL(rect.Left, v.Left()); - VERIFY_ARE_EQUAL(rect.Right - 1, v.RightInclusive()); - VERIFY_ARE_EQUAL(rect.Right, v.RightExclusive()); - VERIFY_ARE_EQUAL(rect.Top, v.Top()); - VERIFY_ARE_EQUAL(rect.Bottom - 1, v.BottomInclusive()); - VERIFY_ARE_EQUAL(rect.Bottom, v.BottomExclusive()); - VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); - VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(rect.left, v.Left()); + VERIFY_ARE_EQUAL(rect.right - 1, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.right, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.top, v.Top()); + VERIFY_ARE_EQUAL(rect.bottom - 1, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.bottom, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.height, v.Height()); + VERIFY_ARE_EQUAL(dimensions.width, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); } @@ -96,29 +96,29 @@ class ViewportTests TEST_METHOD(CreateFromDimensionsWidthHeight) { til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; til::point origin; - origin.X = rect.Left; - origin.Y = rect.Top; + origin.x = rect.left; + origin.y = rect.top; til::size dimensions; - dimensions.X = rect.Right - rect.Left + 1; - dimensions.Y = rect.Bottom - rect.Top + 1; + dimensions.width = rect.right - rect.left + 1; + dimensions.height = rect.bottom - rect.top + 1; - const auto v = Viewport::FromDimensions(origin, dimensions.X, dimensions.Y); + const auto v = Viewport::FromDimensions(origin, dimensions.width, dimensions.height); - VERIFY_ARE_EQUAL(rect.Left, v.Left()); - VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); - VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); - VERIFY_ARE_EQUAL(rect.Top, v.Top()); - VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); - VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); - VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); - VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(rect.left, v.Left()); + VERIFY_ARE_EQUAL(rect.right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.top, v.Top()); + VERIFY_ARE_EQUAL(rect.bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.height, v.Height()); + VERIFY_ARE_EQUAL(dimensions.width, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); } @@ -126,29 +126,29 @@ class ViewportTests TEST_METHOD(CreateFromDimensions) { til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; til::point origin; - origin.X = rect.Left; - origin.Y = rect.Top; + origin.x = rect.left; + origin.y = rect.top; til::size dimensions; - dimensions.X = rect.Right - rect.Left + 1; - dimensions.Y = rect.Bottom - rect.Top + 1; + dimensions.width = rect.right - rect.left + 1; + dimensions.height = rect.bottom - rect.top + 1; const auto v = Viewport::FromDimensions(origin, dimensions); - VERIFY_ARE_EQUAL(rect.Left, v.Left()); - VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); - VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); - VERIFY_ARE_EQUAL(rect.Top, v.Top()); - VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); - VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); - VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); - VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(rect.left, v.Left()); + VERIFY_ARE_EQUAL(rect.right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.top, v.Top()); + VERIFY_ARE_EQUAL(rect.bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.height, v.Height()); + VERIFY_ARE_EQUAL(dimensions.width, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); } @@ -156,29 +156,29 @@ class ViewportTests TEST_METHOD(CreateFromDimensionsNoOrigin) { til::inclusive_rect rect; - rect.Top = 0; - rect.Left = 0; - rect.Bottom = 5; - rect.Right = 20; + rect.top = 0; + rect.left = 0; + rect.bottom = 5; + rect.right = 20; til::point origin; - origin.X = rect.Left; - origin.Y = rect.Top; + origin.x = rect.left; + origin.y = rect.top; til::size dimensions; - dimensions.X = rect.Right - rect.Left + 1; - dimensions.Y = rect.Bottom - rect.Top + 1; + dimensions.width = rect.right - rect.left + 1; + dimensions.height = rect.bottom - rect.top + 1; const auto v = Viewport::FromDimensions(dimensions); - VERIFY_ARE_EQUAL(rect.Left, v.Left()); - VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); - VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); - VERIFY_ARE_EQUAL(rect.Top, v.Top()); - VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); - VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); - VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); - VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(rect.left, v.Left()); + VERIFY_ARE_EQUAL(rect.right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.top, v.Top()); + VERIFY_ARE_EQUAL(rect.bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.height, v.Height()); + VERIFY_ARE_EQUAL(dimensions.width, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); } @@ -186,17 +186,17 @@ class ViewportTests TEST_METHOD(CreateFromCoord) { til::point origin; - origin.X = 12; - origin.Y = 24; + origin.x = 12; + origin.y = 24; const auto v = Viewport::FromCoord(origin); - VERIFY_ARE_EQUAL(origin.X, v.Left()); - VERIFY_ARE_EQUAL(origin.X, v.RightInclusive()); - VERIFY_ARE_EQUAL(origin.X + 1, v.RightExclusive()); - VERIFY_ARE_EQUAL(origin.Y, v.Top()); - VERIFY_ARE_EQUAL(origin.Y, v.BottomInclusive()); - VERIFY_ARE_EQUAL(origin.Y + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(origin.x, v.Left()); + VERIFY_ARE_EQUAL(origin.x, v.RightInclusive()); + VERIFY_ARE_EQUAL(origin.x + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(origin.y, v.Top()); + VERIFY_ARE_EQUAL(origin.y, v.BottomInclusive()); + VERIFY_ARE_EQUAL(origin.y + 1, v.BottomExclusive()); VERIFY_ARE_EQUAL(1, v.Height()); VERIFY_ARE_EQUAL(1, v.Width()); VERIFY_ARE_EQUAL(origin, v.Origin()); @@ -208,69 +208,69 @@ class ViewportTests TEST_METHOD(IsInBoundsCoord) { til::inclusive_rect r; - r.Top = 3; - r.Bottom = 5; - r.Left = 10; - r.Right = 20; + r.top = 3; + r.bottom = 5; + r.left = 10; + r.right = 20; const auto v = Viewport::FromInclusive(r); til::point c; - c.X = r.Left; - c.Y = r.Top; + c.x = r.left; + c.y = r.top; VERIFY_IS_TRUE(v.IsInBounds(c), L"Top left corner in bounds."); - c.Y = r.Bottom; + c.y = r.bottom; VERIFY_IS_TRUE(v.IsInBounds(c), L"Bottom left corner in bounds."); - c.X = r.Right; + c.x = r.right; VERIFY_IS_TRUE(v.IsInBounds(c), L"Bottom right corner in bounds."); - c.Y = r.Top; + c.y = r.top; VERIFY_IS_TRUE(v.IsInBounds(c), L"Top right corner in bounds."); - c.X++; + c.x++; VERIFY_IS_FALSE(v.IsInBounds(c), L"One right out the top right is out of bounds."); - c.X--; - c.Y--; + c.x--; + c.y--; VERIFY_IS_FALSE(v.IsInBounds(c), L"One up out the top right is out of bounds."); - c.X = r.Left; - c.Y = r.Top; - c.X--; + c.x = r.left; + c.y = r.top; + c.x--; VERIFY_IS_FALSE(v.IsInBounds(c), L"One left out the top left is out of bounds."); - c.X++; - c.Y--; + c.x++; + c.y--; VERIFY_IS_FALSE(v.IsInBounds(c), L"One up out the top left is out of bounds."); - c.X = r.Left; - c.Y = r.Bottom; - c.X--; + c.x = r.left; + c.y = r.bottom; + c.x--; VERIFY_IS_FALSE(v.IsInBounds(c), L"One left out the bottom left is out of bounds."); - c.X++; - c.Y++; + c.x++; + c.y++; VERIFY_IS_FALSE(v.IsInBounds(c), L"One down out the bottom left is out of bounds."); - c.X = r.Right; - c.Y = r.Bottom; - c.X++; + c.x = r.right; + c.y = r.bottom; + c.x++; VERIFY_IS_FALSE(v.IsInBounds(c), L"One right out the bottom right is out of bounds."); - c.X--; - c.Y++; + c.x--; + c.y++; VERIFY_IS_FALSE(v.IsInBounds(c), L"One down out the bottom right is out of bounds."); } TEST_METHOD(IsInBoundsViewport) { til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; const auto original = rect; @@ -279,38 +279,38 @@ class ViewportTests auto test = Viewport::FromInclusive(rect); VERIFY_IS_TRUE(view.IsInBounds(test), L"Same size/position viewport is in bounds."); - rect.Top++; - rect.Bottom--; - rect.Left++; - rect.Right--; + rect.top++; + rect.bottom--; + rect.left++; + rect.right--; test = Viewport::FromInclusive(rect); VERIFY_IS_TRUE(view.IsInBounds(test), L"Viewport inscribed inside viewport is in bounds."); rect = original; - rect.Top--; + rect.top--; test = Viewport::FromInclusive(rect); VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one taller upwards is out of bounds."); rect = original; - rect.Bottom++; + rect.bottom++; test = Viewport::FromInclusive(rect); VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one taller downwards is out of bounds."); rect = original; - rect.Left--; + rect.left--; test = Viewport::FromInclusive(rect); VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one wider leftwards is out of bounds."); rect = original; - rect.Right++; + rect.right++; test = Viewport::FromInclusive(rect); VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one wider rightwards is out of bounds."); rect = original; - rect.Left++; - rect.Right++; - rect.Top++; - rect.Bottom++; + rect.left++; + rect.right++; + rect.top++; + rect.bottom++; test = Viewport::FromInclusive(rect); VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport offset at the origin but same size is out of bounds."); } @@ -318,82 +318,82 @@ class ViewportTests TEST_METHOD(ClampCoord) { til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; const auto view = Viewport::FromInclusive(rect); til::point pos; - pos.X = rect.Left; - pos.Y = rect.Top; + pos.x = rect.left; + pos.y = rect.top; auto before = pos; view.Clamp(pos); VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in top left corner."); - pos.X = rect.Left; - pos.Y = rect.Bottom; + pos.x = rect.left; + pos.y = rect.bottom; before = pos; view.Clamp(pos); VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in bottom left corner."); - pos.X = rect.Right; - pos.Y = rect.Bottom; + pos.x = rect.right; + pos.y = rect.bottom; before = pos; view.Clamp(pos); VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in bottom right corner."); - pos.X = rect.Right; - pos.Y = rect.Top; + pos.x = rect.right; + pos.y = rect.top; before = pos; view.Clamp(pos); VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in top right corner."); til::point expected; - expected.X = rect.Right; - expected.Y = rect.Top; + expected.x = rect.right; + expected.y = rect.top; pos = expected; - pos.X++; - pos.Y--; + pos.x++; + pos.y--; before = pos; view.Clamp(pos); VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the top right corner back."); VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the top right corner."); - expected.X = rect.Left; - expected.Y = rect.Top; + expected.x = rect.left; + expected.y = rect.top; pos = expected; - pos.X--; - pos.Y--; + pos.x--; + pos.y--; before = pos; view.Clamp(pos); VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the top left corner back."); VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the top left corner."); - expected.X = rect.Left; - expected.Y = rect.Bottom; + expected.x = rect.left; + expected.y = rect.bottom; pos = expected; - pos.X--; - pos.Y++; + pos.x--; + pos.y++; before = pos; view.Clamp(pos); VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the bottom left corner back."); VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the bottom left corner."); - expected.X = rect.Right; - expected.Y = rect.Bottom; + expected.x = rect.right; + expected.y = rect.bottom; pos = expected; - pos.X++; - pos.Y++; + pos.x++; + pos.y++; before = pos; view.Clamp(pos); @@ -410,19 +410,19 @@ class ViewportTests { // Create the rectangle/view we will clamp to. til::inclusive_rect rect; - rect.Top = 3; - rect.Bottom = 5; - rect.Left = 10; - rect.Right = 20; + rect.top = 3; + rect.bottom = 5; + rect.left = 10; + rect.right = 20; const auto view = Viewport::FromInclusive(rect); Log::Comment(L"Make a rectangle that is larger than and fully encompasses our clamping rectangle."); til::inclusive_rect testRect; - testRect.Top = rect.Top - 3; - testRect.Bottom = rect.Bottom + 3; - testRect.Left = rect.Left - 3; - testRect.Right = rect.Right + 3; + testRect.top = rect.top - 3; + testRect.bottom = rect.bottom + 3; + testRect.left = rect.left - 3; + testRect.right = rect.right + 3; auto testView = Viewport::FromInclusive(testRect); @@ -430,28 +430,28 @@ class ViewportTests VERIFY_ARE_EQUAL(view, actual, L"All sides should get reduced down to the size of the given rect."); Log::Comment(L"Make a rectangle that is fully inscribed inside our clamping rectangle."); - testRect.Top = rect.Top + 1; - testRect.Bottom = rect.Bottom - 1; - testRect.Left = rect.Left + 1; - testRect.Right = rect.Right - 1; + testRect.top = rect.top + 1; + testRect.bottom = rect.bottom - 1; + testRect.left = rect.left + 1; + testRect.right = rect.right - 1; testView = Viewport::FromInclusive(testRect); actual = view.Clamp(testView); VERIFY_ARE_EQUAL(testView, actual, L"Verify that nothing changed because this rectangle already sat fully inside the clamping rectangle."); Log::Comment(L"Craft a rectangle where the left is outside the right, right is outside the left, top is outside the bottom, and bottom is outside the top."); - testRect.Top = rect.Bottom + 10; - testRect.Bottom = rect.Top - 10; - testRect.Left = rect.Right + 10; - testRect.Right = rect.Left - 10; + testRect.top = rect.bottom + 10; + testRect.bottom = rect.top - 10; + testRect.left = rect.right + 10; + testRect.right = rect.left - 10; testView = Viewport::FromInclusive(testRect); Log::Comment(L"We expect it to be pulled back so each coordinate is in bounds, but the rectangle is still invalid (since left will be > right)."); til::inclusive_rect expected; - expected.Top = rect.Bottom; - expected.Bottom = rect.Top; - expected.Left = rect.Right; - expected.Right = rect.Left; + expected.top = rect.bottom; + expected.bottom = rect.top; + expected.left = rect.right; + expected.right = rect.left; const auto expectedView = Viewport::FromInclusive(expected); actual = view.Clamp(testView); @@ -463,44 +463,44 @@ class ViewportTests auto success = false; til::inclusive_rect edges; - edges.Left = 10; - edges.Right = 19; - edges.Top = 20; - edges.Bottom = 29; + edges.left = 10; + edges.right = 19; + edges.top = 20; + edges.bottom = 29; const auto v = Viewport::FromInclusive(edges); til::point original; til::point screen; // #1 coord inside region - original.X = screen.X = 15; - original.Y = screen.Y = 25; + original.x = screen.x = 15; + original.y = screen.y = 25; success = v.IncrementInBounds(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, original.X + 1); - VERIFY_ARE_EQUAL(screen.Y, original.Y); + VERIFY_ARE_EQUAL(screen.x, original.x + 1); + VERIFY_ARE_EQUAL(screen.y, original.y); // #2 coord right edge, not bottom - original.X = screen.X = edges.Right; - original.Y = screen.Y = 25; + original.x = screen.x = edges.right; + original.y = screen.y = 25; success = v.IncrementInBounds(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Left); - VERIFY_ARE_EQUAL(screen.Y, original.Y + 1); + VERIFY_ARE_EQUAL(screen.x, edges.left); + VERIFY_ARE_EQUAL(screen.y, original.y + 1); // #3 coord right edge, bottom - original.X = screen.X = edges.Right; - original.Y = screen.Y = edges.Bottom; + original.x = screen.x = edges.right; + original.y = screen.y = edges.bottom; success = v.IncrementInBounds(screen); VERIFY_IS_FALSE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Right); - VERIFY_ARE_EQUAL(screen.Y, edges.Bottom); + VERIFY_ARE_EQUAL(screen.x, edges.right); + VERIFY_ARE_EQUAL(screen.y, edges.bottom); } TEST_METHOD(IncrementInBoundsCircular) @@ -508,44 +508,44 @@ class ViewportTests auto success = false; til::inclusive_rect edges; - edges.Left = 10; - edges.Right = 19; - edges.Top = 20; - edges.Bottom = 29; + edges.left = 10; + edges.right = 19; + edges.top = 20; + edges.bottom = 29; const auto v = Viewport::FromInclusive(edges); til::point original; til::point screen; // #1 coord inside region - original.X = screen.X = 15; - original.Y = screen.Y = 25; + original.x = screen.x = 15; + original.y = screen.y = 25; success = v.IncrementInBoundsCircular(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, original.X + 1); - VERIFY_ARE_EQUAL(screen.Y, original.Y); + VERIFY_ARE_EQUAL(screen.x, original.x + 1); + VERIFY_ARE_EQUAL(screen.y, original.y); // #2 coord right edge, not bottom - original.X = screen.X = edges.Right; - original.Y = screen.Y = 25; + original.x = screen.x = edges.right; + original.y = screen.y = 25; success = v.IncrementInBoundsCircular(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Left); - VERIFY_ARE_EQUAL(screen.Y, original.Y + 1); + VERIFY_ARE_EQUAL(screen.x, edges.left); + VERIFY_ARE_EQUAL(screen.y, original.y + 1); // #3 coord right edge, bottom - original.X = screen.X = edges.Right; - original.Y = screen.Y = edges.Bottom; + original.x = screen.x = edges.right; + original.y = screen.y = edges.bottom; success = v.IncrementInBoundsCircular(screen); VERIFY_IS_FALSE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Left); - VERIFY_ARE_EQUAL(screen.Y, edges.Top); + VERIFY_ARE_EQUAL(screen.x, edges.left); + VERIFY_ARE_EQUAL(screen.y, edges.top); } TEST_METHOD(DecrementInBounds) @@ -553,44 +553,44 @@ class ViewportTests auto success = false; til::inclusive_rect edges; - edges.Left = 10; - edges.Right = 19; - edges.Top = 20; - edges.Bottom = 29; + edges.left = 10; + edges.right = 19; + edges.top = 20; + edges.bottom = 29; const auto v = Viewport::FromInclusive(edges); til::point original; til::point screen; // #1 coord inside region - original.X = screen.X = 15; - original.Y = screen.Y = 25; + original.x = screen.x = 15; + original.y = screen.y = 25; success = v.DecrementInBounds(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, original.X - 1); - VERIFY_ARE_EQUAL(screen.Y, original.Y); + VERIFY_ARE_EQUAL(screen.x, original.x - 1); + VERIFY_ARE_EQUAL(screen.y, original.y); // #2 coord left edge, not top - original.X = screen.X = edges.Left; - original.Y = screen.Y = 25; + original.x = screen.x = edges.left; + original.y = screen.y = 25; success = v.DecrementInBounds(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Right); - VERIFY_ARE_EQUAL(screen.Y, original.Y - 1); + VERIFY_ARE_EQUAL(screen.x, edges.right); + VERIFY_ARE_EQUAL(screen.y, original.y - 1); // #3 coord left edge, top - original.X = screen.X = edges.Left; - original.Y = screen.Y = edges.Top; + original.x = screen.x = edges.left; + original.y = screen.y = edges.top; success = v.DecrementInBounds(screen); VERIFY_IS_FALSE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Left); - VERIFY_ARE_EQUAL(screen.Y, edges.Top); + VERIFY_ARE_EQUAL(screen.x, edges.left); + VERIFY_ARE_EQUAL(screen.y, edges.top); } TEST_METHOD(DecrementInBoundsCircular) @@ -598,44 +598,44 @@ class ViewportTests auto success = false; til::inclusive_rect edges; - edges.Left = 10; - edges.Right = 19; - edges.Top = 20; - edges.Bottom = 29; + edges.left = 10; + edges.right = 19; + edges.top = 20; + edges.bottom = 29; const auto v = Viewport::FromInclusive(edges); til::point original; til::point screen; // #1 coord inside region - original.X = screen.X = 15; - original.Y = screen.Y = 25; + original.x = screen.x = 15; + original.y = screen.y = 25; success = v.DecrementInBoundsCircular(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, original.X - 1); - VERIFY_ARE_EQUAL(screen.Y, original.Y); + VERIFY_ARE_EQUAL(screen.x, original.x - 1); + VERIFY_ARE_EQUAL(screen.y, original.y); // #2 coord left edge, not top - original.X = screen.X = edges.Left; - original.Y = screen.Y = 25; + original.x = screen.x = edges.left; + original.y = screen.y = 25; success = v.DecrementInBoundsCircular(screen); VERIFY_IS_TRUE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Right); - VERIFY_ARE_EQUAL(screen.Y, original.Y - 1); + VERIFY_ARE_EQUAL(screen.x, edges.right); + VERIFY_ARE_EQUAL(screen.y, original.y - 1); // #3 coord left edge, top - original.X = screen.X = edges.Left; - original.Y = screen.Y = edges.Top; + original.x = screen.x = edges.left; + original.y = screen.y = edges.top; success = v.DecrementInBoundsCircular(screen); VERIFY_IS_FALSE(success); - VERIFY_ARE_EQUAL(screen.X, edges.Right); - VERIFY_ARE_EQUAL(screen.Y, edges.Bottom); + VERIFY_ARE_EQUAL(screen.x, edges.right); + VERIFY_ARE_EQUAL(screen.y, edges.bottom); } til::CoordType RandomCoord() @@ -659,8 +659,8 @@ class ViewportTests // 20x20 box til::inclusive_rect srectEdges; - srectEdges.Top = srectEdges.Left = 0; - srectEdges.Bottom = srectEdges.Right = sRowWidth - 1; + srectEdges.top = srectEdges.left = 0; + srectEdges.bottom = srectEdges.right = sRowWidth - 1; const auto v = Viewport::FromInclusive(srectEdges); @@ -668,22 +668,22 @@ class ViewportTests for (UINT i = 0; i < cTestLoopInstances; i++) { til::point coordPos; - coordPos.X = RandomCoord() % 20; - coordPos.Y = RandomCoord() % 20; + coordPos.x = RandomCoord() % 20; + coordPos.y = RandomCoord() % 20; auto sAddAmount = RandomCoord() % (sRowWidth * sRowWidth); til::point coordFinal; - coordFinal.X = (coordPos.X + sAddAmount) % sRowWidth; - coordFinal.Y = coordPos.Y + ((coordPos.X + sAddAmount) / sRowWidth); + coordFinal.x = (coordPos.x + sAddAmount) % sRowWidth; + coordFinal.y = coordPos.y + ((coordPos.x + sAddAmount) / sRowWidth); - Log::Comment(String().Format(L"Add To Position: (%d, %d) Amount to add: %d", coordPos.Y, coordPos.X, sAddAmount)); + Log::Comment(String().Format(L"Add To Position: (%d, %d) Amount to add: %d", coordPos.y, coordPos.x, sAddAmount)); // Movement result is expected to be true, unless there's an error. auto fExpectedResult = true; // if we've calculated past the final row, then the function will reset to the original position and the output will be false. - if (coordFinal.Y >= sRowWidth) + if (coordFinal.y >= sRowWidth) { coordFinal = coordPos; fExpectedResult = false; @@ -692,37 +692,37 @@ class ViewportTests const bool fActualResult = v.MoveInBounds(sAddAmount, coordPos); VERIFY_ARE_EQUAL(fExpectedResult, fActualResult); - VERIFY_ARE_EQUAL(coordPos.X, coordFinal.X); - VERIFY_ARE_EQUAL(coordPos.Y, coordFinal.Y); + VERIFY_ARE_EQUAL(coordPos.x, coordFinal.x); + VERIFY_ARE_EQUAL(coordPos.y, coordFinal.y); - Log::Comment(String().Format(L"Actual: (%d, %d) Expected: (%d, %d)", coordPos.Y, coordPos.X, coordFinal.Y, coordFinal.X)); + Log::Comment(String().Format(L"Actual: (%d, %d) Expected: (%d, %d)", coordPos.y, coordPos.x, coordFinal.y, coordFinal.x)); } } TEST_METHOD(CompareInBounds) { til::inclusive_rect edges; - edges.Left = 10; - edges.Right = 19; - edges.Top = 20; - edges.Bottom = 29; + edges.left = 10; + edges.right = 19; + edges.top = 20; + edges.bottom = 29; const auto v = Viewport::FromInclusive(edges); til::point first, second; - first.X = 12; - first.Y = 24; + first.x = 12; + first.y = 24; second = first; - second.X += 2; + second.x += 2; VERIFY_ARE_EQUAL(-2, v.CompareInBounds(first, second), L"Second and first on same row. Second is right of first."); VERIFY_ARE_EQUAL(2, v.CompareInBounds(second, first), L"Reverse params, should get opposite direction, same magnitude."); - first.X = edges.Left; - first.Y = 24; + first.x = edges.left; + first.y = 24; - second.X = edges.Right; - second.Y = first.Y - 1; + second.x = edges.right; + second.y = first.y - 1; VERIFY_ARE_EQUAL(1, v.CompareInBounds(first, second), L"Second is up a line at the right edge from first at the line below on the left edge."); VERIFY_ARE_EQUAL(-1, v.CompareInBounds(second, first), L"Reverse params, should get opposite direction, same magnitude."); @@ -731,20 +731,20 @@ class ViewportTests TEST_METHOD(Offset) { til::inclusive_rect edges; - edges.Top = 0; - edges.Left = 0; - edges.Right = 10; - edges.Bottom = 10; + edges.top = 0; + edges.left = 0; + edges.right = 10; + edges.bottom = 10; const auto original = Viewport::FromInclusive(edges); Log::Comment(L"Move down and to the right first."); til::point adjust{ 7, 2 }; til::inclusive_rect expectedEdges; - expectedEdges.Top = edges.Top + adjust.Y; - expectedEdges.Bottom = edges.Bottom + adjust.Y; - expectedEdges.Left = edges.Left + adjust.X; - expectedEdges.Right = edges.Right + adjust.X; + expectedEdges.top = edges.top + adjust.y; + expectedEdges.bottom = edges.bottom + adjust.y; + expectedEdges.left = edges.left + adjust.x; + expectedEdges.right = edges.right + adjust.x; auto expected = Viewport::FromInclusive(expectedEdges); @@ -754,10 +754,10 @@ class ViewportTests Log::Comment(L"Now try moving up and to the left."); adjust = { -3, -5 }; - expectedEdges.Top = edges.Top + adjust.Y; - expectedEdges.Bottom = edges.Bottom + adjust.Y; - expectedEdges.Left = edges.Left + adjust.X; - expectedEdges.Right = edges.Right + adjust.X; + expectedEdges.top = edges.top + adjust.y; + expectedEdges.bottom = edges.bottom + adjust.y; + expectedEdges.left = edges.left + adjust.x; + expectedEdges.right = edges.right + adjust.x; expected = Viewport::FromInclusive(expectedEdges); actual = Viewport::Offset(original, adjust); @@ -767,24 +767,24 @@ class ViewportTests TEST_METHOD(Union) { til::inclusive_rect srOne; - srOne.Left = 4; - srOne.Right = 10; - srOne.Top = 6; - srOne.Bottom = 14; + srOne.left = 4; + srOne.right = 10; + srOne.top = 6; + srOne.bottom = 14; const auto one = Viewport::FromInclusive(srOne); til::inclusive_rect srTwo; - srTwo.Left = 5; - srTwo.Right = 13; - srTwo.Top = 2; - srTwo.Bottom = 10; + srTwo.left = 5; + srTwo.right = 13; + srTwo.top = 2; + srTwo.bottom = 10; const auto two = Viewport::FromInclusive(srTwo); til::inclusive_rect srExpected; - srExpected.Left = srOne.Left < srTwo.Left ? srOne.Left : srTwo.Left; - srExpected.Right = srOne.Right > srTwo.Right ? srOne.Right : srTwo.Right; - srExpected.Top = srOne.Top < srTwo.Top ? srOne.Top : srTwo.Top; - srExpected.Bottom = srOne.Bottom > srTwo.Bottom ? srOne.Bottom : srTwo.Bottom; + srExpected.left = srOne.left < srTwo.left ? srOne.left : srTwo.left; + srExpected.right = srOne.right > srTwo.right ? srOne.right : srTwo.right; + srExpected.top = srOne.top < srTwo.top ? srOne.top : srTwo.top; + srExpected.bottom = srOne.bottom > srTwo.bottom ? srOne.bottom : srTwo.bottom; const auto expected = Viewport::FromInclusive(srExpected); @@ -795,24 +795,24 @@ class ViewportTests TEST_METHOD(Intersect) { til::inclusive_rect srOne; - srOne.Left = 4; - srOne.Right = 10; - srOne.Top = 6; - srOne.Bottom = 14; + srOne.left = 4; + srOne.right = 10; + srOne.top = 6; + srOne.bottom = 14; const auto one = Viewport::FromInclusive(srOne); til::inclusive_rect srTwo; - srTwo.Left = 5; - srTwo.Right = 13; - srTwo.Top = 2; - srTwo.Bottom = 10; + srTwo.left = 5; + srTwo.right = 13; + srTwo.top = 2; + srTwo.bottom = 10; const auto two = Viewport::FromInclusive(srTwo); til::inclusive_rect srExpected; - srExpected.Left = srOne.Left > srTwo.Left ? srOne.Left : srTwo.Left; - srExpected.Right = srOne.Right < srTwo.Right ? srOne.Right : srTwo.Right; - srExpected.Top = srOne.Top > srTwo.Top ? srOne.Top : srTwo.Top; - srExpected.Bottom = srOne.Bottom < srTwo.Bottom ? srOne.Bottom : srTwo.Bottom; + srExpected.left = srOne.left > srTwo.left ? srOne.left : srTwo.left; + srExpected.right = srOne.right < srTwo.right ? srOne.right : srTwo.right; + srExpected.top = srOne.top > srTwo.top ? srOne.top : srTwo.top; + srExpected.bottom = srOne.bottom < srTwo.bottom ? srOne.bottom : srTwo.bottom; const auto expected = Viewport::FromInclusive(srExpected); @@ -823,29 +823,29 @@ class ViewportTests TEST_METHOD(SubtractFour) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); til::inclusive_rect srRemove; - srRemove.Top = 3; - srRemove.Left = 3; - srRemove.Bottom = 6; - srRemove.Right = 6; + srRemove.top = 3; + srRemove.left = 3; + srRemove.bottom = 6; + srRemove.right = 6; const auto remove = Viewport::FromInclusive(srRemove); std::vector expected; // til::inclusive_rect constructed as: Left, Top, Right, Bottom // Top View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srOriginal.top, srOriginal.right, srRemove.top - 1 })); // Bottom View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Bottom + 1, srOriginal.Right, srOriginal.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srRemove.bottom + 1, srOriginal.right, srOriginal.bottom })); // Left View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srRemove.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srRemove.top, srRemove.left - 1, srRemove.bottom })); // Right View - expected.emplace_back(Viewport::FromInclusive({ srRemove.Right + 1, srRemove.Top, srOriginal.Right, srRemove.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srRemove.right + 1, srRemove.top, srOriginal.right, srRemove.bottom })); const auto actual = Viewport::Subtract(original, remove); @@ -862,27 +862,27 @@ class ViewportTests TEST_METHOD(SubtractThree) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); til::inclusive_rect srRemove; - srRemove.Top = 3; - srRemove.Left = 3; - srRemove.Bottom = 6; - srRemove.Right = 15; + srRemove.top = 3; + srRemove.left = 3; + srRemove.bottom = 6; + srRemove.right = 15; const auto remove = Viewport::FromInclusive(srRemove); std::vector expected; // til::inclusive_rect constructed as: Left, Top, Right, Bottom // Top View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srOriginal.top, srOriginal.right, srRemove.top - 1 })); // Bottom View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Bottom + 1, srOriginal.Right, srOriginal.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srRemove.bottom + 1, srOriginal.right, srOriginal.bottom })); // Left View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srRemove.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srRemove.top, srRemove.left - 1, srRemove.bottom })); const auto actual = Viewport::Subtract(original, remove); @@ -899,25 +899,25 @@ class ViewportTests TEST_METHOD(SubtractTwo) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); til::inclusive_rect srRemove; - srRemove.Top = 3; - srRemove.Left = 3; - srRemove.Bottom = 15; - srRemove.Right = 15; + srRemove.top = 3; + srRemove.left = 3; + srRemove.bottom = 15; + srRemove.right = 15; const auto remove = Viewport::FromInclusive(srRemove); std::vector expected; // til::inclusive_rect constructed as: Left, Top, Right, Bottom // Top View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srOriginal.top, srOriginal.right, srRemove.top - 1 })); // Left View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srOriginal.Bottom })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srRemove.top, srRemove.left - 1, srOriginal.bottom })); const auto actual = Viewport::Subtract(original, remove); @@ -934,23 +934,23 @@ class ViewportTests TEST_METHOD(SubtractOne) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); til::inclusive_rect srRemove; - srRemove.Top = 3; - srRemove.Left = -12; - srRemove.Bottom = 15; - srRemove.Right = 15; + srRemove.top = 3; + srRemove.left = -12; + srRemove.bottom = 15; + srRemove.right = 15; const auto remove = Viewport::FromInclusive(srRemove); std::vector expected; // til::inclusive_rect constructed as: Left, Top, Right, Bottom // Top View - expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + expected.emplace_back(Viewport::FromInclusive({ srOriginal.left, srOriginal.top, srOriginal.right, srRemove.top - 1 })); const auto foo = expected.cbegin(); @@ -969,17 +969,17 @@ class ViewportTests TEST_METHOD(SubtractZero) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); til::inclusive_rect srRemove; - srRemove.Top = 12; - srRemove.Left = 12; - srRemove.Bottom = 15; - srRemove.Right = 15; + srRemove.top = 12; + srRemove.left = 12; + srRemove.bottom = 15; + srRemove.right = 15; const auto remove = Viewport::FromInclusive(srRemove); std::vector expected; @@ -1000,10 +1000,10 @@ class ViewportTests TEST_METHOD(SubtractSame) { til::inclusive_rect srOriginal; - srOriginal.Top = 0; - srOriginal.Left = 0; - srOriginal.Bottom = 10; - srOriginal.Right = 10; + srOriginal.top = 0; + srOriginal.left = 0; + srOriginal.bottom = 10; + srOriginal.right = 10; const auto original = Viewport::FromInclusive(srOriginal); const auto remove = original; diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index c87256acfe..902f711771 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -81,9 +81,9 @@ void VtIoTests::ModeParsingTest() Viewport SetUpViewport() { til::inclusive_rect view; - view.Top = view.Left = 0; - view.Bottom = 31; - view.Right = 79; + view.top = view.left = 0; + view.bottom = 31; + view.right = 79; return Viewport::FromInclusive(view); } @@ -254,7 +254,7 @@ void VtIoTests::DtorTestStackAllocMany() } } -class MockRenderData : public IRenderData, IUiaData +class MockRenderData : public IRenderData { public: Microsoft::Console::Types::Viewport GetViewport() noexcept override @@ -373,10 +373,6 @@ public: 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/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index 5f2331d649..279012e71f 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); @@ -118,9 +122,9 @@ class Microsoft::Console::Render::VtRendererTest Viewport VtRendererTest::SetUpViewport() { til::inclusive_rect view; - view.Top = view.Left = 0; - view.Bottom = 31; - view.Right = 79; + view.top = view.left = 0; + view.bottom = 31; + view.right = 79; return Viewport::FromInclusive(view); } @@ -250,7 +254,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled one down, only top line is invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 1; + invalid.bottom = 1; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); @@ -268,7 +272,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 3; + invalid.bottom = 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -292,7 +296,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled one up, only bottom line is invalid. ----")); invalid = view.ToExclusive(); - invalid.Top = invalid.Bottom - 1; + invalid.top = invalid.bottom - 1; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); @@ -309,7 +313,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three up, only bottom 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Top = invalid.Bottom - 3; + invalid.top = invalid.bottom - 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -339,7 +343,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 3; + invalid.bottom = 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -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); @@ -925,7 +1086,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled one down, only top line is invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 1; + invalid.bottom = 1; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); @@ -942,7 +1103,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 3; + invalid.bottom = 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -966,7 +1127,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled one up, only bottom line is invalid. ----")); invalid = view.ToExclusive(); - invalid.Top = invalid.Bottom - 1; + invalid.top = invalid.bottom - 1; const auto runs = engine->_invalidMap.runs(); VERIFY_ARE_EQUAL(1u, runs.size()); @@ -983,7 +1144,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three up, only bottom 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Top = invalid.Bottom - 3; + invalid.top = invalid.bottom - 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -1013,7 +1174,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); invalid = view.ToExclusive(); - invalid.Bottom = 3; + invalid.bottom = 3; // we should have 3 runs and build a rectangle out of them const auto runs = engine->_invalidMap.runs(); @@ -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/host/ut_host/sources b/src/host/ut_host/sources index 37d8eb14f9..9b47dded30 100644 --- a/src/host/ut_host/sources +++ b/src/host/ut_host/sources @@ -28,7 +28,6 @@ SOURCES = \ ClipboardTests.cpp \ SelectionTests.cpp \ Utf8ToWideCharParserTests.cpp \ - Utf16ParserTests.cpp \ OutputCellIteratorTests.cpp \ InitTests.cpp \ TitleTests.cpp \ @@ -38,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/host/utils.cpp b/src/host/utils.cpp index c451122cb3..74d0884541 100644 --- a/src/host/utils.cpp +++ b/src/host/utils.cpp @@ -13,12 +13,12 @@ using Microsoft::Console::Interactivity::ServiceLocator; til::CoordType CalcWindowSizeX(const til::inclusive_rect& rect) noexcept { - return rect.Right - rect.Left + 1; + return rect.right - rect.left + 1; } til::CoordType CalcWindowSizeY(const til::inclusive_rect& rect) noexcept { - return rect.Bottom - rect.Top + 1; + return rect.bottom - rect.top + 1; } til::CoordType CalcCursorYOffsetInPixels(const til::CoordType sFontSizeY, const ULONG ulSize) noexcept @@ -69,11 +69,11 @@ std::wstring _LoadString(const UINT id) LANGID LangId; const auto Status = GetConsoleLangId(gci.OutputCP, &LangId); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { ItemLength = s_LoadStringEx(ServiceLocator::LocateGlobals().hInstance, id, ItemString, ARRAYSIZE(ItemString), LangId); } - if (!NT_SUCCESS(Status) || ItemLength == 0) + if (FAILED_NTSTATUS(Status) || ItemLength == 0) { ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, id, ItemString, ARRAYSIZE(ItemString)); } @@ -169,23 +169,23 @@ UINT s_LoadStringEx(_In_ HINSTANCE hModule, _In_ UINT wID, _Out_writes_(cchBuffe // - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom) int Utils::s_CompareCoords(const til::size bufferSize, const til::point coordFirst, const til::point coordSecond) noexcept { - const auto cRowWidth = bufferSize.X; + const auto cRowWidth = bufferSize.width; // Assert that our coordinates are within the expected boundaries - const auto cRowHeight = bufferSize.Y; - FAIL_FAST_IF(!(coordFirst.X >= 0 && coordFirst.X < cRowWidth)); - FAIL_FAST_IF(!(coordSecond.X >= 0 && coordSecond.X < cRowWidth)); - FAIL_FAST_IF(!(coordFirst.Y >= 0 && coordFirst.Y < cRowHeight)); - FAIL_FAST_IF(!(coordSecond.Y >= 0 && coordSecond.Y < cRowHeight)); + const auto cRowHeight = bufferSize.height; + FAIL_FAST_IF(!(coordFirst.x >= 0 && coordFirst.x < cRowWidth)); + FAIL_FAST_IF(!(coordSecond.x >= 0 && coordSecond.x < cRowWidth)); + FAIL_FAST_IF(!(coordFirst.y >= 0 && coordFirst.y < cRowHeight)); + FAIL_FAST_IF(!(coordSecond.y >= 0 && coordSecond.y < cRowHeight)); // First set the distance vertically // If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160. // For the same row, it'll be 0 rows * 80 character width = 0 difference. - auto retVal = (coordFirst.Y - coordSecond.Y) * cRowWidth; + auto retVal = (coordFirst.y - coordSecond.y) * cRowWidth; // Now adjust for horizontal differences // If first is in position 15 and second is in position 30, first is -15 left in relation to 30. - retVal += (coordFirst.X - coordSecond.X); + retVal += (coordFirst.x - coordSecond.x); // Further notes: // If we already moved behind one row, this will help correct for when first is right of second. @@ -228,9 +228,9 @@ int Utils::s_CompareCoords(const til::point coordFirst, const til::point coordSe til::point Utils::s_GetOppositeCorner(const til::inclusive_rect& srRectangle, const til::point coordCorner) noexcept { // Assert we were given coordinates that are indeed one of the corners of the rectangle. - FAIL_FAST_IF(!(coordCorner.X == srRectangle.Left || coordCorner.X == srRectangle.Right)); - FAIL_FAST_IF(!(coordCorner.Y == srRectangle.Top || coordCorner.Y == srRectangle.Bottom)); + FAIL_FAST_IF(!(coordCorner.x == srRectangle.left || coordCorner.x == srRectangle.right)); + FAIL_FAST_IF(!(coordCorner.y == srRectangle.top || coordCorner.y == srRectangle.bottom)); - return { (srRectangle.Left == coordCorner.X) ? srRectangle.Right : srRectangle.Left, - (srRectangle.Top == coordCorner.Y) ? srRectangle.Bottom : srRectangle.Top }; + return { (srRectangle.left == coordCorner.x) ? srRectangle.right : srRectangle.left, + (srRectangle.top == coordCorner.y) ? srRectangle.bottom : srRectangle.top }; } diff --git a/src/inc/HostAndPropsheetIncludes.h b/src/inc/HostAndPropsheetIncludes.h index 1f27311913..2a8d588b30 100644 --- a/src/inc/HostAndPropsheetIncludes.h +++ b/src/inc/HostAndPropsheetIncludes.h @@ -14,16 +14,8 @@ #include #undef WIN32_NO_STATUS -// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs -typedef _Return_type_success_(return >= 0) LONG NTSTATUS; -/*lint -save -e624 */ // Don't complain about different typedefs. -typedef NTSTATUS *PNTSTATUS; -/*lint -restore */ // Resume checking for different typedefs. -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#include -// End From ntdef.h - -#define INLINE_NTSTATUS_FROM_WIN32 1 // Must use inline NTSTATUS or it will call the wrapped function twice. #pragma warning(push) #pragma warning(disable:4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. #include diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index 66ee52d5eb..7a7ec3a6a5 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -17,6 +17,8 @@ // Block minwindef.h min/max macros to prevent conflict #define NOMINMAX +// Exclude rarely-used stuff from Windows headers +#define WIN32_LEAN_AND_MEAN #include #include @@ -39,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -51,22 +54,19 @@ #include // WIL -#include -#include -#include -#include -#include -#include #include +#include #include -#include +// Due to the use of RESOURCE_SUPPRESS_STL in result.h, we need to include resource.h first, which happens +// implicitly through the includes above. If RESOURCE_SUPPRESS_STL is gone, the order doesn't matter anymore. +#include +#include // GSL // Block GSL Multi Span include because it both has C++17 deprecated iterators // and uses the C-namespaced "max" which conflicts with Windows definitions. -#define GSL_MULTI_SPAN_H -#include -#include +#include +#include // CppCoreCheck #include diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp index f70697c33b..a61407e5b1 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/inc/conime.h b/src/inc/conime.h index 18ebf245f0..96ca7c3e0b 100644 --- a/src/inc/conime.h +++ b/src/inc/conime.h @@ -32,8 +32,8 @@ constexpr BYTE CONIME_CURSOR_LEFT = 0x20; [[nodiscard]] HRESULT ImeEndComposition(); [[nodiscard]] HRESULT ImeComposeData(std::wstring_view text, - gsl::span attributes, - gsl::span colorArray); + std::span attributes, + std::span colorArray); [[nodiscard]] HRESULT ImeClearComposeData(); diff --git a/src/inc/conpty-static.h b/src/inc/conpty-static.h index 27fa1437ae..4d22048868 100644 --- a/src/inc/conpty-static.h +++ b/src/inc/conpty-static.h @@ -24,23 +24,18 @@ #endif #define PSEUDOCONSOLE_RESIZE_QUIRK (2u) -#define PSEUDOCONSOLE_WIN32_INPUT_MODE (4u) #define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u) CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); - CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size); - CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC); - CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show); - CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent); +CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC); CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC); - -CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleNoWait(HPCON hPC); +CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleTimeout(HPCON hPC, DWORD dwMilliseconds); CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC); diff --git a/src/inc/conpty.h b/src/inc/conpty.h index 6f51ba9424..73b9cebb93 100644 --- a/src/inc/conpty.h +++ b/src/inc/conpty.h @@ -29,7 +29,7 @@ bool SignalResizeWindow(const HANDLE hSignal, // this pipe. For keys that don't have character representations, the // caller should use the `TERM=xterm` VT sequences for encoding the input. // * hOutput: The caller should read from this pipe. The headless conhost will -// "render" it's state to a stream of utf-8 encoded text with VT sequences. +// "render" its state to a stream of utf-8 encoded text with VT sequences. // * hSignal: The caller can use this to resize the size of the underlying PTY // using the SignalResizeWindow function. // Arguments: diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp index 40e584b543..1803acb0ae 100644 --- a/src/inc/test/CommonState.hpp +++ b/src/inc/test/CommonState.hpp @@ -20,11 +20,8 @@ unit testing projects in the codebase without a bunch of overhead. #pragma once -#define VERIFY_SUCCESS_NTSTATUS(x) VERIFY_IS_TRUE(NT_SUCCESS(x)) - #include "../host/globals.h" #include "../host/inputReadHandleData.h" -#include "../buffer/out/CharRow.hpp" #include "../interactivity/inc/ServiceLocator.hpp" class CommonState @@ -99,12 +96,12 @@ public: Globals& g = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals(); CONSOLE_INFORMATION& gci = g.getConsoleInformation(); til::size coordWindowSize; - coordWindowSize.X = viewWidth; - coordWindowSize.Y = viewHeight; + coordWindowSize.width = viewWidth; + coordWindowSize.height = viewHeight; til::size coordScreenBufferSize; - coordScreenBufferSize.X = bufferWidth; - coordScreenBufferSize.Y = bufferHeight; + coordScreenBufferSize.width = bufferWidth; + coordScreenBufferSize.height = bufferHeight; UINT uiCursorSize = 12; @@ -147,7 +144,7 @@ public: 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, @@ -176,8 +173,8 @@ public: Globals& g = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals(); CONSOLE_INFORMATION& gci = g.getConsoleInformation(); til::size coordScreenBufferSize; - coordScreenBufferSize.X = bufferWidth; - coordScreenBufferSize.Y = bufferHeight; + coordScreenBufferSize.width = bufferWidth; + coordScreenBufferSize.height = bufferHeight; UINT uiCursorSize = 12; @@ -240,27 +237,8 @@ public: for (til::CoordType iRow = 0; iRow < cRowsToFill; iRow++) { - ROW& row = textBuffer.GetRowByOffset(iRow); - FillRow(&row); - } - - textBuffer.GetCursor().SetYPosition(cRowsToFill); - } - - void FillTextBufferBisect() - { - CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); - // fill with some text that fills the whole row and has bisecting double byte characters - const auto cRowsToFill = s_csBufferHeight; - - VERIFY_IS_TRUE(gci.HasActiveOutputBuffer()); - - TextBuffer& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); - - for (til::CoordType iRow = 0; iRow < cRowsToFill; iRow++) - { - ROW& row = textBuffer.GetRowByOffset(iRow); - FillBisect(&row); + ROW& row = textBuffer.GetMutableRowByOffset(iRow); + FillRow(&row, iRow & 1); } textBuffer.GetCursor().SetYPosition(cRowsToFill); @@ -278,54 +256,43 @@ private: std::unique_ptr m_backupTextBufferInfo; std::unique_ptr m_readHandle; - void FillRow(ROW* pRow) + void FillRow(ROW* pRow, bool wrapForced) { // fill a row // 9 characters, 6 spaces. 15 total // か = \x304b // き = \x304d - const PCWSTR pwszText = L"AB" - L"\x304b\x304b" - L"C" - L"\x304d\x304d" - L"DE "; - const size_t length = wcslen(pwszText); - std::vector attrs(length, DbcsAttribute()); - // set double-byte/double-width attributes - attrs[2].SetLeading(); - attrs[3].SetTrailing(); - attrs[5].SetLeading(); - attrs[6].SetTrailing(); + uint16_t column = 0; + for (const auto& ch : std::wstring_view{ L"AB\u304bC\u304dDE " }) + { + const uint16_t width = ch >= 0x80 ? 2 : 1; + pRow->ReplaceCharacters(column, width, { &ch, 1 }); + column += width; + } - CharRow& charRow = pRow->GetCharRow(); - OverwriteColumns(pwszText, pwszText + length, attrs.cbegin(), charRow.begin()); - - // set some colors - TextAttribute Attr = TextAttribute(0); - pRow->GetAttrRow().Reset(Attr); // A = bright red on dark gray // This string starts at index 0 - Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY); - pRow->GetAttrRow().SetAttrToEnd(0, Attr); + auto Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY); + pRow->SetAttrToEnd(0, Attr); // BかC = dark gold on bright blue // This string starts at index 1 Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY); - pRow->GetAttrRow().SetAttrToEnd(1, Attr); + pRow->SetAttrToEnd(1, Attr); // き = bright white on dark purple // This string starts at index 5 Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_BLUE); - pRow->GetAttrRow().SetAttrToEnd(5, Attr); + pRow->SetAttrToEnd(5, Attr); // DE = black on dark green // This string starts at index 7 Attr = TextAttribute(BACKGROUND_GREEN); - pRow->GetAttrRow().SetAttrToEnd(7, Attr); + pRow->SetAttrToEnd(7, Attr); // odd rows forced a wrap - if (pRow->GetId() % 2 != 0) + if (wrapForced) { pRow->SetWrapForced(true); } @@ -334,41 +301,4 @@ private: pRow->SetWrapForced(false); } } - - void FillBisect(ROW* pRow) - { - const CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation(); - // length 80 string of text with bisecting characters at the beginning and end. - // positions of き(\x304d) are at 0, 27-28, 39-40, 67-68, 79 - auto pwszText = - L"\x304d" - L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" - L"\x304d\x304d" - L"0123456789" - L"\x304d\x304d" - L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" - L"\x304d\x304d" - L"0123456789" - L"\x304d"; - const size_t length = wcslen(pwszText); - - std::vector attrs(length, DbcsAttribute()); - // set double-byte/double-width attributes - attrs[0].SetTrailing(); - attrs[27].SetLeading(); - attrs[28].SetTrailing(); - attrs[39].SetLeading(); - attrs[40].SetTrailing(); - attrs[67].SetLeading(); - attrs[68].SetTrailing(); - attrs[79].SetLeading(); - - CharRow& charRow = pRow->GetCharRow(); - OverwriteColumns(pwszText, pwszText + length, attrs.cbegin(), charRow.begin()); - - // everything gets default attributes - pRow->GetAttrRow().Reset(gci.GetActiveOutputBuffer().GetAttributes()); - - pRow->SetWrapForced(true); - } }; diff --git a/src/inc/til.h b/src/inc/til.h index f7bcc5ac11..ee4f29df34 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -3,6 +3,15 @@ #pragma once +// This is a copy of how DirectXMath.h determines _XM_SSE_INTRINSICS_ and _XM_ARM_NEON_INTRINSICS_. +#if (defined(_M_IX86) || defined(_M_X64) || __i386__ || __x86_64__) && !defined(_M_HYBRID_X86_ARM64) && !defined(_M_ARM64EC) +#define TIL_SSE_INTRINSICS +#elif defined(_M_ARM) || defined(_M_ARM64) || defined(_M_HYBRID_X86_ARM64) || defined(_M_ARM64EC) || __arm__ || __aarch64__ +#define TIL_ARM_NEON_INTRINSICS +#else +#define TIL_NO_INTRINSICS +#endif + #define _TIL_INLINEPREFIX __declspec(noinline) inline #include "til/at.h" diff --git a/src/inc/til/at.h b/src/inc/til/at.h index 28a715be86..bb1596a4d6 100644 --- a/src/inc/til/at.h +++ b/src/inc/til/at.h @@ -5,53 +5,21 @@ namespace til { - namespace details - { - // This was lifted from gsl::details::is_span. - template - struct is_span_oracle : std::false_type - { - }; - -#ifdef GSL_SPAN_H - template - struct is_span_oracle> : std::true_type - { - }; -#endif - - template - struct is_span : public is_span_oracle> - { - }; - } - // The at function declares that you've already sufficiently checked that your array access // is in range before retrieving an item inside it at an offset. // This is to save double/triple/quadruple testing in circumstances where you are already // pivoting on the length of a set and now want to pull elements out of it by offset // without checking again. // gsl::at will do the check again. As will .at(). And using [] will have a warning in audit. - // This template is explicitly disabled if T is of type gsl::span, as it would interfere with + // This template is explicitly disabled if T is of type std::span, as it would interfere with // the overload below. template constexpr auto at(T&& cont, const I i) noexcept -> decltype(auto) { -#ifdef GSL_SPAN_H - if constexpr (details::is_span::value) - { -#pragma warning(suppress : 26481) // Suppress bounds.1 check for doing pointer arithmetic +#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. - return cont.data()[i]; - } - else -#endif - { -#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 gsl::span or std::string_view - return cont[i]; - } +#pragma warning(suppress : 26445) // Suppress lifetime check for a reference to std::span or std::string_view + return cont[i]; } } diff --git a/src/inc/til/bitmap.h b/src/inc/til/bitmap.h index f3527914bf..3821350047 100644 --- a/src/inc/til/bitmap.h +++ b/src/inc/til/bitmap.h @@ -264,7 +264,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return const_iterator(_bits, til::rect{ _sz }, _sz.area()); } - const gsl::span runs() const + const std::span runs() const { // If we don't have cached runs, rebuild. if (!_runs.has_value()) @@ -351,20 +351,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" void set(const til::point pt) { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); - _runs.reset(); // reset cached runs on any non-const method - - _bits.set(_rc.index_of(pt)); + if (_rc.contains(pt)) + { + _runs.reset(); // reset cached runs on any non-const method + _bits.set(_rc.index_of(pt)); + } } - void set(const til::rect& rc) + void set(til::rect rc) { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc)); _runs.reset(); // reset cached runs on any non-const method - for (auto row = rc.top; row < rc.bottom; ++row) + rc &= _rc; + + const auto width = rc.width(); + const auto stride = _rc.width(); + auto idx = _rc.index_of({ rc.left, rc.top }); + + for (auto row = rc.top; row < rc.bottom; ++row, idx += stride) { - _bits.set(_rc.index_of(til::point{ rc.left, row }), rc.width(), true); + _bits.set(idx, width, true); } } diff --git a/src/inc/til/bytes.h b/src/inc/til/bytes.h new file mode 100644 index 0000000000..f2ab2c6e22 --- /dev/null +++ b/src/inc/til/bytes.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "type_traits.h" + +namespace til +{ + template + constexpr void bytes_advance(Target& target, size_t count) + { + if (count > target.size()) + { + throw std::length_error{ "insufficient space left" }; + } + + target = { target.data() + count, target.size() - count }; + } + + template + constexpr bool bytes_can_put(const Target& target) + { + return target.size() >= sizeof(T); + } + + template + constexpr void bytes_put(Target& target, const T& value) + { + using TargetType = typename Target::value_type; + constexpr auto size = sizeof(value); + + if (size > target.size()) + { + throw std::length_error{ "insufficient space left" }; + } + + std::copy_n(reinterpret_cast(&value), size, target.data()); + target = { target.data() + size, target.size() - size }; + } + + template + requires TriviallyCopyable + constexpr void bytes_transfer(Target& target, Source& source) + { + using TargetType = typename Target::value_type; + constexpr auto elementSize = sizeof(typename Source::value_type); + + const auto sourceCount = std::min(source.size(), target.size() / elementSize); + const auto targetCount = sourceCount * elementSize; + + std::copy_n(reinterpret_cast(source.data()), targetCount, target.data()); + + target = { target.data() + targetCount, target.size() - targetCount }; + source = { source.data() + sourceCount, source.size() - sourceCount }; + } +} diff --git a/src/inc/til/coalesce.h b/src/inc/til/coalesce.h index 2bc789b477..e9948d362e 100644 --- a/src/inc/til/coalesce.h +++ b/src/inc/til/coalesce.h @@ -8,7 +8,7 @@ namespace til // Method Description: // - Base case provided to handle the last argument to coalesce_value() template - T coalesce_value(const T& base) + T coalesce_value(const T& base) noexcept { return base; } diff --git a/src/inc/til/color.h b/src/inc/til/color.h index e90ca88efc..cfeee35228 100644 --- a/src/inc/til/color.h +++ b/src/inc/til/color.h @@ -9,7 +9,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // a number of other color types. #pragma warning(push) // we can't depend on GSL here, so we use static_cast for explicit narrowing -#pragma warning(disable : 26472) +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#pragma warning(disable : 26495) // Variable 'til::color::::abgr' is uninitialized. Always initialize a member variable (type.6). struct color { // Clang (10) has no trouble optimizing the COLORREF conversion operator, below, to a diff --git a/src/inc/til/env.h b/src/inc/til/env.h new file mode 100644 index 0000000000..4d95f9f2f9 --- /dev/null +++ b/src/inc/til/env.h @@ -0,0 +1,670 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + +#ifdef UNIT_TESTING +class EnvTests; +#endif + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + namespace details + { + + namespace vars + { + inline constexpr wil::zwstring_view system_root{ L"SystemRoot" }; + inline constexpr wil::zwstring_view system_drive{ L"SystemDrive" }; + inline constexpr wil::zwstring_view all_users_profile{ L"ALLUSERSPROFILE" }; + inline constexpr wil::zwstring_view public_var{ L"PUBLIC" }; + inline constexpr wil::zwstring_view program_data{ L"ProgramData" }; + inline constexpr wil::zwstring_view computer_name{ L"COMPUTERNAME" }; + inline constexpr wil::zwstring_view user_name{ L"USERNAME" }; + inline constexpr wil::zwstring_view user_domain{ L"USERDOMAIN" }; + inline constexpr wil::zwstring_view user_dns_domain{ L"USERDNSDOMAIN" }; + inline constexpr wil::zwstring_view home_drive{ L"HOMEDRIVE" }; + inline constexpr wil::zwstring_view home_share{ L"HOMESHARE" }; + inline constexpr wil::zwstring_view home_path{ L"HOMEPATH" }; + inline constexpr wil::zwstring_view user_profile{ L"USERPROFILE" }; + inline constexpr wil::zwstring_view app_data{ L"APPDATA" }; + inline constexpr wil::zwstring_view local_app_data{ L"LOCALAPPDATA" }; + + inline constexpr wil::zwstring_view program_files{ L"ProgramFiles" }; + inline constexpr wil::zwstring_view program_files_x86{ L"ProgramFiles(x86)" }; + inline constexpr wil::zwstring_view program_files_arm64{ L"ProgramFiles(Arm)" }; + inline constexpr wil::zwstring_view program_w6432{ L"ProgramW6432" }; + inline constexpr wil::zwstring_view common_program_files{ L"CommonProgramFiles" }; + inline constexpr wil::zwstring_view common_program_files_x86{ L"CommonProgramFiles(x86)" }; + inline constexpr wil::zwstring_view common_program_files_arm64{ L"CommonProgramFiles(Arm)" }; + inline constexpr wil::zwstring_view common_program_w6432{ L"CommonProgramW6432" }; + + const std::array program_files_map{ + std::pair{ L"ProgramFilesDir", program_files }, + std::pair{ L"CommonFilesDir", common_program_files }, +#ifdef _WIN64 +#ifdef _M_ARM64 + std::pair{ L"ProgramFilesDir (Arm)", program_files_arm64 }, + std::pair{ L"CommonFilesDir (Arm)", common_program_files_arm64 }, +#endif + std::pair{ L"ProgramFilesDir (x86)", program_files_x86 }, + std::pair{ L"CommonFilesDir (x86)", common_program_files_x86 }, + std::pair{ L"ProgramW6432Dir", program_w6432 }, + std::pair{ L"CommonW6432Dir", common_program_w6432 }, +#endif + }; + + namespace reg + { + inline constexpr wil::zwstring_view program_files_root{ LR"(Software\Microsoft\Windows\CurrentVersion)" }; + inline constexpr wil::zwstring_view system_env_var_root{ LR"(SYSTEM\CurrentControlSet\Control\Session Manager\Environment)" }; + inline constexpr wil::zwstring_view user_env_var_root{ LR"(Environment)" }; + inline constexpr wil::zwstring_view user_volatile_env_var_root{ LR"(Volatile Environment)" }; + inline constexpr wil::zwstring_view user_volatile_session_env_var_root_pattern{ LR"(Volatile Environment\{0:d})" }; + }; + }; + + namespace wil_env + { + /** Looks up the computer name and fails if it is not found. */ + template + HRESULT GetComputerNameW(string_type& result) WI_NOEXCEPT + { + return wil::AdaptFixedSizeToAllocatedResult(result, [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNul) -> HRESULT { + // If the function succeeds, the return value is the number of characters stored in the buffer + // pointed to by lpBuffer, not including the terminating null character. + // + // If lpBuffer is not large enough to hold the data, the return value is the buffer size, in + // characters, required to hold the string and its terminating null character and the contents of + // lpBuffer are undefined. + // + // If the function fails, the return value is zero. If the specified environment variable was not + // found in the environment block, GetLastError returns ERROR_ENVVAR_NOT_FOUND. + + ::SetLastError(ERROR_SUCCESS); + + DWORD length = static_cast(valueLength); + + const auto result = ::GetComputerNameW(value, &length); + *valueLengthNeededWithNul = length; + RETURN_IF_WIN32_BOOL_FALSE_EXPECTED(result); + if (*valueLengthNeededWithNul < valueLength) + { + (*valueLengthNeededWithNul)++; // It fit, account for the null. + } + return S_OK; + }); + } + + /** Looks up the computer name and returns null if it is not found. */ + template + HRESULT TryGetComputerNameW(string_type& result) WI_NOEXCEPT + { + const auto hr = wil_env::GetComputerNameW(result); + RETURN_HR_IF(hr, FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_ENVVAR_NOT_FOUND))); + return S_OK; + } + +#ifdef WIL_ENABLE_EXCEPTIONS + /** Looks up the computer name and fails if it is not found. */ + template + string_type GetComputerNameW() + { + string_type result; + THROW_IF_FAILED((wil_env::GetComputerNameW(result))); + return result; + } + + /** Looks up the computer name and returns null if it is not found. */ + template + string_type TryGetComputerNameW() + { + string_type result; + THROW_IF_FAILED((wil_env::TryGetComputerNameW(result))); + return result; + } +#endif + + /** Looks up a registry value from 'key' and fails if it is not found. */ + template + HRESULT RegQueryValueExW(HKEY key, PCWSTR valueName, string_type& result) WI_NOEXCEPT + { + return wil::AdaptFixedSizeToAllocatedResult(result, [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNul) -> HRESULT { + auto length = gsl::narrow(valueLength * sizeof(wchar_t)); + const auto status = ::RegQueryValueExW(key, valueName, nullptr, nullptr, reinterpret_cast(value), &length); + // length will receive the number of bytes including trailing null byte. Convert to a number of wchar_t's. + // AdaptFixedSizeToAllocatedResult will then resize buffer to valueLengthNeededWithNull. + // We're rounding up to prevent infinite loops if the data isn't a REG_SZ and length isn't divisible by 2. + *valueLengthNeededWithNul = (length + sizeof(wchar_t) - 1) / sizeof(wchar_t); + return status == ERROR_MORE_DATA ? S_OK : HRESULT_FROM_WIN32(status); + }); + } + + /** Looks up a registry value from 'key' and returns null if it is not found. */ + template + HRESULT TryRegQueryValueExW(HKEY key, PCWSTR valueName, string_type& result) WI_NOEXCEPT + { + const auto hr = wil_env::TryRegQueryValueExW(key, valueName, result); + RETURN_HR_IF(hr, FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_ENVVAR_NOT_FOUND))); + return S_OK; + } + +#ifdef WIL_ENABLE_EXCEPTIONS + /** Looks up a registry value from 'key' and fails if it is not found. */ + template + string_type RegQueryValueExW(HKEY key, PCWSTR valueName) + { + string_type result; + THROW_IF_FAILED((wil_env::RegQueryValueExW(key, valueName, result))); + return result; + } + + /** Looks up a registry value from 'key' and returns null if it is not found. */ + template + string_type TryRegQueryValueExW(HKEY key, PCWSTR valueName) + { + string_type result; + THROW_IF_FAILED((wil_env::TryRegQueryValueExW(key, valueName, result))); + return result; + } +#endif + + //! A strongly typed version of the Win32 API GetShortPathNameW. + //! Return a path in an allocated buffer for handling long paths. + template + HRESULT GetShortPathNameW(PCWSTR file, string_type& path) + { + const auto hr = wil::AdaptFixedSizeToAllocatedResult(path, [&](_Out_writes_(valueLength) PWSTR value, size_t valueLength, _Out_ size_t* valueLengthNeededWithNull) -> HRESULT { + // Note that GetShortPathNameW() is not limited to MAX_PATH + // but it does take a fixed size buffer. + *valueLengthNeededWithNull = ::GetShortPathNameW(file, value, static_cast(valueLength)); + RETURN_LAST_ERROR_IF(*valueLengthNeededWithNull == 0); + WI_ASSERT((*value != L'\0') == (*valueLengthNeededWithNull < valueLength)); + if (*valueLengthNeededWithNull < valueLength) + { + (*valueLengthNeededWithNull)++; // it fit, account for the null + } + return S_OK; + }); + return hr; + } + +#ifdef WIL_ENABLE_EXCEPTIONS + //! A strongly typed version of the Win32 API GetShortPathNameW. + //! Return a path in an allocated buffer for handling long paths. + template + string_type GetShortPathNameW(PCWSTR file) + { + string_type result; + THROW_IF_FAILED((GetShortPathNameW(file, result))); + return result; + } +#endif + }; + }; + + class env + { + private: +#ifdef UNIT_TESTING + friend class ::EnvTests; +#endif + + std::map _envMap{}; + + // We make copies of the environment variable names to ensure they are null terminated. + void get(wil::zwstring_view variable) + { + if (auto value = wil::TryGetEnvironmentVariableW(variable.c_str()); !value.empty()) + { + save_to_map(std::wstring{ variable }, std::move(value)); + } + } + + void get_computer_name() + { + if (auto value = til::details::wil_env::TryGetComputerNameW(); !value.empty()) + { + save_to_map(std::wstring{ til::details::vars::computer_name }, std::move(value)); + } + } + + void get_user_name_and_domain() + try + { + const auto token = wil::open_current_access_token(); + const auto user = wil::get_token_information(token.get()); + + DWORD accountNameSize = 0, userDomainSize = 0; + SID_NAME_USE sidNameUse; + SetLastError(ERROR_SUCCESS); + if (LookupAccountSidW(nullptr, user.get()->User.Sid, nullptr, &accountNameSize, nullptr, &userDomainSize, &sidNameUse) || GetLastError() == ERROR_INSUFFICIENT_BUFFER) + { + std::wstring accountName, userDomain; + accountName.resize(accountNameSize); + userDomain.resize(userDomainSize); + + SetLastError(ERROR_SUCCESS); + if (LookupAccountSidW(nullptr, user.get()->User.Sid, accountName.data(), &accountNameSize, userDomain.data(), &userDomainSize, &sidNameUse)) + { + strip_trailing_null(accountName); + strip_trailing_null(userDomain); + save_to_map(std::wstring{ til::details::vars::user_name }, std::move(accountName)); + save_to_map(std::wstring{ til::details::vars::user_domain }, std::move(userDomain)); + } + } + } + CATCH_LOG() + + void get_program_files() + { + wil::unique_hkey key; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, til::details::vars::reg::program_files_root.data(), 0, KEY_READ, &key) == ERROR_SUCCESS) + { + for (auto& [keyName, varName] : til::details::vars::program_files_map) + { + auto value = til::details::wil_env::RegQueryValueExW(key.get(), keyName); + set_user_environment_var(varName, value); + } + } + } + + void get_vars_from_registry(HKEY rootKey, wil::zwstring_view subkey) + { + wil::unique_hkey key; + if (RegOpenKeyExW(rootKey, subkey.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS) + { + DWORD maxValueNameSize = 0, maxValueDataSize = 0; + if (RegQueryInfoKeyW(key.get(), nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &maxValueNameSize, &maxValueDataSize, nullptr, nullptr) == ERROR_SUCCESS) + { + maxValueNameSize++; // the query does not include the terminating null, but needs that space to enum + std::wstring valueName; + std::basic_string valueData; + valueName.resize(maxValueNameSize); + valueData.resize(maxValueDataSize); + + for (DWORD pass = 0; pass < 2; ++pass) + { + DWORD valueNameSize = maxValueNameSize; + DWORD valueDataSize = maxValueDataSize; + + DWORD index = 0; + DWORD type = 0; + + while (true) + { + const DWORD result = RegEnumValueW(key.get(), index, valueName.data(), &valueNameSize, nullptr, &type, valueData.data(), &valueDataSize); + if (result != ERROR_SUCCESS) + { + break; + } + valueName.resize(valueNameSize); + valueData.resize(valueDataSize); + + if (valueNameSize) + { + const bool isPathVar = is_path_var(valueName); + + // On some systems we've seen path variables that are REG_SZ instead + // of REG_EXPAND_SZ. We should always treat them as REG_EXPAND_SZ. + if (isPathVar && type == REG_SZ) + { + type = REG_EXPAND_SZ; + } + + std::wstring data; + if (pass == 0 && (type == REG_SZ) && valueDataSize >= sizeof(wchar_t)) + { + data = { + reinterpret_cast(valueData.data()), valueData.size() / sizeof(wchar_t) + }; + } + else if (pass == 1 && (type == REG_EXPAND_SZ) && valueDataSize >= sizeof(wchar_t)) + { + data = { + reinterpret_cast(valueData.data()), valueData.size() / sizeof(wchar_t) + }; + data = expand_environment_strings(data.data()); + } + + // Because Registry data may or may not be null terminated... check if we've managed + // to store an extra null in the wstring by telling it to create itself from pointer and size. + // If we did, pull it off. + strip_trailing_null(data); + + if (!data.empty()) + { + if (isPathVar) + { + concat_var(valueName, std::move(data)); + } + else + { + set_user_environment_var(valueName, std::move(data)); + } + } + } + + valueName.resize(maxValueNameSize); + valueData.resize(maxValueDataSize); + valueNameSize = maxValueNameSize; + valueDataSize = maxValueDataSize; + index++; + } + index = 0; + } + } + } + } + + std::wstring expand_environment_strings(std::wstring_view input) + { + std::wstring expanded; + expanded.reserve(input.size()); + bool isInEnvVarName = false; + std::wstring currentEnvVarName; + for (const auto character : input) + { + if (character == L'%') + { + if (isInEnvVarName) + { + if (const auto envVarValue = _envMap.find(currentEnvVarName); envVarValue != _envMap.end()) + { + expanded.append(envVarValue->second); + } + else + { + expanded.push_back(L'%'); + expanded.append(currentEnvVarName); + expanded.push_back(L'%'); + } + isInEnvVarName = false; + currentEnvVarName.clear(); + } + else + { + isInEnvVarName = true; + } + } + else + { + if (isInEnvVarName) + { + currentEnvVarName.push_back(character); + } + else + { + expanded.push_back(character); + } + } + } + if (isInEnvVarName) + { + expanded.push_back('%'); + expanded.append(currentEnvVarName); + } + return expanded; + } + + void concat_var(std::wstring var, std::wstring value) + { + if (const auto existing = _envMap.find(var); existing != _envMap.end()) + { + // If it doesn't already trail with a ;, add one. + // Otherwise, just take advantage of the one it has. + if (existing->second.back() != L';') + { + existing->second.push_back(L';'); + } + existing->second.append(value); + } + else + { + save_to_map(std::move(var), std::move(value)); + } + } + + void save_to_map(std::wstring var, std::wstring value) + { + if (!var.empty() && !value.empty()) + { + _envMap.insert_or_assign(std::move(var), std::move(value)); + } + } + + std::wstring check_for_temp(std::wstring_view var, std::wstring_view value) + { + static constexpr std::wstring_view temp{ L"temp" }; + static constexpr std::wstring_view tmp{ L"tmp" }; + if (til::compare_string_ordinal(var, temp) == CSTR_EQUAL || + til::compare_string_ordinal(var, tmp) == CSTR_EQUAL) + { + return til::details::wil_env::GetShortPathNameW(value.data()); + } + else + { + return std::wstring{ value }; + } + } + + static bool is_path_var(std::wstring_view input) noexcept + { + static constexpr std::wstring_view path{ L"Path" }; + static constexpr std::wstring_view libPath{ L"LibPath" }; + static constexpr std::wstring_view os2LibPath{ L"Os2LibPath" }; + return til::compare_string_ordinal(input, path) == CSTR_EQUAL || + til::compare_string_ordinal(input, libPath) == CSTR_EQUAL || + til::compare_string_ordinal(input, os2LibPath) == CSTR_EQUAL; + } + + static void strip_trailing_null(std::wstring& str) noexcept + { + if (!str.empty() && str.back() == L'\0') + { + str.pop_back(); + } + } + + void parse(const wchar_t* lastCh) + { + for (; *lastCh != '\0'; ++lastCh) + { + // Copy current entry into temporary map. + const size_t cchEntry{ ::wcslen(lastCh) }; + const std::wstring_view entry{ lastCh, cchEntry }; + + // Every entry is of the form "name=value\0". + const auto pos = entry.find_first_of(L"=", 0, 1); + THROW_HR_IF(E_UNEXPECTED, pos == std::wstring::npos); + + std::wstring name{ entry.substr(0, pos) }; // portion before '=' + std::wstring value{ entry.substr(pos + 1) }; // portion after '=' + + // Don't replace entries that already exist. + _envMap.try_emplace(std::move(name), std::move(value)); + lastCh += cchEntry; + } + } + + public: + env() = default; + + env(const wchar_t* block) + { + parse(block); + } + + // Function Description: + // - Creates a new environment with the current process's unicode environment + // variables. + // Return Value: + // - A new environment + static til::env from_current_environment() + { + LPWCH currentEnvVars{}; + auto freeCurrentEnv = wil::scope_exit([&] { + if (currentEnvVars) + { + FreeEnvironmentStringsW(currentEnvVars); + currentEnvVars = nullptr; + } + }); + + currentEnvVars = ::GetEnvironmentStringsW(); + THROW_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars); + + return til::env{ currentEnvVars }; + } + + void set_user_environment_var(std::wstring_view var, std::wstring_view value) + { + auto valueString = expand_environment_strings(value); + valueString = check_for_temp(var, valueString); + save_to_map(std::wstring{ var }, std::move(valueString)); + } + + void regenerate() + { + // Generally replicates the behavior of shell32!RegenerateUserEnvironment + get(til::details::vars::system_root); + get(til::details::vars::system_drive); + get(til::details::vars::all_users_profile); + get(til::details::vars::public_var); + get(til::details::vars::program_data); + get_computer_name(); + get_user_name_and_domain(); + get(til::details::vars::user_dns_domain); + get(til::details::vars::home_drive); + get(til::details::vars::home_share); + get(til::details::vars::home_path); + get(til::details::vars::user_profile); + get(til::details::vars::app_data); + get(til::details::vars::local_app_data); + get_program_files(); + get_vars_from_registry(HKEY_LOCAL_MACHINE, til::details::vars::reg::system_env_var_root); + // not processing autoexec.bat + get_vars_from_registry(HKEY_CURRENT_USER, til::details::vars::reg::user_env_var_root); + get_vars_from_registry(HKEY_CURRENT_USER, til::details::vars::reg::user_volatile_env_var_root); + get_vars_from_registry(HKEY_CURRENT_USER, fmt::format(til::details::vars::reg::user_volatile_session_env_var_root_pattern, NtCurrentTeb()->ProcessEnvironmentBlock->SessionId)); + } + + std::wstring to_string() const + { + std::wstring result; + for (const auto& [name, value] : _envMap) + { + result += name; + result += L"="; + result += value; + result.append(L"\0", 1); // Override string's natural propensity to stop at \0 + } + result.append(L"\0", 1); + return result; + } + + void clear() noexcept + { + // Can't zero the keys, but at least we can zero the values. + for (auto& [name, value] : _envMap) + { + ::SecureZeroMemory(value.data(), value.size() * sizeof(decltype(value.begin())::value_type)); + } + + _envMap.clear(); + } + + // Function Description: + // - Creates a new environment block using the provided vector as appropriate + // (resizing if needed) based on the current environment variable map + // matching the format of GetEnvironmentStringsW. + // Arguments: + // - newEnvVars: The vector that will be used to create the new environment block. + // Return Value: + // - S_OK if we succeeded, or an appropriate HRESULT for failing + HRESULT to_environment_strings_w(std::vector& newEnvVars) + try + { + // Clear environment block before use. + constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) }; + + if (!newEnvVars.empty()) + { + ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); + } + + // Resize environment block to fit map. + size_t cchEnv{ 2 }; // For the block's double NULL-terminator. + for (const auto& [name, value] : _envMap) + { + // Final form of "name=value\0". + cchEnv += name.size() + 1 + value.size() + 1; + } + newEnvVars.resize(cchEnv); + + // Ensure new block is wiped if we exit due to failure. + auto zeroNewEnv = wil::scope_exit([&]() noexcept { + ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); + }); + + // Transform each map entry and copy it into the new environment block. + auto pEnvVars{ newEnvVars.data() }; + auto cbRemaining{ cchEnv * cbChar }; + for (const auto& [name, value] : _envMap) + { + // Final form of "name=value\0" for every entry. + { + const auto cchSrc{ name.size() }; + const auto cbSrc{ cchSrc * cbChar }; + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0); + pEnvVars += cchSrc; + cbRemaining -= cbSrc; + } + + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0); + ++pEnvVars; + cbRemaining -= cbChar; + + { + const auto cchSrc{ value.size() }; + const auto cbSrc{ cchSrc * cbChar }; + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0); + pEnvVars += cchSrc; + cbRemaining -= cbSrc; + } + + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0); + ++pEnvVars; + cbRemaining -= cbChar; + } + + // Environment block only has to be NULL-terminated, but double NULL-terminate anyway. + RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0); + cbRemaining -= cbChar * 2; + + RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0); + + zeroNewEnv.release(); // success; don't wipe new environment block on exit + + return S_OK; + } + CATCH_RETURN(); + + auto& as_map() noexcept + { + return _envMap; + } + }; +}; + +#pragma warning(pop) diff --git a/src/inc/til/flat_set.h b/src/inc/til/flat_set.h new file mode 100644 index 0000000000..07942abf71 --- /dev/null +++ b/src/inc/til/flat_set.h @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#pragma warning(push) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique instead (r.11). + +namespace til +{ + // A simple hash function for simple hash maps. + // As demonstrated in https://doi.org/10.14778/2850583.2850585, a simple "multiply and shift" hash performs + // very well with linear probing hash maps and I found this to be true as well in my own testing. This hash + // function doesn't do the "shift" part, because linear_flat_set already does it by an appropriate amount. + constexpr size_t flat_set_hash_integer(size_t v) noexcept + { + // These two multipliers are the same as used by the PCG family of random number generators. + // The 32-Bit version is described in https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5. + // The 64-Bit version is the multiplier as used by Donald Knuth for MMIX and found by C. E. Haynes. +#ifdef _WIN64 + return v * UINT64_C(6364136223846793005); +#else + return v * UINT32_C(747796405); +#endif + } + + // A basic, hashmap with linear probing. A `LoadFactor` of 2 equals + // a max. load of roughly 50% and a `LoadFactor` of 4 roughly 25%. + // + // `GrowthExponent` controls how fast the set grows and corresponds to + // a rate of 2^GrowthExponent. In other words, a `GrowthExponent` of 3 + // equals a growth rate of 8x every time the capacity has been reached. + // + // It performs best with: + // * small and cheap T + // * >= 50% successful lookups + // * <= 50% load factor (LoadFactor >= 2, which is the minimum anyways) + template + struct linear_flat_set + { + static_assert(LoadFactor >= 2); + static_assert(GrowthExponent >= 1); + + linear_flat_set() = default; + + linear_flat_set(const linear_flat_set&) = delete; + linear_flat_set& operator=(const linear_flat_set&) = delete; + + linear_flat_set(linear_flat_set&& other) noexcept : + _map{ std::move(other._map) }, + _capacity{ std::exchange(other._capacity, 0) }, + _load{ std::exchange(other._load, 0) }, + _shift{ std::exchange(other._shift, initialShift) }, + _mask{ std::exchange(other._mask, 0) } + { + } + + linear_flat_set& operator=(linear_flat_set&& other) noexcept + { + _map = std::move(other._map); + _capacity = std::exchange(other._capacity, 0); + _load = std::exchange(other._load, 0); + _shift = std::exchange(other._shift, initialShift); + _mask = std::exchange(other._mask, 0); + return *this; + } + + bool empty() const noexcept + { + return _load == 0; + } + + size_t size() const noexcept + { + return _load / LoadFactor; + } + + std::span container() const noexcept + { + return { _map.get(), _capacity }; + } + + void clear() noexcept + { + if (_map) + { + std::fill_n(_map.get(), _capacity, T{}); + _load = 0; + } + } + + template + T* lookup(U&& key) const noexcept + { + if (!_map) + { + return nullptr; + } + + const auto hash = ::std::hash{}(key) >> _shift; + + for (auto i = hash;; ++i) + { + auto& slot = _map[i & _mask]; + if (!slot) + { + return nullptr; + } + if (slot == key) [[likely]] + { + return &slot; + } + } + } + + template + std::pair insert(U&& key) + { + // Putting this into the lookup path is a little pessimistic, but it + // allows us to default-construct this hashmap with a size of 0. + if (_load >= _capacity) [[unlikely]] + { + _bumpSize(); + } + + // The most common, basic and performant hash function is to multiply the value + // by some prime number and divide by the number of slots. It's been shown + // many times in literature that such a scheme performs the best on average. + // As such, we perform the divide here to get the topmost bits down. + // See flat_set_hash_integer. + const auto hash = ::std::hash{}(key) >> _shift; + + for (auto i = hash;; ++i) + { + auto& slot = _map[i & _mask]; + if (!slot) + { + slot = std::forward(key); + _load += LoadFactor; + return { slot, true }; + } + if (slot == key) [[likely]] + { + return { slot, false }; + } + } + } + + private: + __declspec(noinline) void _bumpSize() + { + // For instance at a GrowthExponent of 1: + // A _shift of 0 would result in a newShift of 0xfffff... + // A _shift of 1 would result in a newCapacity of 0 + if (_shift <= GrowthExponent) + { + throw std::bad_array_new_length{}; + } + + const auto newShift = _shift - GrowthExponent; + const auto newCapacity = size_t{ 1 } << (digits - newShift); + const auto newMask = newCapacity - 1; + auto newMap = std::make_unique(newCapacity); + + // This mirrors the insert() function, but without the lookup part. + for (auto& oldSlot : container()) + { + if (!oldSlot) + { + continue; + } + + const auto hash = ::std::hash{}(oldSlot) >> newShift; + + for (auto i = hash;; ++i) + { + auto& slot = newMap[i & newMask]; + if (!slot) + { + slot = std::move_if_noexcept(oldSlot); + break; + } + } + } + + _map = std::move(newMap); + _capacity = newCapacity; + _shift = newShift; + _mask = newMask; + } + + static constexpr auto digits = std::numeric_limits::digits; + // This results in an initial capacity of 8 items, independent of the LoadFactor. + static constexpr auto initialShift = digits - LoadFactor - 1; + + std::unique_ptr _map; + size_t _capacity = 0; + size_t _load = 0; + size_t _shift = initialShift; + size_t _mask = 0; + }; +} + +#pragma warning(pop) diff --git a/src/inc/til/generational.h b/src/inc/til/generational.h new file mode 100644 index 0000000000..261fbc392b --- /dev/null +++ b/src/inc/til/generational.h @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + struct generation_t + { + auto operator<=>(const generation_t&) const = default; + + constexpr void bump() noexcept + { + _value++; + } + + uint32_t _value = 0; + }; + + // It can be costly, difficult, or often impossible to compare two + // instances of a struct. This little helper can simplify this. + // + // The underlying idea is that changes in state occur much less often than + // the amount of data that's being processed in between. As such, this + // helper assumes that _any_ modification to the struct it wraps is a + // state change. When you compare the modified instance with another + // the comparison operator will then always return false. This makes + // state changes potentially more costly, because more state might be + // invalidated than was necessary, but on the other hand it makes both, + // the code simpler and the fast-path (no state change) much faster. + template + struct generational + { + generational() = default; + explicit constexpr generational(auto&&... args) : + _value{ std::forward(args)... } {} + explicit constexpr generational(generation_t generation, auto&&... args) : + _generation{ generation }, + _value{ std::forward(args)... } {} + + constexpr bool operator==(const generational& rhs) const noexcept { return generation() == rhs.generation(); } + constexpr bool operator!=(const generational& rhs) const noexcept { return generation() != rhs.generation(); } + + constexpr generation_t generation() const noexcept + { + return _generation; + } + + [[nodiscard]] constexpr const T* operator->() const noexcept + { + return &_value; + } + + [[nodiscard]] constexpr const T& operator*() const noexcept + { + return _value; + } + + [[nodiscard]] constexpr T* write() noexcept + { + _generation.bump(); + return &_value; + } + + private: + generation_t _generation; + T _value{}; + }; +} diff --git a/src/inc/til/math.h b/src/inc/til/math.h index 8f9a51e439..183f9691ab 100644 --- a/src/inc/til/math.h +++ b/src/inc/til/math.h @@ -17,7 +17,7 @@ namespace til constexpr O narrow_float(T val) { const auto o = gsl::narrow_cast(val); - if (std::isnan(val) || static_cast(o) != val) + if (static_cast(o) != val) { throw gsl::narrowing_error{}; } diff --git a/src/inc/til/mutex.h b/src/inc/til/mutex.h index 6882aa97dd..b08dcd4b2b 100644 --- a/src/inc/til/mutex.h +++ b/src/inc/til/mutex.h @@ -9,7 +9,8 @@ namespace til class shared_mutex_guard { public: - shared_mutex_guard(T& data, std::shared_mutex& mutex) : +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function 'shared_mutex>()' which may throw exceptions (f.6).) + shared_mutex_guard(T& data, std::shared_mutex& mutex) noexcept : _data{ data }, _lock{ mutex } { @@ -21,17 +22,17 @@ namespace til shared_mutex_guard(shared_mutex_guard&&) = default; shared_mutex_guard& operator=(shared_mutex_guard&&) = default; - [[nodiscard]] constexpr T* operator->() const + [[nodiscard]] constexpr T* operator->() const noexcept { return &_data; } - [[nodiscard]] constexpr T& operator*() const& + [[nodiscard]] constexpr T& operator*() const& noexcept { return _data; } - [[nodiscard]] constexpr T&& operator*() const&& + [[nodiscard]] constexpr T&& operator*() const&& noexcept { return std::move(_data); } diff --git a/src/inc/til/pmr.h b/src/inc/til/pmr.h index a900449c5e..71fc596e31 100644 --- a/src/inc/til/pmr.h +++ b/src/inc/til/pmr.h @@ -39,7 +39,7 @@ namespace til::pmr { if (align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { - return ::operator new (bytes, std::align_val_t{ align }); + return ::operator new(bytes, std::align_val_t{ align }); } return ::operator new(bytes); @@ -49,7 +49,7 @@ namespace til::pmr { if (align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { - return ::operator delete (ptr, bytes, std::align_val_t{ align }); + return ::operator delete(ptr, bytes, std::align_val_t{ align }); } ::operator delete(ptr, bytes); diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 29d2a9e2f5..332b6fd8e0 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -6,6 +6,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { using CoordType = int32_t; + inline constexpr CoordType CoordTypeMin = INT32_MIN; + inline constexpr CoordType CoordTypeMax = INT32_MAX; namespace details { @@ -23,19 +25,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" struct point { - // **** TRANSITIONAL **** - // The old COORD type uses uppercase member names. - // We'll migrate to lowercase ones in the future. - union - { - CoordType x = 0; - CoordType X; - }; - union - { - CoordType y = 0; - CoordType Y; - }; + CoordType x = 0; + CoordType y = 0; constexpr point() noexcept = default; diff --git a/src/inc/til/rect.h b/src/inc/til/rect.h index d6d4f9d019..7ad7fe7ab7 100644 --- a/src/inc/til/rect.h +++ b/src/inc/til/rect.h @@ -14,29 +14,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { struct inclusive_rect { - // **** TRANSITIONAL **** - // The old SMALL_RECT type uses uppercase member names. - // We'll migrate to lowercase ones in the future. - union - { - CoordType left = 0; - CoordType Left; - }; - union - { - CoordType top = 0; - CoordType Top; - }; - union - { - CoordType right = 0; - CoordType Right; - }; - union - { - CoordType bottom = 0; - CoordType Bottom; - }; + CoordType left = 0; + CoordType top = 0; + CoordType right = 0; + CoordType bottom = 0; constexpr bool operator==(const inclusive_rect& rhs) const noexcept { @@ -164,29 +145,10 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { using const_iterator = details::_rectangle_const_iterator; - // **** TRANSITIONAL **** - // The old SMALL_RECT type uses uppercase member names. - // We'll migrate to lowercase ones in the future. - union - { - CoordType left = 0; - CoordType Left; - }; - union - { - CoordType top = 0; - CoordType Top; - }; - union - { - CoordType right = 0; - CoordType Right; - }; - union - { - CoordType bottom = 0; - CoordType Bottom; - }; + CoordType left = 0; + CoordType top = 0; + CoordType right = 0; + CoordType bottom = 0; constexpr rect() noexcept = default; @@ -651,8 +613,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" #ifdef _WINCONTYPES_ // NOTE: This will convert from INCLUSIVE on the way in because // that is generally how SMALL_RECTs are handled in console code and via the APIs. - explicit constexpr rect(const inclusive_rect other) noexcept : - rect{ other.Left, other.Top, other.Right + 1, other.Bottom + 1 } + explicit constexpr rect(const inclusive_rect& other) noexcept : + rect{ other.left, other.top, other.right + 1, other.bottom + 1 } { } diff --git a/src/inc/til/rle.h b/src/inc/til/rle.h index 18dd928e70..34b6c929a0 100644 --- a/src/inc/til/rle.h +++ b/src/inc/til/rle.h @@ -402,6 +402,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _runs; } + container& runs() noexcept + { + return _runs; + } + // Get the value at the position const_reference at(size_type position) const { @@ -421,7 +426,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Returns the range [start_index, end_index) as a new vector. // It works just like std::string::substr(), but with absolute indices. - [[nodiscard]] basic_rle slice(size_type start_index, size_type end_index) const noexcept + [[nodiscard]] basic_rle slice(size_type start_index, size_type end_index) const { if (end_index > _total_length) { @@ -441,14 +446,14 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // --> It's safe to subtract 1 from end_index rle_scanner scanner(_runs.begin(), _runs.end()); - auto [begin_run, start_run_pos] = scanner.scan(start_index); - auto [end_run, end_run_pos] = scanner.scan(end_index - 1); + const auto [begin_run, start_run_pos] = scanner.scan(start_index); + const auto [end_run, end_run_pos] = scanner.scan(end_index - 1); container slice{ begin_run, end_run + 1 }; slice.back().length = end_run_pos + 1; slice.front().length -= start_run_pos; - return { std::move(slice), static_cast(end_index - start_index) }; + return { std::move(slice), gsl::narrow_cast(end_index - start_index) }; } // Replace the range [start_index, end_index) with the given value. @@ -458,7 +463,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { _check_indices(start_index, end_index); - const rle_type replacement{ value, static_cast(end_index - start_index) }; + const rle_type replacement{ value, gsl::narrow_cast(end_index - start_index) }; _replace_unchecked(start_index, end_index, { &replacement, 1 }); } @@ -473,12 +478,21 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Replace the range [start_index, end_index) with replacements. // If end_index is larger than size() it's set to size(). // start_index must be smaller or equal to end_index. - void replace(size_type start_index, size_type end_index, const gsl::span replacements) + void replace(size_type start_index, size_type end_index, const std::span replacements) { _check_indices(start_index, end_index); _replace_unchecked(start_index, end_index, replacements); } + // Replace the range [start_index, end_index) with replacements. + // If end_index is larger than size() it's set to size(). + // start_index must be smaller or equal to end_index. + void replace(size_type start_index, size_type end_index, const basic_rle& replacements) + { + _check_indices(start_index, end_index); + _replace_unchecked(start_index, end_index, replacements._runs); + } + // Replaces every instance of old_value in this vector with new_value. void replace_values(const value_type& old_value, const value_type& new_value) { @@ -637,7 +651,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" size_type total = 0; }; - basic_rle(container&& runs, size_type size) : + basic_rle(container&& runs, size_type size) noexcept : _runs(std::forward(runs)), _total_length(size) { @@ -692,7 +706,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } // Replace the range [start_index, end_index) with replacements. - void _replace_unchecked(size_type start_index, size_type end_index, const gsl::span replacements) + void _replace_unchecked(size_type start_index, size_type end_index, const std::span replacements) { // // @@ -940,7 +954,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { begin->length = begin_pos; // begin is part of the to-be-replaced range. - // We've used the run begin is pointing to adjust it's length. + // We've used the run begin is pointing to adjust its length. // --> We must increment it in order to not overwrite it in [Step4]. ++begin; } diff --git a/src/inc/til/size.h b/src/inc/til/size.h index 2db967f6fd..72dbb60285 100644 --- a/src/inc/til/size.h +++ b/src/inc/til/size.h @@ -9,22 +9,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { struct size { - // **** TRANSITIONAL **** - // The old COORD type uses uppercase X/Y member names. - // We'll migrate to lowercase width/height in the future. - union - { - CoordType width = 0; - CoordType X; - CoordType cx; - }; - union - { - CoordType height = 0; - CoordType Y; - CoordType cy; - }; - constexpr size() noexcept = default; constexpr size(CoordType width, CoordType height) noexcept : @@ -91,7 +75,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } template>> - constexpr size scale(TilMath math, const T scale) const + [[nodiscard]] constexpr size scale(TilMath math, const T scale) const { return { math, @@ -100,7 +84,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" }; } - constexpr size divide_ceil(const size other) const + [[nodiscard]] constexpr size divide_ceil(const size other) const { // The integer ceil division `((a - 1) / b) + 1` only works for numbers >0. // Support for negative numbers wasn't deemed useful at this point. @@ -190,6 +174,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { return wil::str_printf(L"[W:%d, H:%d]", width, height); } + + CoordType width = 0; + CoordType height = 0; }; constexpr size wrap_coord_size(const COORD sz) noexcept diff --git a/src/inc/til/small_vector.h b/src/inc/til/small_vector.h index 127795e131..a88927e715 100644 --- a/src/inc/til/small_vector.h +++ b/src/inc/til/small_vector.h @@ -16,6 +16,8 @@ #pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe ... (stl.1). // small_vector::_data references potentially uninitialized data and so we can't pass it regular iterators which reference initialized data. #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +// small_vector::_buffer is explicitly uninitialized, because we manage its initialization manually. +#pragma warning(disable : 26495) // Variable '...' is uninitialized. Always initialize a member variable (type.6). namespace til { @@ -276,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); @@ -328,7 +336,9 @@ namespace til using const_reverse_iterator = std::reverse_iterator; small_vector() noexcept : - _data{ &_buffer[0] } + _data{ &_buffer[0] }, + _capacity{ N }, + _size{ 0 } { } @@ -355,56 +365,38 @@ namespace til small_vector(const small_vector& other) : small_vector{} { - operator=(other); + _copy_assign(other); } // NOTE: If an exception is thrown while copying, the vector is left empty. small_vector& operator=(const small_vector& other) { - clear(); - reserve(other._size); - - std::uninitialized_copy(other.begin(), other.end(), _uninitialized_begin()); - _size = other._size; + if (this != &other) + { + clear(); + _copy_assign(other); + } return *this; } - small_vector(small_vector&& other) noexcept : - small_vector{} + small_vector(small_vector&& other) noexcept { - operator=(std::move(other)); + _move_assign(other); } small_vector& operator=(small_vector&& other) noexcept { - std::destroy(begin(), end()); - if (_capacity != N) + if (this != &other) { - _deallocate(_data); - } + std::destroy(begin(), end()); + if (_capacity != N) + { + _deallocate(_data); + } - if (other._capacity == N) - { - _data = &_buffer[0]; - _capacity = N; - _size = other._size; - // The earlier static_assert(std::is_nothrow_move_constructible_v) - // ensures that we don't exit in a weird state with invalid `_size`. -#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). - std::uninitialized_move(other.begin(), other.end(), _uninitialized_begin()); - std::destroy(other.begin(), other.end()); + _move_assign(other); } - else - { - _data = other._data; - _capacity = other._capacity; - _size = other._size; - } - - other._data = &other._buffer[0]; - other._capacity = N; - other._size = 0; return *this; } @@ -620,6 +612,24 @@ namespace til _capacity = capacity; } + // This is a very unsafe shortcut to free the buffer and get a direct + // hold to the _buffer. The caller can then fill it with `size` items. + [[nodiscard]] T* unsafe_shrink_to_size(size_t size) noexcept + { + assert(size <= N); + + if (_capacity != N) + { + _deallocate(_data); + } + + _data = &_buffer[0]; + _capacity = N; + _size = size; + + return &_buffer[0]; + } + void push_back(const T& value) { emplace_back(value); @@ -756,6 +766,38 @@ namespace til #endif } + void _copy_assign(const small_vector& other) + { + reserve(other._size); + std::uninitialized_copy(other.begin(), other.end(), _uninitialized_begin()); + _size = other._size; + } + + void _move_assign(small_vector& other) noexcept + { + if (other._capacity == N) + { + _data = &_buffer[0]; + _capacity = N; + _size = other._size; + // The earlier static_assert(std::is_nothrow_move_constructible_v) + // ensures that we don't exit in a weird state with invalid `_size`. +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). + std::uninitialized_move(other.begin(), other.end(), _uninitialized_begin()); + std::destroy(other.begin(), other.end()); + } + else + { + _data = other._data; + _capacity = other._capacity; + _size = other._size; + } + + other._data = &other._buffer[0]; + other._capacity = N; + other._size = 0; + } + size_type _ensure_fits(size_type add) { const auto new_size = _size + add; @@ -878,8 +920,8 @@ namespace til } T* _data; - size_t _capacity = N; - size_t _size = 0; + size_t _capacity; + size_t _size; T _buffer[N]; }; } diff --git a/src/inc/til/spsc.h b/src/inc/til/spsc.h index 366930693a..9732cd5796 100644 --- a/src/inc/til/spsc.h +++ b/src/inc/til/spsc.h @@ -24,9 +24,9 @@ namespace til::spsc namespace details { - static constexpr size_type position_mask = std::numeric_limits::max() >> 2u; // 0b00111.... - static constexpr size_type revolution_flag = 1u << (std::numeric_limits::digits - 2u); // 0b01000.... - static constexpr size_type drop_flag = 1u << (std::numeric_limits::digits - 1u); // 0b10000.... + inline constexpr size_type position_mask = std::numeric_limits::max() >> 2u; // 0b00111.... + inline constexpr size_type revolution_flag = 1u << (std::numeric_limits::digits - 2u); // 0b01000.... + inline constexpr size_type drop_flag = 1u << (std::numeric_limits::digits - 1u); // 0b10000.... struct block_initially_policy { diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h index f5d82f113a..765e53ba5f 100644 --- a/src/inc/til/static_map.h +++ b/src/inc/til/static_map.h @@ -8,13 +8,8 @@ // There is no requirement that keys be sorted, as it will // use constexpr std::sort during construction. // -// Until we can use C++20, this is no cheaper than using -// a static std::unordered_map that is initialized at -// startup or on first use. -// To build something that can be constexpr as of C++17, -// use til::presorted_static_map and make certain that -// your pairs are sorted as they would have been sorted -// by your comparator. +// Use til::presorted_static_map and make certain that +// your pairs are sorted if you want to skip the std::sort. // A failure to sort your keys will result in unusual // runtime behavior, but no error messages will be // generated. @@ -32,31 +27,30 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" }; } - template, size_t N = 0, typename SortedInput = details::unsorted_input_t> + template class static_map { public: using const_iterator = typename std::array, N>::const_iterator; template - constexpr explicit static_map(const Args&... args) noexcept : - _predicate{}, - _array{ { args... } } + constexpr explicit static_map(Args&&... args) noexcept : + _array{ { std::forward(args)... } } { static_assert(sizeof...(Args) == N); if constexpr (!SortedInput::value) { - const auto compareKeys = [&](const auto& p1, const auto& p2) { return _predicate(p1.first, p2.first); }; - std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting starting C++20 + std::sort(_array.begin(), _array.end()); } } - [[nodiscard]] constexpr const_iterator find(const K& key) const noexcept + [[nodiscard]] constexpr const_iterator find(const auto& key) const noexcept { - const auto compareKey = [&](const auto& p) { return _predicate(p.first, key); }; - const auto iter{ std::partition_point(_array.begin(), _array.end(), compareKey) }; + const auto iter = std::partition_point(_array.begin(), _array.end(), [&](const auto& p) { + return p.first < key; + }); - if (iter == _array.end() || _predicate(key, iter->first)) + if (iter == _array.end() || key != iter->first) { return _array.end(); } @@ -69,7 +63,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _array.end(); } - [[nodiscard]] constexpr const V& at(const K& key) const + [[nodiscard]] constexpr const V& at(const auto& key) const { const auto iter{ find(key) }; @@ -87,25 +81,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } private: - Compare _predicate; std::array, N> _array; }; - template, size_t N = 0> - class presorted_static_map : public static_map + template + class presorted_static_map : public static_map { public: template - constexpr explicit presorted_static_map(const Args&... args) noexcept : - static_map{ args... } {}; + constexpr explicit presorted_static_map(Args&&... args) noexcept : + static_map{ std::forward(args)... } + { + } }; // this is a deduction guide that ensures two things: // 1. static_map's member types are all the same // 2. static_map's fourth template argument (otherwise undeduced) is how many pairs it contains template - static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest), details::unsorted_input_t>; + static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, 1 + sizeof...(Rest)>; template - presorted_static_map(First, Rest...) -> presorted_static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest)>; + presorted_static_map(First, Rest...) -> presorted_static_map...>, typename First::first_type, void>, typename First::second_type, 1 + sizeof...(Rest)>; } diff --git a/src/inc/til/string.h b/src/inc/til/string.h index c6cfb28ce7..c32de1e053 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -36,7 +36,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" inline constexpr uint8_t F_ = 0b10; // stripped in clean_filename inline constexpr uint8_t _P = 0b01; // stripped in clean_path inline constexpr uint8_t FP = 0b11; // stripped in clean_filename and clean_path - static constexpr std::array pathFilter{ { + inline constexpr std::array pathFilter{ { // clang-format off __ /* NUL */, __ /* SOH */, __ /* STX */, __ /* ETX */, __ /* EOT */, __ /* ENQ */, __ /* ACK */, __ /* BEL */, __ /* BS */, __ /* HT */, __ /* LF */, __ /* VT */, __ /* FF */, __ /* CR */, __ /* SO */, __ /* SI */, __ /* DLE */, __ /* DC1 */, __ /* DC2 */, __ /* DC3 */, __ /* DC4 */, __ /* NAK */, __ /* SYN */, __ /* ETB */, __ /* CAN */, __ /* EM */, __ /* SUB */, __ /* ESC */, __ /* FS */, __ /* GS */, __ /* RS */, __ /* US */, @@ -116,6 +116,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } inline constexpr unsigned long to_ulong_error = ULONG_MAX; + inline constexpr int to_int_error = INT_MAX; // Just like std::wcstoul, but without annoying locales and null-terminating strings. // It has been fuzz-tested against clang's strtoul implementation. @@ -342,4 +343,55 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { return prefix_split<>(str, needle); } + + // + // A case-insensitive wide-character map is used to store environment variables + // due to documented requirements: + // + // "All strings in the environment block must be sorted alphabetically by name. + // The sort is case-insensitive, Unicode order, without regard to locale. + // Because the equal sign is a separator, it must not be used in the name of + // an environment variable." + // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables + // + // - Returns CSTR_LESS_THAN, CSTR_EQUAL or CSTR_GREATER_THAN + [[nodiscard]] inline int compare_string_ordinal(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept + { + const auto result = CompareStringOrdinal( + lhs.data(), + ::base::saturated_cast(lhs.size()), + rhs.data(), + ::base::saturated_cast(rhs.size()), + TRUE); + FAIL_FAST_LAST_ERROR_IF(!result); + return result; + } + + struct wstring_case_insensitive_compare + { + [[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept + { + return compare_string_ordinal(lhs, rhs) == CSTR_LESS_THAN; + } + }; + + // Implement to_int in terms of to_ulong by negating its result. to_ulong does not expect + // to be passed signed numbers and will return an error accordingly. That error when + // compared against -1 evaluates to true. We account for that by returning to_int_error if to_ulong + // returns an error. + constexpr int to_int(const std::wstring_view& str, unsigned long base = 0) noexcept + { + auto result = to_ulong_error; + const auto signPosition = str.find(L"-"); + const bool hasSign = signPosition != std::wstring_view::npos; + result = hasSign ? to_ulong(str.substr(signPosition + 1), base) : to_ulong(str, base); + + // Check that result is valid and will fit in an int. + if (result == to_ulong_error || (result > INT_MAX)) + { + return to_int_error; + } + + return hasSign ? result * -1 : result; + } } diff --git a/src/inc/til/type_traits.h b/src/inc/til/type_traits.h new file mode 100644 index 0000000000..a4a1017319 --- /dev/null +++ b/src/inc/til/type_traits.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til +{ + namespace details + { + template + struct is_contiguous_view : std::false_type + { + }; + template + struct is_contiguous_view> : std::true_type + { + }; + template + struct is_contiguous_view> : std::true_type + { + }; + + template + struct is_byte : std::false_type + { + }; + template<> + struct is_byte : std::true_type + { + }; + template<> + struct is_byte : std::true_type + { + }; + } + + template + concept Byte = details::is_byte>::value; + + template + concept ContiguousView = details::is_contiguous_view>::value; + + template + concept ContiguousBytes = ContiguousView && Byte; + + template + concept TriviallyCopyable = std::is_trivially_copyable_v; +} diff --git a/src/inc/til/unicode.h b/src/inc/til/unicode.h new file mode 100644 index 0000000000..9f703dcb81 --- /dev/null +++ b/src/inc/til/unicode.h @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til +{ + namespace details + { + inline constexpr wchar_t UNICODE_REPLACEMENT = 0xFFFD; + } + + static constexpr bool is_surrogate(const wchar_t wch) noexcept + { + return (wch & 0xF800) == 0xD800; + } + + static constexpr bool is_leading_surrogate(const wchar_t wch) noexcept + { + return (wch & 0xFC00) == 0xD800; + } + + static constexpr bool is_trailing_surrogate(const wchar_t wch) noexcept + { + return (wch & 0xFC00) == 0xDC00; + } + + // Verifies the beginning of the given UTF16 string and returns the first UTF16 sequence + // or U+FFFD otherwise. It's not really useful and at the time of writing only a + // single caller uses this. It's best to delete this if you read this comment. + constexpr std::wstring_view utf16_next(std::wstring_view wstr) noexcept + { + auto it = wstr.begin(); + const auto end = wstr.end(); + auto ptr = &details::UNICODE_REPLACEMENT; + size_t len = 1; + + if (it != end) + { + const auto wch = *it; + ptr = &*it; + + if (is_surrogate(wch)) + { + ++it; + const auto wch2 = it != end ? *it : wchar_t{}; + if (is_leading_surrogate(wch) && is_trailing_surrogate(wch2)) + { + len = 2; + ++it; + } + else + { + ptr = &details::UNICODE_REPLACEMENT; + } + } + } + + return { ptr, len }; + } + + // 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 + { + if (idx < wstr.size() && is_leading_surrogate(til::at(wstr, idx++))) + { + if (idx < wstr.size() && is_trailing_surrogate(til::at(wstr, idx))) + { + ++idx; + } + } + return idx; + } + + // 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: + // for (const auto& str : til::utf16_iterator{ input }) { ... } + struct utf16_iterator + { + struct sentinel + { + }; + + struct iterator + { + using iterator_category = std::forward_iterator_tag; + using value_type = std::wstring_view; + using reference = value_type&; + using pointer = value_type*; + using difference_type = std::ptrdiff_t; + + explicit constexpr iterator(utf16_iterator& p) noexcept : + _iter{ p } + { + } + + const value_type& operator*() const noexcept + { + return _iter.value(); + } + + iterator& operator++() noexcept + { + _iter._advance = true; + return *this; + } + + bool operator!=(const sentinel&) const noexcept + { + return _iter.valid(); + } + + private: + utf16_iterator& _iter; + }; + + explicit constexpr utf16_iterator(std::wstring_view wstr) noexcept : + _it{ wstr.begin() }, _end{ wstr.end() }, _advance{ _it != _end } + { + } + + iterator begin() noexcept + { + return iterator{ *this }; + } + + sentinel end() noexcept + { + return sentinel{}; + } + + private: + bool valid() const noexcept + { + return _it != _end; + } + + void advance() noexcept + { + const auto wch = *_it; + auto ptr = &*_it; + size_t len = 1; + + ++_it; + + if (is_surrogate(wch)) + { + const auto wch2 = _it != _end ? *_it : wchar_t{}; + if (is_leading_surrogate(wch) && is_trailing_surrogate(wch2)) + { + len = 2; + ++_it; + } + else + { + ptr = &details::UNICODE_REPLACEMENT; + } + } + + _value = { ptr, len }; + _advance = false; + } + + const std::wstring_view& value() noexcept + { + if (_advance) + { + advance(); + } + return _value; + } + + std::wstring_view::iterator _it; + std::wstring_view::iterator _end; + std::wstring_view _value; + bool _advance = true; + }; +} diff --git a/src/inc/til/winrt.h b/src/inc/til/winrt.h new file mode 100644 index 0000000000..e5faece318 --- /dev/null +++ b/src/inc/til/winrt.h @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + template + struct property + { + explicit constexpr property(auto&&... args) : + _value{ std::forward(args)... } {} + + property& operator=(const property& other) = default; + + T operator()() const noexcept + { + return _value; + } + void operator()(auto&& arg) + { + _value = std::forward(arg); + } + explicit operator bool() const noexcept + { +#ifdef WINRT_Windows_Foundation_H + if constexpr (std::is_same_v) + { + return !_value.empty(); + } + else +#endif + { + return _value; + } + } + bool operator==(const property& other) const noexcept + { + return _value == other._value; + } + bool operator!=(const property& other) const noexcept + { + return _value != other._value; + } + bool operator==(const T& other) const noexcept + { + return _value == other; + } + bool operator!=(const T& other) const noexcept + { + return _value != other; + } + + private: + T _value; + }; + +#ifdef WINRT_Windows_Foundation_H + + template + struct event + { + event() = default; + winrt::event_token operator()(const ArgsT& handler) { return _handlers.add(handler); } + void operator()(const winrt::event_token& token) { _handlers.remove(token); } + operator bool() const noexcept { return _handlers; } + template + void raise(auto&&... args) + { + _handlers(std::forward(args)...); + } + winrt::event _handlers; + }; + + template + struct typed_event + { + typed_event() = default; + winrt::event_token operator()(const winrt::Windows::Foundation::TypedEventHandler& handler) { return _handlers.add(handler); } + void operator()(const winrt::event_token& token) { _handlers.remove(token); } + operator bool() const noexcept { return _handlers; } + template + void raise(Arg const&... args) + { + _handlers(std::forward(args)...); + } + winrt::event> _handlers; + }; +#endif +#ifdef WINRT_Windows_UI_Xaml_DataH + + using property_changed_event = event; + + // Making a til::observable_property unfortunately doesn't seem feasible. + // It's gonna just result in more macros, which no one wants. + // + // 1. We don't know who the sender is, or would require `this` to always be + // the first parameter to one of these observable_property's. + // + // 2. We don't know what our own name is. We need to actually raise an event + // with the name of the variable as the parameter. Only way to do that is + // with something like + // + // til::observable Foo(this, L"Foo", 42) + // + // which then kinda implies the creation of: + // + // #define OBSERVABLE(type, name, ...) til::observable_property name{ this, L## #name, this.PropertyChanged, __VA_ARGS__ }; + // + // Which is just silly + +#endif +} diff --git a/src/interactivity/base/ApiDetector.cpp b/src/interactivity/base/ApiDetector.cpp index 8382cab157..c448abd0eb 100644 --- a/src/interactivity/base/ApiDetector.cpp +++ b/src/interactivity/base/ApiDetector.cpp @@ -48,7 +48,7 @@ using namespace Microsoft::Console::Interactivity; HMODULE hModule = nullptr; status = TryLoadWellKnownLibrary(lpApiHost, &hModule); - if (NT_SUCCESS(status) && lpProcedure) + if (SUCCEEDED_NTSTATUS(status) && lpProcedure) { status = TryLocateProcedure(hModule, lpProcedure); } @@ -96,7 +96,7 @@ using namespace Microsoft::Console::Interactivity; // versioning API's behave sanely. status = TryLoadWellKnownLibrary(lpLibrary, LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER, phModule); - if (!NT_SUCCESS(status) && GetLastError() == ERROR_INVALID_PARAMETER) + if (FAILED_NTSTATUS(status) && GetLastError() == ERROR_INVALID_PARAMETER) { status = TryLoadWellKnownLibrary(lpLibrary, LOAD_LIBRARY_SEARCH_SYSTEM32, phModule); } @@ -141,7 +141,7 @@ using namespace Microsoft::Console::Interactivity; void ApiDetector::SetLevelAndFreeIfNecessary(_In_ NTSTATUS status, _In_ HMODULE hModule, _Out_ ApiLevel* level) { - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { *level = ApiLevel::Win32; } diff --git a/src/interactivity/base/EventSynthesis.cpp b/src/interactivity/base/EventSynthesis.cpp index 398b72730c..b26f8a369c 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::Invalid; + 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 modifierState = HIBYTE(keyState); - - auto altGrSet = false; - auto shiftSet = false; - std::deque> keyEvents; - - // add modifier key event if necessary - if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) - { - altGrSet = true; - keyEvents.push_back(std::make_unique(true, - 1ui16, - static_cast(VK_MENU), - altScanCode, - UNICODE_NULL, - (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); - } - else if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) - { - shiftSet = true; - keyEvents.push_back(std::make_unique(true, - 1ui16, - static_cast(VK_SHIFT), - leftShiftScanCode, - UNICODE_NULL, - SHIFT_PRESSED)); - } - const auto vk = LOBYTE(keyState); - const auto virtualScanCode = gsl::narrow(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC)); - KeyEvent keyEvent{ true, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }; + 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); - // add modifier flags if necessary - if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::Shift); - } - if (WI_IsFlagSet(modifierState, VkKeyScanModState::CtrlPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::LeftCtrl); - } - if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) - { - keyEvent.ActivateModifierKey(ModifierKeyState::RightAlt); - } - - // add key event down and up - keyEvents.push_back(std::make_unique(keyEvent)); - keyEvent.SetKeyDown(false); - keyEvents.push_back(std::make_unique(keyEvent)); - - // add modifier key up event if (altGrSet) { - keyEvents.push_back(std::make_unique(false, - 1ui16, - static_cast(VK_MENU), - altScanCode, - UNICODE_NULL, - ENHANCED_KEY)); + keyEvents.push_back(SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)); } else if (shiftSet) { - keyEvents.push_back(std::make_unique(false, - 1ui16, - static_cast(VK_SHIFT), - leftShiftScanCode, - UNICODE_NULL, - 0)); + keyEvents.push_back(SynthesizeKeyEvent(true, 1, VK_SHIFT, leftShiftScanCode, 0, SHIFT_PRESSED)); } - return keyEvents; + 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); + + keyEvents.push_back(keyEvent); + keyEvent.Event.KeyEvent.bKeyDown = FALSE; + keyEvents.push_back(keyEvent); + + // handle yucky alt-gr keys + if (altGrSet) + { + keyEvents.push_back(SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY)); + } + else if (shiftSet) + { + keyEvents.push_back(SynthesizeKeyEvent(false, 1, VK_SHIFT, leftShiftScanCode, 0, 0)); + } } // 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; + char converted = 0; + const auto result = WideCharToMultiByte(codepage, 0, &wch, 1, &converted, 1, nullptr, nullptr); - //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) + 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)); + const auto charString = std::to_string(static_cast(converted)); - // unsigned char values are in the range [0, 255] so we need to be - // able to store up to 4 chars from the conversion (including the end of string char) - auto charString = std::to_string(uch); - - for (auto& ch : std::string_view(charString)) + 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/base/HostSignalInputThread.cpp b/src/interactivity/base/HostSignalInputThread.cpp index bed01bbd01..9ea9cefff5 100644 --- a/src/interactivity/base/HostSignalInputThread.cpp +++ b/src/interactivity/base/HostSignalInputThread.cpp @@ -56,7 +56,7 @@ template T HostSignalInputThread::_ReceiveTypedPacket() { T msg = { 0 }; - THROW_HR_IF(E_ABORT, !_GetData(gsl::as_writable_bytes(gsl::span{ &msg, 1 }))); + THROW_HR_IF(E_ABORT, !_GetData(std::as_writable_bytes(std::span{ &msg, 1 }))); // If the message is smaller than what we expected // then it was malformed and we need to throw. @@ -80,7 +80,7 @@ T HostSignalInputThread::_ReceiveTypedPacket() { HostSignals signalId; - while (_GetData(gsl::as_writable_bytes(gsl::span{ &signalId, 1 }))) + while (_GetData(std::as_writable_bytes(std::span{ &signalId, 1 }))) { switch (signalId) { @@ -128,7 +128,7 @@ T HostSignalInputThread::_ReceiveTypedPacket() // - True if we could skip forward successfully. False otherwise. bool HostSignalInputThread::_AdvanceReader(DWORD byteCount) { - std::array buffer; + std::array buffer; while (byteCount > 0) { @@ -152,7 +152,7 @@ bool HostSignalInputThread::_AdvanceReader(DWORD byteCount) // - buffer - Buffer to fill with data. // Return Value: // - True if data was retrieved successfully. False otherwise. -bool HostSignalInputThread::_GetData(gsl::span buffer) +bool HostSignalInputThread::_GetData(std::span buffer) { DWORD bytesRead = 0; // If we failed to read because the terminal broke our pipe (usually due diff --git a/src/interactivity/base/HostSignalInputThread.hpp b/src/interactivity/base/HostSignalInputThread.hpp index 429e492955..71deef02e6 100644 --- a/src/interactivity/base/HostSignalInputThread.hpp +++ b/src/interactivity/base/HostSignalInputThread.hpp @@ -37,7 +37,7 @@ namespace Microsoft::Console T _ReceiveTypedPacket(); [[nodiscard]] HRESULT _InputThread(); - bool _GetData(gsl::span buffer); + bool _GetData(std::span buffer); bool _AdvanceReader(DWORD byteCount); void _Shutdown(); diff --git a/src/interactivity/base/InteractivityFactory.cpp b/src/interactivity/base/InteractivityFactory.cpp index 4294ef5981..fe8fc55cd8 100644 --- a/src/interactivity/base/InteractivityFactory.cpp +++ b/src/interactivity/base/InteractivityFactory.cpp @@ -38,7 +38,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr newControl; try @@ -64,7 +64,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { control.swap(newControl); } @@ -80,7 +80,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr newThread; try @@ -106,7 +106,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { thread.swap(newThread); } @@ -122,7 +122,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr newApi; try @@ -148,7 +148,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { api.swap(newApi); } @@ -164,7 +164,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr newMetrics; try @@ -190,7 +190,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { metrics.swap(newMetrics); } @@ -206,7 +206,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr newNotifier; try @@ -232,7 +232,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { notifier.swap(newNotifier); } @@ -248,7 +248,7 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { std::unique_ptr NewProvider; try @@ -274,7 +274,7 @@ using namespace Microsoft::Console::Interactivity; status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { provider.swap(NewProvider); } @@ -300,19 +300,21 @@ using namespace Microsoft::Console::Interactivity; ApiLevel level; auto status = ApiDetector::DetectNtUserWindow(&level); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { try { static const auto PSEUDO_WINDOW_CLASS = L"PseudoConsoleWindow"; - WNDCLASS pseudoClass{ 0 }; + WNDCLASSEXW pseudoClass{ 0 }; switch (level) { case ApiLevel::Win32: { + pseudoClass.cbSize = sizeof(WNDCLASSEXW); pseudoClass.lpszClassName = PSEUDO_WINDOW_CLASS; pseudoClass.lpfnWndProc = s_PseudoWindowProc; - RegisterClass(&pseudoClass); + pseudoClass.cbWndExtra = GWL_CONSOLE_WNDALLOC; // this is required to store the owning thread/process override in NTUSER + auto windowClassAtom{ RegisterClassExW(&pseudoClass) }; // Note that because we're not specifying WS_CHILD, this window // will become an _owned_ window, not a _child_ window. This is @@ -333,7 +335,7 @@ using namespace Microsoft::Console::Interactivity; // Attempt to create window. hwnd = CreateWindowExW(exStyles, - PSEUDO_WINDOW_CLASS, + reinterpret_cast(windowClassAtom), nullptr, windowStyle, 0, @@ -350,7 +352,7 @@ using namespace Microsoft::Console::Interactivity; const auto gle = GetLastError(); status = NTSTATUS_FROM_WIN32(gle); } - + _pseudoConsoleWindowHwnd = hwnd; break; } #ifdef BUILD_ONECORE_INTERACTIVITY @@ -462,6 +464,18 @@ using namespace Microsoft::Console::Interactivity; _WritePseudoWindowCallback((bool)wParam); } } + case WM_GETOBJECT: + { + if (static_cast(lParam) == static_cast(UiaRootObjectId)) + { + if (nullptr == _pPseudoConsoleUiaProvider) + { + LOG_IF_FAILED(WRL::MakeAndInitialize(&_pPseudoConsoleUiaProvider, _pseudoConsoleWindowHwnd)); + } + return UiaReturnRawElementProvider(hWnd, wParam, lParam, _pPseudoConsoleUiaProvider.Get()); + } + return 0; + } } // If we get this far, call the default window proc return DefWindowProcW(hWnd, Message, wParam, lParam); diff --git a/src/interactivity/base/InteractivityFactory.hpp b/src/interactivity/base/InteractivityFactory.hpp index 5497841749..2a0b3460ee 100644 --- a/src/interactivity/base/InteractivityFactory.hpp +++ b/src/interactivity/base/InteractivityFactory.hpp @@ -8,6 +8,7 @@ #include "ApiDetector.hpp" #include "../inc/IInteractivityFactory.hpp" +#include "PseudoConsoleWindowAccessibilityProvider.hpp" #include @@ -40,5 +41,8 @@ namespace Microsoft::Console::Interactivity private: void _WritePseudoWindowCallback(bool showOrHide); + + HWND _pseudoConsoleWindowHwnd{ nullptr }; + WRL::ComPtr _pPseudoConsoleUiaProvider{ nullptr }; }; } diff --git a/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.cpp b/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.cpp new file mode 100644 index 0000000000..09d7507639 --- /dev/null +++ b/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "PseudoConsoleWindowAccessibilityProvider.hpp" + +using namespace Microsoft::Console::Interactivity; + +HRESULT PseudoConsoleWindowAccessibilityProvider::RuntimeClassInitialize(HWND pseudoConsoleHwnd) noexcept +{ + RETURN_HR_IF_NULL(E_INVALIDARG, pseudoConsoleHwnd); + _pseudoConsoleHwnd = pseudoConsoleHwnd; + return S_OK; +} + +IFACEMETHODIMP PseudoConsoleWindowAccessibilityProvider::get_ProviderOptions(_Out_ ProviderOptions* pOptions) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, pOptions); + *pOptions = ProviderOptions_ServerSideProvider; + return S_OK; +} + +IFACEMETHODIMP PseudoConsoleWindowAccessibilityProvider::GetPatternProvider(_In_ PATTERNID /*iid*/, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, ppInterface); + *ppInterface = nullptr; + return S_OK; +} + +IFACEMETHODIMP PseudoConsoleWindowAccessibilityProvider::GetPropertyValue(_In_ PROPERTYID propertyId, + _Out_ VARIANT* pVariant) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, pVariant); + + pVariant->vt = VT_EMPTY; + + // Returning the default will leave the property as the default + // so we only really need to touch it for the properties we want to implement + switch (propertyId) + { + case UIA_ControlTypePropertyId: + { + pVariant->vt = VT_I4; + pVariant->lVal = UIA_WindowControlTypeId; + break; + } + case UIA_NamePropertyId: + { + static constexpr auto AutomationPropertyName = L"Internal Console Management Window"; + pVariant->bstrVal = SysAllocString(AutomationPropertyName); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + break; + } + case UIA_IsControlElementPropertyId: + case UIA_IsContentElementPropertyId: + case UIA_IsKeyboardFocusablePropertyId: + case UIA_HasKeyboardFocusPropertyId: + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_FALSE; + break; + } + default: + break; + } + return S_OK; +} + +IFACEMETHODIMP PseudoConsoleWindowAccessibilityProvider::get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, ppProvider); + RETURN_HR_IF_NULL(gsl::narrow_cast(UIA_E_ELEMENTNOTAVAILABLE), _pseudoConsoleHwnd); + return UiaHostProviderFromHwnd(_pseudoConsoleHwnd, ppProvider); +} diff --git a/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.hpp b/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.hpp new file mode 100644 index 0000000000..ab30eeeec4 --- /dev/null +++ b/src/interactivity/base/PseudoConsoleWindowAccessibilityProvider.hpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "precomp.h" + +namespace Microsoft::Console::Interactivity +{ + class PseudoConsoleWindowAccessibilityProvider final : + public WRL::RuntimeClass, IRawElementProviderSimple> + { + public: + PseudoConsoleWindowAccessibilityProvider() = default; + ~PseudoConsoleWindowAccessibilityProvider() = default; + HRESULT RuntimeClassInitialize(HWND pseudoConsoleHwnd) noexcept; + + PseudoConsoleWindowAccessibilityProvider(const PseudoConsoleWindowAccessibilityProvider&) = delete; + PseudoConsoleWindowAccessibilityProvider(PseudoConsoleWindowAccessibilityProvider&&) = delete; + PseudoConsoleWindowAccessibilityProvider& operator=(const PseudoConsoleWindowAccessibilityProvider&) = delete; + PseudoConsoleWindowAccessibilityProvider& operator=(PseudoConsoleWindowAccessibilityProvider&&) = delete; + + // IRawElementProviderSimple methods + IFACEMETHODIMP get_ProviderOptions(_Out_ ProviderOptions* pOptions) override; + IFACEMETHODIMP GetPatternProvider(_In_ PATTERNID iid, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface) override; + IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID idProp, + _Out_ VARIANT* pVariant) override; + IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) override; + + private: + HWND _pseudoConsoleHwnd; + }; +} diff --git a/src/interactivity/base/RemoteConsoleControl.cpp b/src/interactivity/base/RemoteConsoleControl.cpp index 7cd03e3631..a93c8dc003 100644 --- a/src/interactivity/base/RemoteConsoleControl.cpp +++ b/src/interactivity/base/RemoteConsoleControl.cpp @@ -35,7 +35,8 @@ template DWORD bytesWritten = 0; if (!WriteFile(pipe, &packet, sizeof(packet), &bytesWritten, nullptr)) { - NT_RETURN_NTSTATUS(static_cast(NTSTATUS_FROM_WIN32(::GetLastError()))); + const auto gle = ::GetLastError(); + NT_RETURN_NTSTATUS(static_cast(NTSTATUS_FROM_WIN32(gle))); } if (bytesWritten != sizeof(packet)) @@ -74,4 +75,11 @@ template return _SendTypedPacket(_pipe.get(), HostSignals::EndTask, data); } +[[nodiscard]] NTSTATUS RemoteConsoleControl::SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId) +{ + // This call doesn't need to get forwarded to the root conhost. Just handle + // it in-proc, to set the owner of OpenConsole + return _control.SetWindowOwner(hwnd, processId, threadId); +} + #pragma endregion diff --git a/src/interactivity/base/RemoteConsoleControl.hpp b/src/interactivity/base/RemoteConsoleControl.hpp index b922b9beaa..902052678e 100644 --- a/src/interactivity/base/RemoteConsoleControl.hpp +++ b/src/interactivity/base/RemoteConsoleControl.hpp @@ -27,6 +27,7 @@ namespace Microsoft::Console::Interactivity [[nodiscard]] NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId); [[nodiscard]] NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground); [[nodiscard]] NTSTATUS EndTask(_In_ DWORD dwProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags); + [[nodiscard]] NTSTATUS SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId); private: wil::unique_handle _pipe; diff --git a/src/interactivity/base/ServiceLocator.cpp b/src/interactivity/base/ServiceLocator.cpp index 47b7138f45..5900000162 100644 --- a/src/interactivity/base/ServiceLocator.cpp +++ b/src/interactivity/base/ServiceLocator.cpp @@ -45,13 +45,21 @@ void ServiceLocator::SetOneCoreTeardownFunction(void (*pfn)()) noexcept void ServiceLocator::RundownAndExit(const HRESULT hr) { - static std::atomic locked; + // The TriggerTeardown() call below depends on the render thread being able to acquire the + // console lock, so that it can safely progress with flushing the last frame. Since there's no + // coming back from this function (it's [[noreturn]]), it's safe to unlock the console here. + auto& gci = s_globals.getConsoleInformation(); + while (gci.IsConsoleLocked()) + { + gci.UnlockConsole(); + } // MSFT:40146639 // The premise of this function is that 1 thread enters and 0 threads leave alive. // We need to prevent anyone from calling us until we actually ExitProcess(), // so that we don't TriggerTeardown() twice. LockConsole() can't be used here, // because doing so would prevent the render thread from progressing. + static std::atomic locked; if (locked.exchange(true, std::memory_order_relaxed)) { // If we reach this point, another thread is already in the process of exiting. @@ -130,11 +138,11 @@ void ServiceLocator::RundownAndExit(const HRESULT hr) { status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = s_interactivityFactory->CreateConsoleInputThread(s_consoleInputThread); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { *thread = s_consoleInputThread.get(); } @@ -224,7 +232,7 @@ IConsoleControl* ServiceLocator::LocateConsoleControl() status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = s_interactivityFactory->CreateConsoleControl(s_consoleControl); } @@ -251,7 +259,7 @@ IHighDpiApi* ServiceLocator::LocateHighDpiApi() status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = s_interactivityFactory->CreateHighDpiApi(s_highDpiApi); } @@ -273,7 +281,7 @@ IWindowMetrics* ServiceLocator::LocateWindowMetrics() status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = s_interactivityFactory->CreateWindowMetrics(s_windowMetrics); } @@ -300,7 +308,7 @@ ISystemConfigurationProvider* ServiceLocator::LocateSystemConfigurationProvider( status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = s_interactivityFactory->CreateSystemConfigurationProvider(s_systemConfigurationProvider); } @@ -333,7 +341,7 @@ HWND ServiceLocator::LocatePseudoWindow(const HWND owner) status = ServiceLocator::LoadInteractivityFactory(); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { HWND hwnd; status = s_interactivityFactory->CreatePseudoWindow(hwnd, owner); diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj b/src/interactivity/base/lib/InteractivityBase.vcxproj index 0b9a03e97a..7517d99f4f 100644 --- a/src/interactivity/base/lib/InteractivityBase.vcxproj +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj @@ -28,6 +28,7 @@ Create + @@ -49,9 +50,10 @@ + - + \ No newline at end of file diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj.filters b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters index d761db55b4..3cad47ac50 100644 --- a/src/interactivity/base/lib/InteractivityBase.vcxproj.filters +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters @@ -36,6 +36,12 @@ Source Files + + Source Files + + + Source Files + @@ -89,6 +95,9 @@ Header Files + + Header Files + diff --git a/src/interactivity/base/sources.inc b/src/interactivity/base/sources.inc index 5d77a022b0..0e77324d00 100644 --- a/src/interactivity/base/sources.inc +++ b/src/interactivity/base/sources.inc @@ -42,10 +42,11 @@ SOURCES = \ ..\VtApiRedirection.cpp \ ..\EventSynthesis.cpp \ ..\RemoteConsoleControl.cpp \ - ..\HostSignalInputThread.cpp \ + ..\HostSignalInputThread.cpp \ + ..\PseudoConsoleWindowAccessibilityProvider.cpp \ INCLUDES = \ $(INCLUDES); \ ..; \ ..\..\..\..\..\ConIoSrv; \ - + diff --git a/src/interactivity/inc/EventSynthesis.hpp b/src/interactivity/inc/EventSynthesis.hpp index a1ebb64f1a..0a114c4369 100644 --- a/src/interactivity/inc/EventSynthesis.hpp +++ b/src/interactivity/inc/EventSynthesis.hpp @@ -14,16 +14,10 @@ Author: --*/ #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/inc/IConsoleControl.hpp b/src/interactivity/inc/IConsoleControl.hpp index 7129cff68f..66fccee661 100644 --- a/src/interactivity/inc/IConsoleControl.hpp +++ b/src/interactivity/inc/IConsoleControl.hpp @@ -24,5 +24,6 @@ namespace Microsoft::Console::Interactivity [[nodiscard]] virtual NTSTATUS NotifyConsoleApplication(DWORD dwProcessId) = 0; [[nodiscard]] virtual NTSTATUS SetForeground(HANDLE hProcess, BOOL fForeground) = 0; [[nodiscard]] virtual NTSTATUS EndTask(DWORD dwProcessId, DWORD dwEventType, ULONG ulCtrlFlags) = 0; + [[nodiscard]] virtual NTSTATUS SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId) = 0; }; } diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index 5ba1a0a185..ce6b903ffd 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -29,8 +29,8 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid { _runLength = sizeof(CD_IO_CHARACTER) * DisplayWidth; - _fontSize.X = FontWidth > SHORT_MAX ? SHORT_MAX : gsl::narrow_cast(FontWidth); - _fontSize.Y = FontHeight > SHORT_MAX ? SHORT_MAX : gsl::narrow_cast(FontHeight); + _fontSize.width = FontWidth > SHORT_MAX ? SHORT_MAX : gsl::narrow_cast(FontWidth); + _fontSize.height = FontHeight > SHORT_MAX ? SHORT_MAX : gsl::narrow_cast(FontHeight); } [[nodiscard]] HRESULT BgfxEngine::Invalidate(const til::rect* /*psrRegion*/) noexcept @@ -79,7 +79,7 @@ try { const auto Status = ConIoSrvComm::GetConIoSrvComm()->RequestUpdateDisplay(0); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { for (SIZE_T i = 0; i < _displayHeight; i++) { @@ -126,20 +126,20 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT BgfxEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT BgfxEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept { try { - const auto y = gsl::narrow_cast(coord.Y); + const auto y = gsl::narrow_cast(coord.y); const auto NewRun = reinterpret_cast(_sharedViewBase + (y * 2 * _runLength) + _runLength); for (SIZE_T i = 0; i < clusters.size() && i < _displayWidth; i++) { - NewRun[coord.X + i].Character = til::at(clusters, i).GetTextAsSingle(); - NewRun[coord.X + i].Attribute = _currentLegacyColorAttribute; + NewRun[coord.x + i].Character = til::at(clusters, i).GetTextAsSingle(); + NewRun[coord.x + i].Attribute = _currentLegacyColorAttribute; } return S_OK; @@ -166,8 +166,8 @@ try // TODO: MSFT: 11448021 - Modify BGFX to support rendering full-width // characters and a full-width cursor. CD_IO_CURSOR_INFORMATION CursorInfo; - CursorInfo.Row = gsl::narrow(options.coordCursor.Y); - CursorInfo.Column = gsl::narrow(options.coordCursor.X); + CursorInfo.Row = gsl::narrow(options.coordCursor.y); + CursorInfo.Column = gsl::narrow(options.coordCursor.x); CursorInfo.Height = options.ulCursorHeightPercent; CursorInfo.IsVisible = TRUE; @@ -215,7 +215,7 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT BgfxEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT BgfxEngine::GetDirtyArea(std::span& area) noexcept { _dirtyArea.bottom = gsl::narrow_cast(std::max(static_cast(0), _displayHeight)); _dirtyArea.right = gsl::narrow_cast(std::max(static_cast(0), _displayWidth)); diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 00476d805f..31fa49c852 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -47,7 +47,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; @@ -67,7 +67,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/interactivity/onecore/ConIoSrvComm.cpp b/src/interactivity/onecore/ConIoSrvComm.cpp index 4107ad24a2..3c543e442f 100644 --- a/src/interactivity/onecore/ConIoSrvComm.cpp +++ b/src/interactivity/onecore/ConIoSrvComm.cpp @@ -105,6 +105,7 @@ ConIoSrvComm::~ConIoSrvComm() ALPC_MESSAGE_HANDLE_INFORMATION HandleInfo; // Initialize the attributes of the port object. +#pragma warning(suppress : 26477) // This macro contains a bare NULL InitializeObjectAttributes(&ObjectAttributes, nullptr, 0, @@ -162,7 +163,7 @@ ConIoSrvComm::~ConIoSrvComm() nullptr, ConnectionMessageAttributes, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { const auto ViewAttributes = ALPC_GET_DATAVIEW_ATTRIBUTES(ConnectionMessageAttributes); const auto HandleAttributes = ALPC_GET_HANDLE_ATTRIBUTES(ConnectionMessageAttributes); @@ -174,7 +175,7 @@ ConIoSrvComm::~ConIoSrvComm() Status = STATUS_UNSUCCESSFUL; } - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Get each handle out. ALPC does not allow to pass indirect handles // all at once; they must be retrieved one by one. @@ -188,7 +189,7 @@ ConIoSrvComm::~ConIoSrvComm() &HandleInfo, sizeof(HandleInfo), nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { if (Index == 0) { @@ -265,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 (...) { @@ -365,8 +364,8 @@ VOID ConIoSrvComm::HandleFocusEvent(const CIS_EVENT* const Event) til::size coordFont; LOG_IF_FAILED(pWddmConEngine->GetFontSize(&coordFont)); - rcDisplay.right *= coordFont.X; - rcDisplay.bottom *= coordFont.Y; + rcDisplay.right *= coordFont.width; + rcDisplay.bottom *= coordFont.height; // Ask the screen buffer to resize itself (and all related components) based on the screen size. globals.getConsoleInformation().GetActiveOutputBuffer().ProcessResizeWindow(&rcDisplay, &rcOld); @@ -464,7 +463,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) Message.Type = CIS_MSG_TYPE_GETDISPLAYSIZE; auto Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *pCdDisplaySize = Message.GetDisplaySizeParams.DisplaySize; Status = Message.GetDisplaySizeParams.ReturnValue; @@ -479,7 +478,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) Message.Type = CIS_MSG_TYPE_GETFONTSIZE; auto Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *pCdFontSize = Message.GetFontSizeParams.FontSize; Status = Message.GetFontSizeParams.ReturnValue; @@ -495,7 +494,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) Message.SetCursorParams.CursorInformation = *pCdCursorInformation; auto Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = Message.SetCursorParams.ReturnValue; } @@ -510,7 +509,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) Message.UpdateDisplayParams.RowIndex = gsl::narrow(RowIndex); auto Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = Message.UpdateDisplayParams.ReturnValue; } @@ -523,7 +522,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) NTSTATUS Status; Status = EnsureConnection(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { CIS_MSG Message = { 0 }; Message.Type = CIS_MSG_TYPE_MAPVIRTUALKEY; @@ -531,7 +530,7 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) Message.MapVirtualKeyParams.MapType = uMapType; Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *puReturnValue = Message.MapVirtualKeyParams.ReturnValue; } @@ -545,14 +544,14 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) NTSTATUS Status; Status = EnsureConnection(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { CIS_MSG Message = { 0 }; Message.Type = CIS_MSG_TYPE_VKKEYSCAN; Message.VkKeyScanParams.Character = wCharacter; Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *psReturnValue = Message.VkKeyScanParams.ReturnValue; } @@ -566,14 +565,14 @@ VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) NTSTATUS Status; Status = EnsureConnection(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { CIS_MSG Message = { 0 }; Message.Type = CIS_MSG_TYPE_GETKEYSTATE; Message.GetKeyStateParams.VirtualKey = iVirtualKey; Status = SendRequestReceiveReply(&Message); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *psReturnValue = Message.GetKeyStateParams.ReturnValue; } @@ -603,7 +602,7 @@ UINT ConIoSrvComm::ConIoMapVirtualKeyW(UINT uCode, UINT uMapType) UINT ReturnValue; Status = RequestMapVirtualKey(uCode, uMapType, &ReturnValue); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { ReturnValue = 0; SetLastError(ERROR_PROC_NOT_FOUND); @@ -619,7 +618,7 @@ SHORT ConIoSrvComm::ConIoVkKeyScanW(WCHAR ch) SHORT ReturnValue; Status = RequestVkKeyScan(ch, &ReturnValue); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { ReturnValue = 0; SetLastError(ERROR_PROC_NOT_FOUND); @@ -635,7 +634,7 @@ SHORT ConIoSrvComm::ConIoGetKeyState(int nVirtKey) SHORT ReturnValue; Status = RequestGetKeyState(nVirtKey, &ReturnValue); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { ReturnValue = 0; SetLastError(ERROR_PROC_NOT_FOUND); @@ -656,13 +655,13 @@ SHORT ConIoSrvComm::ConIoGetKeyState(int nVirtKey) const auto DisplaySize = Metrics->GetMaxClientRectInPixels(); auto Status = GetLastError(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Same with the font size. CD_IO_FONT_SIZE FontSize{}; Status = RequestGetFontSize(&FontSize); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { try { diff --git a/src/interactivity/onecore/ConsoleControl.cpp b/src/interactivity/onecore/ConsoleControl.cpp index 248d02b3ac..7841bfbe19 100644 --- a/src/interactivity/onecore/ConsoleControl.cpp +++ b/src/interactivity/onecore/ConsoleControl.cpp @@ -38,4 +38,9 @@ using namespace Microsoft::Console::Interactivity::OneCore; sizeof(*a)); } +[[nodiscard]] NTSTATUS ConsoleControl::SetWindowOwner(HWND, DWORD, DWORD) noexcept +{ + return STATUS_SUCCESS; +} + #pragma endregion diff --git a/src/interactivity/onecore/ConsoleControl.hpp b/src/interactivity/onecore/ConsoleControl.hpp index 4771c6c222..4b917a2f15 100644 --- a/src/interactivity/onecore/ConsoleControl.hpp +++ b/src/interactivity/onecore/ConsoleControl.hpp @@ -27,5 +27,6 @@ namespace Microsoft::Console::Interactivity::OneCore [[nodiscard]] NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId) noexcept override; [[nodiscard]] NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground) noexcept override; [[nodiscard]] NTSTATUS EndTask(_In_ DWORD dwProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags) override; + [[nodiscard]] NTSTATUS SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId) noexcept override; }; } diff --git a/src/interactivity/onecore/ConsoleInputThread.cpp b/src/interactivity/onecore/ConsoleInputThread.cpp index a893119c31..6abad86b69 100644 --- a/src/interactivity/onecore/ConsoleInputThread.cpp +++ b/src/interactivity/onecore/ConsoleInputThread.cpp @@ -23,7 +23,7 @@ DWORD WINAPI ConsoleInputThreadProcOneCore(LPVOID /*lpParam*/) auto Status = Server->Connect(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { const auto DisplayMode = Server->GetDisplayMode(); @@ -32,7 +32,7 @@ DWORD WINAPI ConsoleInputThreadProcOneCore(LPVOID /*lpParam*/) // Create and set the console window. static ConsoleWindow wnd; - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { LOG_IF_FAILED(ServiceLocator::SetConsoleWindowInstance(&wnd)); @@ -51,7 +51,7 @@ DWORD WINAPI ConsoleInputThreadProcOneCore(LPVOID /*lpParam*/) break; } - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { globals.getConsoleInformation().GetActiveOutputBuffer().RefreshFontWithRenderer(); } @@ -59,7 +59,7 @@ DWORD WINAPI ConsoleInputThreadProcOneCore(LPVOID /*lpParam*/) globals.ntstatusConsoleInputInitStatus = Status; globals.hConsoleInputInitEvent.SetEvent(); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { try { diff --git a/src/interactivity/onecore/WindowMetrics.cpp b/src/interactivity/onecore/WindowMetrics.cpp index 3be88cd0eb..a025716f26 100644 --- a/src/interactivity/onecore/WindowMetrics.cpp +++ b/src/interactivity/onecore/WindowMetrics.cpp @@ -29,8 +29,8 @@ til::rect WindowMetrics::GetMinClientRectInPixels() // the values for that at the end. // If we don't... then at least we have a non-zero rectangle. til::size FontSize; - FontSize.X = HEADLESS_FONT_SIZE_WIDTH; - FontSize.Y = HEADLESS_FONT_SIZE_HEIGHT; + FontSize.width = HEADLESS_FONT_SIZE_WIDTH; + FontSize.height = HEADLESS_FONT_SIZE_HEIGHT; til::rect DisplaySize; DisplaySize.right = HEADLESS_DISPLAY_SIZE_WIDTH; @@ -67,19 +67,19 @@ til::rect WindowMetrics::GetMinClientRectInPixels() auto Status = Server->RequestGetDisplaySize(&DisplaySizeIoctl); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = Server->RequestGetFontSize(&FontSizeIoctl); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { DisplaySize.top = 0; DisplaySize.left = 0; DisplaySize.bottom = gsl::narrow_cast(DisplaySizeIoctl.Height); DisplaySize.right = gsl::narrow_cast(DisplaySizeIoctl.Width); - FontSize.X = gsl::narrow_cast(FontSizeIoctl.Width); - FontSize.Y = gsl::narrow_cast(FontSizeIoctl.Height); + FontSize.width = gsl::narrow_cast(FontSizeIoctl.Width); + FontSize.height = gsl::narrow_cast(FontSizeIoctl.Height); } } else @@ -106,8 +106,8 @@ til::rect WindowMetrics::GetMinClientRectInPixels() } // The result is expected to be in pixels, not rows/columns. - DisplaySize.right *= FontSize.X; - DisplaySize.bottom *= FontSize.Y; + DisplaySize.right *= FontSize.width; + DisplaySize.bottom *= FontSize.height; return DisplaySize; } diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp index 1f9eacc90f..30138f6dad 100644 --- a/src/interactivity/win32/Clipboard.cpp +++ b/src/interactivity/win32/Clipboard.cpp @@ -108,7 +108,9 @@ void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData, try { - auto inEvents = TextToKeyEvents(pData, cchData); + const auto vtInputMode = gci.pInputBuffer->IsInVirtualTerminalInputMode(); + const auto bracketedPasteMode = gci.GetBracketedPasteMode(); + auto inEvents = TextToKeyEvents(pData, cchData, vtInputMode && bracketedPasteMode); gci.pInputBuffer->Write(inEvents); } catch (...) @@ -127,16 +129,31 @@ void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData, // Arguments: // - pData - the text to convert // - cchData - the size of pData, in wchars +// - bracketedPaste - should this be bracketed with paste control sequences // Return Value: // - deque of KeyEvents that represent the string passed in // Note: // - will throw exception on error -std::deque> Clipboard::TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, - const size_t cchData) +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(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0)); + keyEvents.push_back(SynthesizeKeyEvent(false, 1, 0, 0, wch, 0)); + }); + }; + + // When a bracketed paste is requested, we need to wrap the text with + // control sequences which indicate that the content has been pasted. + if (bracketedPaste) + { + pushControlSequence(L"\x1b[200~"); + } for (size_t i = 0; i < cchData; ++i) { @@ -148,8 +165,10 @@ std::deque> Clipboard::TextToKeyEvents(_In_reads_(c const auto skipLinefeed = (i != 0 && currentChar == UNICODE_LINEFEED && pData[i - 1] == UNICODE_CARRIAGERETURN); + // filter out escape if bracketed paste mode is enabled + const auto skipEscape = (bracketedPaste && currentChar == UNICODE_ESC); - if (!charAllowed || skipLinefeed) + if (!charAllowed || skipLinefeed || skipEscape) { continue; } @@ -175,13 +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; } @@ -227,7 +247,8 @@ void Clipboard::StoreSelectionToClipboard(const bool copyFormatting) const auto text = buffer.GetText(includeCRLF, trimTrailingWhitespace, selectionRects, - GetAttributeColors); + GetAttributeColors, + selection.IsKeyboardMarkSelection()); CopyTextToSystemClipboard(text, copyFormatting); } @@ -277,7 +298,7 @@ void Clipboard::CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& fontData = gci.GetActiveOutputBuffer().GetCurrentFont(); - const auto iFontHeightPoints = fontData.GetUnscaledSize().Y * 72 / ServiceLocator::LocateGlobals().dpi; + const auto iFontHeightPoints = fontData.GetUnscaledSize().height * 72 / ServiceLocator::LocateGlobals().dpi; const auto bgColor = gci.GetRenderSettings().GetAttributeColors({}).second; auto HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor); diff --git a/src/interactivity/win32/ConsoleControl.cpp b/src/interactivity/win32/ConsoleControl.cpp index 78e66110df..833f36b1cd 100644 --- a/src/interactivity/win32/ConsoleControl.cpp +++ b/src/interactivity/win32/ConsoleControl.cpp @@ -52,6 +52,18 @@ using namespace Microsoft::Console::Interactivity::Win32; sizeof(ConsoleEndTaskParams)); } +[[nodiscard]] NTSTATUS ConsoleControl::SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId) noexcept +{ + CONSOLEWINDOWOWNER ConsoleOwner; + ConsoleOwner.hwnd = hwnd; + ConsoleOwner.ProcessId = processId; + ConsoleOwner.ThreadId = threadId; + + return Control(ConsoleControl::ControlType::ConsoleSetWindowOwner, + &ConsoleOwner, + sizeof(ConsoleOwner)); +} + #pragma endregion #pragma region Public Methods diff --git a/src/interactivity/win32/ConsoleControl.hpp b/src/interactivity/win32/ConsoleControl.hpp index fdd490a528..1e9efe6c59 100644 --- a/src/interactivity/win32/ConsoleControl.hpp +++ b/src/interactivity/win32/ConsoleControl.hpp @@ -48,6 +48,7 @@ namespace Microsoft::Console::Interactivity::Win32 [[nodiscard]] NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId); [[nodiscard]] NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground); [[nodiscard]] NTSTATUS EndTask(_In_ DWORD dwProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags); + [[nodiscard]] NTSTATUS SetWindowOwner(HWND hwnd, DWORD processId, DWORD threadId) noexcept override; // Public Members [[nodiscard]] NTSTATUS Control(_In_ ConsoleControl::ControlType ConsoleCommand, diff --git a/src/interactivity/win32/SystemConfigurationProvider.cpp b/src/interactivity/win32/SystemConfigurationProvider.cpp index c4c74865c9..58472898d1 100644 --- a/src/interactivity/win32/SystemConfigurationProvider.cpp +++ b/src/interactivity/win32/SystemConfigurationProvider.cpp @@ -112,7 +112,7 @@ void SystemConfigurationProvider::GetSettingsFromLink( &iShowWindow, &wHotKey); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Convert results back to appropriate types and set. if (SUCCEEDED(IntToWord(iShowWindow, &wShowWindow))) @@ -148,7 +148,7 @@ void SystemConfigurationProvider::GetSettingsFromLink( } } - if (NT_SUCCESS(Status) && fReadConsoleProperties) + if (SUCCEEDED_NTSTATUS(Status) && fReadConsoleProperties) { // copy settings pLinkSettings->InitFromStateInfo(&csi); diff --git a/src/interactivity/win32/WindowIme.cpp b/src/interactivity/win32/WindowIme.cpp index 7bb29b76e8..f66338d7de 100644 --- a/src/interactivity/win32/WindowIme.cpp +++ b/src/interactivity/win32/WindowIme.cpp @@ -30,13 +30,13 @@ RECT GetImeSuggestionWindowPos() // that the "relative" position is that it is on the 11th line from the top (or 10th by index). // Correct by subtracting the top/left corner from the cursor's position. const auto srViewport = screenBuffer.GetViewport().ToInclusive(); - coordCursor.X -= srViewport.Left; - coordCursor.Y -= srViewport.Top; + coordCursor.x -= srViewport.left; + coordCursor.y -= srViewport.top; // Map the point to be just under the current cursor position. Convert from coordinate to pixels using font. POINT ptSuggestion; - ptSuggestion.x = (coordCursor.X + 1) * coordFont.X; - ptSuggestion.y = (coordCursor.Y) * coordFont.Y; + ptSuggestion.x = (coordCursor.x + 1) * coordFont.width; + ptSuggestion.y = (coordCursor.y) * coordFont.height; // Adjust client point to screen point via HWND. ClientToScreen(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), &ptSuggestion); @@ -49,8 +49,8 @@ RECT GetImeSuggestionWindowPos() // Add 1 line height and a few characters of width to represent the area where we're writing text. // This could be more exact by looking up the CONVAREA but it works well enough this way. // If there is a future issue with the pop-up window, tweak these metrics. - rcSuggestion.bottom += coordFont.Y; - rcSuggestion.right += (coordFont.X * 10); + rcSuggestion.bottom += coordFont.height; + rcSuggestion.right += (coordFont.width * 10); return rcSuggestion; } diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp index da28f2dfc0..f6bab9254c 100644 --- a/src/interactivity/win32/clipboard.hpp +++ b/src/interactivity/win32/clipboard.hpp @@ -35,8 +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); + 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/find.cpp b/src/interactivity/win32/find.cpp index 896c5885a1..4e7cf36a58 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.MoveToCurrentSelection(); } 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/menu.cpp b/src/interactivity/win32/menu.cpp index e2a1f06bcb..7b59887daa 100644 --- a/src/interactivity/win32/menu.cpp +++ b/src/interactivity/win32/menu.cpp @@ -56,7 +56,7 @@ Menu::Menu(HMENU hMenu, HMENU hHeirMenu) : auto pNewMenu = new (std::nothrow) Menu(hMenu, hHeirMenu); status = NT_TESTNULL(pNewMenu); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { int ItemLength; // Load the submenu to the system menu. @@ -480,47 +480,41 @@ void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) // Maximum number of characters we could fit on the given monitor. const auto coordLargest = ScreenInfo.GetLargestWindowSizeInCharacters(); - coordWindow.X = std::min(coordLargest.X, coordWindow.X); - coordWindow.Y = std::min(coordLargest.Y, coordWindow.Y); + coordWindow.width = std::min(coordLargest.width, coordWindow.width); + coordWindow.height = std::min(coordLargest.height, coordWindow.height); if (gci.GetWrapText()) { // Then if wrap text is on, the buffer width gets fixed to the window width value. - coordBuffer.X = coordWindow.X; + coordBuffer.width = coordWindow.width; // However, we're not done. The "max window size" is if we had no scroll bar. // We need to adjust slightly more if there's space reserved for a vertical scroll bar // which happens when the buffer Y is taller than the window Y. - if (coordBuffer.Y > coordWindow.Y) + if (coordBuffer.height > coordWindow.height) { // Since we need a scroll bar in the Y direction, clamp the buffer width to make sure that // it is leaving appropriate space for a scroll bar. const auto coordScrollBars = ScreenInfo.GetScrollBarSizesInCharacters(); - const auto sMaxBufferWidthWithScroll = coordLargest.X - coordScrollBars.X; + const auto sMaxBufferWidthWithScroll = coordLargest.width - coordScrollBars.width; - coordBuffer.X = std::min(coordBuffer.X, sMaxBufferWidthWithScroll); + coordBuffer.width = std::min(coordBuffer.width, sMaxBufferWidthWithScroll); } } // Now adjust the buffer size first to whatever we want it to be if it's different than before. const auto coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); - if (coordBuffer.X != coordScreenBufferSize.X || - coordBuffer.Y != coordScreenBufferSize.Y) + 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. const auto coordMaxForBuffer = ScreenInfo.GetMaxWindowSizeInCharacters(); - coordWindow.X = std::min(coordWindow.X, coordMaxForBuffer.X); - coordWindow.Y = std::min(coordWindow.Y, coordMaxForBuffer.Y); + coordWindow.width = std::min(coordWindow.width, coordMaxForBuffer.width); + coordWindow.height = std::min(coordWindow.height, coordMaxForBuffer.height); // Then finish by updating the window. This will update the window size, // as well as the screen buffer's viewport. diff --git a/src/interactivity/win32/screenInfoUiaProvider.cpp b/src/interactivity/win32/screenInfoUiaProvider.cpp index 80e3785040..a8b6bc417d 100644 --- a/src/interactivity/win32/screenInfoUiaProvider.cpp +++ b/src/interactivity/win32/screenInfoUiaProvider.cpp @@ -12,7 +12,7 @@ using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::Interactivity::Win32; using namespace Microsoft::WRL; -HRESULT ScreenInfoUiaProvider::RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, +HRESULT ScreenInfoUiaProvider::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ WindowUiaProvider* const pUiaParent) { RETURN_HR_IF_NULL(E_INVALIDARG, pUiaParent); diff --git a/src/interactivity/win32/screenInfoUiaProvider.hpp b/src/interactivity/win32/screenInfoUiaProvider.hpp index 1e38c51d6b..37e36a0aa5 100644 --- a/src/interactivity/win32/screenInfoUiaProvider.hpp +++ b/src/interactivity/win32/screenInfoUiaProvider.hpp @@ -33,7 +33,7 @@ namespace Microsoft::Console::Interactivity::Win32 { public: ScreenInfoUiaProvider() = default; - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ WindowUiaProvider* const pUiaParent); // IRawElementProviderFragment methods diff --git a/src/interactivity/win32/uiaTextRange.cpp b/src/interactivity/win32/uiaTextRange.cpp index 6873444674..d060b3e398 100644 --- a/src/interactivity/win32/uiaTextRange.cpp +++ b/src/interactivity/win32/uiaTextRange.cpp @@ -13,13 +13,13 @@ using namespace Microsoft::WRL; using Microsoft::Console::Interactivity::ServiceLocator; // degenerate range constructor. -HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters) noexcept +HRESULT UiaTextRange::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters) noexcept { return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, wordDelimiters); } // degenerate range at cursor position -HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT UiaTextRange::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const Cursor& cursor, const std::wstring_view wordDelimiters) noexcept @@ -28,7 +28,7 @@ HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, } // specific endpoint range -HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT UiaTextRange::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const til::point start, const til::point end, @@ -39,7 +39,7 @@ HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, } // returns a degenerate text range of the start of the row closest to the y value of point -HRESULT UiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT UiaTextRange::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const UiaPoint point, const std::wstring_view wordDelimiters) diff --git a/src/interactivity/win32/uiaTextRange.hpp b/src/interactivity/win32/uiaTextRange.hpp index 1d3fc3478f..7ea79364d5 100644 --- a/src/interactivity/win32/uiaTextRange.hpp +++ b/src/interactivity/win32/uiaTextRange.hpp @@ -27,18 +27,18 @@ namespace Microsoft::Console::Interactivity::Win32 UiaTextRange() = default; // degenerate range - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // degenerate range at cursor position - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const Cursor& cursor, _In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // specific endpoint range - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const til::point start, _In_ const til::point end, @@ -46,7 +46,7 @@ namespace Microsoft::Console::Interactivity::Win32 _In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // range from a UiaPoint - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const UiaPoint point, _In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter); diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 34b3ff374c..8096b878c8 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -281,7 +281,7 @@ class UiaTextRangeTests SCREEN_INFORMATION* _pScreenInfo; TextBuffer* _pTextBuffer; UiaTextRange* _range; - IUiaData* _pUiaData; + Microsoft::Console::Render::IRenderData* _pUiaData; struct ExpectedResult { @@ -360,18 +360,13 @@ class UiaTextRangeTests // fill first half of text buffer with text for (auto i = 0; i < _pTextBuffer->TotalRowCount() / 2; ++i) { - auto& row = _pTextBuffer->GetRowByOffset(i); - auto& charRow = row.GetCharRow(); - for (auto& cell : charRow) + const std::wstring_view glyph{ i % 2 == 0 ? L" " : L"X" }; + auto& row = _pTextBuffer->GetMutableRowByOffset(i); + const auto width = row.size(); + + for (uint16_t x = 0; x < width; ++x) { - if (i % 2 == 0) - { - cell.Char() = L' '; - } - else - { - cell.Char() = L'X'; - } + row.ReplaceCharacters(x, 1, glyph); } } @@ -494,21 +489,13 @@ 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& charRow = row.GetCharRow(); - for (auto j = 0; j < charRow.size(); ++j) + auto& row = _pTextBuffer->GetMutableRowByOffset(i); + const auto width = row.size(); + + for (uint16_t x = 0; x < width; ++x) { - // every 5th cell is a space, otherwise a letter - // this is used to simulate words - auto cell = charRow.GlyphAt(j); - if (j % 5 == 0) - { - cell = L" "; - } - else - { - cell = L"x"; - } + const std::wstring_view glyph{ x % 5 == 0 ? L" " : L"x" }; + row.ReplaceCharacters(x, 1, glyph); } } @@ -590,7 +577,7 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character) { Log::Comment(NoThrowString().Format(L"%s - Test 2", toString(textUnit))); - const til::point end = { boundaries.start.X + 1, boundaries.start.Y }; + const til::point end = { boundaries.start.x + 1, boundaries.start.y }; verifyExpansion(textUnit, boundaries.start, end); } @@ -602,7 +589,7 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character && textUnit != TextUnit_Document) { Log::Comment(NoThrowString().Format(L"%s - Test 4", toString(textUnit))); - const til::point end = { boundaries.end.X + 1, boundaries.end.Y }; + const til::point end = { boundaries.end.x + 1, boundaries.end.y }; verifyExpansion(textUnit, boundaries.start, end); } @@ -610,7 +597,7 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character) { Log::Comment(NoThrowString().Format(L"%s - Test 5", toString(textUnit))); - const til::point start = { boundaries.start.X + 1, boundaries.start.Y }; + const til::point start = { boundaries.start.x + 1, boundaries.start.y }; verifyExpansion(textUnit, start, start); } @@ -618,8 +605,8 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character) { Log::Comment(NoThrowString().Format(L"%s - Test 6", toString(textUnit))); - const til::point start = { boundaries.start.X + 1, boundaries.start.Y }; - const til::point end = { start.X + 1, start.Y }; + const til::point start = { boundaries.start.x + 1, boundaries.start.y }; + const til::point end = { start.x + 1, start.y }; verifyExpansion(textUnit, start, end); } @@ -627,7 +614,7 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character) { Log::Comment(NoThrowString().Format(L"%s - Test 7", toString(textUnit))); - const til::point start = { boundaries.start.X + 1, boundaries.start.Y }; + const til::point start = { boundaries.start.x + 1, boundaries.start.y }; verifyExpansion(textUnit, start, boundaries.end); } @@ -635,8 +622,8 @@ class UiaTextRangeTests if (textUnit != TextUnit_Character && textUnit != TextUnit_Document) { Log::Comment(NoThrowString().Format(L"%s - Test 8", toString(textUnit))); - const til::point start = { boundaries.start.X + 1, boundaries.start.Y }; - const til::point end = { boundaries.end.X + 1, boundaries.end.Y }; + const til::point start = { boundaries.start.x + 1, boundaries.start.y }; + const til::point end = { boundaries.end.x + 1, boundaries.end.y }; verifyExpansion(textUnit, start, end); } } @@ -738,7 +725,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().Y + 1 }; + const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().y + 1 }; // clang-format off const std::vector testData @@ -826,7 +813,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().Y + 1 }; + const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().y + 1 }; // clang-format off const std::vector testData @@ -857,13 +844,13 @@ class UiaTextRangeTests MoveTest{ L"can move backward from bottom row", - {0, documentEnd.Y}, - {lastColumnIndex, documentEnd.Y}, + {0, documentEnd.y}, + {lastColumnIndex, documentEnd.y}, -3, { -3, - {0, documentEnd.Y - 3}, - {0, documentEnd.Y - 3} + {0, documentEnd.y - 3}, + {0, documentEnd.y - 3} } }, @@ -914,7 +901,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEnd{ _pTextBuffer->GetSize().RightInclusive(), _pTextBuffer->GetLastNonSpaceCharacter().Y }; + const til::point documentEnd{ _pTextBuffer->GetSize().RightInclusive(), _pTextBuffer->GetLastNonSpaceCharacter().y }; // clang-format off const std::array testData @@ -967,7 +954,7 @@ class UiaTextRangeTests { 1, {0,0}, - {0, documentEnd.Y + 1} + {0, documentEnd.y + 1} } }, @@ -986,40 +973,40 @@ class UiaTextRangeTests MoveEndpointTest{ L"can't move _end past the beginning of the document when _end is positioned at the end", - {0, documentEnd.Y}, - {0, documentEnd.Y + 1}, + {0, documentEnd.y}, + {0, documentEnd.y + 1}, 1, TextPatternRangeEndpoint_End, { 0, - {0, documentEnd.Y}, - {0, documentEnd.Y + 1}, + {0, documentEnd.y}, + {0, documentEnd.y + 1}, } }, MoveEndpointTest{ L"can partially move _end to the end of the document when it is closer than the move count requested", {0, 0}, - {lastColumnIndex - 3, documentEnd.Y}, + {lastColumnIndex - 3, documentEnd.y}, 5, TextPatternRangeEndpoint_End, { 4, {0, 0}, - {0, documentEnd.Y + 1}, + {0, documentEnd.y + 1}, } }, MoveEndpointTest{ L"can't move _start past the end of the document", - {lastColumnIndex - 4, documentEnd.Y}, - {0, documentEnd.Y + 1}, + {lastColumnIndex - 4, documentEnd.y}, + {0, documentEnd.y + 1}, 5, TextPatternRangeEndpoint_Start, { 5, - {0, documentEnd.Y + 1}, - {0, documentEnd.Y + 1}, + {0, documentEnd.y + 1}, + {0, documentEnd.y + 1}, } }, @@ -1060,7 +1047,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().Y + 1 }; + const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().y + 1 }; // clang-format off const std::vector testData @@ -1198,7 +1185,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().Y + 1 }; + const til::point documentEnd{ _pTextBuffer->GetSize().Left(), _pTextBuffer->GetLastNonSpaceCharacter().y + 1 }; // clang-format off const std::vector testData = @@ -1304,7 +1291,7 @@ class UiaTextRangeTests // at the end exclusive, the UTR should refuse to move past // the end. const auto lastNonspaceCharPos{ _pTextBuffer->GetLastNonSpaceCharacter() }; - const til::point documentEnd{ 0, lastNonspaceCharPos.Y + 1 }; + const til::point documentEnd{ 0, lastNonspaceCharPos.y + 1 }; // Iterate over each TextUnit. If we don't support // the given TextUnit, we're supposed to fallback @@ -1339,7 +1326,7 @@ class UiaTextRangeTests // GH#6986: This is used as the "end of the buffer" to help screen readers run faster // instead of parsing through thousands of empty lines of text. - const til::point documentEndInclusive{ bufferSize.right - 1, _pTextBuffer->GetLastNonSpaceCharacter().Y }; + const til::point documentEndInclusive{ bufferSize.right - 1, _pTextBuffer->GetLastNonSpaceCharacter().y }; const til::point documentEndExclusive{ bufferSize.left, documentEndInclusive.y + 1 }; const til::point lastLineStart{ bufferSize.left, documentEndInclusive.y }; @@ -1661,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); @@ -1708,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)); @@ -1941,7 +1922,7 @@ class UiaTextRangeTests auto fill{ true }; while (iter.Pos() != docEnd) { - if (iter.Pos().X == bufferSize.left) + if (iter.Pos().x == bufferSize.left) { fill = true; } diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index f8274b6fbd..5b0dde1894 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -92,17 +92,17 @@ Window::~Window() { auto status = s_RegisterWindowClass(); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { auto pNewWindow = new (std::nothrow) Window(); status = NT_TESTNULL(pNewWindow); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { status = pNewWindow->_MakeWindow(pSettings, pScreen); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { LOG_IF_FAILED(ServiceLocator::SetConsoleWindowInstance(pNewWindow)); } @@ -147,13 +147,14 @@ Window::~Window() // Load icons status = Icon::Instance().GetIcons(&wc.hIcon, &wc.hIconSm); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { s_atomWindowClass = RegisterClassExW(&wc); if (s_atomWindowClass == 0) { - status = NTSTATUS_FROM_WIN32(GetLastError()); + const auto gle = GetLastError(); + status = NTSTATUS_FROM_WIN32(gle); } } } @@ -249,7 +250,7 @@ void Window::_UpdateSystemMetrics() const status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { auto& siAttached = GetScreenInfo(); @@ -260,7 +261,7 @@ void Window::_UpdateSystemMetrics() const // Figure out coordinates and how big to make the window from the desired client viewport size // Put left, top, right and bottom into rectProposed for checking against monitor screens below - til::rect rectProposed = { pSettings->GetWindowOrigin().X, pSettings->GetWindowOrigin().Y, 0, 0 }; + til::rect rectProposed = { pSettings->GetWindowOrigin().width, pSettings->GetWindowOrigin().height, 0, 0 }; _CalculateWindowRect(pSettings->GetWindowSize(), &rectProposed); //returns with rectangle filled out if (!WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION)) @@ -328,41 +329,43 @@ void Window::_UpdateSystemMetrics() const status = NTSTATUS_FROM_WIN32(gle); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { _hWnd = hWnd; #if TIL_FEATURE_CONHOSTDXENGINE_ENABLED if (pDxEngine) { - status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pDxEngine->SetHwnd(hWnd)))); - - if (NT_SUCCESS(status)) + HRESULT hr = S_OK; + if (SUCCEEDED(hr = pDxEngine->SetHwnd(hWnd))) { - status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pDxEngine->Enable()))); + hr = pDxEngine->Enable(); } + status = NTSTATUS_FROM_HRESULT(hr); } else #endif #if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED if (pAtlasEngine) { - status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pAtlasEngine->SetHwnd(hWnd)))); + const auto hr = pAtlasEngine->SetHwnd(hWnd); + status = NTSTATUS_FROM_HRESULT(hr); } else #endif { - status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pGdiEngine->SetHwnd(hWnd)))); + const auto hr = pGdiEngine->SetHwnd(hWnd); + status = NTSTATUS_FROM_HRESULT(hr); } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { // Set alpha on window if requested ApplyWindowOpacity(); status = Menu::CreateInstance(hWnd); - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { gci.ConsoleIme.RefreshAreaAttributes(); @@ -430,7 +433,7 @@ void Window::_CloseWindow() const gci.Flags |= CONSOLE_IS_ICONIC; } - if (NT_SUCCESS(status)) + if (SUCCEEDED_NTSTATUS(status)) { ShowWindow(hWnd, wShowWindow); @@ -463,8 +466,8 @@ void Window::ChangeViewport(const til::inclusive_rect& NewWindow) auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); if (pNotifier != nullptr) { - pNotifier->NotifyConsoleUpdateScrollEvent(ScreenInfo.GetViewport().Left() - NewWindow.Left, - ScreenInfo.GetViewport().Top() - NewWindow.Top); + pNotifier->NotifyConsoleUpdateScrollEvent(ScreenInfo.GetViewport().Left() - NewWindow.left, + ScreenInfo.GetViewport().Top() - NewWindow.top); } // The new window is OK. Store it in screeninfo and refresh screen. @@ -594,8 +597,8 @@ void Window::_UpdateWindowSize(const til::size sizeNew) nullptr, 0, 0, - sizeNew.cx, - sizeNew.cy, + sizeNew.width, + sizeNew.height, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_DRAWFRAME); } } @@ -628,33 +631,33 @@ void Window::_UpdateWindowSize(const til::size sizeNew) // Now do the multiplication of characters times pixels per char. This is the client area pixel size. til::size WindowSize; - WindowSize.cx = WindowDimensions.X * ScreenFontSize.X; - WindowSize.cy = WindowDimensions.Y * ScreenFontSize.Y; + WindowSize.width = WindowDimensions.width * ScreenFontSize.width; + WindowSize.height = WindowDimensions.height * ScreenFontSize.height; // Fill a rectangle to call the system to adjust the client rect into a window rect til::rect rectSizeTemp; - rectSizeTemp.right = WindowSize.cx; - rectSizeTemp.bottom = WindowSize.cy; + rectSizeTemp.right = WindowSize.width; + rectSizeTemp.bottom = WindowSize.height; FAIL_FAST_IF(!(rectSizeTemp.top == 0 && rectSizeTemp.left == 0)); ServiceLocator::LocateWindowMetrics()->ConvertClientRectToWindowRect(&rectSizeTemp); // Measure the adjusted rectangle dimensions and fill up the size variable - WindowSize.cx = rectSizeTemp.width(); - WindowSize.cy = rectSizeTemp.height(); + WindowSize.width = rectSizeTemp.width(); + WindowSize.height = rectSizeTemp.height(); - if (WindowDimensions.Y != 0) + if (WindowDimensions.height != 0) { // We want the alt to have scroll bars if the main has scroll bars. // The bars are disabled, but they're still there. // This keeps the window, viewport, and SB size from changing when swapping. if (!siAttached.GetMainBuffer().IsMaximizedX()) { - WindowSize.cy += ServiceLocator::LocateGlobals().sHorizontalScrollSize; + WindowSize.height += ServiceLocator::LocateGlobals().sHorizontalScrollSize; } if (!siAttached.GetMainBuffer().IsMaximizedY()) { - WindowSize.cx += ServiceLocator::LocateGlobals().sVerticalScrollSize; + WindowSize.width += ServiceLocator::LocateGlobals().sVerticalScrollSize; } } @@ -665,18 +668,18 @@ void Window::_UpdateWindowSize(const til::size sizeNew) // To figure out if it's substantial, calculate what the window size would be if it were one character larger than what we just proposed til::size WindowSizeMax; - WindowSizeMax.cx = WindowSize.cx + ScreenFontSize.X; - WindowSizeMax.cy = WindowSize.cy + ScreenFontSize.Y; + WindowSizeMax.width = WindowSize.width + ScreenFontSize.width; + WindowSizeMax.height = WindowSize.height + ScreenFontSize.height; // And figure out the current window size as well. const auto rcWindowCurrent = GetWindowRect(); til::size WindowSizeCurrent; - WindowSizeCurrent.cx = rcWindowCurrent.width(); - WindowSizeCurrent.cy = rcWindowCurrent.height(); + WindowSizeCurrent.width = rcWindowCurrent.width(); + WindowSizeCurrent.height = rcWindowCurrent.height(); // If the current window has a few extra sub-character pixels between the proposed size (WindowSize) and the next size up (WindowSizeMax), then don't change anything. - const auto fDeltaXSubstantial = !(WindowSizeCurrent.cx >= WindowSize.cx && WindowSizeCurrent.cx < WindowSizeMax.cx); - const auto fDeltaYSubstantial = !(WindowSizeCurrent.cy >= WindowSize.cy && WindowSizeCurrent.cy < WindowSizeMax.cy); + const auto fDeltaXSubstantial = !(WindowSizeCurrent.width >= WindowSize.width && WindowSizeCurrent.width < WindowSizeMax.width); + const auto fDeltaYSubstantial = !(WindowSizeCurrent.height >= WindowSize.height && WindowSizeCurrent.height < WindowSizeMax.height); // If either change was substantial, update the window accordingly to the newly proposed value. if (fDeltaXSubstantial || fDeltaYSubstantial) @@ -747,51 +750,51 @@ void Window::VerticalScroll(const WORD wScrollCommand, const WORD wAbsoluteChang { case SB_LINEUP: { - NewOrigin.Y--; + NewOrigin.y--; break; } case SB_LINEDOWN: { - NewOrigin.Y++; + NewOrigin.y++; break; } case SB_PAGEUP: { - NewOrigin.Y -= viewport.Height() - 1; + NewOrigin.y -= viewport.Height() - 1; break; } case SB_PAGEDOWN: { - NewOrigin.Y += viewport.Height() - 1; + NewOrigin.y += viewport.Height() - 1; break; } case SB_THUMBTRACK: { gci.Flags |= CONSOLE_SCROLLBAR_TRACKING; - NewOrigin.Y = wAbsoluteChange; + NewOrigin.y = wAbsoluteChange; break; } case SB_THUMBPOSITION: { UnblockWriteConsole(CONSOLE_SCROLLBAR_TRACKING); - NewOrigin.Y = wAbsoluteChange; + NewOrigin.y = wAbsoluteChange; break; } case SB_TOP: { - NewOrigin.Y = 0; + NewOrigin.y = 0; break; } case SB_BOTTOM: { - NewOrigin.Y = sScreenBufferSizeY - viewport.Height(); + NewOrigin.y = sScreenBufferSizeY - viewport.Height(); break; } @@ -801,7 +804,7 @@ void Window::VerticalScroll(const WORD wScrollCommand, const WORD wAbsoluteChang } } - NewOrigin.Y = std::clamp(NewOrigin.Y, 0, sScreenBufferSizeY - viewport.Height()); + NewOrigin.y = std::clamp(NewOrigin.y, 0, sScreenBufferSizeY - viewport.Height()); LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); } @@ -826,44 +829,44 @@ void Window::HorizontalScroll(const WORD wScrollCommand, const WORD wAbsoluteCha { case SB_LINEUP: { - NewOrigin.X--; + NewOrigin.x--; break; } case SB_LINEDOWN: { - NewOrigin.X++; + NewOrigin.x++; break; } case SB_PAGEUP: { - NewOrigin.X -= viewport.Width() - 1; + NewOrigin.x -= viewport.Width() - 1; break; } case SB_PAGEDOWN: { - NewOrigin.X += viewport.Width() - 1; + NewOrigin.x += viewport.Width() - 1; break; } case SB_THUMBTRACK: case SB_THUMBPOSITION: { - NewOrigin.X = wAbsoluteChange; + NewOrigin.x = wAbsoluteChange; break; } case SB_TOP: { - NewOrigin.X = 0; + NewOrigin.x = 0; break; } case SB_BOTTOM: { - NewOrigin.X = (WORD)(sScreenBufferSizeX - viewport.Width()); + NewOrigin.x = (WORD)(sScreenBufferSizeX - viewport.Width()); break; } @@ -872,7 +875,7 @@ void Window::HorizontalScroll(const WORD wScrollCommand, const WORD wAbsoluteCha return; } } - NewOrigin.X = std::clamp(NewOrigin.X, 0, sScreenBufferSizeX - viewport.Width()); + NewOrigin.x = std::clamp(NewOrigin.x, 0, sScreenBufferSizeX - viewport.Width()); LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); } @@ -958,11 +961,11 @@ void Window::s_CalculateWindowRect(const til::size coordWindowInChars, til::size sizeWindow; // Initially use the given size in characters * font size to get client area pixel size - sizeWindow.cx = coordWindowInChars.X * coordFontSize.X; - sizeWindow.cy = coordWindowInChars.Y * coordFontSize.Y; + sizeWindow.width = coordWindowInChars.width * coordFontSize.width; + sizeWindow.height = coordWindowInChars.height * coordFontSize.height; // Create a proposed rectangle - til::rect rectProposed = { prectWindow->left, prectWindow->top, prectWindow->left + sizeWindow.cx, prectWindow->top + sizeWindow.cy }; + til::rect rectProposed = { prectWindow->left, prectWindow->top, prectWindow->left + sizeWindow.width, prectWindow->top + sizeWindow.height }; // Now adjust the client area into a window size // 1. Start with default window style @@ -984,13 +987,13 @@ void Window::s_CalculateWindowRect(const til::size coordWindowInChars, // Finally compensate for scroll bars // If the window is smaller than the buffer in width, add space at the bottom for a horizontal scroll bar - if (coordWindowInChars.X < coordBufferSize.X) + if (coordWindowInChars.width < coordBufferSize.width) { rectProposed.bottom += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CYHSCROLL, iDpi); } // If the window is smaller than the buffer in height, add space at the right for a vertical scroll bar - if (coordWindowInChars.Y < coordBufferSize.Y) + if (coordWindowInChars.height < coordBufferSize.height) { rectProposed.right += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CXVSCROLL, iDpi); } @@ -1274,23 +1277,23 @@ void Window::s_ReinitializeFontsForDPIChange() HKEY hCurrentUserKey, hConsoleKey, hTitleKey; // Open the current user registry key. auto Status = RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(pwszTitle, &hCurrentUserKey, &hConsoleKey, &hTitleKey); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Save window size auto windowRect = pWindow->GetWindowRect(); const auto windowDimensions = gci.GetActiveOutputBuffer().GetViewport().Dimensions(); - DWORD dwValue = MAKELONG(windowDimensions.X, windowDimensions.Y); + DWORD dwValue = MAKELONG(windowDimensions.width, windowDimensions.height); Status = RegistrySerialization::s_UpdateValue(hConsoleKey, hTitleKey, CONSOLE_REGISTRY_WINDOWSIZE, REG_DWORD, reinterpret_cast(&dwValue), static_cast(sizeof(dwValue))); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { const auto coordScreenBufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); - auto screenBufferWidth = coordScreenBufferSize.X; - auto screenBufferHeight = coordScreenBufferSize.Y; + auto screenBufferWidth = coordScreenBufferSize.width; + auto screenBufferHeight = coordScreenBufferSize.height; dwValue = MAKELONG(screenBufferWidth, screenBufferHeight); Status = RegistrySerialization::s_UpdateValue(hConsoleKey, hTitleKey, @@ -1298,7 +1301,7 @@ void Window::s_ReinitializeFontsForDPIChange() REG_DWORD, reinterpret_cast(&dwValue), static_cast(sizeof(dwValue))); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Save window position if (fAutoPos) @@ -1336,7 +1339,7 @@ void Window::s_ReinitializeFontsForDPIChange() // Open the current user registry key. auto Status = RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(pwszTitle, &hCurrentUserKey, &hConsoleKey, &hTitleKey); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Save window opacity DWORD dwValue; diff --git a/src/interactivity/win32/window.hpp b/src/interactivity/win32/window.hpp index c7a53aa74c..45db4e7fbe 100644 --- a/src/interactivity/win32/window.hpp +++ b/src/interactivity/win32/window.hpp @@ -16,9 +16,14 @@ Author(s): #include "../inc/IConsoleWindow.hpp" -namespace Microsoft::Console::Render +namespace Microsoft::Console::Render::Atlas { class AtlasEngine; +} + +namespace Microsoft::Console::Render +{ + using AtlasEngine = Atlas::AtlasEngine; class DxEngine; class GdiEngine; } diff --git a/src/interactivity/win32/windowUiaProvider.cpp b/src/interactivity/win32/windowUiaProvider.cpp index 744f566d02..664101fc56 100644 --- a/src/interactivity/win32/windowUiaProvider.cpp +++ b/src/interactivity/win32/windowUiaProvider.cpp @@ -6,7 +6,7 @@ #include "screenInfoUiaProvider.hpp" #include "windowUiaProvider.hpp" -#include "../types/IUiaData.h" +#include "../renderer/inc/IRenderData.hpp" #include "../host/renderData.hpp" #include "../inc/ServiceLocator.hpp" @@ -20,9 +20,9 @@ try auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); - IUiaData* uiaData = &gci.renderData; + Render::IRenderData* renderData = &gci.renderData; - RETURN_IF_FAILED(WRL::MakeAndInitialize(&_pScreenInfoProvider, uiaData, this)); + RETURN_IF_FAILED(WRL::MakeAndInitialize(&_pScreenInfoProvider, renderData, this)); // TODO GitHub #1914: Re-attach Tracing to UIA Tree //Tracing::s_TraceUia(pWindowProvider, ApiCall::Create, nullptr); diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index a724878020..5866400a73 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -30,10 +30,6 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Bit 29 is whether ALT was held when the message was posted. #define WM_SYSKEYDOWN_ALT_PRESSED (0x20000000) -// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx -// "If the high-order bit is 1, the key is down; otherwise, it is up." -static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; - // ---------------------------- // Helpers // ---------------------------- @@ -78,7 +74,13 @@ VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pP else { // Find a process to own the console window. If there are none then let's use conhost's. - pProcessData = gci.ProcessHandleList.GetFirstProcess(); + pProcessData = gci.ProcessHandleList.GetRootProcess(); + if (!pProcessData) + { + // No root process ID? Pick the oldest existing process. + pProcessData = gci.ProcessHandleList.GetOldestProcess(); + } + if (pProcessData != nullptr) { dwProcessId = pProcessData->dwProcessId; @@ -92,16 +94,8 @@ VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pP } } - CONSOLEWINDOWOWNER ConsoleOwner; - ConsoleOwner.hwnd = hwnd; - ConsoleOwner.ProcessId = dwProcessId; - ConsoleOwner.ThreadId = dwThreadId; - // Comment out this line to enable UIA tree to be visible until UIAutomationCore.dll can support our scenario. - LOG_IF_FAILED(ServiceLocator::LocateConsoleControl() - ->Control(ConsoleControl::ControlType::ConsoleSetWindowOwner, - &ConsoleOwner, - sizeof(ConsoleOwner))); + LOG_IF_NTSTATUS_FAILED(ServiceLocator::LocateConsoleControl()->SetWindowOwner(hwnd, dwProcessId, dwThreadId)); } // ---------------------------- @@ -121,30 +115,8 @@ bool HandleTerminalMouseEvent(const til::point cMousePosition, const short sModifierKeystate, const short sWheelDelta) { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // If the modes don't align, this is unhandled by default. - auto fWasHandled = false; - - // Virtual terminal input mode - if (IsInVirtualTerminalInputMode()) - { - const TerminalInput::MouseButtonState state{ - WI_IsFlagSet(OneCoreSafeGetKeyState(VK_LBUTTON), KeyPressed), - WI_IsFlagSet(OneCoreSafeGetKeyState(VK_MBUTTON), KeyPressed), - WI_IsFlagSet(OneCoreSafeGetKeyState(VK_RBUTTON), KeyPressed) - }; - - // GH#6401: VT applications should be able to receive mouse events from outside the - // terminal buffer. This is likely to happen when the user drags the cursor offscreen. - // We shouldn't throw away perfectly good events when they're offscreen, so we just - // clamp them to be within the range [(0, 0), (W, H)]. - auto clampedPosition{ cMousePosition }; - const auto clampViewport{ gci.GetActiveOutputBuffer().GetViewport().ToOrigin() }; - clampViewport.Clamp(clampedPosition); - fWasHandled = gci.GetActiveInputBuffer()->GetTerminalInput().HandleMouse(clampedPosition, uiButton, sModifierKeystate, sWheelDelta, state); - } - - return fWasHandled; + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.pInputBuffer->WriteMouseEvent(cMousePosition, uiButton, sModifierKeystate, sWheelDelta); } void HandleKeyEvent(const HWND hWnd, @@ -155,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) { @@ -174,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 @@ -195,23 +168,37 @@ void HandleKeyEvent(const HWND hWnd, // --- END LOAD BEARING CODE --- } - KeyEvent keyEvent{ !!bKeyDown, RepeatCount, VirtualKeyCode, VirtualScanCode, UNICODE_NULL, 0 }; + // 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; + } + } - if (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR) + 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(0); + keyEvent.Event.KeyEvent.uChar.UnicodeChar = static_cast(wParam); } } else @@ -221,8 +208,7 @@ void HandleKeyEvent(const HWND hWnd, { return; } - keyEvent.SetActiveModifierKeys(ControlKeyState); - keyEvent.SetCharData(0); + keyEvent.Event.KeyEvent.dwControlKeyState = ControlKeyState; } const INPUT_KEY_INFO inputKeyInfo(VirtualKeyCode, ControlKeyState); @@ -411,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)) { @@ -437,7 +423,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]; @@ -630,8 +616,8 @@ BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, // translate mouse position into characters, if necessary. auto ScreenFontSize = ScreenInfo.GetScreenFontSize(); - MousePosition.X /= ScreenFontSize.X; - MousePosition.Y /= ScreenFontSize.Y; + MousePosition.x /= ScreenFontSize.width; + MousePosition.y /= ScreenFontSize.height; const auto fShiftPressed = WI_IsFlagSet(OneCoreSafeGetKeyState(VK_SHIFT), KEY_PRESSED); @@ -680,27 +666,27 @@ BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, } } - MousePosition.X += ScreenInfo.GetViewport().Left(); - MousePosition.Y += ScreenInfo.GetViewport().Top(); + MousePosition.x += ScreenInfo.GetViewport().Left(); + MousePosition.y += ScreenInfo.GetViewport().Top(); const auto coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); // make sure mouse position is clipped to screen buffer - if (MousePosition.X < 0) + if (MousePosition.x < 0) { - MousePosition.X = 0; + MousePosition.x = 0; } - else if (MousePosition.X >= coordScreenBufferSize.X) + else if (MousePosition.x >= coordScreenBufferSize.width) { - MousePosition.X = coordScreenBufferSize.X - 1; + MousePosition.x = coordScreenBufferSize.width - 1; } - if (MousePosition.Y < 0) + if (MousePosition.y < 0) { - MousePosition.Y = 0; + MousePosition.y = 0; } - else if (MousePosition.Y >= coordScreenBufferSize.Y) + else if (MousePosition.y >= coordScreenBufferSize.height) { - MousePosition.Y = coordScreenBufferSize.Y - 1; + MousePosition.y = coordScreenBufferSize.height - 1; } // Process the transparency mousewheel message before the others so that we can @@ -931,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; } @@ -988,13 +960,13 @@ LRESULT CALLBACK DialogHookProc(int nCode, WPARAM /*wParam*/, LPARAM lParam) NTSTATUS InitWindowsSubsystem(_Out_ HHOOK* phhook) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto ProcessData = gci.ProcessHandleList.FindProcessInList(ConsoleProcessList::ROOT_PROCESS_ID); + auto ProcessData = gci.ProcessHandleList.GetRootProcess(); FAIL_FAST_IF(!(ProcessData != nullptr && ProcessData->fRootProcess)); // Create and activate the main window auto Status = Window::CreateInstance(&gci, gci.ScreenBuffers); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { RIPMSG2(RIP_WARNING, "CreateWindowsWindow failed with status 0x%x, gle = 0x%x", Status, GetLastError()); return Status; @@ -1048,10 +1020,14 @@ DWORD WINAPI ConsoleInputThreadProcWin32(LPVOID /*lpParameter*/) // successfully created with the owner configured when the window is // first created. See GH#13066 for details. ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo()->CreatePseudoWindow(); + + // Register the pseudoconsole window as being owned by the root process. + const auto pseudoWindow = ServiceLocator::LocatePseudoWindow(); + SetConsoleWindowOwner(pseudoWindow, nullptr); } UnlockConsole(); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { ServiceLocator::LocateGlobals().ntstatusConsoleInputInitStatus = Status; ServiceLocator::LocateGlobals().hConsoleInputInitEvent.SetEvent(); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 8b173e1eac..376e93fe03 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -362,8 +362,8 @@ using namespace Microsoft::Console::Types; rcSuggested.right = rcSuggested.left + lpwpos->cx; rcSuggested.bottom = rcSuggested.top + lpwpos->cy; til::size szSuggested; - szSuggested.cx = rcSuggested.width(); - szSuggested.cy = rcSuggested.height(); + szSuggested.width = rcSuggested.width(); + szSuggested.height = rcSuggested.height(); // Figure out the current dimensions for comparison. auto rcCurrent = GetWindowRect(); @@ -429,10 +429,10 @@ using namespace Microsoft::Console::Types; // or worse yet, keep it from moving entirely. We'll get a WM_DPICHANGED, // resize the window, and then process the restriction in a few window messages. if (((int)dpiOfMaximum == g.dpi) && - ((szSuggested.cx > rcMaximum.width()) || (szSuggested.cy > rcMaximum.height()))) + ((szSuggested.width > rcMaximum.width()) || (szSuggested.height > rcMaximum.height()))) { - lpwpos->cx = std::min(rcMaximum.width(), szSuggested.cx); - lpwpos->cy = std::min(rcMaximum.height(), szSuggested.cy); + lpwpos->cx = std::min(rcMaximum.width(), szSuggested.width); + lpwpos->cy = std::min(rcMaximum.height(), szSuggested.height); // We usually add SWP_NOMOVE so that if the user is dragging the left or top edge // and hits the restriction, then the window just stops growing, it doesn't @@ -735,7 +735,7 @@ using namespace Microsoft::Console::Types; } break; } -#endif DBG +#endif // DBG case EVENT_CONSOLE_CARET: case EVENT_CONSOLE_UPDATE_REGION: diff --git a/src/propsheet/ColorsPage.cpp b/src/propsheet/ColorsPage.cpp index 775fb3f95f..cfc80bb2a6 100644 --- a/src/propsheet/ColorsPage.cpp +++ b/src/propsheet/ColorsPage.cpp @@ -6,7 +6,7 @@ #include "ColorControl.h" // The property sheet will never load on OneCore, so we do -// not need to redirect users to the OneCore safe versionf +// not need to redirect users to the OneCore safe version // of these functions. #undef VkKeyScanW #undef MapVirtualKeyW diff --git a/src/propsheet/console.cpp b/src/propsheet/console.cpp index 72bcbd53ed..68b146c2e1 100644 --- a/src/propsheet/console.cpp +++ b/src/propsheet/console.cpp @@ -104,10 +104,10 @@ void SaveConsoleSettingsIfNeeded(const HWND hwnd) if (gpStateInfo->LinkTitle != nullptr) { SetGlobalRegistryValues(); - if (!NT_SUCCESS(ShortcutSerialization::s_SetLinkValues(gpStateInfo, - g_fEastAsianSystem, - g_fForceV2, - gpStateInfo->fIsV2Console))) + if (FAILED_NTSTATUS(ShortcutSerialization::s_SetLinkValues(gpStateInfo, + g_fEastAsianSystem, + g_fForceV2, + gpStateInfo->fIsV2Console))) { WCHAR szMessage[MAX_PATH + 100]; WCHAR awchBuffer[MAX_PATH] = { 0 }; diff --git a/src/propsheet/console.def b/src/propsheet/console.def index 652778cb3a..6ca456f7f1 100644 --- a/src/propsheet/console.def +++ b/src/propsheet/console.def @@ -2,5 +2,5 @@ LIBRARY Console EXPORTS CPlApplet - DllGetClassObject private - DllCanUnloadNow private \ No newline at end of file + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE diff --git a/src/propsheet/fontdlg.cpp b/src/propsheet/fontdlg.cpp index fe7a9fccf6..96afbcacb0 100644 --- a/src/propsheet/fontdlg.cpp +++ b/src/propsheet/fontdlg.cpp @@ -725,7 +725,7 @@ int FontListCreate( /* * This only enumerates face names and font sizes if necessary. */ - if (!NT_SUCCESS(EnumerateFonts(bLB ? EF_OEMFONT : EF_TTFONT))) + if (FAILED_NTSTATUS(EnumerateFonts(bLB ? EF_OEMFONT : EF_TTFONT))) { return LB_ERR; } @@ -1196,8 +1196,8 @@ int FindCreateFont( { // retrieve default font face name for this codepage, and then set it as our current face WCHAR szDefaultCodepageTTFont[LF_FACESIZE] = { 0 }; - if (NT_SUCCESS(GetTTFontFaceForCodePage(CodePage, szDefaultCodepageTTFont, ARRAYSIZE(szDefaultCodepageTTFont))) && - NT_SUCCESS(StringCchCopyW(DefaultTTFaceName, ARRAYSIZE(DefaultTTFaceName), szDefaultCodepageTTFont))) + if (SUCCEEDED_NTSTATUS(GetTTFontFaceForCodePage(CodePage, szDefaultCodepageTTFont, ARRAYSIZE(szDefaultCodepageTTFont))) && + SUCCEEDED_NTSTATUS(StringCchCopyW(DefaultTTFaceName, ARRAYSIZE(DefaultTTFaceName), szDefaultCodepageTTFont))) { pwszFace = DefaultTTFaceName; Size.X = 0; diff --git a/src/propsheet/misc.cpp b/src/propsheet/misc.cpp index 17d089e379..054eef9612 100644 --- a/src/propsheet/misc.cpp +++ b/src/propsheet/misc.cpp @@ -883,7 +883,7 @@ static bool IsSizePresentInList(const __in SHORT sSizeDesired, __in_ecount(nTTPo return fSizePresent; } -// Given a face name, determine if the size provided is custom (i.e. not on the hardcoded list of sizes). Note that the +// Given a face name, determine if the size provided is custom (i.e. not on the hard-coded list of sizes). Note that the // list of sizes we use varies depending on the codepage being used bool IsFontSizeCustom(__in PCWSTR pszFaceName, const __in SHORT sSize) { @@ -906,7 +906,7 @@ static bool IsCurrentFontSizeCustom() return IsFontSizeCustom(gpStateInfo->FaceName, gpStateInfo->FontSize.Y); } -// Given a size, iterate through all TT fonts and load them in the provided size (only used for custom (non-hardcoded) +// Given a size, iterate through all TT fonts and load them in the provided size (only used for custom (non-hard-coded) // font sizes) void CreateSizeForAllTTFonts(const __in SHORT sSize) { @@ -1040,7 +1040,7 @@ EnumerateFonts( pFN->dwFlag |= EF_ENUMERATED; } - // Now check to see if the currently selected font is using a custom size not in the hardcoded list (TTPoints or + // Now check to see if the currently selected font is using a custom size not in the hard-coded list (TTPoints or // TTPointsDbcs depending on locale). If so, make sure we populate all of our fonts at that size. if (IsCurrentFontSizeCustom()) { diff --git a/src/propsheet/registry.cpp b/src/propsheet/registry.cpp index 7c3b7364c2..64d392aa76 100644 --- a/src/propsheet/registry.cpp +++ b/src/propsheet/registry.cpp @@ -176,7 +176,7 @@ DWORD GetRegistryValues( // Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return 0; } @@ -194,7 +194,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { dwRet = dwValue; } @@ -214,7 +214,7 @@ DWORD GetRegistryValues( Status = RegistrySerialization::s_OpenKey(hConsoleKey, pStateInfo->OriginalTitle, &hTitleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { RegCloseKey(hConsoleKey); RegCloseKey(hCurrentUserKey); @@ -232,7 +232,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->ScreenAttributes = (WORD)dwValue; } @@ -247,7 +247,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->PopupAttributes = (WORD)dwValue; } @@ -268,7 +268,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->ColorTable[i] = dwValue; } @@ -283,7 +283,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->InsertMode = !!dwValue; } @@ -297,7 +297,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->QuickEdit = !!dwValue; } @@ -313,7 +313,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { if (IsValidCodePage(dwValue)) { @@ -330,7 +330,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->ScreenBufferSize.X = LOWORD(dwValue); pStateInfo->ScreenBufferSize.Y = HIWORD(dwValue); @@ -345,7 +345,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->WindowSize.X = LOWORD(dwValue); pStateInfo->WindowSize.Y = HIWORD(dwValue); @@ -360,7 +360,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->WindowPosX = (SHORT)LOWORD(dwValue); pStateInfo->WindowPosY = (SHORT)HIWORD(dwValue); @@ -377,7 +377,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->FontSize.X = LOWORD(dwValue); pStateInfo->FontSize.Y = HIWORD(dwValue); @@ -392,7 +392,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->FontFamily = dwValue; } @@ -406,7 +406,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->FontWeight = dwValue; } @@ -420,7 +420,7 @@ DWORD GetRegistryValues( REG_SZ, (PBYTE)awchBuffer, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { RtlCopyMemory(pStateInfo->FaceName, awchBuffer, sizeof(awchBuffer)); } @@ -434,7 +434,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->CursorSize = dwValue; } @@ -448,7 +448,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->HistoryBufferSize = dwValue; } @@ -462,7 +462,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->NumberOfHistoryBuffers = dwValue; } @@ -476,7 +476,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->HistoryNoDup = dwValue; } @@ -490,7 +490,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->fWrapText = dwValue; } @@ -504,7 +504,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->fFilterOnPaste = dwValue; } @@ -518,7 +518,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->fCtrlKeyShortcutsDisabled = dwValue; } @@ -532,7 +532,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->fLineSelection = dwValue; } @@ -546,7 +546,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { if (dwValue <= BYTE_MAX) { @@ -561,7 +561,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->CursorColor = dwValue; } @@ -572,7 +572,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->CursorType = dwValue; } @@ -584,7 +584,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->InterceptCopyPaste = !!dwValue; } @@ -596,7 +596,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->DefaultForeground = dwValue; } @@ -608,7 +608,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->DefaultBackground = dwValue; } @@ -620,7 +620,7 @@ DWORD GetRegistryValues( REG_DWORD, (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pStateInfo->TerminalScrolling = dwValue; } @@ -693,7 +693,7 @@ VOID SetRegistryValues( // Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return; } @@ -719,7 +719,7 @@ VOID SetRegistryValues( Status = RegistrySerialization::s_CreateKey(hConsoleKey, pStateInfo->OriginalTitle, &hTitleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { RegCloseKey(hConsoleKey); RegCloseKey(hCurrentUserKey); diff --git a/src/propsheet/util.cpp b/src/propsheet/util.cpp index d7a168aa38..6e57de30a6 100644 --- a/src/propsheet/util.cpp +++ b/src/propsheet/util.cpp @@ -67,7 +67,7 @@ BOOL InitializeConsoleState() OEMCP = GetOEMCP(); g_fIsComCtlV6Present = IsComCtlV6Present(); - return NT_SUCCESS(InitializeDbcsMisc()); + return SUCCEEDED_NTSTATUS(InitializeDbcsMisc()); } void UninitializeConsoleState() diff --git a/src/propslib/DelegationConfig.cpp b/src/propslib/DelegationConfig.cpp index 2f3074894b..29339baa2e 100644 --- a/src/propslib/DelegationConfig.cpp +++ b/src/propslib/DelegationConfig.cpp @@ -32,7 +32,7 @@ using namespace ABI::Windows::ApplicationModel::AppExtensions; #define DELEGATION_CONSOLE_EXTENSION_NAME L"com.microsoft.windows.console.host" #define DELEGATION_TERMINAL_EXTENSION_NAME L"com.microsoft.windows.terminal.host" -static [[nodiscard]] HRESULT _lookupCatalog(PCWSTR extensionName, std::vector& vec) noexcept +[[nodiscard]] static HRESULT _lookupCatalog(PCWSTR extensionName, std::vector& vec) noexcept { ComPtr catalogStatics; RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory(HStringReference(RuntimeClass_Windows_ApplicationModel_AppExtensions_AppExtensionCatalog).Get(), &catalogStatics)); @@ -165,7 +165,7 @@ try packages.push_back({ ConhostDelegationPair }); // Get consoles and terminals. - // If we fail to look up any, we should still have ONE come back to us as the hardcoded default console host. + // If we fail to look up any, we should still have ONE come back to us as the hard-coded default console host. // The errors aren't really useful except for debugging, so log only. std::vector consoles; LOG_IF_FAILED(_lookupCatalog(DELEGATION_CONSOLE_EXTENSION_NAME, consoles)); @@ -213,20 +213,20 @@ try } CATCH_RETURN() -[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultConsoleById(const IID& iid, const bool useRegExe) noexcept +[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultConsoleById(const IID& iid) noexcept { - return s_Set(DELEGATION_CONSOLE_KEY_NAME, iid, useRegExe); + return s_Set(DELEGATION_CONSOLE_KEY_NAME, iid); } -[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultTerminalById(const IID& iid, const bool useRegExe) noexcept +[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultTerminalById(const IID& iid) noexcept { - return s_Set(DELEGATION_TERMINAL_KEY_NAME, iid, useRegExe); + return s_Set(DELEGATION_TERMINAL_KEY_NAME, iid); } -[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultByPackage(const DelegationPackage& package, const bool useRegExe) noexcept +[[nodiscard]] HRESULT DelegationConfig::s_SetDefaultByPackage(const DelegationPackage& package) noexcept { - RETURN_IF_FAILED(s_SetDefaultConsoleById(package.pair.console, useRegExe)); - RETURN_IF_FAILED(s_SetDefaultTerminalById(package.pair.terminal, useRegExe)); + RETURN_IF_FAILED(s_SetDefaultConsoleById(package.pair.console)); + RETURN_IF_FAILED(s_SetDefaultTerminalById(package.pair.terminal)); return S_OK; } @@ -285,64 +285,23 @@ CATCH_RETURN() return { DelegationPairKind::Custom, values[0], values[1] }; } -[[nodiscard]] HRESULT DelegationConfig::s_Set(PCWSTR value, const CLSID clsid, const bool useRegExe) noexcept +[[nodiscard]] HRESULT DelegationConfig::s_Set(PCWSTR value, const CLSID clsid) noexcept try { - // BODGY - // A Centennial application is not allowed to write the system registry and is redirected - // to a per-package copy-on-write hive. - // The restricted capability "unvirtualizedResources" can be combined with - // desktop6:RegistryWriteVirtualization to opt-out... but... - // - It will no longer be possible to double-click install through the App Installer - // - It requires a special exception to submit to the store - // - There MAY be some cleanup logic where the app catalog may try to undo - // whatever the package did. - // This works around it by shelling out to reg.exe because somehow that's just peachy. - if (useRegExe) - { - wil::unique_cotaskmem_string str; - RETURN_IF_FAILED(StringFromCLSID(clsid, &str)); + wil::unique_hkey currentUserKey; + wil::unique_hkey consoleKey; - auto regExePath = wil::ExpandEnvironmentStringsW(L"%WINDIR%\\System32\\reg.exe"); + RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_OpenConsoleKey(¤tUserKey, &consoleKey)); - auto command = wil::str_printf(L"%s ADD HKCU\\Console\\%%%%Startup /v %s /t REG_SZ /d %s /f", regExePath.c_str(), value, str.get()); + // Create method for registry is a "create if not exists, otherwise open" function. + wil::unique_hkey startupKey; + RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_CreateKey(consoleKey.get(), L"%%Startup", &startupKey)); - wil::unique_process_information pi; - STARTUPINFOEX siEx{ 0 }; - siEx.StartupInfo.cb = sizeof(siEx); + wil::unique_cotaskmem_string str; + RETURN_IF_FAILED(StringFromCLSID(clsid, &str)); - RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( - nullptr, - command.data(), - nullptr, // lpProcessAttributes - nullptr, // lpThreadAttributes - false, // bInheritHandles - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, // dwCreationFlags - nullptr, // lpEnvironment - nullptr, - &siEx.StartupInfo, // lpStartupInfo - &pi // lpProcessInformation - )); + RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_SetValue(startupKey.get(), value, REG_SZ, reinterpret_cast(str.get()), gsl::narrow(wcslen(str.get()) * sizeof(wchar_t)))); - return S_OK; - } - else - { - wil::unique_hkey currentUserKey; - wil::unique_hkey consoleKey; - - RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_OpenConsoleKey(¤tUserKey, &consoleKey)); - - // Create method for registry is a "create if not exists, otherwise open" function. - wil::unique_hkey startupKey; - RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_CreateKey(consoleKey.get(), L"%%Startup", &startupKey)); - - wil::unique_cotaskmem_string str; - RETURN_IF_FAILED(StringFromCLSID(clsid, &str)); - - RETURN_IF_NTSTATUS_FAILED(RegistrySerialization::s_SetValue(startupKey.get(), value, REG_SZ, reinterpret_cast(str.get()), gsl::narrow(wcslen(str.get()) * sizeof(wchar_t)))); - - return S_OK; - } + return S_OK; } CATCH_RETURN() diff --git a/src/propslib/DelegationConfig.hpp b/src/propslib/DelegationConfig.hpp index 70e3d6cb50..a6a5917bfe 100644 --- a/src/propslib/DelegationConfig.hpp +++ b/src/propslib/DelegationConfig.hpp @@ -99,7 +99,7 @@ public: [[nodiscard]] static HRESULT s_GetAvailablePackages(std::vector& packages, DelegationPackage& def) noexcept; - [[nodiscard]] static HRESULT s_SetDefaultByPackage(const DelegationPackage& pkg, const bool useRegExe = false) noexcept; + [[nodiscard]] static HRESULT s_SetDefaultByPackage(const DelegationPackage& pkg) noexcept; static constexpr CLSID CLSID_Default{}; static constexpr CLSID CLSID_Conhost{ 0xb23d10c0, 0xe52e, 0x411e, { 0x9d, 0x5b, 0xc0, 0x9f, 0xdf, 0x70, 0x9c, 0x7d } }; @@ -114,8 +114,8 @@ public: [[nodiscard]] static DelegationPair s_GetDelegationPair() noexcept; private: - [[nodiscard]] static HRESULT s_SetDefaultConsoleById(const IID& iid, const bool useRegExe) noexcept; - [[nodiscard]] static HRESULT s_SetDefaultTerminalById(const IID& iid, const bool useRegExe) noexcept; + [[nodiscard]] static HRESULT s_SetDefaultConsoleById(const IID& iid) noexcept; + [[nodiscard]] static HRESULT s_SetDefaultTerminalById(const IID& iid) noexcept; - [[nodiscard]] static HRESULT s_Set(PCWSTR value, const CLSID clsid, const bool useRegExe) noexcept; + [[nodiscard]] static HRESULT s_Set(PCWSTR value, const CLSID clsid) noexcept; }; diff --git a/src/propslib/RegistrySerialization.cpp b/src/propslib/RegistrySerialization.cpp index 0427fd3244..2b9de45223 100644 --- a/src/propslib/RegistrySerialization.cpp +++ b/src/propslib/RegistrySerialization.cpp @@ -83,7 +83,7 @@ const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_GlobalProp }; const size_t RegistrySerialization::s_GlobalPropMappingsSize = ARRAYSIZE(s_GlobalPropMappings); -// clang-format off +// clang-format on // Routine Description: // - Reads number from the registry and applies it to the given property if the value exists @@ -93,8 +93,7 @@ const size_t RegistrySerialization::s_GlobalPropMappingsSize = ARRAYSIZE(s_Globa // - pPropMap - Contains property information to use in looking up/storing value data // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_LoadRegDword(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) +[[nodiscard]] NTSTATUS RegistrySerialization::s_LoadRegDword(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) { // find offset into destination structure for this numerical value const auto pbField = (PBYTE)pSettings + pPropMap->dwFieldOffset; @@ -106,9 +105,9 @@ NTSTATUS RegistrySerialization::s_LoadRegDword(const HKEY hKey, const _RegProper pPropMap->pwszValueName, sizeof(dwValue), ToWin32RegistryType(pPropMap->propertyType), - (PBYTE)& dwValue, + (PBYTE)&dwValue, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { switch (pPropMap->propertyType) { @@ -154,8 +153,7 @@ NTSTATUS RegistrySerialization::s_LoadRegDword(const HKEY hKey, const _RegProper // - pPropMap - Contains property information to use in looking up/storing value data // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) +[[nodiscard]] NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) { // find offset into destination structure for this numerical value const auto pbField = (PBYTE)pSettings + pPropMap->dwFieldOffset; @@ -163,9 +161,9 @@ NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPrope // number of characters within the field const auto cchField = pPropMap->cbFieldSize / sizeof(WCHAR); - auto pwchString = new(std::nothrow) WCHAR[cchField]; + auto pwchString = new (std::nothrow) WCHAR[cchField]; auto Status = NT_TESTNULL(pwchString); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = s_QueryValue(hKey, pPropMap->pwszValueName, @@ -173,7 +171,7 @@ NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPrope ToWin32RegistryType(pPropMap->propertyType), (PBYTE)pwchString, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // ensure pwchString is null terminated pwchString[cchField - 1] = UNICODE_NULL; @@ -196,8 +194,7 @@ NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPrope // - phConsoleKey - Returns a handle to the Console subkey // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _Out_ HKEY* phConsoleKey) +[[nodiscard]] NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _Out_ HKEY* phConsoleKey) { // Always set an output value. It will be made valid before the end if everything succeeds. *phCurrentUserKey = static_cast(INVALID_HANDLE_VALUE); @@ -207,9 +204,10 @@ NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _ wil::unique_hkey consoleKey; // Open the current user registry key. - NTSTATUS Status = NTSTATUS_FROM_WIN32(RegOpenCurrentUser(KEY_READ | KEY_WRITE, ¤tUserKey)); + const auto win32Result = RegOpenCurrentUser(KEY_READ | KEY_WRITE, ¤tUserKey); + NTSTATUS Status = NTSTATUS_FROM_WIN32(win32Result); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Open the console registry key. Status = s_OpenKey(currentUserKey.get(), CONSOLE_REGISTRY_STRING, &consoleKey); @@ -221,7 +219,7 @@ NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _ } // If we're successful, give the keys back. - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { *phCurrentUserKey = currentUserKey.release(); *phConsoleKey = consoleKey.release(); @@ -240,10 +238,10 @@ NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _ // - phResult - Filled with handle to the sub key if available. Check return status. // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) +[[nodiscard]] NTSTATUS RegistrySerialization::s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) { - return NTSTATUS_FROM_WIN32(RegOpenKeyW(hKey, pwszSubKey, phResult)); + const auto result = RegOpenKeyW(hKey, pwszSubKey, phResult); + return NTSTATUS_FROM_WIN32(result); } // Routine Description: @@ -253,11 +251,10 @@ NTSTATUS RegistrySerialization::s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR // - pwszValueName - String name of value to delete under that key // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_DeleteValue(const HKEY hKey, _In_ PCWSTR const pwszValueName) +[[nodiscard]] NTSTATUS RegistrySerialization::s_DeleteValue(const HKEY hKey, _In_ PCWSTR const pwszValueName) { const auto result = RegDeleteKeyValueW(hKey, nullptr, pwszValueName); - return result == ERROR_FILE_NOT_FOUND ? S_OK : NTSTATUS_FROM_WIN32(result); + return result == ERROR_FILE_NOT_FOUND ? STATUS_SUCCESS : NTSTATUS_FROM_WIN32(result); } // Routine Description: @@ -270,10 +267,10 @@ NTSTATUS RegistrySerialization::s_DeleteValue(const HKEY hKey, _In_ PCWSTR const // - phResult - Filled with handle to the sub key if created/opened successfully. Check return status. // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_CreateKey(const HKEY hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) +[[nodiscard]] NTSTATUS RegistrySerialization::s_CreateKey(const HKEY hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) { - return NTSTATUS_FROM_WIN32(RegCreateKeyW(hKey, pwszSubKey, phResult)); + const auto result = RegCreateKeyW(hKey, pwszSubKey, phResult); + return NTSTATUS_FROM_WIN32(result); } // Routine Description: @@ -286,19 +283,19 @@ NTSTATUS RegistrySerialization::s_CreateKey(const HKEY hKey, _In_ PCWSTR const p // - cbDataLength - The length in bytes of the data provided // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_SetValue(const HKEY hKey, - _In_ PCWSTR const pValueName, - const DWORD dwType, - _In_reads_bytes_(cbDataLength) BYTE* const pbData, - const DWORD cbDataLength) +[[nodiscard]] NTSTATUS RegistrySerialization::s_SetValue(const HKEY hKey, + _In_ PCWSTR const pValueName, + const DWORD dwType, + _In_reads_bytes_(cbDataLength) BYTE* const pbData, + const DWORD cbDataLength) { - return NTSTATUS_FROM_WIN32(RegSetKeyValueW(hKey, - nullptr, - pValueName, - dwType, - pbData, - cbDataLength)); + const auto result = RegSetKeyValueW(hKey, + nullptr, + pValueName, + dwType, + pbData, + cbDataLength); + return NTSTATUS_FROM_WIN32(result); } // Routine Description: @@ -312,13 +309,12 @@ NTSTATUS RegistrySerialization::s_SetValue(const HKEY hKey, // - pcbDataLength - Number of bytes filled in the given data buffer // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_QueryValue(const HKEY hKey, - _In_ PCWSTR const pwszValueName, - const DWORD cbValueLength, - const DWORD regType, - _Out_writes_bytes_(cbValueLength) BYTE* const pbData, - _Out_opt_ _Out_range_(0, cbValueLength) DWORD* const pcbDataLength) +[[nodiscard]] NTSTATUS RegistrySerialization::s_QueryValue(const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD cbValueLength, + const DWORD regType, + _Out_writes_bytes_(cbValueLength) BYTE* const pbData, + _Out_opt_ _Out_range_(0, cbValueLength) DWORD* const pcbDataLength) { auto cbData = cbValueLength; @@ -354,26 +350,26 @@ NTSTATUS RegistrySerialization::s_QueryValue(const HKEY hKey, // - pbData - Value data buffer to be filled based on data type. Will be null terminated for string types. (REG_SZ, REG_MULTI_SZ, REG_EXPAND_SZ) // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_EnumValue(const HKEY hKey, - const DWORD dwIndex, - const DWORD cbValueLength, - _Out_writes_bytes_(cbValueLength) PWSTR const pwszValueName, - const DWORD cbDataLength, - _Out_writes_bytes_(cbDataLength) BYTE* const pbData) +[[nodiscard]] NTSTATUS RegistrySerialization::s_EnumValue(const HKEY hKey, + const DWORD dwIndex, + const DWORD cbValueLength, + _Out_writes_bytes_(cbValueLength) PWSTR const pwszValueName, + const DWORD cbDataLength, + _Out_writes_bytes_(cbDataLength) BYTE* const pbData) { DWORD cchValueName = cbValueLength / sizeof(WCHAR); auto cbData = cbDataLength; -#pragma prefast(suppress: 26015, "prefast doesn't realize that cbData == cbDataLength and cchValueName == cbValueLength/2") - return NTSTATUS_FROM_WIN32(RegEnumValueW(hKey, - dwIndex, - pwszValueName, - &cchValueName, - nullptr, - nullptr, - pbData, - &cbData)); +#pragma prefast(suppress : 26015, "prefast doesn't realize that cbData == cbDataLength and cchValueName == cbValueLength/2") + const auto result = RegEnumValueW(hKey, + dwIndex, + pwszValueName, + &cchValueName, + nullptr, + nullptr, + pbData, + &cbData); + return NTSTATUS_FROM_WIN32(result); } // Routine Description: @@ -389,16 +385,15 @@ NTSTATUS RegistrySerialization::s_EnumValue(const HKEY hKey, // - cbDataLength - Length of the provided value data buffer. // Return Value: // - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. -[[nodiscard]] -NTSTATUS RegistrySerialization::s_UpdateValue(const HKEY hConsoleKey, - const HKEY hKey, - _In_ PCWSTR const pwszValueName, - const DWORD dwType, - _In_reads_bytes_(cbDataLength) BYTE* pbData, - const DWORD cbDataLength) +[[nodiscard]] NTSTATUS RegistrySerialization::s_UpdateValue(const HKEY hConsoleKey, + const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD dwType, + _In_reads_bytes_(cbDataLength) BYTE* pbData, + const DWORD cbDataLength) { auto Status = STATUS_UNSUCCESSFUL; // This value won't be used, added to avoid compiler warnings. - auto Data = new(std::nothrow) BYTE[cbDataLength]; + auto Data = new (std::nothrow) BYTE[cbDataLength]; if (Data != nullptr) { // If this is not the main console key but the value is the same, @@ -407,7 +402,7 @@ NTSTATUS RegistrySerialization::s_UpdateValue(const HKEY hConsoleKey, if (hConsoleKey != hKey) { Status = s_QueryValue(hConsoleKey, pwszValueName, cbDataLength, dwType, Data, nullptr); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { fDeleteKey = (memcmp(pbData, Data, cbDataLength) == 0); } @@ -427,24 +422,24 @@ NTSTATUS RegistrySerialization::s_UpdateValue(const HKEY hConsoleKey, return Status; } -[[nodiscard]] -NTSTATUS RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(_In_ PCWSTR const title, - _Out_ HKEY* phCurrentUserKey, - _Out_ HKEY* phConsoleKey, - _Out_ HKEY* phTitleKey) +[[nodiscard]] NTSTATUS RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(_In_ PCWSTR const title, + _Out_ HKEY* phCurrentUserKey, + _Out_ HKEY* phConsoleKey, + _Out_ HKEY* phTitleKey) { - NTSTATUS Status = NTSTATUS_FROM_WIN32(RegOpenKeyW(HKEY_CURRENT_USER, - nullptr, - phCurrentUserKey)); - if (NT_SUCCESS(Status)) + const auto win32Result = RegOpenKeyW(HKEY_CURRENT_USER, + nullptr, + phCurrentUserKey); + NTSTATUS Status = NTSTATUS_FROM_WIN32(win32Result); + if (SUCCEEDED_NTSTATUS(Status)) { Status = RegistrySerialization::s_CreateKey(*phCurrentUserKey, CONSOLE_REGISTRY_STRING, phConsoleKey); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { Status = RegistrySerialization::s_CreateKey(*phConsoleKey, title, phTitleKey); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { RegCloseKey(*phConsoleKey); RegCloseKey(*phCurrentUserKey); @@ -459,6 +454,4 @@ NTSTATUS RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(_In_ PCWSTR con return Status; } - - #pragma endregion diff --git a/src/propslib/ShortcutSerialization.cpp b/src/propslib/ShortcutSerialization.cpp index d71b73dbe9..e930b25af8 100644 --- a/src/propslib/ShortcutSerialization.cpp +++ b/src/propslib/ShortcutSerialization.cpp @@ -165,7 +165,7 @@ void ShortcutSerialization::s_SetLinkPropertyDwordValue(_Inout_ IPropertyStore* NT_FE_CONSOLE_PROPS* pNtFEConsoleProps; if (SUCCEEDED(pConsoleLnkDataList->CopyDataBlock(NT_FE_CONSOLE_PROPS_SIG, reinterpret_cast(&pNtFEConsoleProps)))) { - pNtFEConsoleProps->uCodePage = pStateInfo->CodePage; + pStateInfo->CodePage = pNtFEConsoleProps->uCodePage; LocalFree(pNtFEConsoleProps); } } @@ -243,13 +243,13 @@ void ShortcutSerialization::s_GetLinkTitle(_In_ PCWSTR pwszShortcutFilename, const size_t cchShortcutTitle) { auto Status = (cchShortcutTitle > 0) ? STATUS_SUCCESS : STATUS_INVALID_PARAMETER_2; - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { pwszShortcutTitle[0] = L'\0'; WCHAR szTemp[MAX_PATH]; Status = StringCchCopyW(szTemp, ARRAYSIZE(szTemp), pwszShortcutFilename); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // Now load the localized title for the shortcut IShellItem* psi; @@ -268,11 +268,11 @@ void ShortcutSerialization::s_GetLinkTitle(_In_ PCWSTR pwszShortcutFilename, } } - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { // default to an extension-free version of the filename passed in Status = StringCchCopyW(pwszShortcutTitle, cchShortcutTitle, pwszShortcutFilename); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { // don't care if we can't remove the extension (void)PathCchRemoveExtension(pwszShortcutTitle, cchShortcutTitle); diff --git a/src/propslib/TrueTypeFontList.cpp b/src/propslib/TrueTypeFontList.cpp index ceeeb9990b..367b85472a 100644 --- a/src/propslib/TrueTypeFontList.cpp +++ b/src/propslib/TrueTypeFontList.cpp @@ -46,7 +46,7 @@ WORD ConvertStringToDec( auto Status = RegistrySerialization::s_OpenKey(HKEY_LOCAL_MACHINE, MACHINE_REGISTRY_CONSOLE_TTFONT_WIN32_PATH, &hkRegistry); - if (NT_SUCCESS(Status)) + if (SUCCEEDED_NTSTATUS(Status)) { LPTTFONTLIST pTTFontList; @@ -65,7 +65,7 @@ WORD ConvertStringToDec( break; } - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { break; } diff --git a/src/propslib/precomp.h b/src/propslib/precomp.h index 32555ebc72..138de02352 100644 --- a/src/propslib/precomp.h +++ b/src/propslib/precomp.h @@ -10,14 +10,8 @@ #include #undef WIN32_NO_STATUS -// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs -typedef _Return_type_success_(return >= 0) LONG NTSTATUS; -/*lint -save -e624 */ // Don't complain about different typedefs. -typedef NTSTATUS* PNTSTATUS; -/*lint -restore */ // Resume checking for different typedefs. -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#include -#define INLINE_NTSTATUS_FROM_WIN32 1 // Must use inline NTSTATUS or it will call the wrapped function twice. #pragma warning(push) #pragma warning(disable : 4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. #include diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 3b7e0d1c6b..4459c97a8e 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "AtlasEngine.h" +#include "Backend.h" #include "../base/FontCache.h" // #### NOTE #### @@ -20,7 +21,7 @@ #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). #pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). -using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Render::Atlas; // Like gsl::narrow but returns a HRESULT. #pragma warning(push) @@ -34,7 +35,7 @@ constexpr HRESULT api_narrow(U val, T& out) noexcept #pragma warning(pop) template -constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept +constexpr HRESULT vec2_narrow(U x, U y, vec2& out) noexcept { return api_narrow(x, out.x) | api_narrow(y, out.y); } @@ -43,23 +44,23 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept [[nodiscard]] HRESULT AtlasEngine::Invalidate(const til::rect* const psrRegion) noexcept { - //assert(psrRegion->Top < psrRegion->Bottom && psrRegion->Top >= 0 && psrRegion->Bottom <= _api.cellCount.y); + //assert(psrRegion->top < psrRegion->bottom && psrRegion->top >= 0 && psrRegion->bottom <= _api.cellCount.y); // BeginPaint() protects against invalid out of bounds numbers. - _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, gsl::narrow_cast(psrRegion->Top)); - _api.invalidatedRows.y = std::max(_api.invalidatedRows.y, gsl::narrow_cast(psrRegion->Bottom)); + _api.invalidatedRows.start = std::min(_api.invalidatedRows.start, gsl::narrow_cast(psrRegion->top)); + _api.invalidatedRows.end = std::max(_api.invalidatedRows.end, gsl::narrow_cast(psrRegion->bottom)); return S_OK; } [[nodiscard]] HRESULT AtlasEngine::InvalidateCursor(const til::rect* const psrRegion) noexcept { - //assert(psrRegion->Left <= psrRegion->Right && psrRegion->Left >= 0 && psrRegion->Right <= _api.cellCount.x); - //assert(psrRegion->Top <= psrRegion->Bottom && psrRegion->Top >= 0 && psrRegion->Bottom <= _api.cellCount.y); + //assert(psrRegion->left <= psrRegion->right && psrRegion->left >= 0 && psrRegion->right <= _api.cellCount.x); + //assert(psrRegion->top <= psrRegion->bottom && psrRegion->top >= 0 && psrRegion->bottom <= _api.cellCount.y); - const auto left = gsl::narrow_cast(psrRegion->Left); - const auto top = gsl::narrow_cast(psrRegion->Top); - const auto right = gsl::narrow_cast(psrRegion->Right); - const auto bottom = gsl::narrow_cast(psrRegion->Bottom); + const auto left = gsl::narrow_cast(psrRegion->left); + const auto top = gsl::narrow_cast(psrRegion->top); + const auto right = gsl::narrow_cast(psrRegion->right); + const auto bottom = gsl::narrow_cast(psrRegion->bottom); // BeginPaint() protects against invalid out of bounds numbers. _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, left); @@ -71,13 +72,13 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept [[nodiscard]] HRESULT AtlasEngine::InvalidateSystem(const til::rect* const prcDirtyClient) noexcept { - const auto top = prcDirtyClient->top / _api.fontMetrics.cellSize.y; - const auto bottom = prcDirtyClient->bottom / _api.fontMetrics.cellSize.y; + const auto top = prcDirtyClient->top / _api.s->font->cellSize.y; + const auto bottom = prcDirtyClient->bottom / _api.s->font->cellSize.y; // BeginPaint() protects against invalid out of bounds numbers. til::rect rect; - rect.Top = top; - rect.Bottom = bottom; + rect.top = top; + rect.bottom = bottom; return Invalidate(&rect); } @@ -88,8 +89,8 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept // BeginPaint() protects against invalid out of bounds numbers. // TODO: rect can contain invalid out of bounds coordinates when the selection is being // dragged outside of the viewport (and the window begins scrolling automatically). - _api.invalidatedRows.x = gsl::narrow_cast(std::min(_api.invalidatedRows.x, std::max(0, rect.Top))); - _api.invalidatedRows.y = gsl::narrow_cast(std::max(_api.invalidatedRows.y, std::max(0, rect.Bottom))); + _api.invalidatedRows.start = gsl::narrow_cast(std::min(_api.invalidatedRows.start, std::max(0, rect.top))); + _api.invalidatedRows.end = gsl::narrow_cast(std::max(_api.invalidatedRows.end, std::max(0, rect.bottom))); } return S_OK; } @@ -100,7 +101,7 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept // a InvalidateScroll() refer to the new viewport after the scroll. // --> We need to shift the current invalidation rectangles as well. - if (const auto delta = pcoordDelta->X) + if (const auto delta = pcoordDelta->x) { _api.invalidatedCursorArea.left = gsl::narrow_cast(clamp(_api.invalidatedCursorArea.left + delta, u16min, u16max)); _api.invalidatedCursorArea.right = gsl::narrow_cast(clamp(_api.invalidatedCursorArea.right + delta, u16min, u16max)); @@ -108,7 +109,7 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept _api.invalidatedRows = invalidatedRowsAll; } - if (const auto delta = pcoordDelta->Y) + if (const auto delta = pcoordDelta->y) { _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset + delta, i16min, i16max)); @@ -117,13 +118,13 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept if (delta < 0) { - _api.invalidatedRows.x = gsl::narrow_cast(clamp(_api.invalidatedRows.x + delta, u16min, u16max)); - _api.invalidatedRows.y = _api.cellCount.y; + _api.invalidatedRows.start = gsl::narrow_cast(clamp(_api.invalidatedRows.start + delta, u16min, u16max)); + _api.invalidatedRows.end = _api.s->viewportCellCount.y; } else { - _api.invalidatedRows.x = 0; - _api.invalidatedRows.y = gsl::narrow_cast(clamp(_api.invalidatedRows.y + delta, u16min, u16max)); + _api.invalidatedRows.start = 0; + _api.invalidatedRows.end = gsl::narrow_cast(clamp(_api.invalidatedRows.end + delta, u16min, u16max)); } } @@ -145,7 +146,7 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept [[nodiscard]] HRESULT AtlasEngine::InvalidateTitle(const std::wstring_view proposedTitle) noexcept { - WI_SetFlag(_api.invalidations, ApiInvalidations::Title); + _api.invalidatedTitle = true; return S_OK; } @@ -159,8 +160,12 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept return UpdateFont(fontInfoDesired, fontInfo, {}, {}); } -[[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const gsl::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept +[[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept { + const auto softFont = _api.s.write()->font.write(); + softFont->softFontPattern.assign(bitPattern.begin(), bitPattern.end()); + softFont->softFontCellSize.width = std::max(0, cellSize.width); + softFont->softFontCellSize.height = std::max(0, cellSize.height); return S_OK; } @@ -169,28 +174,38 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept u16 newDPI; RETURN_IF_FAILED(api_narrow(dpi, newDPI)); - if (_api.dpi != newDPI) + if (_api.s->font->dpi != newDPI) { - _api.dpi = newDPI; - WI_SetFlag(_api.invalidations, ApiInvalidations::Font); + _api.s.write()->font.write()->dpi = newDPI; } return S_OK; } [[nodiscard]] HRESULT AtlasEngine::UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept +try { - const u16x2 cellCount{ - gsl::narrow_cast(std::max(1, srNewViewport.Right - srNewViewport.Left + 1)), - gsl::narrow_cast(std::max(1, srNewViewport.Bottom - srNewViewport.Top + 1)), + const u16x2 viewportCellCount{ + gsl::narrow(std::max(1, srNewViewport.right - srNewViewport.left + 1)), + gsl::narrow(std::max(1, srNewViewport.bottom - srNewViewport.top + 1)), }; - if (_api.cellCount != cellCount) + const u16x2 viewportOffset{ + gsl::narrow(srNewViewport.left), + gsl::narrow(srNewViewport.top), + }; + + if (_api.s->viewportCellCount != viewportCellCount) { - _api.cellCount = cellCount; - WI_SetFlag(_api.invalidations, ApiInvalidations::Size); + _api.s.write()->viewportCellCount = viewportCellCount; } + if (_api.s->viewportOffset != viewportOffset) + { + _api.s.write()->viewportOffset = viewportOffset; + } + return S_OK; } +CATCH_RETURN() [[nodiscard]] HRESULT AtlasEngine::GetProposedFont(const FontInfoDesired& fontInfoDesired, _Out_ FontInfo& fontInfo, const int dpi) noexcept try @@ -212,7 +227,7 @@ try // https://msdn.microsoft.com/en-us/library/ms969909.aspx LOGFONTW lf; - lf.lfHeight = -MulDiv(requestedSize.Y, dpi, 72); + lf.lfHeight = -MulDiv(requestedSize.height, dpi, 72); lf.lfWidth = 0; lf.lfEscapement = 0; lf.lfOrientation = 0; @@ -244,8 +259,8 @@ try til::size sz; RETURN_HR_IF(E_FAIL, !GetTextExtentPoint32W(hdc.get(), L"M", 1, &sz)); - resultingCellSize.X = sz.cx; - resultingCellSize.Y = sz.cy; + resultingCellSize.width = sz.width; + resultingCellSize.height = sz.height; } #endif @@ -254,17 +269,17 @@ try } CATCH_RETURN() -[[nodiscard]] HRESULT AtlasEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT AtlasEngine::GetDirtyArea(std::span& area) noexcept { - area = gsl::span{ &_api.dirtyRect, 1 }; + area = std::span{ &_api.dirtyRect, 1 }; return S_OK; } [[nodiscard]] HRESULT AtlasEngine::GetFontSize(_Out_ til::size* pFontSize) noexcept { RETURN_HR_IF_NULL(E_INVALIDARG, pFontSize); - pFontSize->X = _api.fontMetrics.cellSize.x; - pFontSize->Y = _api.fontMetrics.cellSize.y; + pFontSize->width = _api.s->font->cellSize.x; + pFontSize->height = _api.s->font->cellSize.y; return S_OK; } @@ -272,13 +287,25 @@ CATCH_RETURN() { RETURN_HR_IF_NULL(E_INVALIDARG, pResult); - wil::com_ptr textLayout; - RETURN_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast(glyph.size()), _getTextFormat(false, false), FLT_MAX, FLT_MAX, textLayout.addressof())); + wil::com_ptr textFormat; + RETURN_IF_FAILED(_p.dwriteFactory->CreateTextFormat( + /* fontFamilyName */ _api.s->font->fontName.c_str(), + /* fontCollection */ _api.s->font->fontCollection.get(), + /* fontWeight */ static_cast(_api.s->font->fontWeight), + /* fontStyle */ DWRITE_FONT_STYLE_NORMAL, + /* fontStretch */ DWRITE_FONT_STRETCH_NORMAL, + /* fontSize */ _api.s->font->fontSize, + /* localeName */ L"", + /* textFormat */ textFormat.put())); - DWRITE_TEXT_METRICS metrics; + wil::com_ptr textLayout; + RETURN_IF_FAILED(_p.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast(glyph.size()), textFormat.get(), FLT_MAX, FLT_MAX, textLayout.addressof())); + + DWRITE_TEXT_METRICS metrics{}; RETURN_IF_FAILED(textLayout->GetMetrics(&metrics)); - *pResult = static_cast(std::ceilf(metrics.width)) > _api.fontMetrics.cellSize.x; + const auto minWidth = (_api.s->font->cellSize.x * 1.2f); + *pResult = metrics.width > minWidth; return S_OK; } @@ -298,47 +325,46 @@ HRESULT AtlasEngine::Enable() noexcept [[nodiscard]] std::wstring_view AtlasEngine::GetPixelShaderPath() noexcept { - return _api.customPixelShaderPath; + return _api.s->misc->customPixelShaderPath; } [[nodiscard]] bool AtlasEngine::GetRetroTerminalEffect() const noexcept { - return _api.useRetroTerminalEffect; + return _api.s->misc->useRetroTerminalEffect; } [[nodiscard]] float AtlasEngine::GetScaling() const noexcept { - return static_cast(_api.dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + return static_cast(_api.s->font->dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); } [[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept { - assert(_api.fontMetrics.cellSize.x != 0); - assert(_api.fontMetrics.cellSize.y != 0); - return Types::Viewport::FromDimensions(viewInPixels.Origin(), { viewInPixels.Width() / _api.fontMetrics.cellSize.x, viewInPixels.Height() / _api.fontMetrics.cellSize.y }); + assert(_api.s->font->cellSize.x != 0); + assert(_api.s->font->cellSize.y != 0); + return Types::Viewport::FromDimensions(viewInPixels.Origin(), { viewInPixels.Width() / _api.s->font->cellSize.x, viewInPixels.Height() / _api.s->font->cellSize.y }); } [[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept { - assert(_api.fontMetrics.cellSize.x != 0); - assert(_api.fontMetrics.cellSize.y != 0); - return Types::Viewport::FromDimensions(viewInCharacters.Origin(), { viewInCharacters.Width() * _api.fontMetrics.cellSize.x, viewInCharacters.Height() * _api.fontMetrics.cellSize.y }); + assert(_api.s->font->cellSize.x != 0); + assert(_api.s->font->cellSize.y != 0); + return Types::Viewport::FromDimensions(viewInCharacters.Origin(), { viewInCharacters.Width() * _api.s->font->cellSize.x, viewInCharacters.Height() * _api.s->font->cellSize.y }); } void AtlasEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept { - const auto mode = gsl::narrow_cast(antialiasingMode); + const auto mode = static_cast(antialiasingMode); if (_api.antialiasingMode != mode) { _api.antialiasingMode = mode; _resolveTransparencySettings(); - WI_SetFlag(_api.invalidations, ApiInvalidations::Font); } } void AtlasEngine::SetCallback(std::function pfn) noexcept { - _api.swapChainChangedCallback = std::move(pfn); + _p.swapChainChangedCallback = std::move(pfn); } void AtlasEngine::EnableTransparentBackground(const bool isTransparent) noexcept @@ -347,7 +373,6 @@ void AtlasEngine::EnableTransparentBackground(const bool isTransparent) noexcept { _api.enableTransparentBackground = isTransparent; _resolveTransparencySettings(); - WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain); } } @@ -357,71 +382,72 @@ void AtlasEngine::SetForceFullRepaintRendering(bool enable) noexcept [[nodiscard]] HRESULT AtlasEngine::SetHwnd(const HWND hwnd) noexcept { - if (_api.hwnd != hwnd) + if (_api.s->target->hwnd != hwnd) { - _api.hwnd = hwnd; - WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain); + _api.s.write()->target.write()->hwnd = hwnd; } return S_OK; } void AtlasEngine::SetPixelShaderPath(std::wstring_view value) noexcept +try { - if (_api.customPixelShaderPath != value) + if (_api.s->misc->customPixelShaderPath != value) { - _api.customPixelShaderPath = value; + _api.s.write()->misc.write()->customPixelShaderPath = value; _resolveTransparencySettings(); - WI_SetFlag(_api.invalidations, ApiInvalidations::Device); } } +CATCH_LOG() void AtlasEngine::SetRetroTerminalEffect(bool enable) noexcept { - if (_api.useRetroTerminalEffect != enable) + if (_api.s->misc->useRetroTerminalEffect != enable) { - _api.useRetroTerminalEffect = enable; + _api.s.write()->misc.write()->useRetroTerminalEffect = enable; _resolveTransparencySettings(); - WI_SetFlag(_api.invalidations, ApiInvalidations::Device); } } void AtlasEngine::SetSelectionBackground(const COLORREF color, const float alpha) noexcept { - const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast(std::lroundf(alpha * 255.0f)) << 24; - if (_api.selectionColor != selectionColor) + const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast(lrintf(alpha * 255.0f)) << 24; + if (_api.s->misc->selectionColor != selectionColor) { - _api.selectionColor = selectionColor; - WI_SetFlag(_api.invalidations, ApiInvalidations::Settings); + _api.s.write()->misc.write()->selectionColor = selectionColor; } } void AtlasEngine::SetSoftwareRendering(bool enable) noexcept { - if (_api.useSoftwareRendering != enable) + if (_api.s->target->useSoftwareRendering != enable) { - _api.useSoftwareRendering = enable; - WI_SetFlag(_api.invalidations, ApiInvalidations::Device); + _api.s.write()->target.write()->useSoftwareRendering = enable; } } void AtlasEngine::SetWarningCallback(std::function pfn) noexcept { - _api.warningCallback = std::move(pfn); + _p.warningCallback = std::move(pfn); } [[nodiscard]] HRESULT AtlasEngine::SetWindowSize(const til::size pixels) noexcept { - u16x2 newSize; - RETURN_IF_FAILED(vec2_narrow(pixels.cx, pixels.cy, newSize)); - - // At the time of writing: - // When Win+D is pressed, `TriggerRedrawCursor` is called and a render pass is initiated. - // As conhost is in the background, GetClientRect will return {0,0} and we'll get called with {0,0}. - // This isn't a valid value for _api.sizeInPixel and would crash _recreateSizeDependentResources(). - if (_api.sizeInPixel != newSize && newSize != u16x2{}) + // When Win+D is pressed, `GetClientRect` returns {0,0}. + // There's probably more situations in which our callers may pass us invalid data. + if (!pixels) { - _api.sizeInPixel = newSize; - WI_SetFlag(_api.invalidations, ApiInvalidations::Size); + return S_OK; + } + + const u16x2 newSize{ + gsl::narrow_cast(clamp(pixels.width, 1, u16max)), + gsl::narrow_cast(clamp(pixels.height, 1, u16max)), + }; + + if (_api.s->targetSize != newSize) + { + _api.s.write()->targetSize = newSize; } return S_OK; @@ -467,10 +493,18 @@ void AtlasEngine::_resolveTransparencySettings() noexcept // If the user asks for ClearType, but also for a transparent background // (which our ClearType shader doesn't simultaneously support) // then we need to sneakily force the renderer to grayscale AA. - _api.realizedAntialiasingMode = _api.enableTransparentBackground && _api.antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : _api.antialiasingMode; - // An opaque background allows us to use true "independent" flips. See AtlasEngine::_createSwapChain(). - // We can't enable them if custom shaders are specified, because it's unknown, whether they support opaque inputs. - _api.backgroundOpaqueMixin = _api.enableTransparentBackground || !_api.customPixelShaderPath.empty() || _api.useRetroTerminalEffect ? 0x00000000 : 0xff000000; + const auto antialiasingMode = _api.enableTransparentBackground && _api.antialiasingMode == AntialiasingMode::ClearType ? AntialiasingMode::Grayscale : _api.antialiasingMode; + const bool enableTransparentBackground = _api.enableTransparentBackground || !_api.s->misc->customPixelShaderPath.empty() || _api.s->misc->useRetroTerminalEffect; + + if (antialiasingMode != _api.s->font->antialiasingMode || enableTransparentBackground != _api.s->target->enableTransparentBackground) + { + const auto s = _api.s.write(); + s->font.write()->antialiasingMode = antialiasingMode; + // An opaque background allows us to use true "independent" flips. See AtlasEngine::_createSwapChain(). + // We can't enable them if custom shaders are specified, because it's unknown, whether they support opaque inputs. + s->target.write()->enableTransparentBackground = enableTransparentBackground; + _api.backgroundOpaqueMixin = enableTransparentBackground ? 0x00000000 : 0xff000000; + } } void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) @@ -486,9 +520,9 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo // Gsub is for GetGlyphs() and Gpos for GetGlyphPlacements(). // // GH#10774: Apparently specifying all of the features is just redundant. - fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1 }); - fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1 }); - fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1 }); + fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1); + fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1); + fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1); for (const auto& p : features) { @@ -507,7 +541,7 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo fontFeatures[2].parameter = p.second; break; default: - fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ tag, p.second }); + fontFeatures.emplace_back(tag, p.second); break; } } @@ -521,9 +555,9 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo // AtlasEngine::_recreateFontDependentResources() relies on these fields to // exist in this particular order in order to create appropriate default axes. - fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f }); - fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f }); - fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, -1.0f }); + fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f); + fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f); + fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_SLANT, -1.0f); for (const auto& p : axes) { @@ -542,27 +576,20 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo fontAxisValues[2].value = p.second; break; default: - fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ tag, p.second }); + fontAxisValues.emplace_back(tag, p.second); break; } } } } - const auto previousCellSize = _api.fontMetrics.cellSize; - _resolveFontMetrics(faceName, fontInfoDesired, fontInfo, &_api.fontMetrics); - _api.fontFeatures = std::move(fontFeatures); - _api.fontAxisValues = std::move(fontAxisValues); - - WI_SetFlag(_api.invalidations, ApiInvalidations::Font); - - if (previousCellSize != _api.fontMetrics.cellSize) - { - WI_SetFlag(_api.invalidations, ApiInvalidations::Size); - } + const auto font = _api.s.write()->font.write(); + _resolveFontMetrics(faceName, fontInfoDesired, fontInfo, font); + font->fontFeatures = std::move(fontFeatures); + font->fontAxisValues = std::move(fontAxisValues); } -void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics) const +void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics) const { const auto requestedFamily = fontInfoDesired.GetFamily(); auto requestedWeight = fontInfoDesired.GetWeight(); @@ -577,7 +604,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo requestedFaceName = L"Consolas"; } } - if (!requestedSize.Y) + if (!requestedSize.height) { fontSize = 12.0f; requestedSize = { 0, 12 }; @@ -587,11 +614,22 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo requestedWeight = DWRITE_FONT_WEIGHT_NORMAL; } - auto fontCollection = FontCache::GetCached(); + wil::com_ptr fontCollection; + THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontCollection(fontCollection.addressof(), FALSE)); u32 index = 0; BOOL exists = false; THROW_IF_FAILED(fontCollection->FindFamilyName(requestedFaceName, &index, &exists)); + + if constexpr (Feature_NearbyFontLoading::IsEnabled()) + { + if (!exists) + { + fontCollection = FontCache::GetCached(); + THROW_IF_FAILED(fontCollection->FindFamilyName(requestedFaceName, &index, &exists)); + } + } + THROW_HR_IF(DWRITE_E_NOFONT, !exists); wil::com_ptr fontFamily; @@ -603,44 +641,52 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo wil::com_ptr fontFace; THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof())); - DWRITE_FONT_METRICS metrics; + DWRITE_FONT_METRICS metrics{}; fontFace->GetMetrics(&metrics); - // According to Wikipedia: - // > One em was traditionally defined as the width of the capital 'M' in the current typeface and point size, - // > because the 'M' was commonly cast the full-width of the square blocks [...] which are used in printing presses. - // Even today M is often the widest character in a font that supports ASCII. - // In the future a more robust solution could be written, until then this simple solution works for most cases. - static constexpr u32 codePoint = L'M'; - u16 glyphIndex; - THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex)); - - DWRITE_GLYPH_METRICS glyphMetrics; - THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics)); - // Point sizes are commonly treated at a 72 DPI scale // (including by OpenType), whereas DirectWrite uses 96 DPI. // Since we want the height in px we multiply by the display's DPI. - const auto fontSizeInDIP = fontSize / 72.0f * 96.0f; - const auto fontSizeInPx = fontSize / 72.0f * _api.dpi; + const auto dpi = static_cast(_api.s->font->dpi); + const auto fontSizeInPx = fontSize / 72.0f * dpi; - const auto designUnitsPerPx = fontSizeInPx / static_cast(metrics.designUnitsPerEm); - const auto ascent = static_cast(metrics.ascent) * designUnitsPerPx; - const auto descent = static_cast(metrics.descent) * designUnitsPerPx; - const auto underlinePosition = static_cast(-metrics.underlinePosition) * designUnitsPerPx; - const auto underlineThickness = static_cast(metrics.underlineThickness) * designUnitsPerPx; - const auto strikethroughPosition = static_cast(-metrics.strikethroughPosition) * designUnitsPerPx; - const auto strikethroughThickness = static_cast(metrics.strikethroughThickness) * designUnitsPerPx; + const auto designUnitsPerPx = fontSizeInPx / static_cast(metrics.designUnitsPerEm); + const auto ascent = static_cast(metrics.ascent) * designUnitsPerPx; + const auto descent = static_cast(metrics.descent) * designUnitsPerPx; + const auto lineGap = static_cast(metrics.lineGap) * designUnitsPerPx; + const auto underlinePosition = static_cast(-metrics.underlinePosition) * designUnitsPerPx; + const auto underlineThickness = static_cast(metrics.underlineThickness) * designUnitsPerPx; + const auto strikethroughPosition = static_cast(-metrics.strikethroughPosition) * designUnitsPerPx; + const auto strikethroughThickness = static_cast(metrics.strikethroughThickness) * designUnitsPerPx; + const auto advanceHeight = ascent + descent + lineGap; - const auto advanceWidth = static_cast(glyphMetrics.advanceWidth) * designUnitsPerPx; + // We use the same character to determine the advance width as CSS for its "ch" unit ("0"). + // According to the CSS spec, if it's impossible to determine the advance width, + // it must be assumed to be 0.5em wide. em in CSS refers to the computed font-size. + auto advanceWidth = 0.5f * fontSizeInPx; + { + static constexpr u32 codePoint = '0'; - // NOTE: Line-gaps shouldn't be taken into account for lineHeight calculations. - // Terminals don't really have "gaps" between lines and instead the expectation - // is that two full block characters above each other don't leave any gaps - // between the lines. "Terminus TTF" for instance sets a line-gap of 90 units - // even though its font bitmap only covers the ascend/descend height. - const auto baseline = std::roundf(ascent); - const auto lineHeight = std::roundf(baseline + descent); + u16 glyphIndex; + THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex)); + + if (glyphIndex) + { + DWRITE_GLYPH_METRICS glyphMetrics{}; + THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics, FALSE)); + advanceWidth = static_cast(glyphMetrics.advanceWidth) * designUnitsPerPx; + } + } + + auto adjustedWidth = std::roundf(fontInfoDesired.GetCellWidth().Resolve(advanceWidth, dpi, fontSizeInPx, advanceWidth)); + auto adjustedHeight = std::roundf(fontInfoDesired.GetCellHeight().Resolve(advanceHeight, dpi, fontSizeInPx, advanceWidth)); + + // Protection against bad user values in GetCellWidth/Y. + // AtlasEngine fails hard with 0 cell sizes. + adjustedWidth = std::max(1.0f, adjustedWidth); + adjustedHeight = std::max(1.0f, adjustedHeight); + + const auto baseline = std::roundf(ascent + (lineGap + adjustedHeight - advanceHeight) / 2.0f); const auto underlinePos = std::roundf(baseline + underlinePosition); const auto underlineWidth = std::max(1.0f, std::roundf(underlineThickness)); const auto strikethroughPos = std::roundf(baseline + strikethroughPosition); @@ -664,25 +710,25 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo doubleUnderlinePosTop = std::max(doubleUnderlinePosTop, baseline + thinLineWidth); // 5. The gap is only the distance _between_ the lines, but we need the distance from the // top border of the top and bottom lines, which includes an additional line width. - const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * _api.dpi)); + const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * dpi)); doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + thinLineWidth); // Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries. - doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, lineHeight - thinLineWidth); + doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - thinLineWidth); - const auto cellWidth = gsl::narrow(std::lroundf(advanceWidth)); - const auto cellHeight = gsl::narrow(lineHeight); + const auto cellWidth = gsl::narrow(lrintf(adjustedWidth)); + const auto cellHeight = gsl::narrow(lrintf(adjustedHeight)); { til::size coordSize; - coordSize.X = cellWidth; - coordSize.Y = cellHeight; + coordSize.width = cellWidth; + coordSize.height = cellHeight; - if (requestedSize.X == 0) + if (requestedSize.width == 0) { // The coordSizeUnscaled parameter to SetFromEngine is used for API functions like GetConsoleFontSize. // Since clients expect that settings the font height to Y yields back a font height of Y, // we're scaling the X relative/proportional to the actual cellWidth/cellHeight ratio. - requestedSize.X = gsl::narrow_cast(std::lroundf(fontSize / cellHeight * cellWidth)); + requestedSize.width = gsl::narrow_cast(lrintf(fontSize / cellHeight * cellWidth)); } fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, coordSize, requestedSize); @@ -692,13 +738,20 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo { std::wstring fontName{ requestedFaceName }; const auto fontWeightU16 = gsl::narrow_cast(requestedWeight); - const auto underlinePosU16 = gsl::narrow_cast(underlinePos); - const auto underlineWidthU16 = gsl::narrow_cast(underlineWidth); - const auto strikethroughPosU16 = gsl::narrow_cast(strikethroughPos); - const auto strikethroughWidthU16 = gsl::narrow_cast(strikethroughWidth); - const auto doubleUnderlinePosTopU16 = gsl::narrow_cast(doubleUnderlinePosTop); - const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast(doubleUnderlinePosBottom); - const auto thinLineWidthU16 = gsl::narrow_cast(thinLineWidth); + const auto advanceWidthU16 = gsl::narrow_cast(lrintf(advanceWidth)); + const auto baselineU16 = gsl::narrow_cast(lrintf(baseline)); + const auto descenderU16 = gsl::narrow_cast(cellHeight - baselineU16); + const auto thinLineWidthU16 = gsl::narrow_cast(lrintf(thinLineWidth)); + + const auto gridBottomPositionU16 = gsl::narrow_cast(cellHeight - thinLineWidth); + const auto gridRightPositionU16 = gsl::narrow_cast(cellWidth - thinLineWidth); + + const auto underlinePosU16 = gsl::narrow_cast(lrintf(underlinePos)); + const auto underlineWidthU16 = gsl::narrow_cast(lrintf(underlineWidth)); + const auto strikethroughPosU16 = gsl::narrow_cast(lrintf(strikethroughPos)); + const auto strikethroughWidthU16 = gsl::narrow_cast(lrintf(strikethroughWidth)); + const auto doubleUnderlinePosTopU16 = gsl::narrow_cast(lrintf(doubleUnderlinePosTop)); + const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast(lrintf(doubleUnderlinePosBottom)); // NOTE: From this point onward no early returns or throwing code should exist, // as we might cause _api to be in an inconsistent state otherwise. @@ -706,16 +759,23 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo fontMetrics->fontCollection = std::move(fontCollection); fontMetrics->fontFamily = std::move(fontFamily); fontMetrics->fontName = std::move(fontName); - fontMetrics->fontSizeInDIP = fontSizeInDIP; - fontMetrics->baselineInDIP = baseline / static_cast(_api.dpi) * 96.0f; - fontMetrics->advanceScale = cellWidth / advanceWidth; + fontMetrics->fontSize = fontSizeInPx; fontMetrics->cellSize = { cellWidth, cellHeight }; fontMetrics->fontWeight = fontWeightU16; - fontMetrics->underlinePos = underlinePosU16; - fontMetrics->underlineWidth = underlineWidthU16; - fontMetrics->strikethroughPos = strikethroughPosU16; - fontMetrics->strikethroughWidth = strikethroughWidthU16; - fontMetrics->doubleUnderlinePos = { doubleUnderlinePosTopU16, doubleUnderlinePosBottomU16 }; + fontMetrics->advanceWidth = advanceWidthU16; + fontMetrics->baseline = baselineU16; + fontMetrics->descender = descenderU16; fontMetrics->thinLineWidth = thinLineWidthU16; + + fontMetrics->gridTop = { 0, thinLineWidthU16 }; + fontMetrics->gridBottom = { gridBottomPositionU16, thinLineWidthU16 }; + fontMetrics->gridLeft = { 0, thinLineWidthU16 }; + fontMetrics->gridRight = { gridRightPositionU16, thinLineWidthU16 }; + + fontMetrics->underline = { underlinePosU16, underlineWidthU16 }; + fontMetrics->strikethrough = { strikethroughPosU16, strikethroughWidthU16 }; + fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, thinLineWidthU16 }; + fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, thinLineWidthU16 }; + fontMetrics->overline = { 0, underlineWidthU16 }; } } diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index c7e91c0c18..e46bb25083 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -4,11 +4,8 @@ #include "pch.h" #include "AtlasEngine.h" -#include -#include -#include -#include - +#include "Backend.h" +#include "DWriteTextAnalysis.h" #include "../../interactivity/win32/CustomWindowMessages.h" // #### NOTE #### @@ -25,44 +22,27 @@ #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). #pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). -using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Render::Atlas; #pragma warning(suppress : 26455) // Default constructor may not throw. Declare it 'noexcept' (f.6). AtlasEngine::AtlasEngine() { #ifdef NDEBUG - THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, _sr.d2dFactory.addressof())); + THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(_p.d2dFactory), nullptr, reinterpret_cast(_p.d2dFactory.addressof()))); #else - static constexpr D2D1_FACTORY_OPTIONS options{ D2D1_DEBUG_LEVEL_INFORMATION }; - THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, options, _sr.d2dFactory.addressof())); + static constexpr D2D1_FACTORY_OPTIONS options{ .debugLevel = D2D1_DEBUG_LEVEL_INFORMATION }; + THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(_p.d2dFactory), &options, reinterpret_cast(_p.d2dFactory.addressof()))); #endif - THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(_sr.dwriteFactory), reinterpret_cast<::IUnknown**>(_sr.dwriteFactory.addressof()))); - if (const auto factory2 = _sr.dwriteFactory.try_query()) - { - THROW_IF_FAILED(factory2->GetSystemFontFallback(_sr.systemFontFallback.addressof())); - } - { - wil::com_ptr textAnalyzer; - THROW_IF_FAILED(_sr.dwriteFactory->CreateTextAnalyzer(textAnalyzer.addressof())); - _sr.textAnalyzer = textAnalyzer.query(); - } + THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(_p.dwriteFactory), reinterpret_cast<::IUnknown**>(_p.dwriteFactory.addressof()))); + _p.dwriteFactory4 = _p.dwriteFactory.try_query(); - _sr.isWindows10OrGreater = IsWindows10OrGreater(); + THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontFallback(_p.systemFontFallback.addressof())); + _p.systemFontFallback1 = _p.systemFontFallback.try_query(); -#ifndef NDEBUG - { - _sr.sourceDirectory = std::filesystem::path{ __FILE__ }.parent_path(); - _sr.sourceCodeWatcher = wil::make_folder_change_reader_nothrow(_sr.sourceDirectory.c_str(), false, wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime, [this](wil::FolderChangeEvent, PCWSTR path) { - if (til::ends_with(path, L".hlsl")) - { - auto expected = INT64_MAX; - const auto invalidationTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); - _sr.sourceCodeInvalidationTime.compare_exchange_strong(expected, invalidationTime.time_since_epoch().count(), std::memory_order_relaxed); - } - }); - } -#endif + wil::com_ptr textAnalyzer; + THROW_IF_FAILED(_p.dwriteFactory->CreateTextAnalyzer(textAnalyzer.addressof())); + _p.textAnalyzer = textAnalyzer.query(); } #pragma region IRenderEngine @@ -72,103 +52,25 @@ AtlasEngine::AtlasEngine() [[nodiscard]] HRESULT AtlasEngine::StartPaint() noexcept try { - if (_api.hwnd) + if (const auto hwnd = _api.s->target->hwnd) { RECT rect; - LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_api.hwnd, &rect)); + LOG_IF_WIN32_BOOL_FALSE(GetClientRect(hwnd, &rect)); std::ignore = SetWindowSize({ rect.right - rect.left, rect.bottom - rect.top }); - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Title)) + if (_api.invalidatedTitle) { - LOG_IF_WIN32_BOOL_FALSE(PostMessageW(_api.hwnd, CM_UPDATE_TITLE, 0, 0)); - WI_ClearFlag(_api.invalidations, ApiInvalidations::Title); + LOG_IF_WIN32_BOOL_FALSE(PostMessageW(hwnd, CM_UPDATE_TITLE, 0, 0)); + _api.invalidatedTitle = false; } } - // It's important that we invalidate here instead of in Present() with the rest. - // Other functions, those called before Present(), might depend on _r fields. - // But most of the time _invalidations will be ::none, making this very cheap. - if (_api.invalidations != ApiInvalidations::None) + if (_p.s != _api.s) { - RETURN_HR_IF(E_UNEXPECTED, _api.cellCount == u16x2{}); - - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Device)) - { - _createResources(); - } - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::SwapChain)) - { - _createSwapChain(); - } - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Size)) - { - _recreateSizeDependentResources(); - } - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Font)) - { - _recreateFontDependentResources(); - } - if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Settings)) - { - _r.selectionColor = _api.selectionColor; - WI_SetFlag(_r.invalidations, RenderInvalidations::ConstBuffer); - WI_ClearFlag(_api.invalidations, ApiInvalidations::Settings); - } - - // Equivalent to InvalidateAll(). - _api.invalidatedRows = invalidatedRowsAll; + _handleSettingsUpdate(); } -#ifndef NDEBUG - if (const auto invalidationTime = _sr.sourceCodeInvalidationTime.load(std::memory_order_relaxed); invalidationTime != INT64_MAX && invalidationTime <= std::chrono::steady_clock::now().time_since_epoch().count()) - { - _sr.sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed); - - try - { - static const auto compile = [](const std::filesystem::path& path, const char* target) { - wil::com_ptr error; - wil::com_ptr blob; - const auto hr = D3DCompileFromFile( - /* pFileName */ path.c_str(), - /* pDefines */ nullptr, - /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, - /* pEntrypoint */ "main", - /* pTarget */ target, - /* Flags1 */ D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS, - /* Flags2 */ 0, - /* ppCode */ blob.addressof(), - /* ppErrorMsgs */ error.addressof()); - - if (error) - { - std::thread t{ [error = std::move(error)]() noexcept { - MessageBoxA(nullptr, static_cast(error->GetBufferPointer()), "Compilation error", MB_ICONERROR | MB_OK); - } }; - t.detach(); - } - - THROW_IF_FAILED(hr); - return blob; - }; - - const auto vs = compile(_sr.sourceDirectory / L"shader_vs.hlsl", "vs_4_1"); - const auto ps = compile(_sr.sourceDirectory / L"shader_ps.hlsl", "ps_4_1"); - - THROW_IF_FAILED(_r.device->CreateVertexShader(vs->GetBufferPointer(), vs->GetBufferSize(), nullptr, _r.vertexShader.put())); - THROW_IF_FAILED(_r.device->CreatePixelShader(ps->GetBufferPointer(), ps->GetBufferSize(), nullptr, _r.pixelShader.put())); - _setShaderResources(); - } - CATCH_LOG() - } -#endif - - if constexpr (debugGlyphGenerationPerformance) - { - _r.glyphs = {}; - _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel }; - } - if constexpr (debugTextParsingPerformance) + if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION) { _api.invalidatedRows = invalidatedRowsAll; _api.scrollOffset = 0; @@ -176,126 +78,173 @@ try // Clamp invalidation rects into valid value ranges. { - _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x); - _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y); - _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x); - _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y); + _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _p.s->viewportCellCount.x); + _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _p.s->viewportCellCount.y); + _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _p.s->viewportCellCount.x); + _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _p.s->viewportCellCount.y); } { - _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y); - _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y); + _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(_api.cellCount.y & 0x7fff); - _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit)); - } + const auto limit = gsl::narrow_cast(_p.s->viewportCellCount.y & 0x7fff); + const auto offset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit)); + const auto nothingInvalid = _api.invalidatedRows.start == _api.invalidatedRows.end; - // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid". - if (_api.scrollOffset != 0) - { - const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y; - const auto offset = static_cast(_api.scrollOffset) * _api.cellCount.x; + _api.scrollOffset = offset; - if (_api.scrollOffset < 0) + // Mark the newly scrolled in rows as invalidated + if (offset < 0) { - // Scroll up (for instance when new text is being written at the end of the buffer). - const u16 endRow = _api.cellCount.y + _api.scrollOffset; - _api.invalidatedRows.x = nothingInvalid ? endRow : std::min(_api.invalidatedRows.x, endRow); - _api.invalidatedRows.y = _api.cellCount.y; - - // scrollOffset/offset = -1 - // +----------+ +----------+ - // | | | xxxxxxxxx| + dst < beg - // | xxxxxxxxx| -> |xxxxxxx | + src | < beg - offset - // |xxxxxxx | | | | v - // +----------+ +----------+ v < end - { - const auto beg = _r.cells.begin(); - const auto end = _r.cells.end(); - std::move(beg - offset, end, beg); - } - { - const auto beg = _r.cellGlyphMapping.begin(); - const auto end = _r.cellGlyphMapping.end(); - std::move(beg - offset, end, beg); - } + const u16 begRow = _p.s->viewportCellCount.y + offset; + _api.invalidatedRows.start = nothingInvalid ? begRow : std::min(_api.invalidatedRows.start, begRow); + _api.invalidatedRows.end = _p.s->viewportCellCount.y; } else { - // Scroll down. - _api.invalidatedRows.x = 0; - _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max(_api.invalidatedRows.y, _api.scrollOffset); + const u16 endRow = offset; + _api.invalidatedRows.start = 0; + _api.invalidatedRows.end = nothingInvalid ? endRow : std::max(_api.invalidatedRows.end, endRow); + } + } + _api.dirtyRect = { + 0, + _api.invalidatedRows.start, + _p.s->viewportCellCount.x, + _api.invalidatedRows.end, + }; + + _p.dirtyRectInPx = { + til::CoordTypeMax, + til::CoordTypeMax, + til::CoordTypeMin, + til::CoordTypeMin, + }; + _p.invalidatedRows = _api.invalidatedRows; + _p.cursorRect = {}; + _p.scrollOffset = _api.scrollOffset; + + // This if condition serves 2 purposes: + // * By setting top/bottom to the full height we ensure that we call Present() without + // any dirty rects and not Present1() on the first frame after the settings change. + // * If the scrollOffset is so large that it scrolls the entire viewport, invalidatedRows will span + // the entire viewport as well. We need to set scrollOffset to 0 then, not just because scrolling + // the contents of the entire swap chain is redundant, but more importantly because the scroll rect + // is the subset of the contents that are being scrolled into. If you scroll the entire viewport + // then the scroll rect is empty, which Present1() will loudly complain about. + if (_p.invalidatedRows == range{ 0, _p.s->viewportCellCount.y }) + { + _p.MarkAllAsDirty(); + } + + if (const auto offset = _p.scrollOffset) + { + if (offset < 0) + { + // scrollOffset/offset = -1 + // +----------+ +----------+ + // | | | xxxxxxxxx| + // | xxxxxxxxx| -> |xxxxxxx | + // |xxxxxxx | | | + // +----------+ +----------+ + const auto dst = std::copy_n(_p.rows.begin() - offset, _p.rows.size() + offset, _p.rowsScratch.begin()); + std::copy_n(_p.rows.begin(), -offset, dst); + } + else + { // scrollOffset/offset = 1 // +----------+ +----------+ - // | xxxxxxxxx| | | + src < beg - // |xxxxxxx | -> | xxxxxxxxx| | ^ - // | | |xxxxxxx | v | < end - offset - // +----------+ +----------+ + dst < end - { - const auto beg = _r.cells.begin(); - const auto end = _r.cells.end(); - std::move_backward(beg, end - offset, end); - } - { - const auto beg = _r.cellGlyphMapping.begin(); - const auto end = _r.cellGlyphMapping.end(); - std::move_backward(beg, end - offset, end); - } + // | xxxxxxxxx| | | + // |xxxxxxx | -> | xxxxxxxxx| + // | | |xxxxxxx | + // +----------+ +----------+ + const auto dst = std::copy_n(_p.rows.end() - offset, offset, _p.rowsScratch.begin()); + std::copy_n(_p.rows.begin(), _p.rows.size() - offset, dst); } - } - _api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y }; - _r.dirtyRect = _api.dirtyRect; - _r.scrollOffset = _api.scrollOffset; + std::swap(_p.rows, _p.rowsScratch); - // Clear the previous cursor. PaintCursor() is only called if the cursor is on. - if (const auto r = _api.invalidatedCursorArea; r.non_empty()) - { - _setCellFlags(r, CellFlags::Cursor, CellFlags::None); - _r.dirtyRect |= til::rect{ r.left, r.top, r.right, r.bottom }; - } - - // This is an important block of code for our TileHashMap. - // We only process glyphs within the dirtyRect, but glyphs outside of the - // dirtyRect are still in use and shouldn't be discarded. This is critical - // if someone uses a tool like tmux to split the terminal horizontally. - // If they then print a lot of Unicode text on just one side, we have to - // ensure that the (for example) plain ASCII glyphs on the other half of the - // viewport are still retained. This bit of code "refreshes" those glyphs and - // brings them to the front of the LRU queue to prevent them from being reused. - { - const std::array ranges{ { - { 0, _api.dirtyRect.top }, - { _api.dirtyRect.bottom, _api.cellCount.y }, - } }; - const auto stride = static_cast(_r.cellCount.x); - - for (const auto& p : ranges) + // Now that the rows have scrolled, their cached dirty rects, naturally also need to do the same. + // It doesn't really matter that some of these will end up being out of bounds, + // because we'll call ShapedRow::Clear() later on which resets them. { - // We (ab)use the .x/.y members of the til::point as the - // respective [from,to) range of rows we need to makeNewest(). - const auto from = p.x; - const auto to = p.y; - - for (auto y = from; y < to; ++y) + const auto deltaPx = offset * _p.s->font->cellSize.y; + for (const auto r : _p.rows) { - auto it = _r.cellGlyphMapping.data() + stride * y; - const auto end = it + stride; - for (; it != end; ++it) + r->dirtyTop += deltaPx; + r->dirtyBottom += deltaPx; + } + } + + // Scrolling the background bitmap is a lot easier because we can rely on memmove which works + // with both forwards and backwards copying. It's a mystery why the STL doesn't have this. + { + const auto srcOffset = std::max(0, -offset) * gsl::narrow_cast(_p.colorBitmapRowStride); + const auto dstOffset = std::max(0, offset) * gsl::narrow_cast(_p.colorBitmapRowStride); + const auto count = _p.colorBitmapDepthStride - std::max(srcOffset, dstOffset); + assert(dstOffset >= 0 && dstOffset + count <= _p.colorBitmapDepthStride); + assert(srcOffset >= 0 && srcOffset + count <= _p.colorBitmapDepthStride); + + auto src = _p.colorBitmap.data() + srcOffset; + auto dst = _p.colorBitmap.data() + dstOffset; + const auto bytes = count * sizeof(u32); + + for (size_t i = 0; i < 2; ++i) + { + // Avoid bumping the colorBitmapGeneration unless necessary. This approx. further halves + // the (already small) GPU load. This could easily be replaced with some custom SIMD + // to avoid going over the memory twice, but... that's a story for another day. + if (memcmp(dst, src, bytes) != 0) { - _r.glyphs.makeNewest(*it); + memmove(dst, src, bytes); + _p.colorBitmapGenerations[i].bump(); } + + src += _p.colorBitmapDepthStride; + dst += _p.colorBitmapDepthStride; } } } + // This serves two purposes. For each invalidated row, this will: + // * Get the old dirty rect and mark that region as needing invalidation during the upcoming Present1(), + // because it'll now be replaced with something else (for instance nothing/whitespace). + // * Clear() them to prepare them for the new incoming content from the TextBuffer. + if (_p.invalidatedRows.non_empty()) + { + const til::CoordType targetSizeX = _p.s->targetSize.x; + const til::CoordType targetSizeY = _p.s->targetSize.y; + + _p.dirtyRectInPx.left = 0; + _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, _p.invalidatedRows.start * _p.s->font->cellSize.y); + _p.dirtyRectInPx.right = targetSizeX; + _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.invalidatedRows.end * _p.s->font->cellSize.y); + + for (auto y = _p.invalidatedRows.start; y < _p.invalidatedRows.end; ++y) + { + const auto r = _p.rows[y]; + const auto clampedTop = clamp(r->dirtyTop, 0, targetSizeY); + const auto clampedBottom = clamp(r->dirtyBottom, 0, targetSizeY); + + if (clampedTop != clampedBottom) + { + _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, clampedTop); + _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, clampedBottom); + } + + r->Clear(y, _p.s->font->cellSize.y); + } + } + +#if ATLAS_DEBUG_CONTINUOUS_REDRAW + _p.MarkAllAsDirty(); +#endif + return S_OK; } -catch (const wil::ResultException& exception) -{ - return _handleException(exception); -} CATCH_RETURN() [[nodiscard]] HRESULT AtlasEngine::EndPaint() noexcept @@ -303,6 +252,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; @@ -334,6 +293,9 @@ CATCH_RETURN() [[nodiscard]] HRESULT AtlasEngine::PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept { + const auto y = gsl::narrow_cast(clamp(targetRow, 0, _p.s->viewportCellCount.y)); + _p.rows[y]->lineRendition = lineRendition; + _api.lineRendition = lineRendition; return S_OK; } @@ -342,16 +304,20 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(gsl::span clusters, til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept +[[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(std::span clusters, til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept try { - const auto y = gsl::narrow_cast(clamp(coord.Y, 0, _api.cellCount.y)); + const auto y = gsl::narrow_cast(clamp(coord.y, 0, _p.s->viewportCellCount.y)); if (_api.lastPaintBufferLineCoord.y != y) { _flushBufferLine(); } + const auto shift = gsl::narrow_cast(_api.lineRendition != LineRendition::SingleWidth); + const auto x = gsl::narrow_cast(clamp(coord.x - (_p.s->viewportOffset.x >> shift), 0, _p.s->viewportCellCount.x)); + auto columnEnd = x; + // _api.bufferLineColumn contains 1 more item than _api.bufferLine, as it represents the // past-the-end index. It'll get appended again later once we built our new _api.bufferLine. if (!_api.bufferLineColumn.empty()) @@ -359,56 +325,52 @@ try _api.bufferLineColumn.pop_back(); } - // `TextBuffer` is buggy and allows a `Trailing` `DbcsAttribute` to be written - // into the first column. Since other code then blindly assumes that there's a - // preceding `Leading` character, we'll get called with a X coordinate of -1. - // - // This block can be removed after GH#13626 is merged. - if (coord.X < 0) - { - size_t offset = 0; - for (const auto& cluster : clusters) - { - offset++; - coord.X += cluster.GetColumns(); - if (coord.X >= 0) - { - _api.bufferLine.insert(_api.bufferLine.end(), coord.X, L' '); - _api.bufferLineColumn.insert(_api.bufferLineColumn.end(), coord.X, 0u); - break; - } - } - - clusters = clusters.subspan(offset); - } - - const auto x = gsl::narrow_cast(clamp(coord.X, 0, _api.cellCount.x)); - // Due to the current IRenderEngine interface (that wasn't refactored yet) we need to assemble // the current buffer line first as the remaining function operates on whole lines of text. { - auto column = x; for (const auto& cluster : clusters) { for (const auto& ch : cluster.GetText()) { _api.bufferLine.emplace_back(ch); - _api.bufferLineColumn.emplace_back(column); + _api.bufferLineColumn.emplace_back(columnEnd); } - - column += gsl::narrow_cast(cluster.GetColumns()); + columnEnd += gsl::narrow_cast(cluster.GetColumns()); } - _api.bufferLineColumn.emplace_back(column); + _api.bufferLineColumn.emplace_back(columnEnd); + } - const BufferLineMetadata metadata{ _api.currentColor, _api.flags }; - FAIL_FAST_IF(column > _api.bufferLineMetadata.size()); - std::fill_n(_api.bufferLineMetadata.data() + x, column - x, metadata); + { + const auto row = _p.colorBitmap.begin() + _p.colorBitmapRowStride * y; + auto beg = row + (static_cast(x) << shift); + auto end = row + (static_cast(columnEnd) << shift); + + const u32 colors[] = { + u32ColorPremultiply(_api.currentBackground), + _api.currentForeground, + }; + + for (size_t i = 0; i < 2; ++i) + { + const auto color = colors[i]; + + for (auto it = beg; it != end; ++it) + { + if (*it != color) + { + _p.colorBitmapGenerations[i].bump(); + std::fill(it, end, color); + break; + } + } + + beg += _p.colorBitmapDepthStride; + end += _p.colorBitmapDepthStride; + } } _api.lastPaintBufferLineCoord = { x, y }; - _api.bufferLineWasHyperlinked = false; - return S_OK; } CATCH_RETURN() @@ -416,16 +378,13 @@ CATCH_RETURN() [[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept try { - if (!_api.bufferLineWasHyperlinked && lines.test(GridLines::Underline) && WI_IsFlagClear(_api.flags, CellFlags::Underline)) - { - _api.bufferLineWasHyperlinked = true; - - WI_UpdateFlagsInMask(_api.flags, CellFlags::Underline | CellFlags::UnderlineDotted | CellFlags::UnderlineDouble, CellFlags::Underline); - - const BufferLineMetadata metadata{ _api.currentColor, _api.flags }; - const size_t x = _api.lastPaintBufferLineCoord.x; - std::fill_n(_api.bufferLineMetadata.data() + x, _api.bufferLineMetadata.size() - x, metadata); - } + const auto shift = gsl::narrow_cast(_api.lineRendition != LineRendition::SingleWidth); + const auto x = std::max(0, coordTarget.x - (_p.s->viewportOffset.x >> shift)); + const auto y = gsl::narrow_cast(clamp(coordTarget.y, 0, _p.s->viewportCellCount.y)); + const auto from = gsl::narrow_cast(clamp(x << shift, 0, _p.s->viewportCellCount.x - 1)); + const auto to = gsl::narrow_cast(clamp((x + cchLine) << shift, from, _p.s->viewportCellCount.x)); + const auto fg = gsl::narrow_cast(color) | 0xff000000; + _p.rows[y]->gridLineRanges.emplace_back(lines, fg, from, to); return S_OK; } CATCH_RETURN() @@ -438,14 +397,18 @@ try // As such we got to call _flushBufferLine() here just to be sure. _flushBufferLine(); - const u16r u16rect{ - rect.narrow_left(), - rect.narrow_top(), - rect.narrow_right(), - rect.narrow_bottom(), - }; - _setCellFlags(u16rect, CellFlags::Selected, CellFlags::Selected); - _r.dirtyRect |= rect; + const auto y = gsl::narrow_cast(clamp(rect.top, 0, _p.s->viewportCellCount.y)); + const auto from = gsl::narrow_cast(clamp(rect.left, 0, _p.s->viewportCellCount.x - 1)); + const auto to = gsl::narrow_cast(clamp(rect.right, from, _p.s->viewportCellCount.x)); + + auto& row = *_p.rows[y]; + row.selectionFrom = from; + row.selectionTo = to; + + _p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, from * _p.s->font->cellSize.x); + _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, y * _p.s->font->cellSize.y); + _p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, to * _p.s->font->cellSize.x); + _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.dirtyRectInPx.top + _p.s->font->cellSize.y); return S_OK; } CATCH_RETURN() @@ -459,30 +422,44 @@ try _flushBufferLine(); { - const CachedCursorOptions cachedOptions{ - gsl::narrow_cast(options.fUseColor ? options.cursorColor | 0xff000000 : INVALID_COLOR), - gsl::narrow_cast(options.cursorType), - gsl::narrow_cast(options.ulCursorHeightPercent), + const CursorSettings cachedOptions{ + .cursorColor = gsl::narrow_cast(options.fUseColor ? options.cursorColor | 0xff000000 : INVALID_COLOR), + .cursorType = gsl::narrow_cast(options.cursorType), + .heightPercentage = gsl::narrow_cast(options.ulCursorHeightPercent), }; - if (_r.cursorOptions != cachedOptions) + if (*_api.s->cursor != cachedOptions) { - _r.cursorOptions = cachedOptions; - WI_SetFlag(_r.invalidations, RenderInvalidations::Cursor); + *_api.s.write()->cursor.write() = cachedOptions; + *_p.s.write()->cursor.write() = cachedOptions; } } if (options.isOn) { - const auto point = options.coordCursor; - // TODO: options.coordCursor can contain invalid out of bounds coordinates when - // the window is being resized and the cursor is on the last line of the viewport. - const auto x = gsl::narrow_cast(clamp(point.X, 0, _r.cellCount.x - 1)); - const auto y = gsl::narrow_cast(clamp(point.Y, 0, _r.cellCount.y - 1)); const auto cursorWidth = 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar)); - const auto right = gsl::narrow_cast(clamp(x + cursorWidth, 0, _r.cellCount.x - 0)); - const auto bottom = gsl::narrow_cast(y + 1); - _setCellFlags({ x, y, right, bottom }, CellFlags::Cursor, CellFlags::Cursor); - _r.dirtyRect |= til::rect{ x, y, right, bottom }; + const auto top = options.coordCursor.y; + const auto bottom = top + 1; + const auto shift = gsl::narrow_cast(_p.rows[top]->lineRendition != LineRendition::SingleWidth); + auto left = options.coordCursor.x - (_p.s->viewportOffset.x >> shift); + auto right = left + cursorWidth; + + left <<= shift; + right <<= shift; + + _p.cursorRect = { + std::max(left, 0), + std::max(top, 0), + std::min(right, _p.s->viewportCellCount.x), + std::min(bottom, _p.s->viewportCellCount.y), + }; + + if (_p.cursorRect) + { + _p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, left * _p.s->font->cellSize.x); + _p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, top * _p.s->font->cellSize.y); + _p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, right * _p.s->font->cellSize.x); + _p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, bottom * _p.s->font->cellSize.y); + } } return S_OK; @@ -498,40 +475,23 @@ try if (!isSettingDefaultBrushes) { - const auto hyperlinkId = textAttributes.GetHyperlinkId(); - - auto flags = CellFlags::None; - WI_SetFlagIf(flags, CellFlags::BorderLeft, textAttributes.IsLeftVerticalDisplayed()); - WI_SetFlagIf(flags, CellFlags::BorderTop, textAttributes.IsTopHorizontalDisplayed()); - WI_SetFlagIf(flags, CellFlags::BorderRight, textAttributes.IsRightVerticalDisplayed()); - WI_SetFlagIf(flags, CellFlags::BorderBottom, textAttributes.IsBottomHorizontalDisplayed()); - WI_SetFlagIf(flags, CellFlags::Underline, textAttributes.IsUnderlined()); - WI_SetFlagIf(flags, CellFlags::UnderlineDotted, hyperlinkId != 0); - WI_SetFlagIf(flags, CellFlags::UnderlineDouble, textAttributes.IsDoublyUnderlined()); - WI_SetFlagIf(flags, CellFlags::Strikethrough, textAttributes.IsCrossedOut()); - - if (_api.hyperlinkHoveredId && _api.hyperlinkHoveredId == hyperlinkId) - { - WI_SetFlag(flags, CellFlags::Underline); - WI_ClearAllFlags(flags, CellFlags::UnderlineDotted | CellFlags::UnderlineDouble); - } - - const u32x2 newColors{ gsl::narrow_cast(fg), gsl::narrow_cast(bg) }; - const AtlasKeyAttributes attributes{ 0, textAttributes.IsIntense() && renderSettings.GetRenderMode(RenderSettings::Mode::IntenseIsBold), textAttributes.IsItalic(), 0 }; + auto attributes = FontRelevantAttributes::None; + WI_SetFlagIf(attributes, FontRelevantAttributes::Bold, textAttributes.IsIntense() && renderSettings.GetRenderMode(RenderSettings::Mode::IntenseIsBold)); + WI_SetFlagIf(attributes, FontRelevantAttributes::Italic, textAttributes.IsItalic()); if (_api.attributes != attributes) { _flushBufferLine(); } - _api.currentColor = newColors; + _api.currentBackground = gsl::narrow_cast(bg); + _api.currentForeground = gsl::narrow_cast(fg); _api.attributes = attributes; - _api.flags = flags; } - else if (textAttributes.BackgroundIsDefault() && bg != _r.backgroundColor) + else if (textAttributes.BackgroundIsDefault() && bg != _api.s->misc->backgroundColor) { - _r.backgroundColor = bg; - WI_SetFlag(_r.invalidations, RenderInvalidations::ConstBuffer); + _api.s.write()->misc.write()->backgroundColor = bg; + _p.s.write()->misc.write()->backgroundColor = bg; } return S_OK; @@ -540,695 +500,119 @@ CATCH_RETURN() #pragma endregion -[[nodiscard]] HRESULT AtlasEngine::_handleException(const wil::ResultException& exception) noexcept +void AtlasEngine::_handleSettingsUpdate() { - const auto hr = exception.GetErrorCode(); - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET || hr == D2DERR_RECREATE_TARGET) + const auto targetChanged = _p.s->target != _api.s->target; + const auto fontChanged = _p.s->font != _api.s->font; + const auto cellCountChanged = _p.s->viewportCellCount != _api.s->viewportCellCount; + + _p.s = _api.s; + + if (targetChanged) { - WI_SetFlag(_api.invalidations, ApiInvalidations::Device); - return E_PENDING; // Indicate a retry to the renderer + // target->useSoftwareRendering affects the selection of our IDXGIAdapter which requires us to reset _p.dxgi. + // This will indirectly also recreate the backend, when AtlasEngine::_recreateAdapter() detects this change. + _p.dxgi = {}; + } + if (fontChanged) + { + _recreateFontDependentResources(); + } + if (cellCountChanged) + { + _recreateCellCountDependentResources(); } - // NOTE: This isn't thread safe, as _handleException is called by AtlasEngine.r.cpp. - // However it's not too much of a concern at the moment as SetWarningCallback() - // is only called once during construction in practice. - if (_api.warningCallback) - { - try - { - _api.warningCallback(hr); - } - CATCH_LOG() - } - - return hr; -} - -void AtlasEngine::_createResources() -{ - _releaseSwapChain(); - _r = {}; - -#ifdef NDEBUG - static constexpr -#endif - auto deviceFlags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT; - -#ifndef NDEBUG - // DXGI debug messages + enabling D3D11_CREATE_DEVICE_DEBUG if the Windows SDK was installed. - if (const wil::unique_hmodule module{ LoadLibraryExW(L"dxgi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) }) - { - deviceFlags |= D3D11_CREATE_DEVICE_DEBUG; - - if (const auto DXGIGetDebugInterface1 = GetProcAddressByFunctionDeclaration(module.get(), DXGIGetDebugInterface1)) - { - if (wil::com_ptr infoQueue; SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(infoQueue.addressof())))) - { - // I didn't want to link with dxguid.lib just for getting DXGI_DEBUG_ALL. This GUID is publicly documented. - static constexpr GUID dxgiDebugAll = { 0xe48ae283, 0xda80, 0x490b, { 0x87, 0xe6, 0x43, 0xe9, 0xa9, 0xcf, 0xda, 0x8 } }; - for (const auto severity : std::array{ DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_WARNING }) - { - infoQueue->SetBreakOnSeverity(dxgiDebugAll, severity, true); - } - } - - if (wil::com_ptr debug; SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(debug.addressof())))) - { - debug->EnableLeakTrackingForThread(); - } - } - } -#endif // NDEBUG - - // D3D device setup (basically a D3D class factory) - { - wil::com_ptr deviceContext; - - static constexpr std::array featureLevels{ - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL_9_1, - }; - - auto hr = E_UNEXPECTED; - - if (!_api.useSoftwareRendering) - { - // Why D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS: - // This flag prevents the driver from creating a large thread pool for things like shader computations - // that would be advantageous for games. For us this has only a minimal performance benefit, - // but comes with a large memory usage overhead. At the time of writing the Nvidia - // driver launches $cpu_thread_count more worker threads without this flag. - hr = D3D11CreateDevice( - /* pAdapter */ nullptr, - /* DriverType */ D3D_DRIVER_TYPE_HARDWARE, - /* Software */ nullptr, - /* Flags */ deviceFlags | D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS, - /* pFeatureLevels */ featureLevels.data(), - /* FeatureLevels */ gsl::narrow_cast(featureLevels.size()), - /* SDKVersion */ D3D11_SDK_VERSION, - /* ppDevice */ _r.device.put(), - /* pFeatureLevel */ nullptr, - /* ppImmediateContext */ deviceContext.put()); - } - if (FAILED(hr)) - { - hr = D3D11CreateDevice( - /* pAdapter */ nullptr, - /* DriverType */ D3D_DRIVER_TYPE_WARP, - /* Software */ nullptr, - /* Flags */ deviceFlags, - /* pFeatureLevels */ featureLevels.data(), - /* FeatureLevels */ gsl::narrow_cast(featureLevels.size()), - /* SDKVersion */ D3D11_SDK_VERSION, - /* ppDevice */ _r.device.put(), - /* pFeatureLevel */ nullptr, - /* ppImmediateContext */ deviceContext.put()); - } - THROW_IF_FAILED(hr); - - _r.deviceContext = deviceContext.query(); - } - -#ifndef NDEBUG - // D3D debug messages - if (deviceFlags & D3D11_CREATE_DEVICE_DEBUG) - { - const auto infoQueue = _r.device.query(); - for (const auto severity : std::array{ D3D11_MESSAGE_SEVERITY_CORRUPTION, D3D11_MESSAGE_SEVERITY_ERROR, D3D11_MESSAGE_SEVERITY_WARNING }) - { - infoQueue->SetBreakOnSeverity(severity, true); - } - } -#endif // NDEBUG - - { - wil::com_ptr dxgiAdapter; - THROW_IF_FAILED(_r.device.query()->GetParent(__uuidof(dxgiAdapter), dxgiAdapter.put_void())); - THROW_IF_FAILED(dxgiAdapter->GetParent(__uuidof(_r.dxgiFactory), _r.dxgiFactory.put_void())); - - DXGI_ADAPTER_DESC1 desc; - THROW_IF_FAILED(dxgiAdapter->GetDesc1(&desc)); - _r.d2dMode = debugForceD2DMode || WI_IsAnyFlagSet(desc.Flags, DXGI_ADAPTER_FLAG_REMOTE | DXGI_ADAPTER_FLAG_SOFTWARE); - } - - const auto featureLevel = _r.device->GetFeatureLevel(); - - if (featureLevel < D3D_FEATURE_LEVEL_10_0) - { - _r.d2dMode = true; - } - else if (featureLevel < D3D_FEATURE_LEVEL_11_0) - { - D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS options; - THROW_IF_FAILED(_r.device->CheckFeatureSupport(D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, &options, sizeof(options))); - if (!options.ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x) - { - _r.d2dMode = true; - } - } - - if (!_r.d2dMode) - { - // Our constant buffer will never get resized - { - D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = sizeof(ConstBuffer); - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put())); - } - - THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put())); - THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put())); - - if (!_api.customPixelShaderPath.empty()) - { - const char* target = nullptr; - switch (featureLevel) - { - case D3D_FEATURE_LEVEL_10_0: - target = "ps_4_0"; - break; - case D3D_FEATURE_LEVEL_10_1: - target = "ps_4_1"; - break; - default: - target = "ps_5_0"; - break; - } - - static constexpr auto flags = - D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR -#ifdef NDEBUG - | D3DCOMPILE_OPTIMIZATION_LEVEL3; -#else - // Only enable strictness and warnings in DEBUG mode - // as these settings makes it very difficult to develop - // shaders as windows terminal is not telling the user - // what's wrong, windows terminal just fails. - // Keep it in DEBUG mode to catch errors in shaders - // shipped with windows terminal - | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS | D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; -#endif - - wil::com_ptr error; - wil::com_ptr blob; - const auto hr = D3DCompileFromFile( - /* pFileName */ _api.customPixelShaderPath.c_str(), - /* pDefines */ nullptr, - /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, - /* pEntrypoint */ "main", - /* pTarget */ target, - /* Flags1 */ flags, - /* Flags2 */ 0, - /* ppCode */ blob.addressof(), - /* ppErrorMsgs */ error.addressof()); - - // Unless we can determine otherwise, assume this shader requires evaluation every frame - _r.requiresContinuousRedraw = true; - - if (SUCCEEDED(hr)) - { - THROW_IF_FAILED(_r.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _r.customPixelShader.put())); - - // Try to determine whether the shader uses the Time variable - wil::com_ptr reflector; - if (SUCCEEDED_LOG(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(reflector.put())))) - { - if (ID3D11ShaderReflectionConstantBuffer* constantBufferReflector = reflector->GetConstantBufferByIndex(0)) // shader buffer - { - if (ID3D11ShaderReflectionVariable* variableReflector = constantBufferReflector->GetVariableByIndex(0)) // time - { - D3D11_SHADER_VARIABLE_DESC variableDescriptor; - if (SUCCEEDED_LOG(variableReflector->GetDesc(&variableDescriptor))) - { - // only if time is used - _r.requiresContinuousRedraw = WI_IsFlagSet(variableDescriptor.uFlags, D3D_SVF_USED); - } - } - } - } - } - else - { - if (error) - { - LOG_HR_MSG(hr, "%*hs", error->GetBufferSize(), error->GetBufferPointer()); - } - else - { - LOG_HR(hr); - } - if (_api.warningCallback) - { - _api.warningCallback(D2DERR_SHADER_COMPILE_FAILED); - } - } - } - else if (_api.useRetroTerminalEffect) - { - THROW_IF_FAILED(_r.device->CreatePixelShader(&custom_shader_ps[0], sizeof(custom_shader_ps), nullptr, _r.customPixelShader.put())); - // We know the built-in retro shader doesn't require continuous redraw. - _r.requiresContinuousRedraw = false; - } - - if (_r.customPixelShader) - { - THROW_IF_FAILED(_r.device->CreateVertexShader(&custom_shader_vs[0], sizeof(custom_shader_vs), nullptr, _r.customVertexShader.put())); - - { - D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = sizeof(CustomConstBuffer); - desc.Usage = D3D11_USAGE_DYNAMIC; - desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.customShaderConstantBuffer.put())); - } - - { - D3D11_SAMPLER_DESC desc{}; - desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - desc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER; - desc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER; - desc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER; - desc.MaxAnisotropy = 1; - desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS; - desc.MaxLOD = D3D11_FLOAT32_MAX; - THROW_IF_FAILED(_r.device->CreateSamplerState(&desc, _r.customShaderSamplerState.put())); - } - - _r.customShaderStartTime = std::chrono::steady_clock::now(); - } - } - - WI_ClearFlag(_api.invalidations, ApiInvalidations::Device); - WI_SetAllFlags(_api.invalidations, ApiInvalidations::SwapChain); -} - -void AtlasEngine::_releaseSwapChain() -{ - // Flush() docs: - // However, if an application must actually destroy an old swap chain and create a new swap chain, - // the application must force the destruction of all objects that the application freed. - // To force the destruction, call ID3D11DeviceContext::ClearState (or otherwise ensure - // no views are bound to pipeline state), and then call Flush on the immediate context. - if (_r.swapChain && _r.deviceContext) - { - if (_r.d2dMode) - { - _r.d2dRenderTarget.reset(); - } - _r.frameLatencyWaitableObject.reset(); - _r.swapChain.reset(); - _r.renderTargetView.reset(); - _r.deviceContext->ClearState(); - _r.deviceContext->Flush(); - } -} - -void AtlasEngine::_createSwapChain() -{ - _releaseSwapChain(); - - // D3D swap chain setup (the thing that allows us to present frames on the screen) - { - // With C++20 we'll finally have designated initializers. - DXGI_SWAP_CHAIN_DESC1 desc{}; - desc.Width = _api.sizeInPixel.x; - desc.Height = _api.sizeInPixel.y; - desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - desc.SampleDesc.Count = 1; - desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - // Sometimes up to 2 buffers are locked, for instance during screen capture or when moving the window. - // 3 buffers seems to guarantee a stable framerate at display frequency at all times. - desc.BufferCount = 3; - desc.Scaling = DXGI_SCALING_NONE; - // DXGI_SWAP_EFFECT_FLIP_DISCARD is a mode that was created at a time were display drivers - // lacked support for Multiplane Overlays (MPO) and were copying buffers was expensive. - // This allowed DWM to quickly draw overlays (like gamebars) on top of rendered content. - // With faster GPU memory in general and with support for MPO in particular this isn't - // really an advantage anymore. Instead DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL allows for a - // more "intelligent" composition and display updates to occur like Panel Self Refresh - // (PSR) which requires dirty rectangles (Present1 API) to work correctly. - desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; - // If our background is opaque we can enable "independent" flips by setting DXGI_ALPHA_MODE_IGNORE. - // As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically. - desc.AlphaMode = _api.backgroundOpaqueMixin ? DXGI_ALPHA_MODE_IGNORE : DXGI_ALPHA_MODE_PREMULTIPLIED; - desc.Flags = debugGeneralPerformance ? 0 : DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; - - wil::com_ptr dxgiFactory; - THROW_IF_FAILED(CreateDXGIFactory1(IID_PPV_ARGS(dxgiFactory.addressof()))); - - if (_api.hwnd) - { - THROW_IF_FAILED(dxgiFactory->CreateSwapChainForHwnd(_r.device.get(), _api.hwnd, &desc, nullptr, nullptr, _r.swapChain.put())); - } - else - { - const wil::unique_hmodule module{ LoadLibraryExW(L"dcomp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) }; - THROW_LAST_ERROR_IF(!module); - const auto DCompositionCreateSurfaceHandle = GetProcAddressByFunctionDeclaration(module.get(), DCompositionCreateSurfaceHandle); - THROW_LAST_ERROR_IF(!DCompositionCreateSurfaceHandle); - - // As per: https://docs.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatesurfacehandle - static constexpr DWORD COMPOSITIONSURFACE_ALL_ACCESS = 0x0003L; - THROW_IF_FAILED(DCompositionCreateSurfaceHandle(COMPOSITIONSURFACE_ALL_ACCESS, nullptr, _api.swapChainHandle.put())); - THROW_IF_FAILED(dxgiFactory.query()->CreateSwapChainForCompositionSurfaceHandle(_r.device.get(), _api.swapChainHandle.get(), &desc, nullptr, _r.swapChain.put())); - } - - if constexpr (!debugGeneralPerformance) - { - const auto swapChain2 = _r.swapChain.query(); - _r.frameLatencyWaitableObject.reset(swapChain2->GetFrameLatencyWaitableObject()); - THROW_LAST_ERROR_IF(!_r.frameLatencyWaitableObject); - } - } - - // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: - // > For every frame it renders, the app should wait on this handle before starting any rendering operations. - // > Note that this requirement includes the first frame the app renders with the swap chain. - _r.waitForPresentation = true; - WaitUntilCanRender(); - - if (_api.swapChainChangedCallback) - { - try - { - _api.swapChainChangedCallback(_api.swapChainHandle.get()); - } - CATCH_LOG(); - } - - WI_ClearFlag(_api.invalidations, ApiInvalidations::SwapChain); - WI_SetAllFlags(_api.invalidations, ApiInvalidations::Size | ApiInvalidations::Font); -} - -void AtlasEngine::_recreateSizeDependentResources() -{ - // ResizeBuffer() docs: - // Before you call ResizeBuffers, ensure that the application releases all references [...]. - // You can use ID3D11DeviceContext::ClearState to ensure that all [internal] references are released. - // The _r.cells check exists simply to prevent us from calling ResizeBuffers() on startup (i.e. when `_r` is empty). - if (_r.cells) - { - if (_r.d2dMode) - { - _r.d2dRenderTarget.reset(); - } - _r.renderTargetView.reset(); - _r.deviceContext->ClearState(); - _r.deviceContext->Flush(); - THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, debugGeneralPerformance ? 0 : DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)); - } - - const auto totalCellCount = static_cast(_api.cellCount.x) * static_cast(_api.cellCount.y); - const auto resize = _api.cellCount != _r.cellCount; - - if (resize) - { - // Let's guess that every cell consists of a surrogate pair. - const auto projectedTextSize = static_cast(_api.cellCount.x) * 2; - // IDWriteTextAnalyzer::GetGlyphs says: - // The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16). - const auto projectedGlyphSize = 3 * projectedTextSize / 2 + 16; - - // This buffer is a bit larger than the others (multiple MB). - // Prevent a memory usage spike, by first deallocating and then allocating. - _r.cells = {}; - _r.cellGlyphMapping = {}; - // Our render loop heavily relies on memcpy() which is between 1.5x - // and 40x faster for allocations with an alignment of 32 or greater. - // (40x on AMD Zen1-3, which have a rep movsb performance issue. MSFT:33358259.) - _r.cells = Buffer{ totalCellCount }; - _r.cellGlyphMapping = Buffer{ totalCellCount }; - _r.cellCount = _api.cellCount; - _r.tileAllocator.setMaxArea(_api.sizeInPixel); - - // .clear() doesn't free the memory of these buffers. - // This code allows them to shrink again. - _api.bufferLine = {}; - _api.bufferLine.reserve(projectedTextSize); - _api.bufferLineColumn.reserve(projectedTextSize + 1); - _api.bufferLineMetadata = Buffer{ _api.cellCount.x }; - _api.analysisResults = {}; - - _api.clusterMap = Buffer{ projectedTextSize }; - _api.textProps = Buffer{ projectedTextSize }; - _api.glyphIndices = Buffer{ projectedGlyphSize }; - _api.glyphProps = Buffer{ projectedGlyphSize }; - _api.glyphAdvances = Buffer{ projectedGlyphSize }; - _api.glyphOffsets = Buffer{ projectedGlyphSize }; - - // Initialize cellGlyphMapping with valid data (whitespace), so that it can be - // safely used by the TileHashMap refresh logic via makeNewest() in StartPaint(). - { - u16x2* coords{}; - AtlasKey key{ { .cellCount = 1 }, 1, L" " }; - AtlasValue value{ CellFlags::None, 1, &coords }; - - coords[0] = _r.tileAllocator.allocate(_r.glyphs); - - const auto it = _r.glyphs.insert(std::move(key), std::move(value)); - _r.glyphQueue.emplace_back(it); - - std::fill(_r.cellGlyphMapping.begin(), _r.cellGlyphMapping.end(), it); - } - } - - if (!_r.d2dMode) - { - // The RenderTargetView is later used with OMSetRenderTargets - // to tell D3D where stuff is supposed to be rendered at. - { - wil::com_ptr buffer; - THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void())); - THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put())); - } - if (_r.customPixelShader) - { - D3D11_TEXTURE2D_DESC desc{}; - desc.Width = _api.sizeInPixel.x; - desc.Height = _api.sizeInPixel.y; - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - desc.SampleDesc = { 1, 0 }; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; - THROW_IF_FAILED(_r.device->CreateTexture2D(&desc, nullptr, _r.customOffscreenTexture.addressof())); - THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.customOffscreenTexture.get(), nullptr, _r.customOffscreenTextureView.addressof())); - THROW_IF_FAILED(_r.device->CreateRenderTargetView(_r.customOffscreenTexture.get(), nullptr, _r.customOffscreenTextureTargetView.addressof())); - } - - // Tell D3D which parts of the render target will be visible. - // Everything outside of the viewport will be black. - { - D3D11_VIEWPORT viewport{}; - viewport.Width = static_cast(_api.sizeInPixel.x); - viewport.Height = static_cast(_api.sizeInPixel.y); - _r.deviceContext->RSSetViewports(1, &viewport); - } - - if (resize) - { - D3D11_BUFFER_DESC desc; - desc.ByteWidth = gsl::narrow(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX! - desc.Usage = D3D11_USAGE_DYNAMIC; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; - desc.StructureByteStride = sizeof(Cell); - THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put())); - THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put())); - } - - // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state. - // This forces us to set up everything up from scratch again. - _setShaderResources(); - } - - WI_ClearFlag(_api.invalidations, ApiInvalidations::Size); - WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer); + _api.invalidatedRows = invalidatedRowsAll; } void AtlasEngine::_recreateFontDependentResources() { - { - // We're likely resizing the atlas anyways and can - // thus also release any of these buffers prematurely. - _r.d2dRenderTarget.reset(); // depends on _r.atlasBuffer - _r.atlasView.reset(); - _r.atlasBuffer.reset(); - } + _api.replacementCharacterFontFace.reset(); + _api.replacementCharacterGlyphIndex = 0; + _api.replacementCharacterLookedUp = false; - // D3D { - const auto scaling = GetScaling(); + wchar_t localeName[LOCALE_NAME_MAX_LENGTH]; - _r.cellSizeDIP.x = static_cast(_api.fontMetrics.cellSize.x) / scaling; - _r.cellSizeDIP.y = static_cast(_api.fontMetrics.cellSize.y) / scaling; - _r.cellCount = _api.cellCount; - _r.dpi = _api.dpi; - _r.fontMetrics = _api.fontMetrics; - _r.dipPerPixel = static_cast(USER_DEFAULT_SCREEN_DPI) / static_cast(_r.dpi); - _r.pixelPerDIP = static_cast(_r.dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - _r.atlasSizeInPixel = { 0, 0 }; - _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel }; - - _r.glyphs = {}; - _r.glyphQueue = {}; - _r.glyphQueue.reserve(64); - } - // D3D specifically for UpdateDpi() - // This compensates for the built in scaling factor in a XAML SwapChainPanel (CompositionScaleX/Y). - if (!_api.hwnd) - { - if (const auto swapChain2 = _r.swapChain.try_query()) + if (FAILED(GetUserDefaultLocaleName(&localeName[0], LOCALE_NAME_MAX_LENGTH))) { - const auto inverseScale = static_cast(USER_DEFAULT_SCREEN_DPI) / static_cast(_api.dpi); - DXGI_MATRIX_3X2_F matrix{}; - matrix._11 = inverseScale; - matrix._22 = inverseScale; - THROW_IF_FAILED(swapChain2->SetMatrixTransform(&matrix)); + memcpy(&localeName[0], L"en-US", 12); + } + + _api.userLocaleName = std::wstring{ &localeName[0] }; + } + + if (_p.s->font->fontAxisValues.empty()) + { + for (auto& axes : _api.textFormatAxes) + { + axes = {}; } } - - // D2D + else { // See AtlasEngine::UpdateFont. // It hardcodes indices 0/1/2 in fontAxisValues to the weight/italic/slant axes. - // If they're -1.0f they haven't been set by the user and must be filled by us. - // When we call SetFontAxisValues() we basically override (disable) DirectWrite's internal font axes, - // and if either of the 3 aren't set we'd make it impossible for the user to see bold/italic text. -#pragma warning(suppress : 26494) // Variable 'standardAxes' is uninitialized. Always initialize an object (type.5). - std::array standardAxes; + // If they're -1 they haven't been set by the user and must be filled by us. + const auto& standardAxes = _p.s->font->fontAxisValues; + auto fontAxisValues = _p.s->font->fontAxisValues; - if (!_api.fontAxisValues.empty()) + for (size_t i = 0; i < 4; ++i) { - Expects(_api.fontAxisValues.size() >= standardAxes.size()); - memcpy(standardAxes.data(), _api.fontAxisValues.data(), sizeof(standardAxes)); - } - - const auto restoreFontAxisValues = wil::scope_exit([&]() noexcept { - if (!_api.fontAxisValues.empty()) - { - memcpy(_api.fontAxisValues.data(), standardAxes.data(), sizeof(standardAxes)); - } - }); - - for (auto italic = 0; italic < 2; ++italic) - { - for (auto bold = 0; bold < 2; ++bold) - { - const auto fontWeight = bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight); - const auto fontStyle = italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; - auto& textFormat = _r.textFormats[italic][bold]; - - wil::com_ptr font; - THROW_IF_FAILED(_r.fontMetrics.fontFamily->GetFirstMatchingFont(fontWeight, DWRITE_FONT_STRETCH_NORMAL, fontStyle, font.addressof())); - THROW_IF_FAILED(font->CreateFontFace(_r.fontFaces[italic << 1 | bold].put())); - - THROW_IF_FAILED(_sr.dwriteFactory->CreateTextFormat(_api.fontMetrics.fontName.c_str(), _api.fontMetrics.fontCollection.get(), fontWeight, fontStyle, DWRITE_FONT_STRETCH_NORMAL, _api.fontMetrics.fontSizeInDIP, L"", textFormat.put())); - THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); - - // DWRITE_LINE_SPACING_METHOD_UNIFORM: - // > Lines are explicitly set to uniform spacing, regardless of contained font sizes. - // > This can be useful to avoid the uneven appearance that can occur from font fallback. - // We want that. Otherwise fallback fonts might be rendered with an incorrect baseline and get cut off vertically. - THROW_IF_FAILED(textFormat->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, _r.cellSizeDIP.y, _api.fontMetrics.baselineInDIP)); - - // NOTE: SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER) breaks certain - // bitmap fonts which expect glyphs to be laid out left-aligned. - - // NOTE: SetAutomaticFontAxes(DWRITE_AUTOMATIC_FONT_AXES_OPTICAL_SIZE) breaks certain - // fonts making them look fairly unslightly. With no option to easily disable this - // feature in Windows Terminal, it's better left disabled by default. - - if (!_api.fontAxisValues.empty()) - { - if (const auto textFormat3 = textFormat.try_query()) - { - // The wght axis defaults to the font weight. - _api.fontAxisValues[0].value = bold || standardAxes[0].value == -1.0f ? static_cast(fontWeight) : standardAxes[0].value; - // The ital axis defaults to 1 if this is italic and 0 otherwise. - _api.fontAxisValues[1].value = italic ? 1.0f : (standardAxes[1].value == -1.0f ? 0.0f : standardAxes[1].value); - // The slnt axis defaults to -12 if this is italic and 0 otherwise. - _api.fontAxisValues[2].value = italic ? -12.0f : (standardAxes[2].value == -1.0f ? 0.0f : standardAxes[2].value); - - THROW_IF_FAILED(textFormat3->SetFontAxisValues(_api.fontAxisValues.data(), gsl::narrow_cast(_api.fontAxisValues.size()))); - _r.textFormatAxes[italic][bold] = { _api.fontAxisValues.data(), _api.fontAxisValues.size() }; - } - } - } + const auto bold = (i & static_cast(FontRelevantAttributes::Bold)) != 0; + const auto italic = (i & static_cast(FontRelevantAttributes::Italic)) != 0; + // The wght axis defaults to the font weight. + fontAxisValues[0].value = bold ? DWRITE_FONT_WEIGHT_BOLD : (standardAxes[0].value < 0 ? static_cast(_p.s->font->fontWeight) : standardAxes[0].value); + // The ital axis defaults to 1 if this is italic and 0 otherwise. + fontAxisValues[1].value = italic ? 1.0f : (standardAxes[1].value < 0 ? 0.0f : standardAxes[1].value); + // The slnt axis defaults to -12 if this is italic and 0 otherwise. + fontAxisValues[2].value = italic ? -12.0f : (standardAxes[2].value < 0 ? 0.0f : standardAxes[2].value); + _api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() }; } } +} + +void AtlasEngine::_recreateCellCountDependentResources() +{ + // Let's guess that every cell consists of a surrogate pair. + const auto projectedTextSize = static_cast(_p.s->viewportCellCount.x) * 2; + // IDWriteTextAnalyzer::GetGlyphs says: + // The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16). + const auto projectedGlyphSize = 3 * projectedTextSize / 2 + 16; + + _api.bufferLine = std::vector{}; + _api.bufferLine.reserve(projectedTextSize); + _api.bufferLineColumn.reserve(projectedTextSize + 1); + + _api.analysisResults = std::vector{}; + _api.clusterMap = Buffer{ projectedTextSize }; + _api.textProps = Buffer{ projectedTextSize }; + _api.glyphIndices = Buffer{ projectedGlyphSize }; + _api.glyphProps = Buffer{ projectedGlyphSize }; + _api.glyphAdvances = Buffer{ projectedGlyphSize }; + _api.glyphOffsets = Buffer{ projectedGlyphSize }; + + _p.unorderedRows = Buffer(_p.s->viewportCellCount.y); + _p.rowsScratch = Buffer(_p.s->viewportCellCount.y); + _p.rows = Buffer(_p.s->viewportCellCount.y); + + // Our render loop heavily relies on memcpy() which is up to between 1.5x (Intel) + // and 40x (AMD) faster for allocations with an alignment of 32 or greater. + // backgroundBitmapStride is a "count" of u32 and not in bytes, + // so we round up to multiple of 8 because 8 * sizeof(u32) == 32. + _p.colorBitmapRowStride = (static_cast(_p.s->viewportCellCount.x) + 7) & ~7; + _p.colorBitmapDepthStride = _p.colorBitmapRowStride * _p.s->viewportCellCount.y; + _p.colorBitmap = Buffer(_p.colorBitmapDepthStride * 2); + _p.backgroundBitmap = { _p.colorBitmap.data(), _p.colorBitmapDepthStride }; + _p.foregroundBitmap = { _p.colorBitmap.data() + _p.colorBitmapDepthStride, _p.colorBitmapDepthStride }; + + memset(_p.colorBitmap.data(), 0, _p.colorBitmap.size() * sizeof(u32)); + + auto it = _p.unorderedRows.data(); + for (auto& r : _p.rows) { - _r.typography.reset(); - - if (!_api.fontFeatures.empty()) - { - _sr.dwriteFactory->CreateTypography(_r.typography.addressof()); - for (const auto& v : _api.fontFeatures) - { - THROW_IF_FAILED(_r.typography->AddFontFeature(v)); - } - } - } - - WI_ClearFlag(_api.invalidations, ApiInvalidations::Font); - WI_SetAllFlags(_r.invalidations, RenderInvalidations::Cursor | RenderInvalidations::ConstBuffer); -} - -IDWriteTextFormat* AtlasEngine::_getTextFormat(bool bold, bool italic) const noexcept -{ - return _r.textFormats[italic][bold].get(); -} - -const AtlasEngine::Buffer& AtlasEngine::_getTextFormatAxis(bool bold, bool italic) const noexcept -{ - return _r.textFormatAxes[italic][bold]; -} - -AtlasEngine::Cell* AtlasEngine::_getCell(u16 x, u16 y) noexcept -{ - assert(x < _r.cellCount.x); - assert(y < _r.cellCount.y); - return _r.cells.data() + static_cast(_r.cellCount.x) * y + x; -} - -AtlasEngine::TileHashMap::iterator* AtlasEngine::_getCellGlyphMapping(u16 x, u16 y) noexcept -{ - assert(x < _r.cellCount.x); - assert(y < _r.cellCount.y); - return _r.cellGlyphMapping.data() + static_cast(_r.cellCount.x) * y + x; -} - -void AtlasEngine::_setCellFlags(u16r coords, CellFlags mask, CellFlags bits) noexcept -{ - assert(coords.left <= coords.right); - assert(coords.top <= coords.bottom); - assert(coords.right <= _r.cellCount.x); - assert(coords.bottom <= _r.cellCount.y); - - const auto filter = ~mask; - const auto width = static_cast(coords.right) - coords.left; - const auto height = static_cast(coords.bottom) - coords.top; - const auto stride = static_cast(_r.cellCount.x); - auto row = _r.cells.data() + static_cast(_r.cellCount.x) * coords.top + coords.left; - const auto end = row + height * stride; - - for (; row != end; row += stride) - { - const auto dataEnd = row + width; - for (auto data = row; data != dataEnd; ++data) - { - const auto current = data->flags; - data->flags = (current & filter) | bits; - } + r = it++; } } @@ -1247,398 +631,370 @@ void AtlasEngine::_flushBufferLine() // This would seriously blow us up otherwise. Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1); - // GH#13962: With the lack of proper LineRendition support, just fill - // the remaining columns with whitespace to prevent any weird artifacts. - for (auto lastColumn = _api.bufferLineColumn.back(); lastColumn < _api.cellCount.x;) - { - ++lastColumn; - _api.bufferLine.emplace_back(L' '); - _api.bufferLineColumn.emplace_back(lastColumn); - } - - // NOTE: - // This entire function is one huge hack to see if it works. - - // UH OH UNICODE MADNESS AHEAD - // - // # What do we want? - // - // Segment a line of text (_api.bufferLine) into unicode "clusters". - // Each cluster is one "whole" glyph with diacritics, ligatures, zero width joiners - // and whatever else, that should be cached as a whole in our texture atlas. - // - // # How do we get that? - // - // ## The unfortunate preface - // - // DirectWrite can be "reluctant" to segment text into clusters and I found no API which offers simply that. - // What it offers are a large number of low level APIs that can sort of be used in combination to do this. - // The resulting text parsing is very slow unfortunately, consuming up to 95% of rendering time in extreme cases. - // - // ## The actual approach - // - // DirectWrite has 2 APIs which can segment text properly (including ligatures and zero width joiners): - // * IDWriteTextAnalyzer1::GetTextComplexity - // * IDWriteTextAnalyzer::GetGlyphs - // - // Both APIs require us to attain an IDWriteFontFace as the functions themselves don't handle font fallback. - // This forces us to call IDWriteFontFallback::MapCharacters first. - // - // Additionally IDWriteTextAnalyzer::GetGlyphs requires an instance of DWRITE_SCRIPT_ANALYSIS, - // which can only be attained by running IDWriteTextAnalyzer::AnalyzeScript first. - // - // Font fallback with IDWriteFontFallback::MapCharacters is very slow. - - const auto textFormat = _getTextFormat(_api.attributes.bold, _api.attributes.italic); - const auto& textFormatAxis = _getTextFormatAxis(_api.attributes.bold, _api.attributes.italic); - - TextAnalysisSource analysisSource{ _api.bufferLine.data(), gsl::narrow(_api.bufferLine.size()) }; - TextAnalysisSink analysisSink{ _api.analysisResults }; - - wil::com_ptr fontCollection; - THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof())); - - wil::com_ptr mappedFontFace; + auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y]; #pragma warning(suppress : 26494) // Variable 'mappedEnd' is uninitialized. Always initialize an object (type.5). for (u32 idx = 0, mappedEnd; idx < _api.bufferLine.size(); idx = mappedEnd) { - if (_sr.systemFontFallback) + u32 mappedLength = 0; + wil::com_ptr mappedFontFace; + _mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast(_api.bufferLine.size()) - idx, &mappedLength, mappedFontFace.addressof()); + mappedEnd = idx + mappedLength; + + if (!mappedFontFace) { - auto scale = 1.0f; - u32 mappedLength = 0; + _mapReplacementCharacter(idx, mappedEnd, row); + continue; + } - if (textFormatAxis) + 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(); + size = size + (size >> 1); + size = std::max(size, mappedLength); + Expects(size > _api.glyphIndices.size()); + _api.glyphIndices = Buffer{ size }; + _api.glyphProps = Buffer{ size }; + } + + if (_api.s->font->fontFeatures.empty()) + { + // 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) { - wil::com_ptr fontFace5; - THROW_IF_FAILED(_sr.systemFontFallback.query()->MapCharacters( - /* analysisSource */ &analysisSource, - /* textPosition */ idx, - /* textLength */ gsl::narrow_cast(_api.bufferLine.size()) - idx, - /* baseFontCollection */ fontCollection.get(), - /* baseFamilyName */ _api.fontMetrics.fontName.c_str(), - /* fontAxisValues */ textFormatAxis.data(), - /* fontAxisValueCount */ gsl::narrow_cast(textFormatAxis.size()), - /* mappedLength */ &mappedLength, - /* scale */ &scale, - /* mappedFontFace */ fontFace5.put())); - mappedFontFace = std::move(fontFace5); - } - else - { - const auto baseWeight = _api.attributes.bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight); - const auto baseStyle = _api.attributes.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; - wil::com_ptr font; + BOOL isTextSimple = FALSE; + THROW_IF_FAILED(_p.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data())); - THROW_IF_FAILED(_sr.systemFontFallback->MapCharacters( - /* analysisSource */ &analysisSource, - /* textPosition */ idx, - /* textLength */ gsl::narrow_cast(_api.bufferLine.size()) - idx, - /* baseFontCollection */ fontCollection.get(), - /* baseFamilyName */ _api.fontMetrics.fontName.c_str(), - /* baseWeight */ baseWeight, - /* baseStyle */ baseStyle, - /* baseStretch */ DWRITE_FONT_STRETCH_NORMAL, - /* mappedLength */ &mappedLength, - /* mappedFont */ font.addressof(), - /* scale */ &scale)); - - mappedFontFace.reset(); - if (font) + if (isTextSimple) { - THROW_IF_FAILED(font->CreateFontFace(mappedFontFace.addressof())); - } - } + const auto shift = gsl::narrow_cast(row.lineRendition != LineRendition::SingleWidth); + const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y; - mappedEnd = idx + mappedLength; - - if (!mappedFontFace) - { - // Task: Replace all characters in this range with unicode replacement characters. - // Input (where "n" is a narrow and "ww" is a wide character): - // _api.bufferLine = "nwwnnw" - // _api.bufferLineColumn = {0, 1, 1, 3, 4, 5, 5, 6} - // n w w n n w w - // Solution: - // Iterate through bufferLineColumn until the value changes, because this indicates we passed over a - // complete (narrow or wide) cell. To do so we'll use col1 (previous column) and col2 (next column). - // Then we emit a replacement character by telling _emplaceGlyph that this range has no font face. - auto pos1 = idx; - auto col1 = _api.bufferLineColumn[pos1]; - for (auto pos2 = idx + 1; pos2 <= mappedEnd; ++pos2) - { - if (const auto col2 = _api.bufferLineColumn[pos2]; col1 != col2) + for (size_t i = 0; i < complexityLength; ++i) { - _emplaceGlyph(nullptr, pos1, pos2); - pos1 = pos2; - col1 = col2; + 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); } } - - continue; + else + { + _mapComplex(mappedFontFace.get(), idx, complexityLength, row); + } } } else { - if (!mappedFontFace) - { - const auto baseWeight = _api.attributes.bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight); - const auto baseStyle = _api.attributes.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; - - wil::com_ptr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(0, fontFamily.addressof())); - - wil::com_ptr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(baseWeight, DWRITE_FONT_STRETCH_NORMAL, baseStyle, font.addressof())); - - THROW_IF_FAILED(font->CreateFontFace(mappedFontFace.put())); - } - - mappedEnd = gsl::narrow_cast(_api.bufferLine.size()); + _mapComplex(mappedFontFace.get(), idx, mappedLength, row); } - // 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 indicesCount = row.glyphIndices.size(); + if (indicesCount > initialIndicesCount) { - BOOL isTextSimple; - THROW_IF_FAILED(_sr.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data())); - - if (isTextSimple) + // IDWriteFontFallback::MapCharacters() isn't just awfully slow, + // it can also repeatedly return the same font face again and again. :) + if (row.mappings.empty() || row.mappings.back().fontFace != mappedFontFace) { - size_t beg = 0; - for (size_t i = 0; i < complexityLength; ++i) - { - if (_emplaceGlyph(mappedFontFace.get(), idx + beg, idx + i + 1)) - { - beg = i + 1; - } - } + row.mappings.emplace_back(std::move(mappedFontFace), gsl::narrow_cast(initialIndicesCount), gsl::narrow_cast(indicesCount)); } else { - _api.analysisResults.clear(); - THROW_IF_FAILED(_sr.textAnalyzer->AnalyzeScript(&analysisSource, idx, complexityLength, &analysisSink)); - //_sr.textAnalyzer->AnalyzeBidi(&atlasAnalyzer, idx, complexityLength, &atlasAnalyzer); - - for (const auto& a : _api.analysisResults) - { - DWRITE_SCRIPT_ANALYSIS scriptAnalysis{ a.script, static_cast(a.shapes) }; - u32 actualGlyphCount = 0; - -#pragma warning(push) -#pragma warning(disable : 26494) // Variable '...' is uninitialized. Always initialize an object (type.5). - // None of these variables need to be initialized. - // features/featureRangeLengths are marked _In_reads_opt_(featureRanges). - // featureRanges is only > 0 when we also initialize all these variables. - DWRITE_TYPOGRAPHIC_FEATURES feature; - const DWRITE_TYPOGRAPHIC_FEATURES* features; - u32 featureRangeLengths; -#pragma warning(pop) - u32 featureRanges = 0; - - if (!_api.fontFeatures.empty()) - { - feature.features = _api.fontFeatures.data(); - feature.featureCount = gsl::narrow_cast(_api.fontFeatures.size()); - features = &feature; - featureRangeLengths = a.textLength; - featureRanges = 1; - } - - if (_api.clusterMap.size() < a.textLength) - { - _api.clusterMap = Buffer{ a.textLength }; - _api.textProps = Buffer{ a.textLength }; - } - - for (auto retry = 0;;) - { - const auto hr = _sr.textAnalyzer->GetGlyphs( - /* textString */ _api.bufferLine.data() + a.textPosition, - /* textLength */ a.textLength, - /* fontFace */ mappedFontFace.get(), - /* isSideways */ false, - /* isRightToLeft */ a.bidiLevel & 1, - /* scriptAnalysis */ &scriptAnalysis, - /* localeName */ nullptr, - /* numberSubstitution */ nullptr, - /* features */ &features, - /* featureRangeLengths */ &featureRangeLengths, - /* featureRanges */ featureRanges, - /* maxGlyphCount */ gsl::narrow_cast(_api.glyphProps.size()), - /* clusterMap */ _api.clusterMap.data(), - /* textProps */ _api.textProps.data(), - /* glyphIndices */ _api.glyphIndices.data(), - /* glyphProps */ _api.glyphProps.data(), - /* actualGlyphCount */ &actualGlyphCount); - - if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) && ++retry < 8) - { - // Grow factor 1.5x. - auto size = _api.glyphProps.size(); - size = size + (size >> 1); - // Overflow check. - Expects(size > _api.glyphProps.size()); - _api.glyphIndices = Buffer{ size }; - _api.glyphProps = Buffer(size); - continue; - } - - THROW_IF_FAILED(hr); - break; - } - - if (_api.glyphAdvances.size() < actualGlyphCount) - { - // Grow the buffer by at least 1.5x and at least of `actualGlyphCount` items. - // The 1.5x growth ensures we don't reallocate every time we need 1 more slot. - auto size = _api.glyphAdvances.size(); - size = size + (size >> 1); - size = std::max(size, actualGlyphCount); - _api.glyphAdvances = Buffer{ size }; - _api.glyphOffsets = Buffer{ size }; - } - - THROW_IF_FAILED(_sr.textAnalyzer->GetGlyphPlacements( - /* textString */ _api.bufferLine.data() + a.textPosition, - /* clusterMap */ _api.clusterMap.data(), - /* textProps */ _api.textProps.data(), - /* textLength */ a.textLength, - /* glyphIndices */ _api.glyphIndices.data(), - /* glyphProps */ _api.glyphProps.data(), - /* glyphCount */ actualGlyphCount, - /* fontFace */ mappedFontFace.get(), - /* fontEmSize */ _api.fontMetrics.fontSizeInDIP, - /* isSideways */ false, - /* isRightToLeft */ a.bidiLevel & 1, - /* scriptAnalysis */ &scriptAnalysis, - /* localeName */ nullptr, - /* features */ &features, - /* featureRangeLengths */ &featureRangeLengths, - /* featureRanges */ featureRanges, - /* glyphAdvances */ _api.glyphAdvances.data(), - /* glyphOffsets */ _api.glyphOffsets.data())); - - _api.textProps[a.textLength - 1].canBreakShapingAfter = 1; - - size_t beg = 0; - for (size_t i = 0; i < a.textLength; ++i) - { - if (_api.textProps[i].canBreakShapingAfter) - { - if (_emplaceGlyph(mappedFontFace.get(), a.textPosition + beg, a.textPosition + i + 1)) - { - beg = i + 1; - } - } - } - } + row.mappings.back().glyphsTo = gsl::narrow_cast(indicesCount); } } } } -// ^^^ Look at that amazing 8-fold nesting level. Lovely. <3 -bool AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, size_t bufferPos2) +void AtlasEngine::_mapCharacters(const wchar_t* text, const u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const { - static constexpr auto replacement = L'\uFFFD'; + TextAnalysisSource analysisSource{ _api.userLocaleName.c_str(), text, textLength }; + const auto& textFormatAxis = _api.textFormatAxes[static_cast(_api.attributes)]; - // This would seriously blow us up otherwise. - Expects(bufferPos1 < bufferPos2 && bufferPos2 <= _api.bufferLine.size()); + // We don't read from scale anyways. +#pragma warning(suppress : 26494) // Variable 'scale' is uninitialized. Always initialize an object (type.5). + f32 scale; - // _flushBufferLine() ensures that bufferLineColumn.size() > bufferLine.size(). - const auto x1 = _api.bufferLineColumn[bufferPos1]; - const auto x2 = _api.bufferLineColumn[bufferPos2]; - - // x1 == x2, if our TextBuffer and DirectWrite disagree where glyph boundaries are. Example: - // Our line of text contains a wide glyph consisting of 2 surrogate pairs "xx" and "yy". - // If DirectWrite considers the first "xx" to be separate from the second "yy", we'll get: - // _api.bufferLine = "...xxyy..." - // _api.bufferLineColumn = {01233335678} - // ^ ^ - // / \ - // bufferPos1 bufferPos2 - // x1: _api.bufferLineColumn[bufferPos1] == 3 - // x1: _api.bufferLineColumn[bufferPos2] == 3 - // --> cellCount (which is x2 - x1) is now 0 (invalid). - // - // Assuming that the TextBuffer implementation doesn't have any bugs... - // I'm not entirely certain why this occurs, but to me, a layperson, it appears as if - // IDWriteFontFallback::MapCharacters() doesn't respect extended grapheme clusters. - // It could also possibly be due to a difference in the supported Unicode version. - if (x1 >= x2 || x2 > _api.cellCount.x) + if (textFormatAxis) { - return false; + THROW_IF_FAILED(_p.systemFontFallback1->MapCharacters( + /* analysisSource */ &analysisSource, + /* textPosition */ 0, + /* textLength */ textLength, + /* baseFontCollection */ _p.s->font->fontCollection.get(), + /* baseFamilyName */ _p.s->font->fontName.c_str(), + /* fontAxisValues */ textFormatAxis.data(), + /* fontAxisValueCount */ gsl::narrow_cast(textFormatAxis.size()), + /* mappedLength */ mappedLength, + /* scale */ &scale, + /* mappedFontFace */ reinterpret_cast(mappedFontFace))); } - - const auto chars = fontFace ? &_api.bufferLine[bufferPos1] : &replacement; - const auto charCount = fontFace ? bufferPos2 - bufferPos1 : 1; - const u16 cellCount = x2 - x1; - - auto attributes = _api.attributes; - attributes.cellCount = cellCount; - - AtlasKey key{ attributes, gsl::narrow(charCount), chars }; - auto it = _r.glyphs.find(key); - - if (it == _r.glyphs.end()) + else { - // Do fonts exist *in practice* which contain both colored and uncolored glyphs? I'm pretty sure... - // However doing it properly means using either of: - // * IDWriteFactory2::TranslateColorGlyphRun - // * IDWriteFactory4::TranslateColorGlyphRun - // * IDWriteFontFace4::GetGlyphImageData - // - // For the first two I wonder how one is supposed to restitch the 27 required parameters from a - // partial (!) text range returned by GetGlyphs(). Our caller breaks the GetGlyphs() result up - // into runs of characters up until the first canBreakShapingAfter == true after all. - // There's no documentation for it and I'm sure I'd mess it up. - // For very obvious reasons I didn't want to write this code. - // - // IDWriteFontFace4::GetGlyphImageData seems like the best approach and DirectWrite uses the - // same code that GetGlyphImageData uses to implement TranslateColorGlyphRun (well partially). - // However, it's a heavy operation and parses the font file on disk on every call (TranslateColorGlyphRun doesn't). - // For less obvious reasons I didn't want to write this code either. - // - // So this is a job for future me/someone. - // Bonus points for doing it without impacting performance. - auto flags = CellFlags::None; - if (fontFace) + const auto baseWeight = WI_IsFlagSet(_api.attributes, FontRelevantAttributes::Bold) ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_p.s->font->fontWeight); + const auto baseStyle = WI_IsFlagSet(_api.attributes, FontRelevantAttributes::Italic) ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; + wil::com_ptr font; + + THROW_IF_FAILED(_p.systemFontFallback->MapCharacters( + /* analysisSource */ &analysisSource, + /* textPosition */ 0, + /* textLength */ textLength, + /* baseFontCollection */ _p.s->font->fontCollection.get(), + /* baseFamilyName */ _p.s->font->fontName.c_str(), + /* baseWeight */ baseWeight, + /* baseStyle */ baseStyle, + /* baseStretch */ DWRITE_FONT_STRETCH_NORMAL, + /* mappedLength */ mappedLength, + /* mappedFont */ font.addressof(), + /* scale */ &scale)); + + if (font) { - const auto fontFace2 = wil::try_com_query(fontFace); - WI_SetFlagIf(flags, CellFlags::ColoredGlyph, fontFace2 && fontFace2->IsColorFont()); + THROW_IF_FAILED(font->CreateFontFace(reinterpret_cast(mappedFontFace))); } - - // The AtlasValue constructor fills the `coords` variable with a pointer to an array - // of at least `cellCount` elements. I did this so that I don't have to type out - // `value.data()->coords` again, despite the constructor having all the data necessary. - u16x2* coords; - AtlasValue value{ flags, cellCount, &coords }; - - for (u16 i = 0; i < cellCount; ++i) - { - coords[i] = _r.tileAllocator.allocate(_r.glyphs); - } - - it = _r.glyphs.insert(std::move(key), std::move(value)); - _r.glyphQueue.emplace_back(it); } - const auto valueData = it->second.data(); - const auto coords = &valueData->coords[0]; - const auto cells = _getCell(x1, _api.lastPaintBufferLineCoord.y); - const auto cellGlyphMappings = _getCellGlyphMapping(x1, _api.lastPaintBufferLineCoord.y); - - for (u32 i = 0; i < cellCount; ++i) - { - cells[i].tileIndex = coords[i]; - // We should apply the column color and flags from each column (instead - // of copying them from the x1) so that ligatures can appear in multiple - // colors with different line styles. - cells[i].flags = valueData->flags | _api.bufferLineMetadata[static_cast(x1) + i].flags; - cells[i].color = _api.bufferLineMetadata[static_cast(x1) + i].colors; - } - - std::fill_n(cellGlyphMappings, cellCount, it); - return true; + // Oh wow! You found a case where scale isn't 1! I tried every font and none + // returned something besides 1. I just couldn't figure out why this exists. + assert(scale == 1); +} + +void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 length, ShapedRow& row) +{ + _api.analysisResults.clear(); + + TextAnalysisSource analysisSource{ _api.userLocaleName.c_str(), _api.bufferLine.data(), gsl::narrow(_api.bufferLine.size()) }; + TextAnalysisSink analysisSink{ _api.analysisResults }; + THROW_IF_FAILED(_p.textAnalyzer->AnalyzeScript(&analysisSource, idx, length, &analysisSink)); + + for (const auto& a : _api.analysisResults) + { + u32 actualGlyphCount = 0; + +#pragma warning(push) +#pragma warning(disable : 26494) // Variable '...' is uninitialized. Always initialize an object (type.5). + // None of these variables need to be initialized. + // features/featureRangeLengths are marked _In_reads_opt_(featureRanges). + // featureRanges is only > 0 when we also initialize all these variables. + DWRITE_TYPOGRAPHIC_FEATURES feature; + const DWRITE_TYPOGRAPHIC_FEATURES* features; + u32 featureRangeLengths; +#pragma warning(pop) + u32 featureRanges = 0; + + if (!_p.s->font->fontFeatures.empty()) + { + // Direct2D, why is this mutable? Why? +#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3). + feature.features = const_cast(_p.s->font->fontFeatures.data()); + feature.featureCount = gsl::narrow_cast(_p.s->font->fontFeatures.size()); + features = &feature; + featureRangeLengths = a.textLength; + featureRanges = 1; + } + + if (_api.clusterMap.size() <= a.textLength) + { + _api.clusterMap = Buffer{ static_cast(a.textLength) + 1 }; + _api.textProps = Buffer{ a.textLength }; + } + + for (auto retry = 0;;) + { + const auto hr = _p.textAnalyzer->GetGlyphs( + /* textString */ _api.bufferLine.data() + a.textPosition, + /* textLength */ a.textLength, + /* fontFace */ mappedFontFace, + /* isSideways */ false, + /* isRightToLeft */ 0, + /* scriptAnalysis */ &a.analysis, + /* localeName */ _api.userLocaleName.c_str(), + /* numberSubstitution */ nullptr, + /* features */ &features, + /* featureRangeLengths */ &featureRangeLengths, + /* featureRanges */ featureRanges, + /* maxGlyphCount */ gsl::narrow_cast(_api.glyphIndices.size()), + /* clusterMap */ _api.clusterMap.data(), + /* textProps */ _api.textProps.data(), + /* glyphIndices */ _api.glyphIndices.data(), + /* glyphProps */ _api.glyphProps.data(), + /* actualGlyphCount */ &actualGlyphCount); + + if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) && ++retry < 8) + { + // Grow factor 1.5x. + auto size = _api.glyphIndices.size(); + size = size + (size >> 1); + // Overflow check. + Expects(size > _api.glyphIndices.size()); + _api.glyphIndices = Buffer{ size }; + _api.glyphProps = Buffer{ size }; + continue; + } + + THROW_IF_FAILED(hr); + break; + } + + if (_api.glyphAdvances.size() < actualGlyphCount) + { + // Grow the buffer by at least 1.5x and at least of `actualGlyphCount` items. + // The 1.5x growth ensures we don't reallocate every time we need 1 more slot. + auto size = _api.glyphAdvances.size(); + size = size + (size >> 1); + size = std::max(size, actualGlyphCount); + _api.glyphAdvances = Buffer{ size }; + _api.glyphOffsets = Buffer{ size }; + } + + THROW_IF_FAILED(_p.textAnalyzer->GetGlyphPlacements( + /* textString */ _api.bufferLine.data() + a.textPosition, + /* clusterMap */ _api.clusterMap.data(), + /* textProps */ _api.textProps.data(), + /* textLength */ a.textLength, + /* glyphIndices */ _api.glyphIndices.data(), + /* glyphProps */ _api.glyphProps.data(), + /* glyphCount */ actualGlyphCount, + /* fontFace */ mappedFontFace, + /* fontEmSize */ _p.s->font->fontSize, + /* isSideways */ false, + /* isRightToLeft */ 0, + /* scriptAnalysis */ &a.analysis, + /* localeName */ _api.userLocaleName.c_str(), + /* features */ &features, + /* featureRangeLengths */ &featureRangeLengths, + /* featureRanges */ featureRanges, + /* glyphAdvances */ _api.glyphAdvances.data(), + /* glyphOffsets */ _api.glyphOffsets.data())); + + _api.clusterMap[a.textLength] = gsl::narrow_cast(actualGlyphCount); + + const auto shift = gsl::narrow_cast(row.lineRendition != LineRendition::SingleWidth); + const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y; + auto prevCluster = _api.clusterMap[0]; + size_t beg = 0; + + for (size_t i = 1; i <= a.textLength; ++i) + { + const auto nextCluster = _api.clusterMap[i]; + if (prevCluster == nextCluster) + { + continue; + } + + const size_t col1 = _api.bufferLineColumn[a.textPosition + beg]; + const size_t col2 = _api.bufferLineColumn[a.textPosition + i]; + const auto fg = colors[col1 << shift]; + + const auto expectedAdvance = (col2 - col1) * _p.s->font->cellSize.x; + f32 actualAdvance = 0; + for (auto j = prevCluster; j < nextCluster; ++j) + { + actualAdvance += _api.glyphAdvances[j]; + } + _api.glyphAdvances[nextCluster - 1] += expectedAdvance - actualAdvance; + + row.colors.insert(row.colors.end(), nextCluster - prevCluster, fg); + + prevCluster = nextCluster; + beg = i; + } + + row.glyphIndices.insert(row.glyphIndices.end(), _api.glyphIndices.begin(), _api.glyphIndices.begin() + actualGlyphCount); + row.glyphAdvances.insert(row.glyphAdvances.end(), _api.glyphAdvances.begin(), _api.glyphAdvances.begin() + actualGlyphCount); + row.glyphOffsets.insert(row.glyphOffsets.end(), _api.glyphOffsets.begin(), _api.glyphOffsets.begin() + actualGlyphCount); + } +} + +void AtlasEngine::_mapReplacementCharacter(u32 from, u32 to, ShapedRow& row) +{ + if (!_api.replacementCharacterLookedUp) + { + bool succeeded = false; + + u32 mappedLength = 0; + _mapCharacters(L"\uFFFD", 1, &mappedLength, _api.replacementCharacterFontFace.put()); + + if (mappedLength == 1) + { + static constexpr u32 codepoint = 0xFFFD; + succeeded = SUCCEEDED(_api.replacementCharacterFontFace->GetGlyphIndicesW(&codepoint, 1, &_api.replacementCharacterGlyphIndex)); + } + + if (!succeeded) + { + _api.replacementCharacterFontFace.reset(); + _api.replacementCharacterGlyphIndex = 0; + } + + _api.replacementCharacterLookedUp = true; + } + + if (!_api.replacementCharacterFontFace) + { + return; + } + + auto pos1 = from; + auto pos2 = pos1; + size_t col1 = _api.bufferLineColumn[from]; + size_t col2 = col1; + auto initialIndicesCount = row.glyphIndices.size(); + const auto softFontAvailable = !_p.s->font->softFontPattern.empty(); + auto currentlyMappingSoftFont = isSoftFontChar(_api.bufferLine[pos1]); + const auto shift = gsl::narrow_cast(row.lineRendition != LineRendition::SingleWidth); + const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y; + + while (pos2 < to) + { + col2 = _api.bufferLineColumn[++pos2]; + if (col1 == col2) + { + continue; + } + + const auto cols = col2 - col1; + const auto ch = static_cast(_api.bufferLine[pos1]); + const auto nowMappingSoftFont = isSoftFontChar(ch); + + row.glyphIndices.emplace_back(nowMappingSoftFont ? ch : _api.replacementCharacterGlyphIndex); + row.glyphAdvances.emplace_back(static_cast(cols * _p.s->font->cellSize.x)); + row.glyphOffsets.emplace_back(); + row.colors.emplace_back(colors[col1 << shift]); + + if (currentlyMappingSoftFont != nowMappingSoftFont) + { + const auto indicesCount = row.glyphIndices.size(); + const auto fontFace = currentlyMappingSoftFont && softFontAvailable ? nullptr : _api.replacementCharacterFontFace.get(); + + if (indicesCount > initialIndicesCount) + { + row.mappings.emplace_back(fontFace, gsl::narrow_cast(initialIndicesCount), gsl::narrow_cast(indicesCount)); + initialIndicesCount = indicesCount; + } + } + + pos1 = pos2; + col1 = col2; + currentlyMappingSoftFont = nowMappingSoftFont; + } + + { + const auto indicesCount = row.glyphIndices.size(); + const auto fontFace = currentlyMappingSoftFont && softFontAvailable ? nullptr : _api.replacementCharacterFontFace.get(); + + if (indicesCount > initialIndicesCount) + { + row.mappings.emplace_back(fontFace, gsl::narrow_cast(initialIndicesCount), gsl::narrow_cast(indicesCount)); + } + } } diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 6a1533b4f6..b135a8989f 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -3,14 +3,13 @@ #pragma once -#include -#include #include +#include +#include -#include "../../renderer/inc/IRenderEngine.hpp" -#include "DWriteTextAnalysis.h" +#include "common.h" -namespace Microsoft::Console::Render +namespace Microsoft::Console::Render::Atlas { struct TextAnalysisSinkResult; @@ -43,17 +42,17 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ResetLineTransform() noexcept override; [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(gsl::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; + [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; - [[nodiscard]] HRESULT UpdateSoftFont(gsl::span bitPattern, til::size cellSize, size_t centeringHint) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(std::span bitPattern, til::size cellSize, size_t centeringHint) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(std::wstring_view glyph, _Out_ bool* pResult) noexcept override; [[nodiscard]] HRESULT UpdateTitle(std::wstring_view newTitle) noexcept override; @@ -80,988 +79,62 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept override; void UpdateHyperlinkHoveredId(uint16_t hoveredId) noexcept override; - // Some helper classes for the implementation. - // public because I don't want to sprinkle the code with friends. - public: -#define ATLAS_POD_OPS(type) \ - constexpr bool operator==(const type& rhs) const noexcept \ - { \ - return __builtin_memcmp(this, &rhs, sizeof(rhs)) == 0; \ - } \ - \ - constexpr bool operator!=(const type& rhs) const noexcept \ - { \ - return __builtin_memcmp(this, &rhs, sizeof(rhs)) != 0; \ - } - -#define ATLAS_FLAG_OPS(type, underlying) \ - friend constexpr type operator~(type v) noexcept { return static_cast(~static_cast(v)); } \ - friend constexpr type operator|(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) | static_cast(rhs)); } \ - friend constexpr type operator&(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) & static_cast(rhs)); } \ - friend constexpr type operator^(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) ^ static_cast(rhs)); } \ - friend constexpr void operator|=(type& lhs, type rhs) noexcept { lhs = lhs | rhs; } \ - friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; } \ - friend constexpr void operator^=(type& lhs, type rhs) noexcept { lhs = lhs ^ rhs; } - - template - struct vec2 - { - T x{}; - T y{}; - - ATLAS_POD_OPS(vec2) - }; - - template - struct vec3 - { - T x{}; - T y{}; - T z{}; - - ATLAS_POD_OPS(vec3) - }; - - template - struct vec4 - { - T x{}; - T y{}; - T z{}; - T w{}; - - ATLAS_POD_OPS(vec4) - }; - - template - struct rect - { - T left{}; - T top{}; - T right{}; - T bottom{}; - - ATLAS_POD_OPS(rect) - - constexpr bool non_empty() const noexcept - { - return (left < right) & (top < bottom); - } - }; - - using u8 = uint8_t; - - using u16 = uint16_t; - using u16x2 = vec2; - using u16r = rect; - - using i16 = int16_t; - - using u32 = uint32_t; - using u32x2 = vec2; - - using i32 = int32_t; - - using f32 = float; - using f32x2 = vec2; - using f32x3 = vec3; - using f32x4 = vec4; - - struct TextAnalyzerResult - { - u32 textPosition = 0; - u32 textLength = 0; - - // These 2 fields represent DWRITE_SCRIPT_ANALYSIS. - // Not using DWRITE_SCRIPT_ANALYSIS drops the struct size from 20 down to 12 bytes. - u16 script = 0; - u8 shapes = 0; - - u8 bidiLevel = 0; - }; - private: - // I wrote `Buffer` instead of using `std::vector`, because I want to convey that these things - // explicitly _don't_ hold resizeable contents, but rather plain content of a fixed size. - // For instance I didn't want a resizeable vector with a `push_back` method for my fixed-size - // viewport arrays - that doesn't make sense after all. `Buffer` also doesn't initialize - // contents to zero, allowing rapid creation/destruction and you can easily specify a custom - // (over-)alignment which can improve rendering perf by up to ~20% over `std::vector`. - template - struct Buffer - { - constexpr Buffer() noexcept = default; - - explicit Buffer(size_t size) : - _data{ allocate(size) }, - _size{ size } - { - std::uninitialized_default_construct_n(_data, size); - } - - Buffer(const T* data, size_t size) : - _data{ allocate(size) }, - _size{ size } - { - // Changing the constructor arguments to accept std::span might - // be a good future extension, but not to improve security here. - // You can trivially construct std::span's from invalid ranges. - // Until then the raw-pointer style is more practical. -#pragma warning(suppress : 26459) // You called an STL function '...' with a raw pointer parameter at position '3' that may be unsafe [...]. - std::uninitialized_copy_n(data, size, _data); - } - - ~Buffer() - { - destroy(); - } - - Buffer(Buffer&& other) noexcept : - _data{ std::exchange(other._data, nullptr) }, - _size{ std::exchange(other._size, 0) } - { - } - -#pragma warning(suppress : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21). - Buffer& operator=(Buffer&& other) noexcept - { - destroy(); - _data = std::exchange(other._data, nullptr); - _size = std::exchange(other._size, 0); - return *this; - } - - explicit operator bool() const noexcept - { - return _data != nullptr; - } - - T& operator[](size_t index) noexcept - { - assert(index < _size); - return _data[index]; - } - - const T& operator[](size_t index) const noexcept - { - assert(index < _size); - return _data[index]; - } - - T* data() noexcept - { - return _data; - } - - const T* data() const noexcept - { - return _data; - } - - size_t size() const noexcept - { - return _size; - } - - T* begin() noexcept - { - return _data; - } - - T* begin() const noexcept - { - return _data; - } - - T* end() noexcept - { - return _data + _size; - } - - T* end() const noexcept - { - return _data + _size; - } - - private: - // These two functions don't need to use scoped objects or standard allocators, - // since this class is in fact an scoped allocator object itself. -#pragma warning(push) -#pragma warning(disable : 26402) // Return a scoped object instead of a heap-allocated if it has a move constructor (r.3). -#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique instead (r.11). - static T* allocate(size_t size) - { - if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) - { - return static_cast(::operator new(size * sizeof(T))); - } - else - { - return static_cast(::operator new(size * sizeof(T), static_cast(Alignment))); - } - } - - static void deallocate(T* data) noexcept - { - if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) - { - ::operator delete(data); - } - else - { - ::operator delete(data, static_cast(Alignment)); - } - } -#pragma warning(pop) - - void destroy() noexcept - { - std::destroy_n(_data, _size); - deallocate(_data); - } - - T* _data = nullptr; - size_t _size = 0; - }; - - // This structure works similar to how std::string works: - // You can think of a std::string as a structure consisting of: - // char* data; - // size_t size; - // size_t capacity; - // where data is some backing memory allocated on the heap. - // - // But std::string employs an optimization called "small string optimization" (SSO). - // To simplify things it could be explained as: - // If the string capacity is small, then the characters are stored inside the "data" - // pointer and you make sure to set the lowest bit in the pointer one way or another. - // Heap allocations are always aligned by at least 4-8 bytes on any platform. - // If the address of the "data" pointer is not even you know data is stored inline. - template - union SmallObjectOptimizer - { - static_assert(std::is_trivially_copyable_v); - static_assert(std::has_unique_object_representations_v); - - T* allocated = nullptr; - T inlined; - - constexpr SmallObjectOptimizer() = default; - - SmallObjectOptimizer(const SmallObjectOptimizer& other) = delete; - SmallObjectOptimizer& operator=(const SmallObjectOptimizer& other) = delete; - - SmallObjectOptimizer(SmallObjectOptimizer&& other) noexcept - { - memcpy(this, &other, std::max(sizeof(allocated), sizeof(inlined))); - other.allocated = nullptr; - } - - SmallObjectOptimizer& operator=(SmallObjectOptimizer&& other) noexcept - { - std::destroy_at(this); - return *std::construct_at(this, std::move(other)); - } - - ~SmallObjectOptimizer() - { - if (!is_inline()) - { -#pragma warning(suppress : 26408) // Avoid malloc() and free(), prefer the nothrow version of new with delete (r.10). - free(allocated); - } - } - - T* initialize(size_t byteSize) - { - if (would_inline(byteSize)) - { - return &inlined; - } - -#pragma warning(suppress : 26408) // Avoid malloc() and free(), prefer the nothrow version of new with delete (r.10). - allocated = THROW_IF_NULL_ALLOC(static_cast(malloc(byteSize))); - return allocated; - } - - constexpr bool would_inline(size_t byteSize) const noexcept - { - return byteSize <= sizeof(T); - } - - bool is_inline() const noexcept - { - // VSO-1430353: __builtin_bitcast crashes the compiler under /permissive-. (BODGY) -#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). - return (reinterpret_cast(allocated) & 1) != 0; - } - - const T* data() const noexcept - { - return is_inline() ? &inlined : allocated; - } - - size_t size() const noexcept - { - return is_inline() ? sizeof(inlined) : _msize(allocated); - } - }; - - struct FontMetrics - { - wil::com_ptr fontCollection; - wil::com_ptr fontFamily; - std::wstring fontName; - float baselineInDIP = 0.0f; - float fontSizeInDIP = 0.0f; - f32 advanceScale = 0; - u16x2 cellSize; - u16 fontWeight = 0; - u16 underlinePos = 0; - u16 underlineWidth = 0; - u16 strikethroughPos = 0; - u16 strikethroughWidth = 0; - u16x2 doubleUnderlinePos; - u16 thinLineWidth = 0; - }; - - // These flags are shared with shader_ps.hlsl. - // If you change this be sure to copy it over to shader_ps.hlsl. - // - // clang-format off - enum class CellFlags : u32 - { - None = 0x00000000, - Inlined = 0x00000001, - - ColoredGlyph = 0x00000002, - - Cursor = 0x00000008, - Selected = 0x00000010, - - BorderLeft = 0x00000020, - BorderTop = 0x00000040, - BorderRight = 0x00000080, - BorderBottom = 0x00000100, - Underline = 0x00000200, - UnderlineDotted = 0x00000400, - UnderlineDouble = 0x00000800, - Strikethrough = 0x00001000, - }; - // clang-format on - ATLAS_FLAG_OPS(CellFlags, u32) - - // This structure is shared with the GPU shader and needs to follow certain alignment rules. - // You can generally assume that only u32 or types of that alignment are allowed. - struct Cell - { - alignas(u32) u16x2 tileIndex; - alignas(u32) CellFlags flags = CellFlags::None; - u32x2 color; - }; - - struct AtlasKeyAttributes - { - u16 inlined : 1; - u16 bold : 1; - u16 italic : 1; - u16 cellCount : 13; - - ATLAS_POD_OPS(AtlasKeyAttributes) - }; - - struct AtlasKeyData - { - AtlasKeyAttributes attributes; - u16 charCount; - wchar_t chars[14]; - }; - - struct AtlasKey - { - AtlasKey(AtlasKeyAttributes attributes, u16 charCount, const wchar_t* chars) - { - const auto size = dataSize(charCount); - const auto data = _data.initialize(size); - attributes.inlined = _data.would_inline(size); - data->attributes = attributes; - data->charCount = charCount; - memcpy(&data->chars[0], chars, static_cast(charCount) * sizeof(AtlasKeyData::chars[0])); - } - - const AtlasKeyData* data() const noexcept - { - return _data.data(); - } - - size_t hash() const noexcept - { - const auto d = data(); -#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1). - return std::_Fnv1a_append_bytes(std::_FNV_offset_basis, reinterpret_cast(d), dataSize(d->charCount)); - } - - bool operator==(const AtlasKey& rhs) const noexcept - { - const auto a = data(); - const auto b = rhs.data(); - return a->charCount == b->charCount && memcmp(a, b, dataSize(a->charCount)) == 0; - } - - private: - SmallObjectOptimizer _data; - - static constexpr size_t dataSize(u16 charCount) noexcept - { - // This returns the actual byte size of a AtlasKeyData struct for the given charCount. - // The `wchar_t chars[2]` is only a buffer for the inlined variant after - // all and the actual charCount can be smaller or larger. Due to this we - // remove the size of the `chars` array and add it's true length on top. - return sizeof(AtlasKeyData) - sizeof(AtlasKeyData::chars) + static_cast(charCount) * sizeof(AtlasKeyData::chars[0]); - } - }; - - struct CachedGlyphLayout - { - wil::com_ptr textLayout; - f32x2 offset; - f32x2 scale; - f32x2 scaleCenter; - D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE; - bool scalingRequired = false; - - explicit operator bool() const noexcept; - void reset() noexcept; - void applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept; - void undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept; - }; - - struct AtlasValueData - { - CellFlags flags = CellFlags::None; - u16x2 coords[7]; - }; - - struct AtlasValue - { - AtlasValue(CellFlags flags, u16 cellCount, u16x2** coords) - { - __assume(coords != nullptr); - const auto size = dataSize(cellCount); - const auto data = _data.initialize(size); - WI_SetFlagIf(flags, CellFlags::Inlined, _data.would_inline(size)); - data->flags = flags; - *coords = &data->coords[0]; - } - - const AtlasValueData* data() const noexcept - { - return _data.data(); - } - - CachedGlyphLayout cachedLayout; - - private: - SmallObjectOptimizer _data; - - static constexpr size_t dataSize(u16 coordCount) noexcept - { - return sizeof(AtlasValueData) - sizeof(AtlasValueData::coords) + static_cast(coordCount) * sizeof(AtlasValueData::coords[0]); - } - }; - - struct AtlasKeyHasher - { - using is_transparent = int; - - size_t operator()(const AtlasKey& v) const noexcept - { - return v.hash(); - } - - size_t operator()(const std::list>::iterator& v) const noexcept - { - return operator()(v->first); - } - }; - - struct AtlasKeyEq - { - using is_transparent = int; - - bool operator()(const AtlasKey& a, const std::list>::iterator& b) const noexcept - { - return a == b->first; - } - - bool operator()(const std::list>::iterator& a, const std::list>::iterator& b) const noexcept - { - return operator()(a->first, b); - } - }; - - struct TileHashMap - { - using iterator = std::list>::iterator; - - TileHashMap() noexcept = default; - - iterator end() noexcept - { - return _lru.end(); - } - - iterator find(const AtlasKey& key) - { - const auto it = _map.find(key); - if (it != _map.end()) - { - // Move the key to the head of the LRU queue. - makeNewest(*it); - return *it; - } - return end(); - } - - iterator insert(AtlasKey&& key, AtlasValue&& value) - { - // Insert the key/value right at the head of the LRU queue, just like find(). - // - // && decays to & if the argument is named, because C++ is a simple language - // and so you have to std::move it again, because C++ is a simple language. - _lru.emplace_front(std::move(key), std::move(value)); - auto it = _lru.begin(); - _map.emplace(it); - return it; - } - - void makeNewest(const iterator& it) - { - _lru.splice(_lru.begin(), _lru, it); - } - - void popOldestTiles(std::vector& out) noexcept - { - Expects(!_lru.empty()); - const auto it = --_lru.end(); - - const auto key = it->first.data(); - const auto value = it->second.data(); - const auto beg = &value->coords[0]; - const auto cellCount = key->attributes.cellCount; - - const auto offset = out.size(); - out.resize(offset + cellCount); - std::copy_n(beg, cellCount, out.begin() + offset); - - _map.erase(it); - _lru.pop_back(); - } - - private: - // Please don't copy this code. It's a proof-of-concept. - // If you need a LRU hash-map, write a custom one with an intrusive - // prev/next linked list (it's easier than you might think!). - std::list> _lru; - std::unordered_set _map; - }; - - // TileAllocator yields `tileSize`-sized tiles for our texture atlas. - // While doing so it'll grow the atlas size() by a factor of 2 if needed. - // Once the setMaxArea() is exceeded it'll stop growing and instead - // snatch tiles back from the oldest TileHashMap entries. - // - // The quadratic growth works by alternating the size() - // between an 1:1 and 2:1 aspect ratio, like so: - // (64,64) -> (128,64) -> (128,128) -> (256,128) -> (256,256) - // These initial tile positions allocate() returns are in a Z - // pattern over the available space in the atlas texture. - // You can log the `return _pos;` in allocate() using "Tracepoint"s - // in Visual Studio if you'd like to understand the Z pattern better. - struct TileAllocator - { - TileAllocator() = default; - - explicit TileAllocator(u16x2 tileSize, u16x2 windowSize) noexcept : - _tileSize{ tileSize } - { - const auto initialSize = std::max(u16{ _absoluteMinSize }, std::bit_ceil(std::max(tileSize.x, tileSize.y))); - _size = { initialSize, initialSize }; - _limit = { gsl::narrow_cast(initialSize - _tileSize.x), gsl::narrow_cast(initialSize - _tileSize.y) }; - setMaxArea(windowSize); - } - - u16x2 size() const noexcept - { - return _size; - } - - void setMaxArea(u16x2 windowSize) noexcept - { - // _generate() uses a quadratic growth factor for _size's area. - // Once it exceeds the _maxArea, it'll start snatching tiles back from the - // TileHashMap using its LRU queue. Since _size will at least reach half - // of _maxSize (because otherwise it could still grow by a factor of 2) - // and by ensuring that _maxArea is at least twice the window size - // we make it impossible* for _generate() to return false before - // TileHashMap contains at least as many tiles as the window contains. - // If that wasn't the case we'd snatch and reuse tiles that are still in use. - // * lhecker's legal department: - // No responsibility is taken for the correctness of this information. - setMaxArea(static_cast(windowSize.x) * static_cast(windowSize.y) * 2); - } - - void setMaxArea(size_t max) noexcept - { - // We need to reserve at least 1 extra `tileArea`, because the tile - // at position {0,0} is already reserved for the cursor texture. - const auto tileArea = static_cast(_tileSize.x) * static_cast(_tileSize.y); - _maxArea = clamp(max + tileArea, _absoluteMinArea, _absoluteMaxArea); - _updateCanGenerate(); - } - - u16x2 allocate(TileHashMap& map) noexcept - { - if (_generate()) - { - return _pos; - } - - if (_cache.empty()) - { - map.popOldestTiles(_cache); - } - - const auto pos = _cache.back(); - _cache.pop_back(); - return pos; - } - - private: - // This method generates the Z pattern coordinates - // described above in the TileAllocator comment. - bool _generate() noexcept - { - if (!_canGenerate) - { - return false; - } - - // We need to backup _pos/_size in case our resize below exceeds _maxArea. - // In that case we have to restore _pos/_size so that if _maxArea is increased - // (window resize for instance), we can pick up where we previously left off. - const auto pos = _pos; - - _pos.x += _tileSize.x; - if (_pos.x <= _limit.x) - { - return true; - } - - _pos.y += _tileSize.y; - if (_pos.y <= _limit.y) - { - _pos.x = _originX; - return true; - } - - // Same as for pos. - const auto size = _size; - - // This implements a quadratic growth factor for _size, by - // alternating between an 1:1 and 2:1 aspect ratio, like so: - // (64,64) -> (128,64) -> (128,128) -> (256,128) -> (256,256) - // This behavior is strictly dependent on setMaxArea(u16x2)'s - // behavior. See it's comment for an explanation. - if (_size.x == _size.y) - { - _size.x *= 2; - _pos.y = 0; - } - else - { - _size.y *= 2; - _pos.x = 0; - } - - _updateCanGenerate(); - if (_canGenerate) - { - _limit = { gsl::narrow_cast(_size.x - _tileSize.x), gsl::narrow_cast(_size.y - _tileSize.y) }; - _originX = _pos.x; - } - else - { - _size = size; - _pos = pos; - } - - return _canGenerate; - } - - void _updateCanGenerate() noexcept - { - _canGenerate = static_cast(_size.x) * static_cast(_size.y) <= _maxArea; - } - - static constexpr u16 _absoluteMinSize = 256; - static constexpr size_t _absoluteMinArea = _absoluteMinSize * _absoluteMinSize; - // TODO: Consider using IDXGIAdapter3::QueryVideoMemoryInfo() and IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEvent() - // That way we can make better to use of a user's available video memory. - static constexpr size_t _absoluteMaxArea = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION * D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION; - - std::vector _cache; - size_t _maxArea = _absoluteMaxArea; - u16x2 _tileSize; - u16x2 _size; - u16x2 _limit; - // Since _pos starts at {0, 0}, it'll result in the first allocate()d tile to be at {_tileSize.x, 0}. - // Coincidentially that's exactly what we want as the cursor texture lives at {0, 0}. - u16x2 _pos; - u16 _originX = 0; - // Indicates whether we've exhausted our Z pattern across the atlas texture. - // If this is false, we have to snatch tiles back from TileHashMap. - bool _canGenerate = true; - }; - - struct CachedCursorOptions - { - u32 cursorColor = INVALID_COLOR; - u16 cursorType = gsl::narrow_cast(CursorType::Legacy); - u8 heightPercentage = 20; - u8 _padding = 0; - - ATLAS_POD_OPS(CachedCursorOptions) - }; - - struct BufferLineMetadata - { - u32x2 colors; - CellFlags flags = CellFlags::None; - }; - - // NOTE: D3D constant buffers sizes must be a multiple of 16 bytes. - struct alignas(16) ConstBuffer - { - // WARNING: Modify this carefully after understanding how HLSL struct packing works. - // The gist is: - // * Minimum alignment is 4 bytes (like `#pragma pack 4`) - // * Members cannot straddle 16 byte boundaries - // This means a structure like {u32; u32; u32; u32x2} would require - // padding so that it is {u32; u32; u32; <4 byte padding>; u32x2}. - // * bool will probably not work the way you want it to, - // because HLSL uses 32-bit bools and C++ doesn't. - alignas(sizeof(f32x4)) f32x4 viewport; - alignas(sizeof(f32x4)) f32 gammaRatios[4]{}; - alignas(sizeof(f32)) f32 enhancedContrast = 0; - alignas(sizeof(u32)) u32 cellCountX = 0; - alignas(sizeof(u32x2)) u32x2 cellSize; - alignas(sizeof(u32)) u32 underlinePos = 0; - alignas(sizeof(u32)) u32 underlineWidth = 0; - alignas(sizeof(u32)) u32 strikethroughPos = 0; - alignas(sizeof(u32)) u32 strikethroughWidth = 0; - alignas(sizeof(u32x2)) u32x2 doubleUnderlinePos; - alignas(sizeof(u32)) u32 thinLineWidth = 0; - alignas(sizeof(u32)) u32 backgroundColor = 0; - alignas(sizeof(u32)) u32 cursorColor = 0; - alignas(sizeof(u32)) u32 selectionColor = 0; - alignas(sizeof(u32)) u32 useClearType = 0; -#pragma warning(suppress : 4324) // 'ConstBuffer': structure was padded due to alignment specifier - }; - - struct alignas(16) CustomConstBuffer - { - // WARNING: Same rules as for ConstBuffer above apply. - alignas(sizeof(f32)) f32 time = 0; - alignas(sizeof(f32)) f32 scale = 0; - alignas(sizeof(f32x2)) f32x2 resolution; - alignas(sizeof(f32x4)) f32x4 background; -#pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier - }; - - // Handled in BeginPaint() - enum class ApiInvalidations : u8 - { - None = 0, - Title = 1 << 0, - Device = 1 << 1, - SwapChain = 1 << 2, - Size = 1 << 3, - Font = 1 << 4, - Settings = 1 << 5, - }; - ATLAS_FLAG_OPS(ApiInvalidations, u8) - - // Handled in Present() - enum class RenderInvalidations : u8 - { - None = 0, - Cursor = 1 << 0, - ConstBuffer = 1 << 1, - }; - ATLAS_FLAG_OPS(RenderInvalidations, u8) - - // MSVC STL (version 22000) implements std::clamp(T, T, T) in terms of the generic - // std::clamp(T, T, T, Predicate) with std::less{} as the argument, - // which introduces branching. While not perfect, this is still better than std::clamp. - template - static constexpr T clamp(T val, T min, T max) - { - return std::max(min, std::min(max, val)); - } - // AtlasEngine.cpp - [[nodiscard]] HRESULT _handleException(const wil::ResultException& exception) noexcept; - __declspec(noinline) void _createResources(); - void _releaseSwapChain(); - __declspec(noinline) void _createSwapChain(); - __declspec(noinline) void _recreateSizeDependentResources(); - __declspec(noinline) void _recreateFontDependentResources(); - IDWriteTextFormat* _getTextFormat(bool bold, bool italic) const noexcept; - const Buffer& _getTextFormatAxis(bool bold, bool italic) const noexcept; - Cell* _getCell(u16 x, u16 y) noexcept; - TileHashMap::iterator* _getCellGlyphMapping(u16 x, u16 y) noexcept; - void _setCellFlags(u16r coords, CellFlags mask, CellFlags bits) noexcept; + ATLAS_ATTR_COLD void _handleSettingsUpdate(); + void _recreateFontDependentResources(); + void _recreateCellCountDependentResources(); void _flushBufferLine(); - bool _emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, size_t bufferPos2); + void _mapCharacters(const wchar_t* text, u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const; + void _mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 length, ShapedRow& row); + ATLAS_ATTR_COLD void _mapReplacementCharacter(u32 from, u32 to, ShapedRow& row); // AtlasEngine.api.cpp void _resolveTransparencySettings() noexcept; void _updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes); - void _resolveFontMetrics(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics = nullptr) const; + void _resolveFontMetrics(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr) const; // AtlasEngine.r.cpp - void _renderWithCustomShader() const; - void _setShaderResources() const; - void _updateConstantBuffer() const noexcept; - void _adjustAtlasSize(); - void _processGlyphQueue(); - void _drawGlyph(const TileHashMap::iterator& it) const; - CachedGlyphLayout _getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const; - void _drawCursor(u16r rect, u32 color, bool clear); - ID2D1Brush* _brushWithColor(u32 color); - void _d2dPresent(); - void _d2dCreateRenderTarget(); - void _d2dDrawDirtyArea(); - u16 _d2dDrawGlyph(const TileHashMap::iterator& it, u16x2 coord, u32 color); - void _d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle = nullptr); - void _d2dFillRectangle(u16r rect, u32 color); - void _d2dCellFlagRendererCursor(u16r rect, u32 color); - void _d2dCellFlagRendererSelected(u16r rect, u32 color); - void _d2dCellFlagRendererUnderline(u16r rect, u32 color); - void _d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color); - void _d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color); - void _d2dCellFlagRendererStrikethrough(u16r rect, u32 color); - - static constexpr bool debugForceD2DMode = false; - static constexpr bool debugGlyphGenerationPerformance = false; - static constexpr bool debugTextParsingPerformance = false || debugGlyphGenerationPerformance; - static constexpr bool debugGeneralPerformance = false || debugTextParsingPerformance; + ATLAS_ATTR_COLD void _recreateAdapter(); + ATLAS_ATTR_COLD void _recreateBackend(); + ATLAS_ATTR_COLD void _handleSwapChainUpdate(); + void _createSwapChain(); + void _destroySwapChain(); + void _resizeBuffers(); + void _updateMatrixTransform(); + void _waitUntilCanRender() noexcept; + void _present(); static constexpr u16 u16min = 0x0000; static constexpr u16 u16max = 0xffff; static constexpr i16 i16min = -0x8000; static constexpr i16 i16max = 0x7fff; static constexpr u16r invalidatedAreaNone = { u16max, u16max, u16min, u16min }; - static constexpr u16x2 invalidatedRowsNone{ u16max, u16min }; - static constexpr u16x2 invalidatedRowsAll{ u16min, u16max }; + static constexpr range invalidatedRowsNone{ u16max, u16min }; + static constexpr range invalidatedRowsAll{ u16min, u16max }; - struct StaticResources - { - wil::com_ptr d2dFactory; - wil::com_ptr dwriteFactory; - wil::com_ptr systemFontFallback; - wil::com_ptr textAnalyzer; - bool isWindows10OrGreater = true; - -#ifndef NDEBUG - std::filesystem::path sourceDirectory; - wil::unique_folder_change_reader_nothrow sourceCodeWatcher; - std::atomic sourceCodeInvalidationTime{ INT64_MAX }; -#endif - } _sr; - - struct Resources - { - // DXGI resources - wil::com_ptr dxgiFactory; - - // D3D resources - wil::com_ptr device; - wil::com_ptr deviceContext; - wil::com_ptr swapChain; - wil::unique_handle frameLatencyWaitableObject; - wil::com_ptr renderTargetView; - wil::com_ptr vertexShader; - wil::com_ptr pixelShader; - wil::com_ptr constantBuffer; - wil::com_ptr cellBuffer; - wil::com_ptr cellView; - wil::com_ptr customOffscreenTexture; - wil::com_ptr customOffscreenTextureView; - wil::com_ptr customOffscreenTextureTargetView; - wil::com_ptr customVertexShader; - wil::com_ptr customPixelShader; - wil::com_ptr customShaderConstantBuffer; - wil::com_ptr customShaderSamplerState; - std::chrono::steady_clock::time_point customShaderStartTime; - - // D2D resources - wil::com_ptr atlasBuffer; - wil::com_ptr atlasView; - wil::com_ptr d2dRenderTarget; - wil::com_ptr brush; - wil::com_ptr fontFaces[4]; - wil::com_ptr textFormats[2][2]; - Buffer textFormatAxes[2][2]; - wil::com_ptr typography; - wil::com_ptr dottedStrokeStyle; - - Buffer cells; // invalidated by ApiInvalidations::Size - Buffer cellGlyphMapping; // invalidated by ApiInvalidations::Size - f32x2 cellSizeDIP; // invalidated by ApiInvalidations::Font, caches _api.cellSize but in DIP - u16x2 cellCount; // invalidated by ApiInvalidations::Font|Size, caches _api.cellCount - u16 dpi = USER_DEFAULT_SCREEN_DPI; // invalidated by ApiInvalidations::Font, caches _api.dpi - FontMetrics fontMetrics; // invalidated by ApiInvalidations::Font, cached _api.fontMetrics - f32 dipPerPixel = 1.0f; // invalidated by ApiInvalidations::Font, caches USER_DEFAULT_SCREEN_DPI / _api.dpi - f32 pixelPerDIP = 1.0f; // invalidated by ApiInvalidations::Font, caches _api.dpi / USER_DEFAULT_SCREEN_DPI - u16x2 atlasSizeInPixel; // invalidated by ApiInvalidations::Font - TileHashMap glyphs; - TileAllocator tileAllocator; - std::vector glyphQueue; - - f32 gamma = 0; - f32 cleartypeEnhancedContrast = 0; - f32 grayscaleEnhancedContrast = 0; - u32 backgroundColor = 0xff000000; - u32 selectionColor = 0x7fffffff; - u32 brushColor = 0xffffffff; - - CachedCursorOptions cursorOptions; - RenderInvalidations invalidations = RenderInvalidations::None; - - til::rect dirtyRect; - i16 scrollOffset = 0; - bool d2dMode = false; - bool waitForPresentation = false; - bool requiresContinuousRedraw = false; - -#ifndef NDEBUG - // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: - // > For every frame it renders, the app should wait on this handle before starting any rendering operations. - // > Note that this requirement includes the first frame the app renders with the swap chain. - bool frameLatencyWaitableObjectUsed = false; -#endif - } _r; + std::unique_ptr _b; + RenderingPayload _p; struct ApiState { + GenerationalSettings s = DirtyGenerationalSettings(); + // This structure is loosely sorted in chunks from "very often accessed together" // to seldom accessed and/or usually not together. + bool invalidatedTitle = false; + // These two are redundant with TargetSettings/MiscellaneousSettings, but that's because _resolveTransparencySettings() + // turns the given settings into potentially different actual settings (which are then written into the Settings). + bool enableTransparentBackground = false; + AntialiasingMode antialiasingMode = DefaultAntialiasingMode; + std::vector bufferLine; std::vector bufferLineColumn; - Buffer bufferLineMetadata; + + std::wstring userLocaleName; + + std::array, 4> textFormatAxes; std::vector analysisResults; Buffer clusterMap; Buffer textProps; @@ -1069,49 +142,33 @@ namespace Microsoft::Console::Render Buffer glyphProps; Buffer glyphAdvances; Buffer glyphOffsets; - std::vector fontFeatures; // changes are flagged as ApiInvalidations::Font|Size - std::vector fontAxisValues; // changes are flagged as ApiInvalidations::Font|Size - FontMetrics fontMetrics; // changes are flagged as ApiInvalidations::Font|Size - u16x2 cellCount; // caches `sizeInPixel / cellSize` - u16x2 sizeInPixel; // changes are flagged as ApiInvalidations::Size + wil::com_ptr replacementCharacterFontFace; + u16 replacementCharacterGlyphIndex = 0; + bool replacementCharacterLookedUp = false; + // PrepareLineTransform() + LineRendition lineRendition = LineRendition::SingleWidth; // UpdateDrawingBrushes() - u32 backgroundOpaqueMixin = 0xff000000; // changes are flagged as ApiInvalidations::SwapChain - u32x2 currentColor; - AtlasKeyAttributes attributes{}; - u16x2 lastPaintBufferLineCoord; - CellFlags flags = CellFlags::None; - // SetSelectionBackground() - u32 selectionColor = 0x7fffffff; + u32 backgroundOpaqueMixin = 0xff000000; + u32 currentBackground = 0; + u32 currentForeground = 0; + FontRelevantAttributes attributes = FontRelevantAttributes::None; + u16x2 lastPaintBufferLineCoord{}; // UpdateHyperlinkHoveredId() u16 hyperlinkHoveredId = 0; - bool bufferLineWasHyperlinked = false; // dirtyRect is a computed value based on invalidatedRows. til::rect dirtyRect; // These "invalidation" fields are reset in EndPaint() u16r invalidatedCursorArea = invalidatedAreaNone; - u16x2 invalidatedRows = invalidatedRowsNone; // x is treated as "top" and y as "bottom" + range invalidatedRows = invalidatedRowsNone; // x is treated as "top" and y as "bottom" i16 scrollOffset = 0; - - std::function warningCallback; - std::function swapChainChangedCallback; - wil::unique_handle swapChainHandle; - HWND hwnd = nullptr; - u16 dpi = USER_DEFAULT_SCREEN_DPI; // changes are flagged as ApiInvalidations::Font|Size - u8 antialiasingMode = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; // changes are flagged as ApiInvalidations::Font - u8 realizedAntialiasingMode = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; // caches antialiasingMode, depends on antialiasingMode and backgroundOpaqueMixin, see _resolveTransparencySettings - bool enableTransparentBackground = false; - - std::wstring customPixelShaderPath; // changes are flagged as ApiInvalidations::Device - bool useRetroTerminalEffect = false; // changes are flagged as ApiInvalidations::Device - bool useSoftwareRendering = false; // changes are flagged as ApiInvalidations::Device - - ApiInvalidations invalidations = ApiInvalidations::Device; } _api; - -#undef ATLAS_POD_OPS -#undef ATLAS_FLAG_OPS }; } + +namespace Microsoft::Console::Render +{ + using AtlasEngine = Atlas::AtlasEngine; +} diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index a8bc092c0b..e7070b708e 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -4,7 +4,8 @@ #include "pch.h" #include "AtlasEngine.h" -#include "dwrite.h" +#include "BackendD2D.h" +#include "BackendD3D.h" // #### NOTE #### // If you see any code in here that contains "_api." you might be seeing a race condition. @@ -22,39 +23,7 @@ #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). #pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). -// https://en.wikipedia.org/wiki/Inversion_list -template -constexpr bool isInInversionList(const std::array& ranges, wchar_t needle) -{ - const auto beg = ranges.begin(); - const auto end = ranges.end(); - decltype(ranges.begin()) it; - - // Linear search is faster than binary search for short inputs. - if constexpr (N < 16) - { - it = std::find_if(beg, end, [=](wchar_t v) { return needle < v; }); - } - else - { - it = std::upper_bound(beg, end, needle); - } - - const auto idx = it - beg; - return (idx & 1) != 0; -} - -template -constexpr T colorFromU32(uint32_t rgba) -{ - const auto r = static_cast((rgba >> 0) & 0xff) / 255.0f; - const auto g = static_cast((rgba >> 8) & 0xff) / 255.0f; - const auto b = static_cast((rgba >> 16) & 0xff) / 255.0f; - const auto a = static_cast((rgba >> 24) & 0xff) / 255.0f; - return { r, g, b, a }; -} - -using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Render::Atlas; #pragma region IRenderEngine @@ -63,1044 +32,464 @@ using namespace Microsoft::Console::Render; [[nodiscard]] HRESULT AtlasEngine::Present() noexcept try { - const til::rect fullRect{ 0, 0, _r.cellCount.x, _r.cellCount.y }; - - // A change in the selection or background color (etc.) forces a full redraw. - if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer) || _r.customPixelShader) + if (!_p.dxgi.adapter || !_p.dxgi.factory->IsCurrent()) { - _r.dirtyRect = fullRect; + _recreateAdapter(); } - if (!_r.dirtyRect) + if (!_b) { - return S_OK; + _recreateBackend(); } - // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: - // > For every frame it renders, the app should wait on this handle before starting any rendering operations. - // > Note that this requirement includes the first frame the app renders with the swap chain. - assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed); - - if (_r.d2dMode) [[unlikely]] + if (_p.swapChain.generation != _p.s.generation()) { - _d2dPresent(); - } - else - { - _adjustAtlasSize(); - _processGlyphQueue(); - - // The values the constant buffer depends on are potentially updated after BeginPaint(). - if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer)) - { - _updateConstantBuffer(); - WI_ClearFlag(_r.invalidations, RenderInvalidations::ConstBuffer); - } - - { -#pragma warning(suppress : 26494) // Variable 'mapped' is uninitialized. Always initialize an object (type.5). - D3D11_MAPPED_SUBRESOURCE mapped; - THROW_IF_FAILED(_r.deviceContext->Map(_r.cellBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); - assert(mapped.RowPitch >= _r.cells.size() * sizeof(Cell)); - memcpy(mapped.pData, _r.cells.data(), _r.cells.size() * sizeof(Cell)); - _r.deviceContext->Unmap(_r.cellBuffer.get(), 0); - } - - if (_r.customPixelShader) [[unlikely]] - { - _renderWithCustomShader(); - } - else - { - _r.deviceContext->OMSetRenderTargets(1, _r.renderTargetView.addressof(), nullptr); - _r.deviceContext->Draw(3, 0); - } - } - - // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: - // > For every frame it renders, the app should wait on this handle before starting any rendering operations. - // > Note that this requirement includes the first frame the app renders with the swap chain. - assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed); - - if (_r.dirtyRect != fullRect) - { - auto dirtyRectInPx = _r.dirtyRect; - dirtyRectInPx.left *= _r.fontMetrics.cellSize.x; - dirtyRectInPx.top *= _r.fontMetrics.cellSize.y; - dirtyRectInPx.right *= _r.fontMetrics.cellSize.x; - dirtyRectInPx.bottom *= _r.fontMetrics.cellSize.y; - - RECT scrollRect{}; - POINT scrollOffset{}; - DXGI_PRESENT_PARAMETERS params{ - .DirtyRectsCount = 1, - .pDirtyRects = dirtyRectInPx.as_win32_rect(), - }; - - if (_r.scrollOffset) - { - scrollRect = { - 0, - std::max(0, _r.scrollOffset), - _r.cellCount.x, - _r.cellCount.y + std::min(0, _r.scrollOffset), - }; - scrollOffset = { - 0, - _r.scrollOffset, - }; - - scrollRect.top *= _r.fontMetrics.cellSize.y; - scrollRect.right *= _r.fontMetrics.cellSize.x; - scrollRect.bottom *= _r.fontMetrics.cellSize.y; - - scrollOffset.y *= _r.fontMetrics.cellSize.y; - - params.pScrollRect = &scrollRect; - params.pScrollOffset = &scrollOffset; - } - - THROW_IF_FAILED(_r.swapChain->Present1(1, 0, ¶ms)); - } - else - { - THROW_IF_FAILED(_r.swapChain->Present(1, 0)); - } - - _r.waitForPresentation = true; - - if (!_r.dxgiFactory->IsCurrent()) - { - WI_SetFlag(_api.invalidations, ApiInvalidations::Device); + _handleSwapChainUpdate(); } + _b->Render(_p); + _present(); return S_OK; } catch (const wil::ResultException& exception) { - // TODO: this writes to _api. - return _handleException(exception); + const auto hr = exception.GetErrorCode(); + + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + { + _p.dxgi = {}; + return E_PENDING; + } + + if (_p.warningCallback) + { + try + { + _p.warningCallback(hr); + } + CATCH_LOG() + } + + _b.reset(); + return hr; } CATCH_RETURN() [[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept { - return debugGeneralPerformance || _r.requiresContinuousRedraw; + return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()); } void AtlasEngine::WaitUntilCanRender() noexcept { - // IDXGISwapChain2::GetFrameLatencyWaitableObject returns an auto-reset event. - // Once we've waited on the event, waiting on it again will block until the timeout elapses. - // _r.waitForPresentation guards against this. - if (!debugGeneralPerformance && std::exchange(_r.waitForPresentation, false)) + if constexpr (ATLAS_DEBUG_RENDER_DELAY) { - WaitForSingleObjectEx(_r.frameLatencyWaitableObject.get(), 100, true); -#ifndef NDEBUG - _r.frameLatencyWaitableObjectUsed = true; -#endif + Sleep(ATLAS_DEBUG_RENDER_DELAY); } + _waitUntilCanRender(); } #pragma endregion -void AtlasEngine::_renderWithCustomShader() const +void AtlasEngine::_recreateAdapter() { - // Render with our main shader just like Present(). +#ifndef NDEBUG + if (IsDebuggerPresent()) { - // OM: Output Merger - _r.deviceContext->OMSetRenderTargets(1, _r.customOffscreenTextureTargetView.addressof(), nullptr); - _r.deviceContext->Draw(3, 0); - } - - // Update the custom shader's constant buffer. - { - CustomConstBuffer data; - data.time = std::chrono::duration(std::chrono::steady_clock::now() - _r.customShaderStartTime).count(); - data.scale = _r.pixelPerDIP; - data.resolution.x = static_cast(_r.cellCount.x * _r.fontMetrics.cellSize.x); - data.resolution.y = static_cast(_r.cellCount.y * _r.fontMetrics.cellSize.y); - data.background = colorFromU32(_r.backgroundColor); - -#pragma warning(suppress : 26494) // Variable 'mapped' is uninitialized. Always initialize an object (type.5). - D3D11_MAPPED_SUBRESOURCE mapped; - THROW_IF_FAILED(_r.deviceContext->Map(_r.customShaderConstantBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); - assert(mapped.RowPitch >= sizeof(data)); - memcpy(mapped.pData, &data, sizeof(data)); - _r.deviceContext->Unmap(_r.customShaderConstantBuffer.get(), 0); - } - - // Render with the custom shader. - { - // OM: Output Merger - // customOffscreenTextureView was just rendered to via customOffscreenTextureTargetView and is - // set as the output target. Before we can use it as an input we have to remove it as an output. - _r.deviceContext->OMSetRenderTargets(1, _r.renderTargetView.addressof(), nullptr); - - // VS: Vertex Shader - _r.deviceContext->VSSetShader(_r.customVertexShader.get(), nullptr, 0); - - // PS: Pixel Shader - _r.deviceContext->PSSetShader(_r.customPixelShader.get(), nullptr, 0); - _r.deviceContext->PSSetConstantBuffers(0, 1, _r.customShaderConstantBuffer.addressof()); - _r.deviceContext->PSSetShaderResources(0, 1, _r.customOffscreenTextureView.addressof()); - _r.deviceContext->PSSetSamplers(0, 1, _r.customShaderSamplerState.addressof()); - - _r.deviceContext->Draw(4, 0); - } - - // For the next frame we need to restore our context state. - { - // VS: Vertex Shader - _r.deviceContext->VSSetShader(_r.vertexShader.get(), nullptr, 0); - - // PS: Pixel Shader - _r.deviceContext->PSSetShader(_r.pixelShader.get(), nullptr, 0); - _r.deviceContext->PSSetConstantBuffers(0, 1, _r.constantBuffer.addressof()); - const std::array resources{ _r.cellView.get(), _r.atlasView.get() }; - _r.deviceContext->PSSetShaderResources(0, gsl::narrow_cast(resources.size()), resources.data()); - _r.deviceContext->PSSetSamplers(0, 0, nullptr); - } -} - -void AtlasEngine::_setShaderResources() const -{ - // IA: Input Assembler - // Our vertex shader uses a trick from Bill Bilodeau published in - // "Vertex Shader Tricks" at GDC14 to draw a fullscreen triangle - // without vertex/index buffers. This prepares our context for this. - _r.deviceContext->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); - _r.deviceContext->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); - _r.deviceContext->IASetInputLayout(nullptr); - _r.deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); - - // VS: Vertex Shader - _r.deviceContext->VSSetShader(_r.vertexShader.get(), nullptr, 0); - - // PS: Pixel Shader - _r.deviceContext->PSSetShader(_r.pixelShader.get(), nullptr, 0); - _r.deviceContext->PSSetConstantBuffers(0, 1, _r.constantBuffer.addressof()); - const std::array resources{ _r.cellView.get(), _r.atlasView.get() }; - _r.deviceContext->PSSetShaderResources(0, gsl::narrow_cast(resources.size()), resources.data()); -} - -void AtlasEngine::_updateConstantBuffer() const noexcept -{ - const auto useClearType = _api.realizedAntialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; - - ConstBuffer data; - data.viewport.x = 0; - data.viewport.y = 0; - data.viewport.z = static_cast(_r.cellCount.x * _r.fontMetrics.cellSize.x); - data.viewport.w = static_cast(_r.cellCount.y * _r.fontMetrics.cellSize.y); - DWrite_GetGammaRatios(_r.gamma, data.gammaRatios); - data.enhancedContrast = useClearType ? _r.cleartypeEnhancedContrast : _r.grayscaleEnhancedContrast; - data.cellCountX = _r.cellCount.x; - data.cellSize.x = _r.fontMetrics.cellSize.x; - data.cellSize.y = _r.fontMetrics.cellSize.y; - data.underlinePos = _r.fontMetrics.underlinePos; - data.underlineWidth = _r.fontMetrics.underlineWidth; - data.strikethroughPos = _r.fontMetrics.strikethroughPos; - data.strikethroughWidth = _r.fontMetrics.strikethroughWidth; - data.doubleUnderlinePos.x = _r.fontMetrics.doubleUnderlinePos.x; - data.doubleUnderlinePos.y = _r.fontMetrics.doubleUnderlinePos.y; - data.thinLineWidth = _r.fontMetrics.thinLineWidth; - data.backgroundColor = _r.backgroundColor; - data.cursorColor = _r.cursorOptions.cursorColor; - data.selectionColor = _r.selectionColor; - data.useClearType = useClearType; -#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). - _r.deviceContext->UpdateSubresource(_r.constantBuffer.get(), 0, nullptr, &data, 0, 0); -} - -void AtlasEngine::_adjustAtlasSize() -{ - // Only grow the atlas texture if our tileAllocator needs it to be larger. - // We have no way of shrinking our tileAllocator at the moment, - // so technically a `requiredSize != _r.atlasSizeInPixel` - // comparison would be sufficient, but better safe than sorry. - const auto requiredSize = _r.tileAllocator.size(); - if (requiredSize.y <= _r.atlasSizeInPixel.y && requiredSize.x <= _r.atlasSizeInPixel.x) - { - return; - } - - wil::com_ptr atlasBuffer; - wil::com_ptr atlasView; - { - D3D11_TEXTURE2D_DESC desc{}; - desc.Width = requiredSize.x; - desc.Height = requiredSize.y; - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - desc.SampleDesc = { 1, 0 }; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; - THROW_IF_FAILED(_r.device->CreateTexture2D(&desc, nullptr, atlasBuffer.addressof())); - THROW_IF_FAILED(_r.device->CreateShaderResourceView(atlasBuffer.get(), nullptr, atlasView.addressof())); - } - - // If a _r.atlasBuffer already existed, we can copy its glyphs - // over to the new texture without re-rendering everything. - const auto copyFromExisting = _r.atlasSizeInPixel != u16x2{}; - if (copyFromExisting) - { - D3D11_BOX box; - box.left = 0; - box.top = 0; - box.front = 0; - box.right = _r.atlasSizeInPixel.x; - box.bottom = _r.atlasSizeInPixel.y; - box.back = 1; - _r.deviceContext->CopySubresourceRegion1(atlasBuffer.get(), 0, 0, 0, 0, _r.atlasBuffer.get(), 0, &box, D3D11_COPY_NO_OVERWRITE); - } - - { - const auto surface = atlasBuffer.query(); - - wil::com_ptr renderingParams; - DWrite_GetRenderParams(_sr.dwriteFactory.get(), &_r.gamma, &_r.cleartypeEnhancedContrast, &_r.grayscaleEnhancedContrast, renderingParams.addressof()); - - D2D1_RENDER_TARGET_PROPERTIES props{}; - props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; - props.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; - props.dpiX = static_cast(_r.dpi); - props.dpiY = static_cast(_r.dpi); - wil::com_ptr renderTarget; - THROW_IF_FAILED(_sr.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, renderTarget.addressof())); - _r.d2dRenderTarget = renderTarget.query(); - - // We don't really use D2D for anything except DWrite, but it - // can't hurt to ensure that everything it does is pixel aligned. - _r.d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); - // In case _api.realizedAntialiasingMode is D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE we'll - // continuously adjust it in AtlasEngine::_drawGlyph. See _drawGlyph. - _r.d2dRenderTarget->SetTextAntialiasMode(static_cast(_api.realizedAntialiasingMode)); - // Ensure that D2D uses the exact same gamma as our shader uses. - _r.d2dRenderTarget->SetTextRenderingParams(renderingParams.get()); - } - { - static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; - THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put())); - _r.brushColor = 0xffffffff; - } - - _r.atlasSizeInPixel = requiredSize; - _r.atlasBuffer = std::move(atlasBuffer); - _r.atlasView = std::move(atlasView); - _setShaderResources(); - - WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer); - WI_SetFlagIf(_r.invalidations, RenderInvalidations::Cursor, !copyFromExisting); -} - -void AtlasEngine::_processGlyphQueue() -{ - if (_r.glyphQueue.empty() && WI_IsFlagClear(_r.invalidations, RenderInvalidations::Cursor)) - { - return; - } - - _r.d2dRenderTarget->BeginDraw(); - - if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor)) - { - _drawCursor({ 0, 0, 1, 1 }, 0xffffffff, true); - WI_ClearFlag(_r.invalidations, RenderInvalidations::Cursor); - } - - for (const auto& it : _r.glyphQueue) - { - _drawGlyph(it); - } - _r.glyphQueue.clear(); - - THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw()); -} - -void AtlasEngine::_drawGlyph(const TileHashMap::iterator& it) const -{ - const auto key = it->first.data(); - const auto value = it->second.data(); - const auto coords = &value->coords[0]; - const auto charsLength = key->charCount; - const auto cellCount = key->attributes.cellCount; - const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic); - const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph); - const auto cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph); - - // Colored glyphs cannot be drawn in linear gamma. - // That's why we're simply alpha-blending them in the shader. - // In order for this to work correctly we have to prevent them from being drawn - // with ClearType, because we would then lack the alpha channel for the glyphs. - if (_api.realizedAntialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE) - { - _r.d2dRenderTarget->SetTextAntialiasMode(coloredGlyph ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE); - } - - for (u16 i = 0; i < cellCount; ++i) - { - const auto coord = coords[i]; - - D2D1_RECT_F rect; - rect.left = static_cast(coord.x) * _r.dipPerPixel; - rect.top = static_cast(coord.y) * _r.dipPerPixel; - rect.right = rect.left + _r.cellSizeDIP.x; - rect.bottom = rect.top + _r.cellSizeDIP.y; - - D2D1_POINT_2F origin; - origin.x = rect.left - i * _r.cellSizeDIP.x; - origin.y = rect.top; - - _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); - _r.d2dRenderTarget->Clear(); - - cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin); - - // Now that we're done using origin to calculate the center point for our transformation - // we can use it for its intended purpose to slightly shift the glyph around. - origin.x += cachedLayout.offset.x; - origin.y += cachedLayout.offset.y; - _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _r.brush.get(), cachedLayout.options); - - cachedLayout.undoScaling(_r.d2dRenderTarget.get()); - - _r.d2dRenderTarget->PopAxisAlignedClip(); - } -} - -AtlasEngine::CachedGlyphLayout AtlasEngine::_getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const -{ - const f32x2 layoutBox{ cellCount * _r.cellSizeDIP.x, _r.cellSizeDIP.y }; - bool scalingRequired = false; - f32x2 offset{ 0, 0 }; - f32x2 scale{ 1, 1 }; - f32x2 scaleCenter; - - // See D2DFactory::DrawText - wil::com_ptr textLayout; - THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(chars, charsLength, textFormat, layoutBox.x, layoutBox.y, textLayout.addressof())); - if (_r.typography) - { - textLayout->SetTypography(_r.typography.get(), { 0, charsLength }); - } - - // Block Element and Box Drawing characters need to be handled separately, - // because unlike regular ones they're supposed to fill the entire layout box. - // - // Ranges: - // * 0x2500-0x257F: Box Drawing - // * 0x2580-0x259F: Block Elements - // * 0xE0A0-0xE0A3,0xE0B0-0xE0C8,0xE0CA-0xE0CA,0xE0CC-0xE0D4: PowerLine - // (https://github.com/ryanoasis/nerd-fonts/wiki/Glyph-Sets-and-Code-Points#powerline-symbols) - // - // The following `blockCharacters` forms a so called "inversion list". - static constexpr std::array blockCharacters{ - // clang-format off - L'\u2500', L'\u2580', - L'\u2580', L'\u25A0', - L'\uE0A0', L'\uE0A4', - L'\uE0B0', L'\uE0C9', - L'\uE0CA', L'\uE0CB', - L'\uE0CC', L'\uE0D5', - // clang-format on - }; - - if (charsLength == 1 && isInInversionList(blockCharacters, chars[0])) - { - wil::com_ptr fontCollection; - THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof())); - const auto baseWeight = textFormat->GetFontWeight(); - const auto baseStyle = textFormat->GetFontStyle(); - - TextAnalysisSource analysisSource{ chars, 1 }; - UINT32 mappedLength = 0; - wil::com_ptr mappedFont; - FLOAT mappedScale = 0; - THROW_IF_FAILED(_sr.systemFontFallback->MapCharacters( - /* analysisSource */ &analysisSource, - /* textPosition */ 0, - /* textLength */ 1, - /* baseFontCollection */ fontCollection.get(), - /* baseFamilyName */ _r.fontMetrics.fontName.data(), - /* baseWeight */ baseWeight, - /* baseStyle */ baseStyle, - /* baseStretch */ DWRITE_FONT_STRETCH_NORMAL, - /* mappedLength */ &mappedLength, - /* mappedFont */ mappedFont.addressof(), - /* scale */ &mappedScale)); - - if (mappedFont) + // DXGIGetDebugInterface1 returns E_NOINTERFACE on systems without the Windows SDK installed. + if (wil::com_ptr infoQueue; SUCCEEDED_LOG(DXGIGetDebugInterface1(0, IID_PPV_ARGS(infoQueue.addressof())))) { - wil::com_ptr fontFace; - THROW_IF_FAILED(mappedFont->CreateFontFace(fontFace.addressof())); - - // Don't adjust the size of block glyphs that are part of the user's chosen font. - if (std::ranges::find(_r.fontFaces, fontFace) == std::end(_r.fontFaces)) + // I didn't want to link with dxguid.lib just for getting DXGI_DEBUG_ALL. This GUID is publicly documented. + static constexpr GUID dxgiDebugAll{ 0xe48ae283, 0xda80, 0x490b, { 0x87, 0xe6, 0x43, 0xe9, 0xa9, 0xcf, 0xda, 0x8 } }; + for (const auto severity : { DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_WARNING, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_INFO }) { - DWRITE_FONT_METRICS metrics; - fontFace->GetMetrics(&metrics); - - static constexpr u32 codePoint = L'\u2588'; // Full Block character - u16 glyphIndex; - THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex)); - - if (glyphIndex) - { - DWRITE_GLYPH_METRICS glyphMetrics; - THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics)); - - const auto fontScale = _r.fontMetrics.fontSizeInDIP / metrics.designUnitsPerEm; - - // How-to-DWRITE_OVERHANG_METRICS given a single glyph: - DWRITE_OVERHANG_METRICS overhang; - overhang.left = static_cast(glyphMetrics.leftSideBearing) * fontScale; - overhang.top = static_cast(glyphMetrics.verticalOriginY - glyphMetrics.topSideBearing) * fontScale - _r.fontMetrics.baselineInDIP; - overhang.right = static_cast(gsl::narrow_cast(glyphMetrics.advanceWidth) - glyphMetrics.rightSideBearing) * fontScale - layoutBox.x; - overhang.bottom = static_cast(gsl::narrow_cast(glyphMetrics.advanceHeight) - glyphMetrics.verticalOriginY - glyphMetrics.bottomSideBearing) * fontScale + _r.fontMetrics.baselineInDIP - layoutBox.y; - - scalingRequired = true; - // Center glyphs. - offset.x = (overhang.left - overhang.right) * 0.5f; - offset.y = (overhang.top - overhang.bottom) * 0.5f; - // We always want box drawing glyphs to exactly match the size of a terminal cell. - // But add 1px to the destination size, so that we don't end up with fractional pixels. - scale.x = (layoutBox.x + _r.pixelPerDIP) / (layoutBox.x + overhang.left + overhang.right); - scale.y = (layoutBox.y + _r.pixelPerDIP) / (layoutBox.y + overhang.top + overhang.bottom); - // Now that the glyph is in the center of the cell thanks - // to the offset, the scaleCenter is center of the cell. - scaleCenter.x = layoutBox.x * 0.5f; - scaleCenter.y = layoutBox.y * 0.5f; - } + LOG_IF_FAILED(infoQueue->SetBreakOnSeverity(dxgiDebugAll, severity, true)); } } } - else +#endif + +#ifndef NDEBUG + static constexpr UINT flags = DXGI_CREATE_FACTORY_DEBUG; +#else + static constexpr UINT flags = 0; +#endif + +// IID_PPV_ARGS doesn't work here for some reason. +#pragma warning(suppress : 26496) // The variable 'hr' does not change after construction, mark it as const (con.4). + auto hr = CreateDXGIFactory2(flags, __uuidof(_p.dxgi.factory), _p.dxgi.factory.put_void()); + +#ifndef NDEBUG + // This might be due to missing the "Graphics debugger and GPU profiler for + // DirectX" tools. Just as a sanity check, try again without + // `DXGI_CREATE_FACTORY_DEBUG` + if (FAILED(hr)) { - DWRITE_OVERHANG_METRICS overhang; - THROW_IF_FAILED(textLayout->GetOverhangMetrics(&overhang)); + hr = CreateDXGIFactory2(0, __uuidof(_p.dxgi.factory), _p.dxgi.factory.put_void()); + } +#endif + THROW_IF_FAILED(hr); - auto actualSizeX = layoutBox.x + overhang.left + overhang.right; + wil::com_ptr adapter; + DXGI_ADAPTER_DESC1 desc{}; - // Long glyphs should be drawn with their proper design size, even if that makes them a bit blurry, - // because otherwise we fail to support "pseudo" block characters like the "===" ligature in Cascadia Code. - // If we didn't force upscale that ligatures it would seemingly shrink shorter and shorter, as its - // glyph advance is often slightly shorter by a fractional pixel or two compared to our terminal's cells. - // It's a trade off that keeps most glyphs "crisp" while retaining support for things like "===". - // At least I can't think of any better heuristic for this at the moment... - if (cellCount > 2) + { + const auto useSoftwareRendering = _p.s->target->useSoftwareRendering; + UINT index = 0; + + do { - const auto advanceScale = _r.fontMetrics.advanceScale; - scalingRequired = true; - scale = { advanceScale, advanceScale }; - actualSizeX *= advanceScale; - } + THROW_IF_FAILED(_p.dxgi.factory->EnumAdapters1(index++, adapter.put())); + THROW_IF_FAILED(adapter->GetDesc1(&desc)); - // We need to offset glyphs that are simply outside of our layout box (layoutBox.x/.y) - // and additionally downsize glyphs that are entirely too large to fit in. - // The DWRITE_OVERHANG_METRICS will tell us how many DIPs the layout box is too large/small. - // It contains a positive number if the glyph is outside and a negative one if it's inside - // the layout box. For example, given a layoutBox.x/.y (and cell size) of 20/30: - // * "M" is the "largest" ASCII character and might be: - // left: -0.6f - // right: -0.6f - // top: -7.6f - // bottom: -7.4f - // "M" doesn't fill the layout box at all! - // This is because we've rounded up the Terminal's cell size to whole pixels in - // _resolveFontMetrics. top/bottom margins are fairly large because we added the - // chosen font's ascender, descender and line gap metrics to get our line height. - // --> offsetX = 0 - // --> offsetY = 0 - // --> scale = 1 - // * The bar diacritic (U+0336 combining long stroke overlay) - // left: -9.0f - // top: -16.3f - // right: 5.6f - // bottom: -11.7f - // right is positive! Our glyph is 5.6 DIPs outside of the layout box and would - // appear cut off during rendering. left is negative at -9, which indicates that - // we can simply shift the glyph by 5.6 DIPs to the left to fit it into our bounds. - // --> offsetX = -5.6f - // --> offsetY = 0 - // --> scale = 1 - // * Any wide emoji in a narrow cell (U+26A0 warning sign) - // left: 6.7f - // top: -4.1f - // right: 6.7f - // bottom: -3.0f - // Our emoji is outside the bounds on both the left and right side and we need to shrink it. - // --> offsetX = 0 - // --> offsetY = 0 - // --> scale = layoutBox.y / (layoutBox.y + left + right) - // = 0.69f - offset.x = std::max(0.0f, overhang.left) - std::max(0.0f, overhang.right); - scaleCenter.x = offset.x; - scaleCenter.y = _r.fontMetrics.baselineInDIP; - - if ((actualSizeX - layoutBox.x) > _r.dipPerPixel) - { - scalingRequired = true; - offset.x = (overhang.left - overhang.right) * 0.5f; - scale.x = layoutBox.x / actualSizeX; - scale.y = scale.x; - scaleCenter.x = layoutBox.x * 0.5f; - } - if (overhang.top > _r.dipPerPixel || overhang.bottom > _r.dipPerPixel) - { - const auto descend = _r.cellSizeDIP.y - _r.fontMetrics.baselineInDIP; - const auto scaleTop = _r.fontMetrics.baselineInDIP / (_r.fontMetrics.baselineInDIP + overhang.top); - const auto scaleBottom = descend / (descend + overhang.bottom); - scalingRequired = true; - scale.x = std::min(scale.x, std::min(scaleTop, scaleBottom)); - scale.y = scale.x; - } + // If useSoftwareRendering is false we exit during the first iteration. Using the default adapter (index 0) + // is the right thing to do under most circumstances, unless you _really_ want to get your hands dirty. + // The alternative is to track the window rectangle in respect to all IDXGIOutputs and select the right + // IDXGIAdapter, while also considering the "graphics preference" override in the windows settings app, etc. + // + // If useSoftwareRendering is true we search until we find the first WARP adapter (usually the last adapter). + } while (useSoftwareRendering && WI_IsFlagClear(desc.Flags, DXGI_ADAPTER_FLAG_SOFTWARE)); } - auto options = D2D1_DRAW_TEXT_OPTIONS_NONE; - // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery - // which doesn't have to run if we know we can't use it anyways in the shader. - WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph); - // !!! IMPORTANT !!! - // DirectWrite/2D snaps the baseline to whole pixels, which is something we technically - // want (it makes text look crisp), but fails in weird ways if `scalingRequired` is true. - // As our scaling matrix's dx/dy (center point) is based on the `origin` coordinates - // each cell we draw gets a unique, fractional baseline which gets rounded differently. - // I'm not 100% sure why that happens, since `origin` is always in full pixels... - // But this causes wide glyphs to draw as tiles that are potentially misaligned vertically by a pixel. - // The resulting text rendering looks especially bad for ligatures like "====" in Cascadia Code, - // where every single "=" might be blatantly misaligned vertically (same for any box drawings). - WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_NO_SNAP, scalingRequired); + if (memcmp(&_p.dxgi.adapterLuid, &desc.AdapterLuid, sizeof(LUID)) != 0) + { + _p.dxgi.adapter = std::move(adapter); + _p.dxgi.adapterLuid = desc.AdapterLuid; + _p.dxgi.adapterFlags = desc.Flags; + _b.reset(); + } +} - // ClearType basically has a 3x higher horizontal resolution. To make our glyphs render the same everywhere, - // it's probably for the best to ensure we initially rasterize them on a whole pixel boundary. - // (https://en.wikipedia.org/wiki/ClearType#How_ClearType_works) - offset.x = roundf(offset.x * _r.pixelPerDIP) * _r.dipPerPixel; - // As explained below, we use D2D1_DRAW_TEXT_OPTIONS_NO_SNAP to prevent a weird issue with baseline snapping. - // But we do want it technically, so this re-implements baseline snapping... I think? - offset.y = roundf(offset.y * _r.pixelPerDIP) * _r.dipPerPixel; +void AtlasEngine::_recreateBackend() +{ + // D3D11 defers the destruction of objects and only one swap chain can be associated with a + // HWND, IWindow, or composition surface at a time. --> Destroy it while we still have the old device. + _destroySwapChain(); - return CachedGlyphLayout{ - .textLayout = textLayout, - .offset = offset, - .scale = scale, - .scaleCenter = scaleCenter, - .options = options, - .scalingRequired = scalingRequired, + auto d2dMode = ATLAS_DEBUG_FORCE_D2D_MODE; + auto deviceFlags = + D3D11_CREATE_DEVICE_SINGLETHREADED +#ifndef NDEBUG + | D3D11_CREATE_DEVICE_DEBUG +#endif + // This flag prevents the driver from creating a large thread pool for things like shader computations + // that would be advantageous for games. For us this has only a minimal performance benefit, + // but comes with a large memory usage overhead. At the time of writing the Nvidia + // driver launches $cpu_thread_count more worker threads without this flag. + | D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS + // Direct2D support. + | D3D11_CREATE_DEVICE_BGRA_SUPPORT; + + if (WI_IsFlagSet(_p.dxgi.adapterFlags, DXGI_ADAPTER_FLAG_SOFTWARE)) + { + WI_ClearFlag(deviceFlags, D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS); + d2dMode = true; + } + + wil::com_ptr device0; + wil::com_ptr deviceContext0; + D3D_FEATURE_LEVEL featureLevel{}; + + static constexpr std::array featureLevels{ + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1, }; -} -void AtlasEngine::_drawCursor(u16r rect, u32 color, bool clear) -{ - // lineWidth is in D2D's DIPs. For instance if we have a 150-200% zoom scale we want to draw a 2px wide line. - // At 150% scale lineWidth thus needs to be 1.33333... because at a zoom scale of 1.5 this results in a 2px wide line. - const auto lineWidth = std::max(1.0f, static_cast((_r.dpi + USER_DEFAULT_SCREEN_DPI / 2) / USER_DEFAULT_SCREEN_DPI * USER_DEFAULT_SCREEN_DPI) / static_cast(_r.dpi)); - const auto cursorType = static_cast(_r.cursorOptions.cursorType); +#pragma warning(suppress : 26496) // The variable 'hr' does not change after construction, mark it as const (con.4). + auto hr = D3D11CreateDevice( + /* pAdapter */ _p.dxgi.adapter.get(), + /* DriverType */ D3D_DRIVER_TYPE_UNKNOWN, + /* Software */ nullptr, + /* Flags */ deviceFlags, + /* pFeatureLevels */ featureLevels.data(), + /* FeatureLevels */ gsl::narrow_cast(featureLevels.size()), + /* SDKVersion */ D3D11_SDK_VERSION, + /* ppDevice */ device0.put(), + /* pFeatureLevel */ &featureLevel, + /* ppImmediateContext */ deviceContext0.put()); - // `clip` is the rectangle within our texture atlas that's reserved for our cursor texture, ... - D2D1_RECT_F clip; - clip.left = static_cast(rect.left) * _r.cellSizeDIP.x; - clip.top = static_cast(rect.top) * _r.cellSizeDIP.y; - clip.right = static_cast(rect.right) * _r.cellSizeDIP.x; - clip.bottom = static_cast(rect.bottom) * _r.cellSizeDIP.y; - - // ... whereas `rect` is just the visible (= usually white) portion of our cursor. - auto box = clip; - - switch (cursorType) +#ifndef NDEBUG + if (hr == DXGI_ERROR_SDK_COMPONENT_MISSING) { - case CursorType::Legacy: - box.top = box.bottom - _r.cellSizeDIP.y * static_cast(_r.cursorOptions.heightPercentage) / 100.0f; - break; - case CursorType::VerticalBar: - box.right = box.left + lineWidth; - break; - case CursorType::EmptyBox: - { - // EmptyBox is drawn as a line and unlike filled rectangles those are drawn centered on their - // coordinates in such a way that the line border extends half the width to each side. - // --> Our coordinates have to be 0.5 DIP off in order to draw a 2px line on a 200% scaling. - const auto halfWidth = lineWidth / 2.0f; - box.left += halfWidth; - box.top += halfWidth; - box.right -= halfWidth; - box.bottom -= halfWidth; - break; + // This might happen if you don't have "Graphics debugger and GPU + // profiler for DirectX" installed in VS. We shouln't just explode if + // you don't though - instead, disable debugging and try again. + WI_ClearFlag(deviceFlags, D3D11_CREATE_DEVICE_DEBUG); + + hr = D3D11CreateDevice( + /* pAdapter */ _p.dxgi.adapter.get(), + /* DriverType */ D3D_DRIVER_TYPE_UNKNOWN, + /* Software */ nullptr, + /* Flags */ deviceFlags, + /* pFeatureLevels */ featureLevels.data(), + /* FeatureLevels */ gsl::narrow_cast(featureLevels.size()), + /* SDKVersion */ D3D11_SDK_VERSION, + /* ppDevice */ device0.put(), + /* pFeatureLevel */ &featureLevel, + /* ppImmediateContext */ deviceContext0.put()); } - case CursorType::Underscore: - case CursorType::DoubleUnderscore: - box.top = box.bottom - lineWidth; - break; - default: - break; +#endif + THROW_IF_FAILED(hr); + + auto device = device0.query(); + auto deviceContext = deviceContext0.query(); + +#ifndef NDEBUG + if (IsDebuggerPresent()) + { + if (const auto d3dInfoQueue = device.try_query()) + { + for (const auto severity : { D3D11_MESSAGE_SEVERITY_CORRUPTION, D3D11_MESSAGE_SEVERITY_ERROR, D3D11_MESSAGE_SEVERITY_WARNING, D3D11_MESSAGE_SEVERITY_INFO }) + { + LOG_IF_FAILED(d3dInfoQueue->SetBreakOnSeverity(severity, true)); + } + } + } +#endif + + if (featureLevel < D3D_FEATURE_LEVEL_10_0) + { + d2dMode = true; + } + else if (featureLevel < D3D_FEATURE_LEVEL_11_0) + { + D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS options{}; + // I'm assuming if `CheckFeatureSupport` fails, it'll leave `options` untouched which will result in `d2dMode |= true`. + std::ignore = device->CheckFeatureSupport(D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, &options, sizeof(options)); + d2dMode |= !options.ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x; } - const auto brush = _brushWithColor(color); + _p.device = std::move(device); + _p.deviceContext = std::move(deviceContext); - // We need to clip the area we draw in to ensure we don't - // accidentally draw into any neighboring texture atlas tiles. - _r.d2dRenderTarget->PushAxisAlignedClip(&clip, D2D1_ANTIALIAS_MODE_ALIASED); - - if (clear) + if (d2dMode) { - _r.d2dRenderTarget->Clear(); - } - - if (cursorType == CursorType::EmptyBox) - { - _r.d2dRenderTarget->DrawRectangle(&box, brush, lineWidth); + _b = std::make_unique(); } else { - _r.d2dRenderTarget->FillRectangle(&box, brush); + _b = std::make_unique(_p); } - if (cursorType == CursorType::DoubleUnderscore) + // !!! NOTE !!! + // Normally the viewport is indirectly marked as dirty by `AtlasEngine::_handleSettingsUpdate()` whenever + // the settings change, but the `!_p.dxgi.factory->IsCurrent()` check is not part of the settings change + // flow and so we have to manually recreate how AtlasEngine.cpp marks viewports as dirty here. + // This ensures that the backends redraw their entire viewports whenever a new swap chain is created. + _p.MarkAllAsDirty(); +} + +void AtlasEngine::_handleSwapChainUpdate() +{ + if (_p.swapChain.targetGeneration != _p.s->target.generation()) { - const auto offset = lineWidth * 2.0f; - box.top -= offset; - box.bottom -= offset; - _r.d2dRenderTarget->FillRectangle(&box, brush); + _createSwapChain(); + } + else if (_p.swapChain.targetSize != _p.s->targetSize) + { + _resizeBuffers(); } - _r.d2dRenderTarget->PopAxisAlignedClip(); -} - -ID2D1Brush* AtlasEngine::_brushWithColor(u32 color) -{ - if (_r.brushColor != color) + if (_p.swapChain.fontGeneration != _p.s->font.generation()) { - const auto d2dColor = colorFromU32(color); - THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&d2dColor, nullptr, _r.brush.put())); - _r.brushColor = color; + _updateMatrixTransform(); } - return _r.brush.get(); + + _p.swapChain.generation = _p.s.generation(); } -AtlasEngine::CachedGlyphLayout::operator bool() const noexcept -{ - return static_cast(textLayout); -} +static constexpr DXGI_SWAP_CHAIN_FLAG swapChainFlags = ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT ? DXGI_SWAP_CHAIN_FLAG{} : DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; -void AtlasEngine::CachedGlyphLayout::reset() noexcept +void AtlasEngine::_createSwapChain() { - textLayout.reset(); -} + _destroySwapChain(); -void AtlasEngine::CachedGlyphLayout::applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept -{ - __assume(d2dRenderTarget != nullptr); + DXGI_SWAP_CHAIN_DESC1 desc{ + .Width = _p.s->targetSize.x, + .Height = _p.s->targetSize.y, + .Format = DXGI_FORMAT_B8G8R8A8_UNORM, + .SampleDesc = { .Count = 1 }, + .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT, + // Sometimes up to 2 buffers are locked, for instance during screen capture or when moving the window. + // 3 buffers seems to guarantee a stable framerate at display frequency at all times. + .BufferCount = 3, + .Scaling = DXGI_SCALING_NONE, + // DXGI_SWAP_EFFECT_FLIP_DISCARD is a mode that was created at a time were display drivers + // lacked support for Multiplane Overlays (MPO) and were copying buffers was expensive. + // This allowed DWM to quickly draw overlays (like gamebars) on top of rendered content. + // With faster GPU memory in general and with support for MPO in particular this isn't + // really an advantage anymore. Instead DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL allows for a + // more "intelligent" composition and display updates to occur like Panel Self Refresh + // (PSR) which requires dirty rectangles (Present1 API) to work correctly. + .SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + // If our background is opaque we can enable "independent" flips by setting DXGI_ALPHA_MODE_IGNORE. + // As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically. + .AlphaMode = _p.s->target->enableTransparentBackground ? DXGI_ALPHA_MODE_PREMULTIPLIED : DXGI_ALPHA_MODE_IGNORE, + .Flags = swapChainFlags, + }; - if (scalingRequired) + wil::com_ptr swapChain1; + wil::unique_handle handle; + + if (_p.s->target->hwnd) { - const D2D1_MATRIX_3X2_F transform{ - scale.x, - 0, - 0, - scale.y, - (origin.x + scaleCenter.x) * (1.0f - scale.x), - (origin.y + scaleCenter.y) * (1.0f - scale.y), + desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + THROW_IF_FAILED(_p.dxgi.factory->CreateSwapChainForHwnd(_p.device.get(), _p.s->target->hwnd, &desc, nullptr, nullptr, swapChain1.addressof())); + } + else + { + const auto module = GetModuleHandleW(L"dcomp.dll"); + const auto DCompositionCreateSurfaceHandle = GetProcAddressByFunctionDeclaration(module, DCompositionCreateSurfaceHandle); + THROW_LAST_ERROR_IF(!DCompositionCreateSurfaceHandle); + + // As per: https://docs.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatesurfacehandle + static constexpr DWORD COMPOSITIONSURFACE_ALL_ACCESS = 0x0003L; + THROW_IF_FAILED(DCompositionCreateSurfaceHandle(COMPOSITIONSURFACE_ALL_ACCESS, nullptr, handle.addressof())); + THROW_IF_FAILED(_p.dxgi.factory.query()->CreateSwapChainForCompositionSurfaceHandle(_p.device.get(), handle.get(), &desc, nullptr, swapChain1.addressof())); + } + + _p.swapChain.swapChain = swapChain1.query(); + _p.swapChain.handle = std::move(handle); + _p.swapChain.frameLatencyWaitableObject.reset(_p.swapChain.swapChain->GetFrameLatencyWaitableObject()); + _p.swapChain.targetGeneration = _p.s->target.generation(); + _p.swapChain.targetSize = _p.s->targetSize; + _p.swapChain.waitForPresentation = true; + + WaitUntilCanRender(); + + if (_p.swapChainChangedCallback) + { + try + { + _p.swapChainChangedCallback(_p.swapChain.handle.get()); + } + CATCH_LOG() + } +} + +void AtlasEngine::_destroySwapChain() +{ + if (_p.swapChain.swapChain) + { + // D3D11 defers the destruction of objects and only one swap chain can be associated with a + // HWND, IWindow, or composition surface at a time. --> Force the destruction of all objects. + _p.swapChain = {}; + if (_b) + { + _b->ReleaseResources(); + } + if (_p.deviceContext) + { + _p.deviceContext->ClearState(); + _p.deviceContext->Flush(); + } + } +} + +void AtlasEngine::_resizeBuffers() +{ + _b->ReleaseResources(); + _p.deviceContext->ClearState(); + + THROW_IF_FAILED(_p.swapChain.swapChain->ResizeBuffers(0, _p.s->targetSize.x, _p.s->targetSize.y, DXGI_FORMAT_UNKNOWN, swapChainFlags)); + _p.swapChain.targetSize = _p.s->targetSize; +} + +void AtlasEngine::_updateMatrixTransform() +{ + if (!_p.s->target->hwnd) + { + // XAML's SwapChainPanel combines the worst of both worlds and always applies a transform + // to the swap chain to make it match the display scale. This undoes the damage. + const DXGI_MATRIX_3X2_F matrix{ + ._11 = static_cast(USER_DEFAULT_SCREEN_DPI) / static_cast(_p.s->font->dpi), + ._22 = static_cast(USER_DEFAULT_SCREEN_DPI) / static_cast(_p.s->font->dpi), }; - d2dRenderTarget->SetTransform(&transform); + THROW_IF_FAILED(_p.swapChain.swapChain->SetMatrixTransform(&matrix)); + } + _p.swapChain.fontGeneration = _p.s->font.generation(); +} + +void AtlasEngine::_waitUntilCanRender() noexcept +{ + // IDXGISwapChain2::GetFrameLatencyWaitableObject returns an auto-reset event. + // Once we've waited on the event, waiting on it again will block until the timeout elapses. + // _waitForPresentation guards against this. + if constexpr (!ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT) + { + if (_p.swapChain.waitForPresentation) + { + WaitForSingleObjectEx(_p.swapChain.frameLatencyWaitableObject.get(), 100, true); + _p.swapChain.waitForPresentation = false; + } } } -void AtlasEngine::CachedGlyphLayout::undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept +void AtlasEngine::_present() { - __assume(d2dRenderTarget != nullptr); + const RECT fullRect{ 0, 0, _p.swapChain.targetSize.x, _p.swapChain.targetSize.y }; - if (scalingRequired) - { - static constexpr D2D1_MATRIX_3X2_F identity{ 1, 0, 0, 1, 0, 0 }; - d2dRenderTarget->SetTransform(&identity); - } -} + DXGI_PRESENT_PARAMETERS params{}; + RECT scrollRect{}; + POINT scrollOffset{}; -void AtlasEngine::_d2dPresent() -{ - if (!_r.d2dRenderTarget) - { - _d2dCreateRenderTarget(); - } - - _d2dDrawDirtyArea(); - - _r.glyphQueue.clear(); - WI_ClearAllFlags(_r.invalidations, RenderInvalidations::Cursor | RenderInvalidations::ConstBuffer); -} - -void AtlasEngine::_d2dCreateRenderTarget() -{ - { - wil::com_ptr buffer; - THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void())); - - const auto surface = buffer.query(); - - D2D1_RENDER_TARGET_PROPERTIES props{}; - props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; - props.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; - props.dpiX = static_cast(_r.dpi); - props.dpiY = static_cast(_r.dpi); - wil::com_ptr renderTarget; - THROW_IF_FAILED(_sr.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, renderTarget.addressof())); - _r.d2dRenderTarget = renderTarget.query(); - - // In case _api.realizedAntialiasingMode is D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE we'll - // continuously adjust it in AtlasEngine::_drawGlyph. See _drawGlyph. - _r.d2dRenderTarget->SetTextAntialiasMode(static_cast(_api.realizedAntialiasingMode)); - } - { - static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; - THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put())); - _r.brushColor = 0xffffffff; - } -} - -void AtlasEngine::_d2dDrawDirtyArea() -{ - struct CellFlagHandler - { - CellFlags filter; - decltype(&AtlasEngine::_d2dCellFlagRendererCursor) func; + // Since rows might be taller than their cells, they might have drawn outside of the viewport. + RECT dirtyRect{ + .left = std::max(_p.dirtyRectInPx.left, 0), + .top = std::max(_p.dirtyRectInPx.top, 0), + .right = std::min(_p.dirtyRectInPx.right, fullRect.right), + .bottom = std::min(_p.dirtyRectInPx.bottom, fullRect.bottom), }; - static constexpr std::array cellFlagHandlers{ - // Ordered by lowest to highest "layer". - // The selection for instance is drawn on top of underlines, not under them. - CellFlagHandler{ CellFlags::Underline, &AtlasEngine::_d2dCellFlagRendererUnderline }, - CellFlagHandler{ CellFlags::UnderlineDotted, &AtlasEngine::_d2dCellFlagRendererUnderlineDotted }, - CellFlagHandler{ CellFlags::UnderlineDouble, &AtlasEngine::_d2dCellFlagRendererUnderlineDouble }, - CellFlagHandler{ CellFlags::Strikethrough, &AtlasEngine::_d2dCellFlagRendererStrikethrough }, - CellFlagHandler{ CellFlags::Cursor, &AtlasEngine::_d2dCellFlagRendererCursor }, - CellFlagHandler{ CellFlags::Selected, &AtlasEngine::_d2dCellFlagRendererSelected }, - }; - - auto left = gsl::narrow(_r.dirtyRect.left); - auto top = gsl::narrow(_r.dirtyRect.top); - auto right = gsl::narrow(_r.dirtyRect.right); - auto bottom = gsl::narrow(_r.dirtyRect.bottom); - if constexpr (debugGlyphGenerationPerformance) + // Present1() dislikes being called with an empty dirty rect. + if (dirtyRect.left >= dirtyRect.right || dirtyRect.top >= dirtyRect.bottom) { - left = 0; - top = 0; - right = _r.cellCount.x; - bottom = _r.cellCount.y; + return; } - _r.d2dRenderTarget->BeginDraw(); - - if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer)) + if constexpr (!ATLAS_DEBUG_SHOW_DIRTY) { - _r.d2dRenderTarget->Clear(colorFromU32(_r.backgroundColor)); - } - - for (u16 y = top; y < bottom; ++y) - { - const Cell* cells = _getCell(0, y); - const TileHashMap::iterator* cellGlyphMappings = _getCellGlyphMapping(0, y); - - // left/right might intersect a wide glyph. We have to extend left/right - // to include the entire glyph so that we can properly render it. - // Since a series of identical narrow glyphs (2 spaces for instance) are stored in cellGlyphMappings - // just like a single wide glyph (2 references to the same glyph in a row), the only way for us to - // know where wide glyphs begin and end is to iterate the entire row and use the stored `cellCount`. - u16 beg = 0; - for (;;) + if (memcmp(&dirtyRect, &fullRect, sizeof(RECT)) != 0) { - const auto cellCount = cellGlyphMappings[beg]->first.data()->attributes.cellCount; - const auto begNext = gsl::narrow_cast(beg + cellCount); + params.DirtyRectsCount = 1; + params.pDirtyRects = &dirtyRect; - if (begNext > left) + if (_p.scrollOffset) { - break; - } + const auto offsetInPx = _p.scrollOffset * _p.s->font->cellSize.y; + const auto width = _p.s->targetSize.x; + // We don't use targetSize.y here, because "height" refers to the bottom coordinate of the last text row + // in the buffer. We then add the "offsetInPx" (which is negative when scrolling text upwards) and thus + // end up with a "bottom" value that is the bottom of the last row of text that we haven't invalidated. + const auto height = _p.s->viewportCellCount.y * _p.s->font->cellSize.y; + const auto top = std::max(0, offsetInPx); + const auto bottom = height + std::min(0, offsetInPx); - beg = begNext; - } - auto end = beg; - for (;;) - { - const auto cellCount = cellGlyphMappings[end]->first.data()->attributes.cellCount; - end += cellCount; + scrollRect = { 0, top, width, bottom }; + scrollOffset = { 0, offsetInPx }; - if (end >= right) - { - break; - } - } - - // Draw background. - { - _r.d2dRenderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY); - - auto x1 = beg; - auto x2 = gsl::narrow_cast(x1 + 1); - auto currentColor = cells[x1].color.y; - - for (; x2 < end; ++x2) - { - const auto color = cells[x2].color.y; - - if (currentColor != color) - { - const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; - _d2dFillRectangle(rect, currentColor); - x1 = x2; - currentColor = color; - } - } - - { - const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; - _d2dFillRectangle(rect, currentColor); - } - - _r.d2dRenderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER); - } - - // Draw text. - for (auto x = beg; x < end;) - { - const auto& it = cellGlyphMappings[x]; - const u16x2 coord{ x, y }; - const auto color = cells[x].color.x; - x += _d2dDrawGlyph(it, coord, color); - } - - // Draw underlines, cursors, selections, etc. - for (const auto& handler : cellFlagHandlers) - { - auto x1 = beg; - auto currentFlags = CellFlags::None; - - for (auto x2 = beg; x2 < end; ++x2) - { - const auto flags = cells[x2].flags & handler.filter; - - if (currentFlags != flags) - { - if (currentFlags != CellFlags::None) - { - const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; - const auto color = cells[x1].color.x; - (this->*handler.func)(rect, color); - } - - x1 = x2; - currentFlags = flags; - } - } - - if (currentFlags != CellFlags::None) - { - const u16r rect{ x1, y, right, gsl::narrow_cast(y + 1) }; - const auto color = cells[x1].color.x; - (this->*handler.func)(rect, color); + params.pScrollRect = &scrollRect; + params.pScrollOffset = &scrollOffset; } } } - THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw()); -} - -// See _drawGlyph() for reference. -AtlasEngine::u16 AtlasEngine::_d2dDrawGlyph(const TileHashMap::iterator& it, const u16x2 coord, const u32 color) -{ - const auto key = it->first.data(); - const auto value = it->second.data(); - const auto charsLength = key->charCount; - const auto cellCount = key->attributes.cellCount; - const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic); - const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph); - - auto& cachedLayout = it->second.cachedLayout; - if (!cachedLayout) + if constexpr (Feature_AtlasEnginePresentFallback::IsEnabled()) { - cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph); + if (FAILED_LOG(_p.swapChain.swapChain->Present1(1, 0, ¶ms))) + { + THROW_IF_FAILED(_p.swapChain.swapChain->Present(1, 0)); + } + } + else + { + THROW_IF_FAILED(_p.swapChain.swapChain->Present1(1, 0, ¶ms)); } - D2D1_RECT_F rect; - rect.left = static_cast(coord.x) * _r.cellSizeDIP.x; - rect.top = static_cast(coord.y) * _r.cellSizeDIP.y; - rect.right = static_cast(coord.x + cellCount) * _r.cellSizeDIP.x; - rect.bottom = rect.top + _r.cellSizeDIP.y; - - D2D1_POINT_2F origin; - origin.x = rect.left; - origin.y = rect.top; - - _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); - - cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin); - - origin.x += cachedLayout.offset.x; - origin.y += cachedLayout.offset.y; - _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _brushWithColor(color), cachedLayout.options); - - cachedLayout.undoScaling(_r.d2dRenderTarget.get()); - - _r.d2dRenderTarget->PopAxisAlignedClip(); - - return cellCount; -} - -void AtlasEngine::_d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle) -{ - const auto w = static_cast(width) * _r.dipPerPixel; - const auto y1 = static_cast(rect.top) * _r.cellSizeDIP.y + static_cast(pos) * _r.dipPerPixel + w * 0.5f; - const auto x1 = static_cast(rect.left) * _r.cellSizeDIP.x; - const auto x2 = static_cast(rect.right) * _r.cellSizeDIP.x; - const auto brush = _brushWithColor(color); - _r.d2dRenderTarget->DrawLine({ x1, y1 }, { x2, y1 }, brush, w, strokeStyle); -} - -void AtlasEngine::_d2dFillRectangle(u16r rect, u32 color) -{ - const D2D1_RECT_F r{ - .left = static_cast(rect.left) * _r.cellSizeDIP.x, - .top = static_cast(rect.top) * _r.cellSizeDIP.y, - .right = static_cast(rect.right) * _r.cellSizeDIP.x, - .bottom = static_cast(rect.bottom) * _r.cellSizeDIP.y, - }; - const auto brush = _brushWithColor(color); - _r.d2dRenderTarget->FillRectangle(r, brush); -} - -void AtlasEngine::_d2dCellFlagRendererCursor(u16r rect, u32 color) -{ - _drawCursor(rect, _r.cursorOptions.cursorColor, false); -} - -void AtlasEngine::_d2dCellFlagRendererSelected(u16r rect, u32 color) -{ - _d2dFillRectangle(rect, _r.selectionColor); -} - -void AtlasEngine::_d2dCellFlagRendererUnderline(u16r rect, u32 color) -{ - _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color); -} - -void AtlasEngine::_d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color) -{ - if (!_r.dottedStrokeStyle) - { - static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; - static constexpr FLOAT dashes[2]{ 1, 2 }; - THROW_IF_FAILED(_sr.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _r.dottedStrokeStyle.addressof())); - } - - _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color, _r.dottedStrokeStyle.get()); -} - -void AtlasEngine::_d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color) -{ - _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.x, _r.fontMetrics.thinLineWidth, color); - _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.y, _r.fontMetrics.thinLineWidth, color); -} - -void AtlasEngine::_d2dCellFlagRendererStrikethrough(u16r rect, u32 color) -{ - _d2dDrawLine(rect, _r.fontMetrics.strikethroughPos, _r.fontMetrics.strikethroughWidth, color); + _p.swapChain.waitForPresentation = true; } diff --git a/src/renderer/atlas/Backend.cpp b/src/renderer/atlas/Backend.cpp new file mode 100644 index 0000000000..0fd5d8841b --- /dev/null +++ b/src/renderer/atlas/Backend.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Backend.h" + +TIL_FAST_MATH_BEGIN + +// Disable a bunch of warnings which get in the way of writing performant code. +#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...]. +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). + +using namespace Microsoft::Console::Render::Atlas; + +void Microsoft::Console::Render::Atlas::GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds) +{ + D2D1_RECT_F rect{}; + THROW_IF_FAILED(d2dRenderTarget->GetGlyphRunWorldBounds(baselineOrigin, glyphRun, DWRITE_MEASURING_MODE_NATURAL, &rect)); + if (rect.top < rect.bottom) + { + bounds.left = std::min(bounds.left, rect.left); + bounds.top = std::min(bounds.top, rect.top); + bounds.right = std::max(bounds.right, rect.right); + bounds.bottom = std::max(bounds.bottom, rect.bottom); + } +} + +wil::com_ptr Microsoft::Console::Render::Atlas::TranslateColorGlyphRun(IDWriteFactory4* dwriteFactory4, D2D_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun) noexcept +{ + static constexpr auto formats = + DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | + DWRITE_GLYPH_IMAGE_FORMATS_CFF | + DWRITE_GLYPH_IMAGE_FORMATS_COLR | + DWRITE_GLYPH_IMAGE_FORMATS_SVG | + DWRITE_GLYPH_IMAGE_FORMATS_PNG | + DWRITE_GLYPH_IMAGE_FORMATS_JPEG | + DWRITE_GLYPH_IMAGE_FORMATS_TIFF | + DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8; + + wil::com_ptr enumerator; + + if (dwriteFactory4) + { + std::ignore = dwriteFactory4->TranslateColorGlyphRun(baselineOrigin, glyphRun, nullptr, formats, DWRITE_MEASURING_MODE_NATURAL, nullptr, 0, enumerator.addressof()); + } + + return enumerator; +} + +bool Microsoft::Console::Render::Atlas::ColorGlyphRunMoveNext(IDWriteColorGlyphRunEnumerator1* enumerator) +{ + BOOL hasRun; + THROW_IF_FAILED(enumerator->MoveNext(&hasRun)); + return hasRun; +} + +const DWRITE_COLOR_GLYPH_RUN1* Microsoft::Console::Render::Atlas::ColorGlyphRunGetCurrentRun(IDWriteColorGlyphRunEnumerator1* enumerator) +{ + const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun = nullptr; + THROW_IF_FAILED(enumerator->GetCurrentRun(&colorGlyphRun)); + return colorGlyphRun; +} + +void Microsoft::Console::Render::Atlas::ColorGlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun, D2D1_RECT_F& bounds) +{ + const D2D1_POINT_2F baselineOrigin{ colorGlyphRun->baselineOriginX, colorGlyphRun->baselineOriginY }; + GlyphRunAccumulateBounds(d2dRenderTarget, baselineOrigin, &colorGlyphRun->glyphRun, bounds); +} + +void Microsoft::Console::Render::Atlas::ColorGlyphRunDraw(ID2D1DeviceContext4* d2dRenderTarget4, ID2D1SolidColorBrush* emojiBrush, ID2D1SolidColorBrush* foregroundBrush, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun) noexcept +{ + ID2D1Brush* runBrush = nullptr; + if (colorGlyphRun->paletteIndex == /*DWRITE_NO_PALETTE_INDEX*/ 0xffff) + { + runBrush = foregroundBrush; + } + else + { + emojiBrush->SetColor(&colorGlyphRun->runColor); + runBrush = emojiBrush; + } + + const D2D1_POINT_2F baselineOrigin{ colorGlyphRun->baselineOriginX, colorGlyphRun->baselineOriginY }; + + switch (colorGlyphRun->glyphImageFormat) + { + case DWRITE_GLYPH_IMAGE_FORMATS_NONE: + break; + case DWRITE_GLYPH_IMAGE_FORMATS_PNG: + case DWRITE_GLYPH_IMAGE_FORMATS_JPEG: + case DWRITE_GLYPH_IMAGE_FORMATS_TIFF: + case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8: + d2dRenderTarget4->DrawColorBitmapGlyphRun(colorGlyphRun->glyphImageFormat, baselineOrigin, &colorGlyphRun->glyphRun, colorGlyphRun->measuringMode, D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT); + break; + case DWRITE_GLYPH_IMAGE_FORMATS_SVG: + d2dRenderTarget4->DrawSvgGlyphRun(baselineOrigin, &colorGlyphRun->glyphRun, runBrush, nullptr, 0, colorGlyphRun->measuringMode); + break; + default: + d2dRenderTarget4->DrawGlyphRun(baselineOrigin, &colorGlyphRun->glyphRun, colorGlyphRun->glyphRunDescription, runBrush, colorGlyphRun->measuringMode); + break; + } +} + +TIL_FAST_MATH_END diff --git a/src/renderer/atlas/Backend.h b/src/renderer/atlas/Backend.h new file mode 100644 index 0000000000..0ca325a538 --- /dev/null +++ b/src/renderer/atlas/Backend.h @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "common.h" + +namespace Microsoft::Console::Render::Atlas +{ + // If set to 1, this will cause the entire viewport to be invalidated at all times. + // Helpful for benchmarking our text shaping code based on DirectWrite. +#define ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION 0 + + // Redraw at display refresh rate at all times. This helps with shader debugging. +#define ATLAS_DEBUG_CONTINUOUS_REDRAW 0 + + // Disables the use of DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT. + // This helps with benchmarking the application as it'll run beyond display refresh rate. +#define ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT 0 + + // Forces the use of Direct2D for text rendering (= BackendD2D). +#define ATLAS_DEBUG_FORCE_D2D_MODE 0 + + // Adds an artificial delay before every render pass. In milliseconds. +#define ATLAS_DEBUG_RENDER_DELAY 0 + + // Shows the dirty rects as given to IDXGISwapChain2::Present1() during each frame. +#define ATLAS_DEBUG_SHOW_DIRTY 0 + + // Dumps the contents of the swap chain on each render pass into the given directory as PNG. + // I highly recommend setting ATLAS_DEBUG_RENDER_DELAY to 250 or similar if this is used. +#define ATLAS_DEBUG_DUMP_RENDER_TARGET 0 +#define ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH LR"(%USERPROFILE%\Downloads\AtlasEngine)" + + template + constexpr T colorFromU32(u32 rgba) + { + const auto r = static_cast((rgba >> 0) & 0xff) / 255.0f; + const auto g = static_cast((rgba >> 8) & 0xff) / 255.0f; + const auto b = static_cast((rgba >> 16) & 0xff) / 255.0f; + const auto a = static_cast((rgba >> 24) & 0xff) / 255.0f; + return { r, g, b, a }; + } + + template + constexpr T colorFromU32Premultiply(u32 rgba) + { + const auto r = static_cast((rgba >> 0) & 0xff) / 255.0f; + const auto g = static_cast((rgba >> 8) & 0xff) / 255.0f; + const auto b = static_cast((rgba >> 16) & 0xff) / 255.0f; + const auto a = static_cast((rgba >> 24) & 0xff) / 255.0f; + return { r * a, g * a, b * a, a }; + } + + constexpr u32 u32ColorPremultiply(u32 rgba) + { + auto rb = rgba & 0x00ff00ff; + auto g = rgba & 0x0000ff00; + const auto a = rgba & 0xff000000; + + const auto m = rgba >> 24; + rb = (rb * m / 0xff) & 0x00ff00ff; + g = (g * m / 0xff) & 0x0000ff00; + + return rb | g | a; + } + + // MSVC STL (version 22000) implements std::clamp(T, T, T) in terms of the generic + // std::clamp(T, T, T, Predicate) with std::less{} as the argument, + // which introduces branching. While not perfect, this is still better than std::clamp. + template + constexpr T clamp(T val, T min, T max) noexcept + { + return val < min ? min : (max < val ? max : val); + } + + constexpr bool isSoftFontChar(wchar_t ch) noexcept + { + return ch >= 0xEF20 && ch < 0xEF80; + } + + inline constexpr D2D1_RECT_F GlyphRunEmptyBounds{ 1e38f, 1e38f, -1e38f, -1e38f }; + void GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds); + + wil::com_ptr TranslateColorGlyphRun(IDWriteFactory4* dwriteFactory4, D2D_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun) noexcept; + bool ColorGlyphRunMoveNext(IDWriteColorGlyphRunEnumerator1* enumerator); + const DWRITE_COLOR_GLYPH_RUN1* ColorGlyphRunGetCurrentRun(IDWriteColorGlyphRunEnumerator1* enumerator); + void ColorGlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun, D2D1_RECT_F& bounds); + void ColorGlyphRunDraw(ID2D1DeviceContext4* d2dRenderTarget4, ID2D1SolidColorBrush* emojiBrush, ID2D1SolidColorBrush* foregroundBrush, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun) noexcept; +} diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp new file mode 100644 index 0000000000..301738954a --- /dev/null +++ b/src/renderer/atlas/BackendD2D.cpp @@ -0,0 +1,687 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "BackendD2D.h" + +#if ATLAS_DEBUG_SHOW_DIRTY +#include "colorbrewer.h" +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET +#include "wic.h" +#endif + +TIL_FAST_MATH_BEGIN + +// Disable a bunch of warnings which get in the way of writing performant code. +#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...]. +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). + +using namespace Microsoft::Console::Render::Atlas; + +void BackendD2D::ReleaseResources() noexcept +{ + _renderTarget.reset(); + _renderTarget4.reset(); + // Ensure _handleSettingsUpdate() is called so that _renderTarget gets recreated. + _generation = {}; +} + +void BackendD2D::Render(RenderingPayload& p) +{ + if (_generation != p.s.generation()) + { + _handleSettingsUpdate(p); + } + + _renderTarget->BeginDraw(); +#if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_DUMP_RENDER_TARGET + // Invalidating the render target helps with spotting Present1() bugs. + _renderTarget->Clear(); +#endif + _drawBackground(p); + _drawCursorPart1(p); + _drawText(p); + _drawCursorPart2(p); + _drawSelection(p); +#if ATLAS_DEBUG_SHOW_DIRTY + _debugShowDirty(p); +#endif + THROW_IF_FAILED(_renderTarget->EndDraw()); + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET + _debugDumpRenderTarget(p); +#endif +} + +bool BackendD2D::RequiresContinuousRedraw() noexcept +{ + return false; +} + +void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) +{ + const auto renderTargetChanged = !_renderTarget; + const auto fontChanged = _fontGeneration != p.s->font.generation(); + const auto cursorChanged = _cursorGeneration != p.s->cursor.generation(); + const auto cellCountChanged = _viewportCellCount != p.s->viewportCellCount; + + if (renderTargetChanged) + { + { + wil::com_ptr buffer; + THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(buffer.addressof()))); + const auto surface = buffer.query(); + + const D2D1_RENDER_TARGET_PROPERTIES props{ + .type = D2D1_RENDER_TARGET_TYPE_DEFAULT, + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = static_cast(p.s->font->dpi), + .dpiY = static_cast(p.s->font->dpi), + }; + // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.addressof()))); + _renderTarget.try_query_to(_renderTarget4.addressof()); + + _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); + _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + } + { + static constexpr D2D1_COLOR_F color{}; + THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _emojiBrush.put())); + THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _brush.put())); + _brushColor = 0; + } + } + + if (!_dottedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 1, 1 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); + } + + if (renderTargetChanged || fontChanged) + { + const auto dpi = static_cast(p.s->font->dpi); + _renderTarget->SetDpi(dpi, dpi); + _renderTarget->SetTextAntialiasMode(static_cast(p.s->font->antialiasingMode)); + } + + if (renderTargetChanged || fontChanged || cellCountChanged) + { + const D2D1_BITMAP_PROPERTIES props{ + .pixelFormat = { DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = static_cast(p.s->font->dpi), + .dpiY = static_cast(p.s->font->dpi), + }; + const D2D1_SIZE_U size{ + p.s->viewportCellCount.x, + p.s->viewportCellCount.y, + }; + const D2D1_MATRIX_3X2_F transform{ + .m11 = static_cast(p.s->font->cellSize.x), + .m22 = static_cast(p.s->font->cellSize.y), + }; + THROW_IF_FAILED(_renderTarget->CreateBitmap(size, nullptr, 0, &props, _backgroundBitmap.put())); + THROW_IF_FAILED(_renderTarget->CreateBitmapBrush(_backgroundBitmap.get(), _backgroundBrush.put())); + _backgroundBrush->SetInterpolationMode(D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR); + _backgroundBrush->SetExtendModeX(D2D1_EXTEND_MODE_MIRROR); + _backgroundBrush->SetExtendModeY(D2D1_EXTEND_MODE_MIRROR); + _backgroundBrush->SetTransform(&transform); + _backgroundBitmapGeneration = {}; + } + + if (fontChanged || cursorChanged) + { + _cursorBitmap.reset(); + _cursorBitmapSize = {}; + } + + _generation = p.s.generation(); + _fontGeneration = p.s->font.generation(); + _cursorGeneration = p.s->cursor.generation(); + _viewportCellCount = p.s->viewportCellCount; +} + +void BackendD2D::_drawBackground(const RenderingPayload& p) noexcept +{ + if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0]) + { + _backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast(p.colorBitmapRowStride * sizeof(u32))); + _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; + } + + // If the terminal was 120x30 cells and 1200x600 pixels large, this would draw the + // background by upscaling a 120x30 pixel bitmap to fill the entire render target. + const D2D1_RECT_F rect{ 0, 0, static_cast(p.s->targetSize.x), static_cast(p.s->targetSize.y) }; + _renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY); + _renderTarget->FillRectangle(&rect, _backgroundBrush.get()); + _renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER); +} + +void BackendD2D::_drawText(RenderingPayload& p) +{ + til::CoordType dirtyTop = til::CoordTypeMax; + til::CoordType dirtyBottom = til::CoordTypeMin; + + // It is possible to create a "_foregroundBrush" similar to how the `_backgroundBrush` is created and + // use that as the brush for text rendering below. That way we wouldn't have to search `row->colors` for color + // changes and could draw entire lines of text in a single call. Unfortunately Direct2D is not particularly + // smart if you do this and chooses to draw the given text into a way too small offscreen texture first and + // then blends it on the screen with the given bitmap brush. While this roughly doubles the performance + // when drawing lots of colors, the extra latency drops performance by >10x when drawing fewer colors. + // Since fewer colors are more common, I've chosen to go with regular solid-color brushes. + u16 y = 0; + for (const auto row : p.rows) + { + auto baselineX = 0.0f; + auto baselineY = static_cast(p.s->font->cellSize.y * y + p.s->font->baseline); + + if (row->lineRendition != LineRendition::SingleWidth) + { + baselineY = _drawTextPrepareLineRendition(p, row, baselineY); + } + + for (const auto& m : row->mappings) + { + const auto colorsBegin = row->colors.begin(); + auto it = colorsBegin + m.glyphsFrom; + const auto end = colorsBegin + m.glyphsTo; + + while (it != end) + { + const auto beg = it; + const auto off = it - colorsBegin; + const auto fg = *it; + + while (++it != end && *it == fg) + { + } + + const auto count = it - beg; + const auto brush = _brushWithColor(fg); + const DWRITE_GLYPH_RUN glyphRun{ + .fontFace = m.fontFace.get(), + .fontEmSize = p.s->font->fontSize, + .glyphCount = gsl::narrow_cast(count), + .glyphIndices = &row->glyphIndices[off], + .glyphAdvances = &row->glyphAdvances[off], + .glyphOffsets = &row->glyphOffsets[off], + }; + const D2D1_POINT_2F baselineOrigin{ + baselineX, + baselineY, + }; + + if (glyphRun.fontFace) + { + D2D1_RECT_F bounds = GlyphRunEmptyBounds; + + if (const auto enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun)) + { + while (ColorGlyphRunMoveNext(enumerator.get())) + { + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); + ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); + } + } + else + { + _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); + GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + } + + if (bounds.top < bounds.bottom) + { + // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. + if (row->lineRendition != LineRendition::DoubleHeightTop) + { + row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); + } + if (row->lineRendition != LineRendition::DoubleHeightBottom) + { + row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); + } + } + } + + for (UINT32 i = 0; i < glyphRun.glyphCount; ++i) + { + baselineX += glyphRun.glyphAdvances[i]; + } + } + } + + if (!row->gridLineRanges.empty()) + { + _drawGridlineRow(p, row, y); + } + + if (row->lineRendition != LineRendition::SingleWidth) + { + _drawTextResetLineRendition(row); + } + + if (p.invalidatedRows.contains(y)) + { + dirtyTop = std::min(dirtyTop, row->dirtyTop); + dirtyBottom = std::max(dirtyBottom, row->dirtyBottom); + } + + ++y; + } + + if (dirtyTop < dirtyBottom) + { + p.dirtyRectInPx.top = std::min(p.dirtyRectInPx.top, dirtyTop); + p.dirtyRectInPx.bottom = std::max(p.dirtyRectInPx.bottom, dirtyBottom); + } +} + +f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept +{ + const auto lineRendition = row->lineRendition; + D2D1_MATRIX_3X2_F transform{ + .m11 = 2.0f, + .m22 = 1.0f, + }; + + if (lineRendition >= LineRendition::DoubleHeightTop) + { + D2D1_RECT_F clipRect{ 0, 0, static_cast(p.s->targetSize.x), static_cast(p.s->targetSize.y) }; + + transform.m22 = 2.0f; + transform.dy = -1.0f * (baselineY + p.s->font->descender); + + // If you print the top half of a double height row (DECDHL), the expectation is that only + // the top half is visible, which requires us to keep the clip rect at the bottom of the row. + // (Vice versa for the bottom half of a double height row.) + if (lineRendition == LineRendition::DoubleHeightTop) + { + const auto delta = static_cast(p.s->font->cellSize.y); + baselineY += delta; + transform.dy -= delta; + clipRect.bottom = static_cast(row->dirtyBottom); + } + else + { + clipRect.top = static_cast(row->dirtyTop); + } + + _renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + } + + _renderTarget->SetTransform(&transform); + return baselineY; +} + +void BackendD2D::_drawTextResetLineRendition(const ShapedRow* row) const noexcept +{ + static constexpr D2D1_MATRIX_3X2_F identity{ .m11 = 1, .m22 = 1 }; + _renderTarget->SetTransform(&identity); + + if (row->lineRendition >= LineRendition::DoubleHeightTop) + { + _renderTarget->PopAxisAlignedClip(); + } +} + +// Returns the theoretical/design design size of the given `DWRITE_GLYPH_RUN`, relative the the given baseline origin. +// This algorithm replicates what DirectWrite does internally to provide `IDWriteTextLayout::GetMetrics`. +f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY) +{ + DWRITE_FONT_METRICS fontMetrics; + glyphRun.fontFace->GetMetrics(&fontMetrics); + + if (glyphRun.glyphCount > _glyphMetrics.size()) + { + // Growth factor 1.5x. + auto size = _glyphMetrics.size(); + size = size + (size >> 1); + size = std::max(size, glyphRun.glyphCount); + // Overflow check. + Expects(size > _glyphMetrics.size()); + _glyphMetrics = Buffer{ size }; + } + + glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false); + + const f32 fontScale = glyphRun.fontEmSize / fontMetrics.designUnitsPerEm; + f32r accumulatedBounds{ + baselineX, + baselineY, + baselineX, + baselineY, + }; + + for (uint32_t i = 0; i < glyphRun.glyphCount; ++i) + { + const auto& glyphMetrics = _glyphMetrics[i]; + const auto glyphAdvance = glyphRun.glyphAdvances ? glyphRun.glyphAdvances[i] : glyphMetrics.advanceWidth * fontScale; + + const auto left = static_cast(glyphMetrics.leftSideBearing) * fontScale; + const auto top = static_cast(glyphMetrics.topSideBearing - glyphMetrics.verticalOriginY) * fontScale; + const auto right = static_cast(gsl::narrow_cast(glyphMetrics.advanceWidth) - glyphMetrics.rightSideBearing) * fontScale; + const auto bottom = static_cast(gsl::narrow_cast(glyphMetrics.advanceHeight) - glyphMetrics.bottomSideBearing - glyphMetrics.verticalOriginY) * fontScale; + + if (left < right && top < bottom) + { + auto glyphX = baselineX; + auto glyphY = baselineY; + if (glyphRun.glyphOffsets) + { + glyphX += glyphRun.glyphOffsets[i].advanceOffset; + glyphY -= glyphRun.glyphOffsets[i].ascenderOffset; + } + + accumulatedBounds.left = std::min(accumulatedBounds.left, left + glyphX); + accumulatedBounds.top = std::min(accumulatedBounds.top, top + glyphY); + accumulatedBounds.right = std::max(accumulatedBounds.right, right + glyphX); + accumulatedBounds.bottom = std::max(accumulatedBounds.bottom, bottom + glyphY); + } + + baselineX += glyphAdvance; + } + + return accumulatedBounds; +} + +void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y) +{ + const auto widthShift = gsl::narrow_cast(row->lineRendition != LineRendition::SingleWidth); + const auto cellSize = p.s->font->cellSize; + const auto rowTop = gsl::narrow_cast(cellSize.y * y); + const auto rowBottom = gsl::narrow_cast(rowTop + cellSize.y); + const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + + const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) { + const auto from = r.from >> widthShift; + const auto to = r.to >> widthShift; + + auto posX = from * cellSize.x + pos.position; + const auto end = to * cellSize.x; + + D2D1_POINT_2F point0{ 0, static_cast(textCellCenter) }; + D2D1_POINT_2F point1{ 0, static_cast(textCellCenter + cellSize.y) }; + const auto brush = _brushWithColor(r.color); + const f32 w = pos.height; + const f32 hw = w * 0.5f; + + for (; posX < end; posX += cellSize.x) + { + const auto centerX = posX + hw; + point0.x = centerX; + point1.x = centerX; + _renderTarget->DrawLine(point0, point1, brush, w, nullptr); + } + }; + const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle) { + const auto from = r.from >> widthShift; + const auto to = r.to >> widthShift; + + const auto brush = _brushWithColor(r.color); + const f32 w = pos.height; + const f32 centerY = textCellCenter + pos.position + w * 0.5f; + const D2D1_POINT_2F point0{ static_cast(from * cellSize.x), centerY }; + const D2D1_POINT_2F point1{ static_cast(to * cellSize.x), centerY }; + _renderTarget->DrawLine(point0, point1, brush, w, strokeStyle); + }; + + for (const auto& r : row->gridLineRanges) + { + // AtlasEngine.cpp shouldn't add any gridlines if they don't do anything. + assert(r.lines.any()); + + if (r.lines.test(GridLines::Left)) + { + appendVerticalLines(r, p.s->font->gridLeft); + } + if (r.lines.test(GridLines::Right)) + { + appendVerticalLines(r, p.s->font->gridRight); + } + if (r.lines.test(GridLines::Top)) + { + appendHorizontalLine(r, p.s->font->gridTop, nullptr); + } + if (r.lines.test(GridLines::Bottom)) + { + appendHorizontalLine(r, p.s->font->gridBottom, nullptr); + } + + if (r.lines.test(GridLines::Underline)) + { + appendHorizontalLine(r, p.s->font->underline, nullptr); + } + if (r.lines.test(GridLines::HyperlinkUnderline)) + { + appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get()); + } + if (r.lines.test(GridLines::DoubleUnderline)) + { + for (const auto pos : p.s->font->doubleUnderline) + { + appendHorizontalLine(r, pos, nullptr); + } + } + if (r.lines.test(GridLines::Strikethrough)) + { + appendHorizontalLine(r, p.s->font->strikethrough, nullptr); + } + } +} + +void BackendD2D::_drawCursorPart1(const RenderingPayload& p) +{ + if (p.cursorRect.empty()) + { + return; + } + + const auto cursorColor = p.s->cursor->cursorColor; + + if (cursorColor != 0xffffffff) + { + const D2D1_RECT_F rect{ + static_cast(p.cursorRect.left * p.s->font->cellSize.x), + static_cast(p.cursorRect.top * p.s->font->cellSize.y), + static_cast(p.cursorRect.right * p.s->font->cellSize.x), + static_cast(p.cursorRect.bottom * p.s->font->cellSize.y), + }; + const auto brush = _brushWithColor(cursorColor); + _drawCursor(p, _renderTarget.get(), rect, brush); + } +} + +void BackendD2D::_drawCursorPart2(const RenderingPayload& p) +{ + if (p.cursorRect.empty()) + { + return; + } + + if (p.s->cursor->cursorColor == 0xffffffff) + { + const auto cursorSize = p.cursorRect.size(); + if (cursorSize != _cursorBitmapSize) + { + _resizeCursorBitmap(p, cursorSize); + } + + const D2D1_POINT_2F target{ + static_cast(p.cursorRect.left * p.s->font->cellSize.x), + static_cast(p.cursorRect.top * p.s->font->cellSize.y), + }; + _renderTarget->DrawImage(_cursorBitmap.get(), &target, nullptr, D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_COMPOSITE_MODE_MASK_INVERT); + } +} + +void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size newSize) +{ + const til::size newSizeInPx{ + newSize.width * p.s->font->cellSize.x, + newSize.height * p.s->font->cellSize.y, + }; + + // CreateCompatibleRenderTarget is a terrific API and does not adopt _any_ of the settings of the + // parent render target (like the AA mode or D2D1_UNIT_MODE_PIXELS). Not sure who came up with that, + // but fact is that we need to set both sizes to override the DPI and fake D2D1_UNIT_MODE_PIXELS. + const D2D1_SIZE_F sizeF{ static_cast(newSizeInPx.width), static_cast(newSizeInPx.height) }; + const D2D1_SIZE_U sizeU{ gsl::narrow_cast(newSizeInPx.width), gsl::narrow_cast(newSizeInPx.height) }; + wil::com_ptr cursorRenderTarget; + _renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof()); + cursorRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + + cursorRenderTarget->BeginDraw(); + cursorRenderTarget->Clear(); + { + const D2D1_RECT_F rect{ 0, 0, sizeF.width, sizeF.height }; + const auto brush = _brushWithColor(0xffffffff); + _drawCursor(p, cursorRenderTarget.get(), rect, brush); + } + THROW_IF_FAILED(cursorRenderTarget->EndDraw()); + + cursorRenderTarget->GetBitmap(_cursorBitmap.put()); + _cursorBitmapSize = newSize; +} + +void BackendD2D::_drawCursor(const RenderingPayload& p, ID2D1RenderTarget* renderTarget, D2D1_RECT_F rect, ID2D1Brush* brush) noexcept +{ + switch (static_cast(p.s->cursor->cursorType)) + { + case CursorType::Legacy: + { + const auto height = p.s->cursor->heightPercentage / 100.0f; + rect.top = roundf((rect.top - rect.bottom) * height + rect.bottom); + renderTarget->FillRectangle(&rect, brush); + break; + } + case CursorType::VerticalBar: + rect.right = rect.left + p.s->font->thinLineWidth; + renderTarget->FillRectangle(&rect, brush); + break; + case CursorType::Underscore: + rect.top += p.s->font->underline.position; + rect.bottom = rect.top + p.s->font->underline.height; + renderTarget->FillRectangle(&rect, brush); + break; + case CursorType::EmptyBox: + { + const auto w = static_cast(p.s->font->thinLineWidth); + const auto wh = w / 2.0f; + rect.left += wh; + rect.top += wh; + rect.right -= wh; + rect.bottom -= wh; + renderTarget->DrawRectangle(&rect, brush, w, nullptr); + break; + } + case CursorType::FullBox: + renderTarget->FillRectangle(&rect, brush); + break; + case CursorType::DoubleUnderscore: + { + auto rect2 = rect; + rect2.top = rect.top + p.s->font->doubleUnderline[0].position; + rect2.bottom = rect2.top + p.s->font->thinLineWidth; + renderTarget->FillRectangle(&rect2, brush); + rect.top = rect.top + p.s->font->doubleUnderline[1].position; + rect.bottom = rect.top + p.s->font->thinLineWidth; + renderTarget->FillRectangle(&rect, brush); + break; + } + default: + break; + } +} + +void BackendD2D::_drawSelection(const RenderingPayload& p) +{ + u16 y = 0; + for (const auto& row : p.rows) + { + if (row->selectionTo > row->selectionFrom) + { + const D2D1_RECT_F rect{ + static_cast(p.s->font->cellSize.x * row->selectionFrom), + static_cast(p.s->font->cellSize.y * y), + static_cast(p.s->font->cellSize.x * row->selectionTo), + static_cast(p.s->font->cellSize.y * (y + 1)), + }; + _fillRectangle(rect, p.s->misc->selectionColor); + } + + y++; + } +} + +#if ATLAS_DEBUG_SHOW_DIRTY +void BackendD2D::_debugShowDirty(const RenderingPayload& p) +{ + _presentRects[_presentRectsPos] = p.dirtyRectInPx; + _presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects); + + for (size_t i = 0; i < std::size(_presentRects); ++i) + { + const auto& rect = _presentRects[(_presentRectsPos + i) % std::size(_presentRects)]; + if (rect.non_empty()) + { + const D2D1_RECT_F rectF{ + static_cast(rect.left), + static_cast(rect.top), + static_cast(rect.right), + static_cast(rect.bottom), + }; + const auto color = colorbrewer::pastel1[i] | 0x1f000000; + _fillRectangle(rectF, color); + } + } +} +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET +void BackendD2D::_debugDumpRenderTarget(const RenderingPayload& p) +{ + if (_dumpRenderTargetCounter == 0) + { + ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast(std::size(_dumpRenderTargetBasePath))); + std::filesystem::create_directories(_dumpRenderTargetBasePath); + } + + wchar_t path[MAX_PATH]; + swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter); + SaveTextureToPNG(_deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]); + _dumpRenderTargetCounter++; +} +#endif + +ID2D1SolidColorBrush* BackendD2D::_brushWithColor(u32 color) +{ + if (_brushColor != color) + { + _brushWithColorUpdate(color); + } + return _brush.get(); +} + +ID2D1SolidColorBrush* BackendD2D::_brushWithColorUpdate(u32 color) +{ + const auto d2dColor = colorFromU32(color); + _brush->SetColor(&d2dColor); + _brushColor = color; + return _brush.get(); +} + +void BackendD2D::_fillRectangle(const D2D1_RECT_F& rect, u32 color) +{ + const auto brush = _brushWithColor(color); + _renderTarget->FillRectangle(&rect, brush); +} + +TIL_FAST_MATH_END diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h new file mode 100644 index 0000000000..3bc40bb22c --- /dev/null +++ b/src/renderer/atlas/BackendD2D.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "Backend.h" + +namespace Microsoft::Console::Render::Atlas +{ + struct BackendD2D : IBackend + { + void ReleaseResources() noexcept override; + void Render(RenderingPayload& payload) override; + bool RequiresContinuousRedraw() noexcept override; + + private: + ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); + void _drawBackground(const RenderingPayload& p) noexcept; + void _drawText(RenderingPayload& p); + ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; + ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; + ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY); + ATLAS_ATTR_COLD void _drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y); + void _drawCursorPart1(const RenderingPayload& p); + void _drawCursorPart2(const RenderingPayload& p); + static void _drawCursor(const RenderingPayload& p, ID2D1RenderTarget* renderTarget, D2D1_RECT_F rect, ID2D1Brush* brush) noexcept; + void _resizeCursorBitmap(const RenderingPayload& p, til::size newSize); + void _drawSelection(const RenderingPayload& p); + void _debugShowDirty(const RenderingPayload& p); + void _debugDumpRenderTarget(const RenderingPayload& p); + ID2D1SolidColorBrush* _brushWithColor(u32 color); + ATLAS_ATTR_COLD ID2D1SolidColorBrush* _brushWithColorUpdate(u32 color); + void _fillRectangle(const D2D1_RECT_F& rect, u32 color); + + wil::com_ptr _renderTarget; + wil::com_ptr _renderTarget4; // Optional. Supported since Windows 10 14393. + wil::com_ptr _dottedStrokeStyle; + wil::com_ptr _backgroundBitmap; + wil::com_ptr _backgroundBrush; + til::generation_t _backgroundBitmapGeneration; + + wil::com_ptr _cursorBitmap; + til::size _cursorBitmapSize; // in columns/rows + + wil::com_ptr _emojiBrush; + wil::com_ptr _brush; + u32 _brushColor = 0; + + Buffer _glyphMetrics; + + til::generation_t _generation; + til::generation_t _fontGeneration; + til::generation_t _cursorGeneration; + u16x2 _viewportCellCount{}; + +#if ATLAS_DEBUG_SHOW_DIRTY + i32r _presentRects[9]{}; + size_t _presentRectsPos = 0; +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET + wchar_t _dumpRenderTargetBasePath[MAX_PATH]{}; + size_t _dumpRenderTargetCounter = 0; +#endif + }; +} diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp new file mode 100644 index 0000000000..846567ca61 --- /dev/null +++ b/src/renderer/atlas/BackendD3D.cpp @@ -0,0 +1,2229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "BackendD3D.h" + +#include +#include +#include +#include + +#include "dwrite.h" +#include "../../types/inc/ColorFix.hpp" + +#if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS +#include "colorbrewer.h" +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET +#include "wic.h" +#endif + +TIL_FAST_MATH_BEGIN + +// This code packs various data into smaller-than-int types to save both CPU and GPU memory. This warning would force +// us to add dozens upon dozens of gsl::narrow_cast<>s throughout the file which is more annoying than helpful. +#pragma warning(disable : 4242) // '=': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4244) // 'initializing': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4267) // 'argument': conversion from '...' to '...', possible loss of data +#pragma warning(disable : 4838) // conversion from '...' to '...' requires a narrowing conversion +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +// Disable a bunch of warnings which get in the way of writing performant code. +#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...]. +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). + +// Initializing large arrays can be very costly compared to how cheap some of these functions are. +#define ALLOW_UNINITIALIZED_BEGIN _Pragma("warning(push)") _Pragma("warning(disable : 26494)") +#define ALLOW_UNINITIALIZED_END _Pragma("warning(pop)") + +using namespace Microsoft::Console::Render::Atlas; + +template<> +struct std::hash +{ + constexpr size_t operator()(u16 key) const noexcept + { + return til::flat_set_hash_integer(key); + } +}; + +template<> +struct std::hash +{ + constexpr size_t operator()(u16 key) const noexcept + { + return til::flat_set_hash_integer(key); + } + + constexpr size_t operator()(const BackendD3D::AtlasGlyphEntry& slot) const noexcept + { + return til::flat_set_hash_integer(slot.glyphIndex); + } +}; + +template<> +struct std::hash +{ + using T = BackendD3D::AtlasFontFaceEntry; + + size_t operator()(const BackendD3D::AtlasFontFaceKey& key) const noexcept + { + return til::flat_set_hash_integer(std::bit_cast(key.fontFace) | static_cast(key.lineRendition)); + } + + size_t operator()(const BackendD3D::AtlasFontFaceEntry& slot) const noexcept + { + const auto& inner = *slot.inner; + return til::flat_set_hash_integer(std::bit_cast(inner.fontFace.get()) | static_cast(inner.lineRendition)); + } +}; + +BackendD3D::BackendD3D(const RenderingPayload& p) +{ + THROW_IF_FAILED(p.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _vertexShader.addressof())); + THROW_IF_FAILED(p.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _pixelShader.addressof())); + + { + static constexpr D3D11_INPUT_ELEMENT_DESC layout[]{ + { "SV_Position", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "shadingType", 0, DXGI_FORMAT_R32_UINT, 1, offsetof(QuadInstance, shadingType), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "position", 0, DXGI_FORMAT_R16G16_SINT, 1, offsetof(QuadInstance, position), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "size", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, size), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "texcoord", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, texcoord), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "color", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, offsetof(QuadInstance, color), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + }; + THROW_IF_FAILED(p.device->CreateInputLayout(&layout[0], std::size(layout), &shader_vs[0], sizeof(shader_vs), _inputLayout.addressof())); + } + + { + static constexpr f32x2 vertices[]{ + { 0, 0 }, + { 1, 0 }, + { 1, 1 }, + { 0, 1 }, + }; + static constexpr D3D11_SUBRESOURCE_DATA initialData{ &vertices[0] }; + + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = sizeof(vertices); + desc.Usage = D3D11_USAGE_IMMUTABLE; + desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, &initialData, _vertexBuffer.addressof())); + } + + { + static constexpr u16 indices[]{ + 0, // { 0, 0 } + 1, // { 1, 0 } + 2, // { 1, 1 } + 2, // { 1, 1 } + 3, // { 0, 1 } + 0, // { 0, 0 } + }; + static constexpr D3D11_SUBRESOURCE_DATA initialData{ &indices[0] }; + + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = sizeof(indices); + desc.Usage = D3D11_USAGE_IMMUTABLE; + desc.BindFlags = D3D11_BIND_INDEX_BUFFER; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, &initialData, _indexBuffer.addressof())); + } + + { + static constexpr D3D11_BUFFER_DESC desc{ + .ByteWidth = sizeof(VSConstBuffer), + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + }; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, nullptr, _vsConstantBuffer.addressof())); + } + + { + static constexpr D3D11_BUFFER_DESC desc{ + .ByteWidth = sizeof(PSConstBuffer), + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + }; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, nullptr, _psConstantBuffer.addressof())); + } + + { + // The final step of the ClearType blending algorithm is a lerp() between the premultiplied alpha + // background color and straight alpha foreground color given the 3 RGB weights in alphaCorrected: + // lerp(background, foreground, weights) + // Which is equivalent to: + // background * (1 - weights) + foreground * weights + // + // This COULD be implemented using dual source color blending like so: + // .SrcBlend = D3D11_BLEND_SRC1_COLOR + // .DestBlend = D3D11_BLEND_INV_SRC1_COLOR + // .BlendOp = D3D11_BLEND_OP_ADD + // Because: + // background * (1 - weights) + foreground * weights + // ^ ^ ^ ^ ^ + // Dest INV_SRC1_COLOR | Src SRC1_COLOR + // OP_ADD + // + // BUT we need simultaneous support for regular "source over" alpha blending + // (SHADING_TYPE_PASSTHROUGH) like this: + // background * (1 - alpha) + foreground + // + // This is why we set: + // .SrcBlend = D3D11_BLEND_ONE + // + // --> We need to multiply the foreground with the weights ourselves. + static constexpr D3D11_BLEND_DESC desc{ + .RenderTarget = { { + .BlendEnable = TRUE, + .SrcBlend = D3D11_BLEND_ONE, + .DestBlend = D3D11_BLEND_INV_SRC1_COLOR, + .BlendOp = D3D11_BLEND_OP_ADD, + .SrcBlendAlpha = D3D11_BLEND_ONE, + .DestBlendAlpha = D3D11_BLEND_INV_SRC1_ALPHA, + .BlendOpAlpha = D3D11_BLEND_OP_ADD, + .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL, + } }, + }; + THROW_IF_FAILED(p.device->CreateBlendState(&desc, _blendState.addressof())); + } + +#ifndef NDEBUG + _sourceDirectory = std::filesystem::path{ __FILE__ }.parent_path(); + _sourceCodeWatcher = wil::make_folder_change_reader_nothrow(_sourceDirectory.c_str(), false, wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime, [this](wil::FolderChangeEvent, PCWSTR path) { + if (til::ends_with(path, L".hlsl")) + { + auto expected = INT64_MAX; + const auto invalidationTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); + _sourceCodeInvalidationTime.compare_exchange_strong(expected, invalidationTime.time_since_epoch().count(), std::memory_order_relaxed); + } + }); +#endif +} + +void BackendD3D::ReleaseResources() noexcept +{ + _renderTargetView.reset(); + _customRenderTargetView.reset(); + // Ensure _handleSettingsUpdate() is called so that _renderTarget gets recreated. + _generation = {}; +} + +void BackendD3D::Render(RenderingPayload& p) +{ + if (_generation != p.s.generation()) + { + _handleSettingsUpdate(p); + } + +#ifndef NDEBUG + _debugUpdateShaders(p); +#endif + + // After a Present() the render target becomes unbound. + p.deviceContext->OMSetRenderTargets(1, _customRenderTargetView ? _customRenderTargetView.addressof() : _renderTargetView.addressof(), nullptr); + + // Invalidating the render target helps with spotting invalid quad instances and Present1() bugs. +#if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_DUMP_RENDER_TARGET + { + static constexpr f32 clearColor[4]{}; + p.deviceContext->ClearView(_renderTargetView.get(), &clearColor[0], nullptr, 0); + } +#endif + + _drawBackground(p); + _drawCursorBackground(p); + _drawText(p); + _drawSelection(p); +#if ATLAS_DEBUG_SHOW_DIRTY + _debugShowDirty(p); +#endif + _flushQuads(p); + + if (_customPixelShader) + { + _executeCustomShader(p); + } + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET + _debugDumpRenderTarget(p); +#endif +} + +bool BackendD3D::RequiresContinuousRedraw() noexcept +{ + return _requiresContinuousRedraw; +} + +void BackendD3D::_handleSettingsUpdate(const RenderingPayload& p) +{ + if (!_renderTargetView) + { + wil::com_ptr buffer; + THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(buffer.addressof()))); + THROW_IF_FAILED(p.device->CreateRenderTargetView(buffer.get(), nullptr, _renderTargetView.put())); + } + + const auto fontChanged = _fontGeneration != p.s->font.generation(); + const auto miscChanged = _miscGeneration != p.s->misc.generation(); + const auto cellCountChanged = _viewportCellCount != p.s->viewportCellCount; + + if (fontChanged) + { + _updateFontDependents(p); + } + if (miscChanged) + { + _recreateCustomShader(p); + } + if (cellCountChanged) + { + _recreateBackgroundColorBitmap(p); + } + + // Similar to _renderTargetView above, we might have to recreate the _customRenderTargetView whenever _swapChainManager + // resets it. We only do it after calling _recreateCustomShader however, since that sets the _customPixelShader. + if (_customPixelShader && !_customRenderTargetView) + { + _recreateCustomRenderTargetView(p); + } + + _recreateConstBuffer(p); + _setupDeviceContextState(p); + + _generation = p.s.generation(); + _fontGeneration = p.s->font.generation(); + _miscGeneration = p.s->misc.generation(); + _targetSize = p.s->targetSize; + _viewportCellCount = p.s->viewportCellCount; +} + +void BackendD3D::_updateFontDependents(const RenderingPayload& p) +{ + const auto& font = *p.s->font; + + DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put()); + // Clearing the atlas requires BeginDraw(), which is expensive. Defer this until we need Direct2D anyways. + _fontChangedResetGlyphAtlas = true; + _textShadingType = font.antialiasingMode == AntialiasingMode::ClearType ? ShadingType::TextClearType : ShadingType::TextGrayscale; + + { + auto ligaturesDisabled = false; + for (const auto& feature : font.fontFeatures) + { + if (feature.nameTag == DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES) + { + ligaturesDisabled = !feature.parameter; + break; + } + } + + if (ligaturesDisabled) + { + _ligatureOverhangTriggerLeft = til::CoordTypeMin; + _ligatureOverhangTriggerRight = til::CoordTypeMax; + } + else + { + const auto halfCellWidth = font.cellSize.x / 2; + _ligatureOverhangTriggerLeft = -halfCellWidth; + _ligatureOverhangTriggerRight = font.advanceWidth + halfCellWidth; + } + } + + if (_d2dRenderTarget) + { + _d2dRenderTargetUpdateFontSettings(p); + } + + _softFontBitmap.reset(); +} + +void BackendD3D::_d2dRenderTargetUpdateFontSettings(const RenderingPayload& p) const noexcept +{ + const auto& font = *p.s->font; + _d2dRenderTarget->SetDpi(font.dpi, font.dpi); + _d2dRenderTarget->SetTextAntialiasMode(static_cast(font.antialiasingMode)); +} + +void BackendD3D::_recreateCustomShader(const RenderingPayload& p) +{ + _customRenderTargetView.reset(); + _customOffscreenTexture.reset(); + _customOffscreenTextureView.reset(); + _customVertexShader.reset(); + _customPixelShader.reset(); + _customShaderConstantBuffer.reset(); + _customShaderSamplerState.reset(); + _requiresContinuousRedraw = false; + + if (!p.s->misc->customPixelShaderPath.empty()) + { + const char* target = nullptr; + switch (p.device->GetFeatureLevel()) + { + case D3D_FEATURE_LEVEL_10_0: + target = "ps_4_0"; + break; + case D3D_FEATURE_LEVEL_10_1: + target = "ps_4_1"; + break; + default: + target = "ps_5_0"; + break; + } + + static constexpr auto flags = + D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR +#ifdef NDEBUG + | D3DCOMPILE_OPTIMIZATION_LEVEL3; +#else + // Only enable strictness and warnings in DEBUG mode + // as these settings makes it very difficult to develop + // shaders as windows terminal is not telling the user + // what's wrong, windows terminal just fails. + // Keep it in DEBUG mode to catch errors in shaders + // shipped with windows terminal + | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS | D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; +#endif + + wil::com_ptr error; + wil::com_ptr blob; + const auto hr = D3DCompileFromFile( + /* pFileName */ p.s->misc->customPixelShaderPath.c_str(), + /* pDefines */ nullptr, + /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, + /* pEntrypoint */ "main", + /* pTarget */ target, + /* Flags1 */ flags, + /* Flags2 */ 0, + /* ppCode */ blob.addressof(), + /* ppErrorMsgs */ error.addressof()); + + // Unless we can determine otherwise, assume this shader requires evaluation every frame + _requiresContinuousRedraw = true; + + if (SUCCEEDED(hr)) + { + THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _customPixelShader.put())); + + // Try to determine whether the shader uses the Time variable + wil::com_ptr reflector; + if (SUCCEEDED_LOG(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(reflector.put())))) + { + if (ID3D11ShaderReflectionConstantBuffer* constantBufferReflector = reflector->GetConstantBufferByIndex(0)) // shader buffer + { + if (ID3D11ShaderReflectionVariable* variableReflector = constantBufferReflector->GetVariableByIndex(0)) // time + { + D3D11_SHADER_VARIABLE_DESC variableDescriptor; + if (SUCCEEDED_LOG(variableReflector->GetDesc(&variableDescriptor))) + { + // only if time is used + _requiresContinuousRedraw = WI_IsFlagSet(variableDescriptor.uFlags, D3D_SVF_USED); + } + } + } + } + } + else + { + if (error) + { + LOG_HR_MSG(hr, "%*hs", error->GetBufferSize(), error->GetBufferPointer()); + } + else + { + LOG_HR(hr); + } + if (p.warningCallback) + { + p.warningCallback(D2DERR_SHADER_COMPILE_FAILED); + } + } + } + else if (p.s->misc->useRetroTerminalEffect) + { + THROW_IF_FAILED(p.device->CreatePixelShader(&custom_shader_ps[0], sizeof(custom_shader_ps), nullptr, _customPixelShader.put())); + // We know the built-in retro shader doesn't require continuous redraw. + _requiresContinuousRedraw = false; + } + + if (_customPixelShader) + { + THROW_IF_FAILED(p.device->CreateVertexShader(&custom_shader_vs[0], sizeof(custom_shader_vs), nullptr, _customVertexShader.put())); + + { + static constexpr D3D11_BUFFER_DESC desc{ + .ByteWidth = sizeof(CustomConstBuffer), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + }; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, nullptr, _customShaderConstantBuffer.put())); + } + + { + static constexpr D3D11_SAMPLER_DESC desc{ + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressV = D3D11_TEXTURE_ADDRESS_BORDER, + .AddressW = D3D11_TEXTURE_ADDRESS_BORDER, + .MaxAnisotropy = 1, + .ComparisonFunc = D3D11_COMPARISON_ALWAYS, + .MaxLOD = D3D11_FLOAT32_MAX, + }; + THROW_IF_FAILED(p.device->CreateSamplerState(&desc, _customShaderSamplerState.put())); + } + + _customShaderStartTime = std::chrono::steady_clock::now(); + } +} + +void BackendD3D::_recreateCustomRenderTargetView(const RenderingPayload& p) +{ + // Avoid memory usage spikes by releasing memory first. + _customOffscreenTexture.reset(); + _customOffscreenTextureView.reset(); + + const D3D11_TEXTURE2D_DESC desc{ + .Width = p.s->targetSize.x, + .Height = p.s->targetSize.y, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_B8G8R8A8_UNORM, + .SampleDesc = { 1, 0 }, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, + }; + THROW_IF_FAILED(p.device->CreateTexture2D(&desc, nullptr, _customOffscreenTexture.addressof())); + THROW_IF_FAILED(p.device->CreateShaderResourceView(_customOffscreenTexture.get(), nullptr, _customOffscreenTextureView.addressof())); + THROW_IF_FAILED(p.device->CreateRenderTargetView(_customOffscreenTexture.get(), nullptr, _customRenderTargetView.addressof())); +} + +void BackendD3D::_recreateBackgroundColorBitmap(const RenderingPayload& p) +{ + // Avoid memory usage spikes by releasing memory first. + _backgroundBitmap.reset(); + _backgroundBitmapView.reset(); + + const D3D11_TEXTURE2D_DESC desc{ + .Width = p.s->viewportCellCount.x, + .Height = p.s->viewportCellCount.y, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .SampleDesc = { 1, 0 }, + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_SHADER_RESOURCE, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + }; + THROW_IF_FAILED(p.device->CreateTexture2D(&desc, nullptr, _backgroundBitmap.addressof())); + THROW_IF_FAILED(p.device->CreateShaderResourceView(_backgroundBitmap.get(), nullptr, _backgroundBitmapView.addressof())); + _backgroundBitmapGeneration = {}; +} + +void BackendD3D::_recreateConstBuffer(const RenderingPayload& p) const +{ + { + VSConstBuffer data{}; + data.positionScale = { 2.0f / p.s->targetSize.x, -2.0f / p.s->targetSize.y }; + p.deviceContext->UpdateSubresource(_vsConstantBuffer.get(), 0, nullptr, &data, 0, 0); + } + { + PSConstBuffer data{}; + data.backgroundColor = colorFromU32Premultiply(p.s->misc->backgroundColor); + data.backgroundCellSize = { static_cast(p.s->font->cellSize.x), static_cast(p.s->font->cellSize.y) }; + data.backgroundCellCount = { static_cast(p.s->viewportCellCount.x), static_cast(p.s->viewportCellCount.y) }; + DWrite_GetGammaRatios(_gamma, data.gammaRatios); + data.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast; + data.underlineWidth = p.s->font->underline.height; + p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0); + } +} + +void BackendD3D::_setupDeviceContextState(const RenderingPayload& p) +{ + // IA: Input Assembler + ID3D11Buffer* vertexBuffers[]{ _vertexBuffer.get(), _instanceBuffer.get() }; + static constexpr UINT strides[]{ sizeof(f32x2), sizeof(QuadInstance) }; + static constexpr UINT offsets[]{ 0, 0 }; + p.deviceContext->IASetIndexBuffer(_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0); + p.deviceContext->IASetInputLayout(_inputLayout.get()); + p.deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + p.deviceContext->IASetVertexBuffers(0, 2, &vertexBuffers[0], &strides[0], &offsets[0]); + + // VS: Vertex Shader + p.deviceContext->VSSetShader(_vertexShader.get(), nullptr, 0); + p.deviceContext->VSSetConstantBuffers(0, 1, _vsConstantBuffer.addressof()); + + // RS: Rasterizer Stage + D3D11_VIEWPORT viewport{}; + viewport.Width = static_cast(p.s->targetSize.x); + viewport.Height = static_cast(p.s->targetSize.y); + p.deviceContext->RSSetViewports(1, &viewport); + + // PS: Pixel Shader + ID3D11ShaderResourceView* resources[]{ _backgroundBitmapView.get(), _glyphAtlasView.get() }; + p.deviceContext->PSSetShader(_pixelShader.get(), nullptr, 0); + p.deviceContext->PSSetConstantBuffers(0, 1, _psConstantBuffer.addressof()); + p.deviceContext->PSSetShaderResources(0, 2, &resources[0]); + + // OM: Output Merger + p.deviceContext->OMSetBlendState(_blendState.get(), nullptr, 0xffffffff); + p.deviceContext->OMSetRenderTargets(1, _customRenderTargetView ? _customRenderTargetView.addressof() : _renderTargetView.addressof(), nullptr); +} + +#ifndef NDEBUG +void BackendD3D::_debugUpdateShaders(const RenderingPayload& p) noexcept +try +{ + const auto invalidationTime = _sourceCodeInvalidationTime.load(std::memory_order_relaxed); + + if (invalidationTime == INT64_MAX || invalidationTime > std::chrono::steady_clock::now().time_since_epoch().count()) + { + return; + } + + _sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed); + + static const auto compile = [](const std::filesystem::path& path, const char* target) { + wil::com_ptr error; + wil::com_ptr blob; + const auto hr = D3DCompileFromFile( + /* pFileName */ path.c_str(), + /* pDefines */ nullptr, + /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, + /* pEntrypoint */ "main", + /* pTarget */ target, + /* Flags1 */ D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS, + /* Flags2 */ 0, + /* ppCode */ blob.addressof(), + /* ppErrorMsgs */ error.addressof()); + + if (error) + { + std::thread t{ [error = std::move(error)]() noexcept { + MessageBoxA(nullptr, static_cast(error->GetBufferPointer()), "Compilation error", MB_ICONERROR | MB_OK); + } }; + t.detach(); + } + + THROW_IF_FAILED(hr); + return blob; + }; + + struct FileVS + { + std::wstring_view filename; + wil::com_ptr BackendD3D::*target; + }; + struct FilePS + { + std::wstring_view filename; + wil::com_ptr BackendD3D::*target; + }; + + static constexpr std::array filesVS{ + FileVS{ L"shader_vs.hlsl", &BackendD3D::_vertexShader }, + }; + static constexpr std::array filesPS{ + FilePS{ L"shader_ps.hlsl", &BackendD3D::_pixelShader }, + }; + + std::array, filesVS.size()> compiledVS; + std::array, filesPS.size()> compiledPS; + + // Compile our files before moving them into `this` below to ensure we're + // always in a consistent state where all shaders are seemingly valid. + for (size_t i = 0; i < filesVS.size(); ++i) + { + const auto blob = compile(_sourceDirectory / filesVS[i].filename, "vs_4_0"); + THROW_IF_FAILED(p.device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledVS[i].addressof())); + } + for (size_t i = 0; i < filesPS.size(); ++i) + { + const auto blob = compile(_sourceDirectory / filesPS[i].filename, "ps_4_0"); + THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledPS[i].addressof())); + } + + for (size_t i = 0; i < filesVS.size(); ++i) + { + this->*filesVS[i].target = std::move(compiledVS[i]); + } + for (size_t i = 0; i < filesPS.size(); ++i) + { + this->*filesPS[i].target = std::move(compiledPS[i]); + } + + _setupDeviceContextState(p); +} +CATCH_LOG() +#endif + +void BackendD3D::_d2dBeginDrawing() noexcept +{ + if (!_d2dBeganDrawing) + { + _d2dRenderTarget->BeginDraw(); + _d2dBeganDrawing = true; + } +} + +void BackendD3D::_d2dEndDrawing() +{ + if (_d2dBeganDrawing) + { + THROW_IF_FAILED(_d2dRenderTarget->EndDraw()); + _d2dBeganDrawing = false; + } +} + +void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p) +{ + // The index returned by _BitScanReverse is undefined when the input is 0. We can simultaneously guard + // against that and avoid unreasonably small textures, by clamping the min. texture size to `minArea`. + // `minArea` results in a 64kB RGBA texture which is the min. alignment for placed memory. + static constexpr u32 minArea = 128 * 128; + static constexpr u32 maxArea = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION * D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION; + + const auto cellArea = static_cast(p.s->font->cellSize.x) * p.s->font->cellSize.y; + const auto targetArea = static_cast(p.s->targetSize.x) * p.s->targetSize.y; + + const auto minAreaByFont = cellArea * 95; // Covers all printable ASCII characters + const auto minAreaByGrowth = static_cast(_rectPacker.width) * _rectPacker.height * 2; + + // It's hard to say what the max. size of the cache should be. Optimally I think we should use as much + // memory as is available, but the rendering code in this project is a big mess and so integrating + // memory pressure feedback (RegisterVideoMemoryBudgetChangeNotificationEvent) is rather difficult. + // As an alternative I'm using 1.25x the size of the swap chain. The 1.25x is there to avoid situations, where + // we're locked into a state, where on every render pass we're starting with a half full atlas, drawing once, + // filling it with the remaining half and drawing again, requiring two rendering passes on each frame. + const auto maxAreaByFont = targetArea + targetArea / 4; + + auto area = std::min(maxAreaByFont, std::max(minAreaByFont, minAreaByGrowth)); + area = clamp(area, minArea, maxArea); + + // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`. + // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152). + // This has 2 benefits: GPUs like power-of-2 textures and it ensures that we don't resize the texture + // every time you resize the window by a pixel. Instead it only grows/shrinks by a factor of 2. + unsigned long index; + _BitScanReverse(&index, area - 1); + const auto u = static_cast(1u << ((index + 2) / 2)); + const auto v = static_cast(1u << ((index + 1) / 2)); + + if (u != _rectPacker.width || v != _rectPacker.height) + { + _resizeGlyphAtlas(p, u, v); + } + + stbrp_init_target(&_rectPacker, u, v, _rectPackerData.data(), _rectPackerData.size()); + + // This is a little imperfect, because it only releases the memory of the glyph mappings, not the memory held by + // any DirectWrite fonts. On the other side, the amount of fonts on a system is always finite, where "finite" + // is pretty low, relatively speaking. Additionally this allows us to cache the boxGlyphs map indefinitely. + // It's not great, but it's not terrible. + for (auto& slot : _glyphAtlasMap.container()) + { + if (slot.inner) + { + slot.inner->glyphs.clear(); + } + } + + _d2dBeginDrawing(); + _d2dRenderTarget->Clear(); + + _fontChangedResetGlyphAtlas = false; +} + +void BackendD3D::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const u16 v) +{ + _d2dRenderTarget.reset(); + _d2dRenderTarget4.reset(); + _glyphAtlas.reset(); + _glyphAtlasView.reset(); + + { + const D3D11_TEXTURE2D_DESC desc{ + .Width = u, + .Height = v, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_B8G8R8A8_UNORM, + .SampleDesc = { 1, 0 }, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, + }; + THROW_IF_FAILED(p.device->CreateTexture2D(&desc, nullptr, _glyphAtlas.addressof())); + THROW_IF_FAILED(p.device->CreateShaderResourceView(_glyphAtlas.get(), nullptr, _glyphAtlasView.addressof())); + } + + { + const auto surface = _glyphAtlas.query(); + + static constexpr D2D1_RENDER_TARGET_PROPERTIES props{ + .type = D2D1_RENDER_TARGET_TYPE_DEFAULT, + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + }; + // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_d2dRenderTarget.addressof()))); + _d2dRenderTarget.try_query_to(_d2dRenderTarget4.addressof()); + + _d2dRenderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); + // We don't really use D2D for anything except DWrite, but it + // can't hurt to ensure that everything it does is pixel aligned. + _d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + // Ensure that D2D uses the exact same gamma as our shader uses. + _d2dRenderTarget->SetTextRenderingParams(_textRenderingParams.get()); + + _d2dRenderTargetUpdateFontSettings(p); + } + + // We have our own glyph cache so Direct2D's cache doesn't help much. + // This saves us 1MB of RAM, which is not much, but also not nothing. + if (_d2dRenderTarget4) + { + wil::com_ptr device; + _d2dRenderTarget4->GetDevice(device.addressof()); + + device->SetMaximumTextureMemory(0); + if (const auto device4 = device.try_query()) + { + device4->SetMaximumColorGlyphCacheMemory(0); + } + } + + { + static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; + THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _emojiBrush.put())); + THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _brush.put())); + } + + ID3D11ShaderResourceView* resources[]{ _backgroundBitmapView.get(), _glyphAtlasView.get() }; + p.deviceContext->PSSetShaderResources(0, 2, &resources[0]); + + _rectPackerData = Buffer{ u }; +} + +BackendD3D::QuadInstance& BackendD3D::_getLastQuad() noexcept +{ + assert(_instancesCount != 0); + return _instances[_instancesCount - 1]; +} + +// NOTE: Up to 5M calls per second -> no std::vector, no std::unordered_map. +// This function is an easy >100x faster than std::vector, can be +// inlined and reduces overall (!) renderer CPU usage by 5%. +BackendD3D::QuadInstance& BackendD3D::_appendQuad() +{ + if (_instancesCount >= _instances.size()) + { + _bumpInstancesSize(); + } + + return _instances[_instancesCount++]; +} + +void BackendD3D::_bumpInstancesSize() +{ + auto newSize = std::max(_instancesCount, _instances.size() * 2); + newSize = std::max(size_t{ 256 }, newSize); + Expects(newSize > _instances.size()); + + // Our render loop heavily relies on memcpy() which is up to between 1.5x (Intel) + // and 40x (AMD) faster for allocations with an alignment of 32 or greater. + auto newInstances = Buffer{ newSize }; + std::copy_n(_instances.data(), _instances.size(), newInstances.data()); + + _instances = std::move(newInstances); +} + +void BackendD3D::_flushQuads(const RenderingPayload& p) +{ + if (!_instancesCount) + { + return; + } + + if (!_cursorRects.empty()) + { + _drawCursorForeground(); + } + + // TODO: Shrink instances buffer + if (_instancesCount > _instanceBufferCapacity) + { + _recreateInstanceBuffers(p); + } + + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + THROW_IF_FAILED(p.deviceContext->Map(_instanceBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + memcpy(mapped.pData, _instances.data(), _instancesCount * sizeof(QuadInstance)); + p.deviceContext->Unmap(_instanceBuffer.get(), 0); + } + + // I found 4 approaches to drawing lots of quads quickly. There are probably even more. + // They can often be found in discussions about "particle" or "point sprite" rendering in game development. + // * Compute Shader: My understanding is that at the time of writing games are moving over to bucketing + // particles into "tiles" on the screen and drawing them with a compute shader. While this improves + // performance, it doesn't mix well with our goal of allowing arbitrary overlaps between glyphs. + // Additionally none of the next 3 approaches use any significant amount of GPU time in the first place. + // * Geometry Shader: Geometry shaders can generate vertices on the fly, which would neatly replace our need + // for an index buffer. However, many sources claim they're significantly slower than the following approaches. + // * DrawIndexed & DrawInstanced: Again, many sources claim that GPU instancing (Draw(Indexed)Instanced) performs + // poorly for small meshes, and instead indexed vertices with a SRV (shader resource view) should be used. + // The popular "Vertex Shader Tricks" talk from Bill Bilodeau at GDC 2014 suggests this approach, explains + // how it works (you divide the `SV_VertexID` by 4 and index into the SRV that contains the per-instance data; + // it's basically manual instancing inside the vertex shader) and shows how it outperforms regular instancing. + // However on my own limited test hardware (built around ~2020), I found that for at least our use case, + // GPU instancing matches the performance of using a custom buffer. In fact on my Nvidia GPU in particular, + // instancing with ~10k instances appears to be about 50% faster and so DrawInstanced was chosen. + // Instead I found that packing instance data as tightly as possible made the biggest performance difference, + // and packing 16 bit integers with ID3D11InputLayout is quite a bit more convenient too. + + p.deviceContext->DrawIndexedInstanced(6, static_cast(_instancesCount), 0, 0, 0); + _instancesCount = 0; +} + +void BackendD3D::_recreateInstanceBuffers(const RenderingPayload& p) +{ + // We use the viewport size of the terminal as the initial estimate for the amount of instances we'll see. + const auto minCapacity = static_cast(p.s->viewportCellCount.x) * p.s->viewportCellCount.y; + auto newCapacity = std::max(_instancesCount, minCapacity); + auto newSize = newCapacity * sizeof(QuadInstance); + // Round up to multiples of 64kB to avoid reallocating too often. + // 64kB is the minimum alignment for committed resources in D3D12. + newSize = (newSize + 0xffff) & ~size_t{ 0xffff }; + newCapacity = newSize / sizeof(QuadInstance); + + _instanceBuffer.reset(); + + { + const D3D11_BUFFER_DESC desc{ + .ByteWidth = gsl::narrow(newSize), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_VERTEX_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .StructureByteStride = sizeof(QuadInstance), + }; + THROW_IF_FAILED(p.device->CreateBuffer(&desc, nullptr, _instanceBuffer.addressof())); + } + + // IA: Input Assembler + ID3D11Buffer* vertexBuffers[]{ _vertexBuffer.get(), _instanceBuffer.get() }; + static constexpr UINT strides[]{ sizeof(f32x2), sizeof(QuadInstance) }; + static constexpr UINT offsets[]{ 0, 0 }; + p.deviceContext->IASetVertexBuffers(0, 2, &vertexBuffers[0], &strides[0], &offsets[0]); + + _instanceBufferCapacity = newCapacity; +} + +void BackendD3D::_drawBackground(const RenderingPayload& p) +{ + // Not uploading the bitmap halves (!) the GPU load for any given frame on 2023 hardware. + if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0]) + { + _uploadBackgroundBitmap(p); + } + + _appendQuad() = { + .shadingType = ShadingType::Background, + .size = p.s->targetSize, + }; +} + +void BackendD3D::_uploadBackgroundBitmap(const RenderingPayload& p) +{ + D3D11_MAPPED_SUBRESOURCE mapped{}; + THROW_IF_FAILED(p.deviceContext->Map(_backgroundBitmap.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + + auto src = std::bit_cast(p.backgroundBitmap.data()); + const auto srcEnd = std::bit_cast(p.backgroundBitmap.data() + p.backgroundBitmap.size()); + const auto srcStride = p.colorBitmapRowStride * sizeof(u32); + auto dst = static_cast(mapped.pData); + + while (src < srcEnd) + { + memcpy(dst, src, srcStride); + src += srcStride; + dst += mapped.RowPitch; + } + + p.deviceContext->Unmap(_backgroundBitmap.get(), 0); + _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; +} + +void BackendD3D::_drawText(RenderingPayload& p) +{ + if (_fontChangedResetGlyphAtlas) + { + _resetGlyphAtlas(p); + } + + til::CoordType dirtyTop = til::CoordTypeMax; + til::CoordType dirtyBottom = til::CoordTypeMin; + + u16 y = 0; + for (const auto row : p.rows) + { + f32 baselineX = 0; + f32 baselineY = y * p.s->font->cellSize.y + p.s->font->baseline; + f32 scaleX = 1; + f32 scaleY = 1; + + if (row->lineRendition != LineRendition::SingleWidth) + { + scaleX = 2; + + if (row->lineRendition >= LineRendition::DoubleHeightTop) + { + scaleY = 2; + baselineY /= 2; + } + } + + for (const auto& m : row->mappings) + { + auto x = m.glyphsFrom; + const AtlasFontFaceKey fontFaceKey{ + .fontFace = m.fontFace.get(), + .lineRendition = row->lineRendition, + }; + + // This goto label exists to allow us to retry rendering a glyph if the glyph atlas was full. + // We need to goto here, because a retry will cause the atlas texture as well as the + // _glyphCache hashmap to be cleared, and so we'll have to call insert() again. + drawGlyphRetry: + const auto [fontFaceEntryOuter, fontFaceInserted] = _glyphAtlasMap.insert(fontFaceKey); + auto& fontFaceEntry = *fontFaceEntryOuter.inner; + + if (fontFaceInserted) + { + _initializeFontFaceEntry(fontFaceEntry); + } + + while (x < m.glyphsTo) + { + const auto [glyphEntry, inserted] = fontFaceEntry.glyphs.insert(row->glyphIndices[x]); + + if (inserted && !_drawGlyph(p, fontFaceEntry, glyphEntry)) + { + // A deadlock in this retry loop is detected in _drawGlyphPrepareRetry. + // + // Yes, I agree, avoid goto. Sometimes. It's not my fault that C++ still doesn't + // have a `continue outerloop;` like other languages had it for decades. :( +#pragma warning(suppress : 26438) // Avoid 'goto' (es.76). +#pragma warning(suppress : 26448) // Consider using gsl::finally if final action is intended (gsl.util). + goto drawGlyphRetry; + } + + if (glyphEntry.data.GetShadingType() != ShadingType::Default) + { + auto l = static_cast(lrintf((baselineX + row->glyphOffsets[x].advanceOffset) * scaleX)); + auto t = static_cast(lrintf((baselineY - row->glyphOffsets[x].ascenderOffset) * scaleY)); + + l += glyphEntry.data.offset.x; + t += glyphEntry.data.offset.y; + + row->dirtyTop = std::min(row->dirtyTop, t); + row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry.data.size.y); + + _appendQuad() = { + .shadingType = glyphEntry.data.GetShadingType(), + .position = { static_cast(l), static_cast(t) }, + .size = glyphEntry.data.size, + .texcoord = glyphEntry.data.texcoord, + .color = row->colors[x], + }; + + if (glyphEntry.data.overlapSplit) + { + _drawTextOverlapSplit(p, y); + } + } + + baselineX += row->glyphAdvances[x]; + ++x; + } + } + + if (!row->gridLineRanges.empty()) + { + _drawGridlines(p, y); + } + + if (p.invalidatedRows.contains(y)) + { + dirtyTop = std::min(dirtyTop, row->dirtyTop); + dirtyBottom = std::max(dirtyBottom, row->dirtyBottom); + } + + ++y; + } + + if (dirtyTop < dirtyBottom) + { + p.dirtyRectInPx.top = std::min(p.dirtyRectInPx.top, dirtyTop); + p.dirtyRectInPx.bottom = std::max(p.dirtyRectInPx.bottom, dirtyBottom); + } + + _d2dEndDrawing(); +} + +// There are a number of coding-oriented fonts that feature ligatures which (for instance) +// translate text like "!=" into a glyph that looks like "≠" (just 2 columns wide and not 1). +// Glyphs like that still need to be colored in potentially multiple colors however, so this +// function will handle these ligatures by splitting them up into multiple QuadInstances. +// +// It works by iteratively splitting the wide glyph into shorter and shorter segments like so +// (whitespaces indicate that the glyph was split up in a leading and trailing half): +// Retry with grayscale AA. + } + + // Even though it says "ALIASED" and even though the docs for the flag still say: + // > [...] that is, each pixel is either fully opaque or fully transparent [...] + // don't be confused: It's grayscale antialiased. lol + THROW_IF_FAILED(glyphRunAnalysis->GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1, &textureBounds)); + + // Allocate a buffer of `width * height` bytes. + THROW_IF_FAILED(glyphRunAnalysis->CreateAlphaTexture(DWRITE_TEXTURE_ALIASED_1x1, &textureBounds, buffer.data(), buffer.size())); + // The buffer now contains a grayscale alpha mask. +#endif + + const auto lineRendition = static_cast(fontFaceEntry.lineRendition); + const auto needsTransform = lineRendition != LineRendition::SingleWidth; + + static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 }; + D2D1_MATRIX_3X2_F transform = identityTransform; + + if (needsTransform) + { + transform.m11 = 2.0f; + transform.m22 = lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f; + _d2dRenderTarget->SetTransform(&transform); + } + + const auto restoreTransform = wil::scope_exit([&]() noexcept { + _d2dRenderTarget->SetTransform(&identityTransform); + }); + + // This calculates the black box of the glyph, or in other words, + // it's extents/size relative to its baseline origin (at 0,0). + // + // bounds.top ------++-----######--+ + // (-7) || ############ + // ||#### #### + // |### ##### + // baseline ______ |### #####| + // origin \|############# | + // (= 0,0) \|########### | + // ++-------###---+ + // ## ### | + // bounds.bottom ---+#########-----+ + // (+2) | | + // bounds.left bounds.right + // (-1) (+14) + // + + bool isColorGlyph = false; + D2D1_RECT_F bounds = GlyphRunEmptyBounds; + + const auto antialiasingCleanup = wil::scope_exit([&]() { + if (isColorGlyph) + { + _d2dRenderTarget4->SetTextAntialiasMode(static_cast(p.s->font->antialiasingMode)); + } + }); + + { + const auto enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), {}, &glyphRun); + + if (!enumerator) + { + THROW_IF_FAILED(_d2dRenderTarget->GetGlyphRunWorldBounds({}, &glyphRun, DWRITE_MEASURING_MODE_NATURAL, &bounds)); + } + else + { + isColorGlyph = true; + _d2dRenderTarget4->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + + while (ColorGlyphRunMoveNext(enumerator.get())) + { + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunAccumulateBounds(_d2dRenderTarget.get(), colorGlyphRun, bounds); + } + } + } + + // Overhangs for box glyphs can produce unsightly effects, where the antialiased edges of horizontal + // and vertical lines overlap between neighboring glyphs and produce "boldened" intersections. + // It looks a little something like this: + // ---+---+--- + // This avoids the issue in most cases by simply clipping the glyph to the size of a single cell. + // The downside is that it fails to work well for custom line heights, etc. + const auto isBoxGlyph = fontFaceEntry.boxGlyphs.lookup(glyphEntry.glyphIndex) != nullptr; + if (isBoxGlyph) + { + // NOTE: As mentioned above, the "origin" of a glyph's coordinate system is its baseline. + bounds.left = std::max(bounds.left, 0.0f); + bounds.top = std::max(bounds.top, static_cast(-p.s->font->baseline) * transform.m22); + bounds.right = std::min(bounds.right, static_cast(p.s->font->cellSize.x) * transform.m11); + bounds.bottom = std::min(bounds.bottom, static_cast(p.s->font->descender) * transform.m22); + } + + // The bounds may be empty if the glyph is whitespace. + if (bounds.left >= bounds.right || bounds.top >= bounds.bottom) + { + return true; + } + + const auto bl = lrintf(bounds.left); + const auto bt = lrintf(bounds.top); + const auto br = lrintf(bounds.right); + const auto bb = lrintf(bounds.bottom); + + stbrp_rect rect{ + .w = br - bl, + .h = bb - bt, + }; + if (!stbrp_pack_rects(&_rectPacker, &rect, 1)) + { + _drawGlyphPrepareRetry(p); + return false; + } + + const D2D1_POINT_2F baselineOrigin{ + static_cast(rect.x - bl), + static_cast(rect.y - bt), + }; + + _d2dBeginDrawing(); + + if (isBoxGlyph) + { + const D2D1_RECT_F clipRect{ + static_cast(rect.x) / transform.m11, + static_cast(rect.y) / transform.m22, + static_cast(rect.x + rect.w) / transform.m11, + static_cast(rect.y + rect.h) / transform.m22, + }; + _d2dRenderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + } + const auto boxGlyphCleanup = wil::scope_exit([&]() { + if (isBoxGlyph) + { + _d2dRenderTarget->PopAxisAlignedClip(); + } + }); + + if (needsTransform) + { + transform.dx = (1.0f - transform.m11) * baselineOrigin.x; + transform.dy = (1.0f - transform.m22) * baselineOrigin.y; + _d2dRenderTarget->SetTransform(&transform); + } + + if (!isColorGlyph) + { + _d2dRenderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, _brush.get(), DWRITE_MEASURING_MODE_NATURAL); + } + else + { + const auto enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + while (ColorGlyphRunMoveNext(enumerator.get())) + { + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunDraw(_d2dRenderTarget4.get(), _emojiBrush.get(), _brush.get(), colorGlyphRun); + } + } + + // Ligatures are drawn with strict cell-wise foreground color, while other text allows colors to overhang + // their cells. This makes sure that italics and such retain their color and don't look "cut off". + // + // The former condition makes sure to exclude diacritics and such from being considered a ligature, + // while the latter condition-pair makes sure to exclude regular BMP wide glyphs that overlap a little. + const auto horizontalScale = lineRendition != LineRendition::SingleWidth ? 2 : 1; + const auto triggerLeft = _ligatureOverhangTriggerLeft * horizontalScale; + const auto triggerRight = _ligatureOverhangTriggerRight * horizontalScale; + const auto overlapSplit = rect.w >= p.s->font->cellSize.x && (bl <= triggerLeft || br >= triggerRight); + + glyphEntry.data.shadingType = static_cast(isColorGlyph ? ShadingType::TextPassthrough : _textShadingType); + glyphEntry.data.overlapSplit = overlapSplit; + glyphEntry.data.offset.x = bl; + glyphEntry.data.offset.y = bt; + glyphEntry.data.size.x = rect.w; + glyphEntry.data.size.y = rect.h; + glyphEntry.data.texcoord.x = rect.x; + glyphEntry.data.texcoord.y = rect.y; + + if (lineRendition >= LineRendition::DoubleHeightTop) + { + _splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry); + } + + return true; +} + +bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry) +{ + stbrp_rect rect{ + .w = p.s->font->cellSize.x, + .h = p.s->font->cellSize.y, + }; + + const auto lineRendition = static_cast(fontFaceEntry.lineRendition); + auto baseline = p.s->font->baseline; + + if (lineRendition != LineRendition::SingleWidth) + { + const auto heightShift = static_cast(lineRendition >= LineRendition::DoubleHeightTop); + rect.w <<= 1; + rect.h <<= heightShift; + baseline <<= heightShift; + } + + if (!stbrp_pack_rects(&_rectPacker, &rect, 1)) + { + _drawGlyphPrepareRetry(p); + return false; + } + + if (!_softFontBitmap) + { + // Allocating such a tiny texture is very wasteful (min. texture size on GPUs + // right now is 64kB), but this is a seldom used feature so it's fine... + const D2D1_SIZE_U size{ + static_cast(p.s->font->softFontCellSize.width), + static_cast(p.s->font->softFontCellSize.height), + }; + const D2D1_BITMAP_PROPERTIES1 bitmapProperties{ + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = static_cast(p.s->font->dpi), + .dpiY = static_cast(p.s->font->dpi), + }; + THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, nullptr, 0, &bitmapProperties, _softFontBitmap.addressof())); + } + + const auto interpolation = p.s->font->antialiasingMode == AntialiasingMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; + const D2D1_RECT_F dest{ + static_cast(rect.x), + static_cast(rect.y), + static_cast(rect.x + rect.w), + static_cast(rect.y + rect.h), + }; + + _d2dBeginDrawing(); + _drawSoftFontGlyphInBitmap(p, glyphEntry); + _d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr); + + glyphEntry.data.shadingType = static_cast(ShadingType::TextGrayscale); + glyphEntry.data.overlapSplit = 0; + glyphEntry.data.offset.x = 0; + glyphEntry.data.offset.y = -baseline; + glyphEntry.data.size.x = rect.w; + glyphEntry.data.size.y = rect.h; + glyphEntry.data.texcoord.x = rect.x; + glyphEntry.data.texcoord.y = rect.y; + + if (lineRendition >= LineRendition::DoubleHeightTop) + { + _splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry); + } + + return true; +} + +void BackendD3D::_drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const +{ + if (!isSoftFontChar(glyphEntry.glyphIndex)) + { + // AtlasEngine::_mapReplacementCharacter should have filtered inputs with isSoftFontChar already. + assert(false); + return; + } + + const auto width = static_cast(p.s->font->softFontCellSize.width); + const auto height = static_cast(p.s->font->softFontCellSize.height); + const auto& softFontPattern = p.s->font->softFontPattern; + + // The isSoftFontChar() range is [0xEF20,0xEF80). + const auto offset = glyphEntry.glyphIndex - 0xEF20u; + const auto offsetBeg = offset * height; + const auto offsetEnd = offsetBeg + height; + + if (offsetEnd > softFontPattern.size()) + { + // Out of range values should not occur, but they do and it's unknown why: GH#15553 + assert(false); + return; + } + + Buffer bitmapData{ width * height }; + auto dst = bitmapData.begin(); + auto it = softFontPattern.begin() + offsetBeg; + const auto end = softFontPattern.begin() + offsetEnd; + + while (it != end) + { + auto srcBits = *it++; + for (size_t x = 0; x < width; x++) + { + const auto srcBitIsSet = (srcBits & 0x8000) != 0; + *dst++ = srcBitIsSet ? 0xffffffff : 0x00000000; + srcBits <<= 1; + } + } + + const auto pitch = static_cast(width * sizeof(u32)); + THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch)); +} + +void BackendD3D::_drawGlyphPrepareRetry(const RenderingPayload& p) +{ + THROW_HR_IF_MSG(E_UNEXPECTED, _glyphAtlasMap.empty(), "BackendD3D::_drawGlyph deadlock"); + _d2dEndDrawing(); + _flushQuads(p); + _resetGlyphAtlas(p); +} + +// If this is a double-height glyph (DECDHL), we need to split it into 2 glyph entries: +// One for the top/bottom half each, because that's how DECDHL works. This will clip the +// `glyphEntry` to only contain the one specified by `fontFaceEntry.lineRendition` +// and create a second entry in our glyph cache hashmap that contains the other half. +void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry) +{ + // Twice the line height, twice the descender gap. For both. + glyphEntry.data.offset.y -= p.s->font->descender; + + const auto isTop = fontFaceEntry.lineRendition == LineRendition::DoubleHeightTop; + + const AtlasFontFaceKey key2{ + .fontFace = fontFaceEntry.fontFace.get(), + .lineRendition = isTop ? LineRendition::DoubleHeightBottom : LineRendition::DoubleHeightTop, + }; + + auto& glyphCache = _glyphAtlasMap.insert(key2).first.inner->glyphs; + auto& entry2 = glyphCache.insert(glyphEntry.glyphIndex).first; + entry2.data = glyphEntry.data; + + auto& top = isTop ? glyphEntry : entry2; + auto& bottom = isTop ? entry2 : glyphEntry; + + const auto topSize = clamp(-glyphEntry.data.offset.y - p.s->font->baseline, 0, static_cast(glyphEntry.data.size.y)); + top.data.offset.y += p.s->font->cellSize.y; + top.data.size.y = topSize; + bottom.data.offset.y += topSize; + bottom.data.size.y = std::max(0, bottom.data.size.y - topSize); + bottom.data.texcoord.y += topSize; + + // Things like diacritics might be so small that they only exist on either half of the + // double-height row. This effectively turns the other (unneeded) side into whitespace. + if (!top.data.size.y) + { + top.data.shadingType = static_cast(ShadingType::Default); + } + if (!bottom.data.size.y) + { + bottom.data.shadingType = static_cast(ShadingType::Default); + } +} + +void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y) +{ + const auto row = p.rows[y]; + + const auto horizontalShift = static_cast(row->lineRendition != LineRendition::SingleWidth); + const auto verticalShift = static_cast(row->lineRendition >= LineRendition::DoubleHeightTop); + + const auto cellSize = p.s->font->cellSize; + const auto dottedLineType = horizontalShift ? ShadingType::DottedLineWide : ShadingType::DottedLine; + + const auto rowTop = static_cast(cellSize.y * y); + const auto rowBottom = static_cast(rowTop + cellSize.y); + + auto textCellTop = rowTop; + if (row->lineRendition == LineRendition::DoubleHeightBottom) + { + textCellTop -= cellSize.y; + } + + const i32 clipTop = row->lineRendition == LineRendition::DoubleHeightBottom ? rowTop : 0; + const i32 clipBottom = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : p.s->targetSize.y; + + const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) { + const auto textCellWidth = cellSize.x << horizontalShift; + const auto offset = pos.position << horizontalShift; + const auto width = static_cast(pos.height << horizontalShift); + + auto posX = r.from * cellSize.x + offset; + const auto end = r.to * cellSize.x; + + for (; posX < end; posX += textCellWidth) + { + _appendQuad() = { + .shadingType = ShadingType::SolidLine, + .position = { static_cast(posX), rowTop }, + .size = { width, p.s->font->cellSize.y }, + .color = r.color, + }; + } + }; + const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ShadingType shadingType) { + const auto offset = pos.position << verticalShift; + const auto height = static_cast(pos.height << verticalShift); + + const auto left = static_cast(r.from * cellSize.x); + const auto width = static_cast((r.to - r.from) * cellSize.x); + + i32 rt = textCellTop + offset; + i32 rb = rt + height; + rt = clamp(rt, clipTop, clipBottom); + rb = clamp(rb, clipTop, clipBottom); + + if (rt < rb) + { + _appendQuad() = { + .shadingType = shadingType, + .position = { left, static_cast(rt) }, + .size = { width, static_cast(rb - rt) }, + .color = r.color, + }; + } + }; + + for (const auto& r : row->gridLineRanges) + { + // AtlasEngine.cpp shouldn't add any gridlines if they don't do anything. + assert(r.lines.any()); + + if (r.lines.test(GridLines::Left)) + { + appendVerticalLines(r, p.s->font->gridLeft); + } + if (r.lines.test(GridLines::Right)) + { + appendVerticalLines(r, p.s->font->gridRight); + } + if (r.lines.test(GridLines::Top)) + { + appendHorizontalLine(r, p.s->font->gridTop, ShadingType::SolidLine); + } + if (r.lines.test(GridLines::Bottom)) + { + appendHorizontalLine(r, p.s->font->gridBottom, ShadingType::SolidLine); + } + + if (r.lines.test(GridLines::Underline)) + { + appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine); + } + if (r.lines.test(GridLines::HyperlinkUnderline)) + { + appendHorizontalLine(r, p.s->font->underline, dottedLineType); + } + if (r.lines.test(GridLines::DoubleUnderline)) + { + for (const auto pos : p.s->font->doubleUnderline) + { + appendHorizontalLine(r, pos, ShadingType::SolidLine); + } + } + if (r.lines.test(GridLines::Strikethrough)) + { + appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine); + } + } +} + +void BackendD3D::_drawCursorBackground(const RenderingPayload& p) +{ + _cursorRects.clear(); + + if (p.cursorRect.empty()) + { + return; + } + + _cursorPosition = { + p.s->font->cellSize.x * p.cursorRect.left, + p.s->font->cellSize.y * p.cursorRect.top, + p.s->font->cellSize.x * p.cursorRect.right, + p.s->font->cellSize.y * p.cursorRect.bottom, + }; + + const auto cursorColor = p.s->cursor->cursorColor; + const auto offset = p.cursorRect.top * p.colorBitmapRowStride; + + for (auto x1 = p.cursorRect.left; x1 < p.cursorRect.right; ++x1) + { + const auto x0 = x1; + const auto bg = p.backgroundBitmap[offset + x1] | 0xff000000; + + for (; x1 < p.cursorRect.right && (p.backgroundBitmap[offset + x1] | 0xff000000) == bg; ++x1) + { + } + + const i16x2 position{ + p.s->font->cellSize.x * x0, + p.s->font->cellSize.y * p.cursorRect.top, + }; + const u16x2 size{ + static_cast(p.s->font->cellSize.x * (x1 - x0)), + p.s->font->cellSize.y, + }; + auto background = cursorColor; + auto foreground = bg; + + if (cursorColor == 0xffffffff) + { + background = bg ^ 0xffffff; + foreground = 0xffffffff; + } + + // The legacy console used to invert colors by just doing `bg ^ 0xc0c0c0`. This resulted + // in a minimum squared distance of just 0.029195 across all possible color combinations. + background = ColorFix::GetPerceivableColor(background, bg, 0.25f * 0.25f); + + auto& c0 = _cursorRects.emplace_back(position, size, background, foreground); + + switch (static_cast(p.s->cursor->cursorType)) + { + case CursorType::Legacy: + { + const auto height = (c0.size.y * p.s->cursor->heightPercentage + 50) / 100; + c0.position.y += c0.size.y - height; + c0.size.y = height; + break; + } + case CursorType::VerticalBar: + c0.size.x = p.s->font->thinLineWidth; + break; + case CursorType::Underscore: + c0.position.y += p.s->font->underline.position; + c0.size.y = p.s->font->underline.height; + break; + case CursorType::EmptyBox: + { + auto& c1 = _cursorRects.emplace_back(c0); + if (x0 == p.cursorRect.left) + { + auto& c = _cursorRects.emplace_back(c0); + // Make line a little shorter vertically so it doesn't overlap with the top/bottom horizontal lines. + c.position.y += p.s->font->thinLineWidth; + c.size.y -= 2 * p.s->font->thinLineWidth; + // The actual adjustment... + c.size.x = p.s->font->thinLineWidth; + } + if (x1 == p.cursorRect.right) + { + auto& c = _cursorRects.emplace_back(c0); + // Make line a little shorter vertically so it doesn't overlap with the top/bottom horizontal lines. + c.position.y += p.s->font->thinLineWidth; + c.size.y -= 2 * p.s->font->thinLineWidth; + // The actual adjustment... + c.position.x += c.size.x - p.s->font->thinLineWidth; + c.size.x = p.s->font->thinLineWidth; + } + c0.size.y = p.s->font->thinLineWidth; + c1.position.y += c1.size.y - p.s->font->thinLineWidth; + c1.size.y = p.s->font->thinLineWidth; + break; + } + case CursorType::FullBox: + break; + case CursorType::DoubleUnderscore: + { + auto& c1 = _cursorRects.emplace_back(c0); + c0.position.y += p.s->font->doubleUnderline[0].position; + c0.size.y = p.s->font->thinLineWidth; + c1.position.y += p.s->font->doubleUnderline[1].position; + c1.size.y = p.s->font->thinLineWidth; + break; + } + default: + break; + } + } + + for (const auto& c : _cursorRects) + { + _appendQuad() = { + .shadingType = ShadingType::Cursor, + .position = c.position, + .size = c.size, + .color = c.background, + }; + } +} + +void BackendD3D::_drawCursorForeground() +{ + // NOTE: _appendQuad() may reallocate the _instances vector. It's important to iterate + // by index, because pointers (or iterators) would get invalidated. It's also important + // to cache the original _instancesCount since it'll get changed with each append. + auto instancesCount = _instancesCount; + size_t instancesOffset = 0; + + assert(instancesCount != 0); + + // All of the text drawing primitives are drawn as a single block, after drawing + // the background and cursor background and before drawing the selection overlay. + // To avoid having to check the shadingType in the loop below, we'll find the + // start and end of this "block" here in advance. + for (; instancesOffset < instancesCount; ++instancesOffset) + { + const auto shadingType = _instances[instancesOffset].shadingType; + if (shadingType >= ShadingType::TextDrawingFirst && shadingType <= ShadingType::TextDrawingLast) + { + break; + } + } + // We can also skip any instances (= any rows) at the beginning that are clearly not overlapping with + // the cursor. This reduces the the CPU cost of this function by roughly half (a few microseconds). + for (; instancesOffset < instancesCount; ++instancesOffset) + { + const auto& it = _instances[instancesOffset]; + if ((it.position.y + it.size.y) > _cursorPosition.top) + { + break; + } + } + + // Now do the same thing as above, but backwards from the end. + for (; instancesCount > instancesOffset; --instancesCount) + { + const auto shadingType = _instances[instancesCount - 1].shadingType; + if (shadingType >= ShadingType::TextDrawingFirst && shadingType <= ShadingType::TextDrawingLast) + { + break; + } + } + for (; instancesCount > instancesOffset; --instancesCount) + { + const auto& it = _instances[instancesCount - 1]; + if (it.position.y < _cursorPosition.bottom) + { + break; + } + } + + // For cursors with multiple rectangles this really isn't all that fast, because it iterates + // over the instances vector multiple times. But I also don't really care, because the + // double-underline and empty-box cursors are pretty annoying to deal with in any case. + // + // It would definitely help if instead of position & size QuadInstances would use left/top/right/bottom + // with f32, because then computing the intersection would be much faster via SIMD. But that would + // make the struct size larger and cost more power to transmit more data to the GPU. ugh. + for (const auto& c : _cursorRects) + { + const int cursorL = c.position.x; + const int cursorT = c.position.y; + const int cursorR = cursorL + c.size.x; + const int cursorB = cursorT + c.size.y; + + for (size_t i = instancesOffset; i < instancesCount; ++i) + { + const auto& it = _instances[i]; + const int instanceL = it.position.x; + const int instanceT = it.position.y; + const int instanceR = instanceL + it.size.x; + const int instanceB = instanceT + it.size.y; + + if (instanceL < cursorR && instanceR > cursorL && instanceT < cursorB && instanceB > cursorT) + { + // The _instances vector is _huge_ (easily up to 50k items) whereas only 1-2 items will actually overlap + // with the cursor. --> Make this loop more compact by putting as much as possible into a function call. + const auto added = _drawCursorForegroundSlowPath(c, i); + i += added; + instancesCount += added; + } + } + } +} + +size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t offset) +{ + // We won't die from copying 24 bytes. It simplifies the code below especially in + // respect to when/if we overwrite the _instances[offset] slot with a cutout. +#pragma warning(suppress : 26820) // This is a potentially expensive copy operation. Consider using a reference unless a copy is required (p.9). + const auto it = _instances[offset]; + + // There's one special exception to the rule: Emojis. We currently don't really support inverting + // (or reversing) colored glyphs like that, so we can return early here and avoid cutting them up. + // It'd be too expensive to check for these rare glyph types inside the _drawCursorForeground() loop. + if (it.shadingType == ShadingType::TextPassthrough) + { + return 0; + } + + const int cursorL = c.position.x; + const int cursorT = c.position.y; + const int cursorR = cursorL + c.size.x; + const int cursorB = cursorT + c.size.y; + + const int instanceL = it.position.x; + const int instanceT = it.position.y; + const int instanceR = instanceL + it.size.x; + const int instanceB = instanceT + it.size.y; + + const auto intersectionL = std::max(cursorL, instanceL); + const auto intersectionT = std::max(cursorT, instanceT); + const auto intersectionR = std::min(cursorR, instanceR); + const auto intersectionB = std::min(cursorB, instanceB); + + // We should only get called if there's actually an intersection. + assert(intersectionL < intersectionR && intersectionT < intersectionB); + + // We need to ensure that the glyph doesn't "dirty" the cursor background with its un-inverted/un-reversed color. + // If it did, and we'd draw the inverted/reversed glyph on top, it would look smudged. + // As such, this cuts a cursor-sized hole into the original glyph and splits it up. + // + // > Always initialize an object + // I would pay money if this warning was a little smarter. The array can remain uninitialized, + // because it acts like a tiny small_vector, but without the assertions. +#pragma warning(suppress : 26494) // Variable 'cutouts' is uninitialized. Always initialize an object (type.5). + rect cutouts[4]; + size_t cutoutCount = 0; + + if (instanceT < intersectionT) + { + cutouts[cutoutCount++] = { instanceL, instanceT, instanceR, intersectionT }; + } + if (instanceB > intersectionB) + { + cutouts[cutoutCount++] = { instanceL, intersectionB, instanceR, instanceB }; + } + if (instanceL < intersectionL) + { + cutouts[cutoutCount++] = { instanceL, intersectionT, intersectionL, intersectionB }; + } + if (instanceR > intersectionR) + { + cutouts[cutoutCount++] = { intersectionR, intersectionT, instanceR, intersectionB }; + } + + const auto addedInstances = cutoutCount ? cutoutCount - 1 : 0; + + // Make place for cutoutCount-many items at position. + // NOTE: _bumpInstancesSize() reallocates the vector and all references to _instances will now be invalid. + if (addedInstances) + { + const auto instancesCount = _instancesCount; + + _instancesCount += addedInstances; + if (_instancesCount >= _instances.size()) + { + _bumpInstancesSize(); + } + + const auto src = _instances.data() + offset; + const auto dst = src + addedInstances; + const auto count = instancesCount - offset; + assert(src >= _instances.begin() && (src + count) < _instances.end()); + assert(dst >= _instances.begin() && (dst + count) < _instances.end()); + memmove(dst, src, count * sizeof(QuadInstance)); + } + + // Now that there's space we can write the glyph cutouts back into the instances vector. + for (size_t i = 0; i < cutoutCount; ++i) + { + const auto& cutout = cutouts[i]; + auto& target = _instances[offset + i]; + + target.shadingType = it.shadingType; + target.position.x = static_cast(cutout.left); + target.position.y = static_cast(cutout.top); + target.size.x = static_cast(cutout.right - cutout.left); + target.size.y = static_cast(cutout.bottom - cutout.top); + target.texcoord.x = static_cast(it.texcoord.x + cutout.left - instanceL); + target.texcoord.y = static_cast(it.texcoord.y + cutout.top - instanceT); + target.color = it.color; + } + + auto color = c.foreground == 0xffffffff ? it.color ^ 0xffffff : c.foreground; + color = ColorFix::GetPerceivableColor(color, c.background, 0.5f * 0.5f); + + // If the cursor covers the entire glyph (like, let's say, a full-box cursor with an ASCII character), + // we don't append a new quad, but rather reuse the one that already exists (cutoutCount == 0). + auto& target = cutoutCount ? _appendQuad() : _instances[offset]; + + target.shadingType = it.shadingType; + target.position.x = static_cast(intersectionL); + target.position.y = static_cast(intersectionT); + target.size.x = static_cast(intersectionR - intersectionL); + target.size.y = static_cast(intersectionB - intersectionT); + target.texcoord.x = static_cast(it.texcoord.x + intersectionL - instanceL); + target.texcoord.y = static_cast(it.texcoord.y + intersectionT - instanceT); + target.color = color; + + return addedInstances; +} + +void BackendD3D::_drawSelection(const RenderingPayload& p) +{ + u16 y = 0; + u16 lastFrom = 0; + u16 lastTo = 0; + + for (const auto& row : p.rows) + { + if (row->selectionTo > row->selectionFrom) + { + // If the current selection line matches the previous one, we can just extend the previous quad downwards. + // The way this is implemented isn't very smart, but we also don't have very many rows to iterate through. + if (row->selectionFrom == lastFrom && row->selectionTo == lastTo) + { + _getLastQuad().size.y += p.s->font->cellSize.y; + } + else + { + _appendQuad() = { + .shadingType = ShadingType::Selection, + .position = { + p.s->font->cellSize.x * row->selectionFrom, + p.s->font->cellSize.y * y, + }, + .size = { + static_cast(p.s->font->cellSize.x * (row->selectionTo - row->selectionFrom)), + p.s->font->cellSize.y, + }, + .color = p.s->misc->selectionColor, + }; + lastFrom = row->selectionFrom; + lastTo = row->selectionTo; + } + } + + y++; + } +} + +#if ATLAS_DEBUG_SHOW_DIRTY +void BackendD3D::_debugShowDirty(const RenderingPayload& p) +{ + _presentRects[_presentRectsPos] = p.dirtyRectInPx; + _presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects); + + for (size_t i = 0; i < std::size(_presentRects); ++i) + { + const auto& rect = _presentRects[(_presentRectsPos + i) % std::size(_presentRects)]; + if (rect.non_empty()) + { + _appendQuad() = { + .shadingType = ShadingType::Selection, + .position = { + static_cast(rect.left), + static_cast(rect.top), + }, + .size = { + static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top), + }, + .color = colorbrewer::pastel1[i] | 0x1f000000, + }; + } + } +} +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET +void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p) +{ + if (_dumpRenderTargetCounter == 0) + { + ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast(std::size(_dumpRenderTargetBasePath))); + std::filesystem::create_directories(_dumpRenderTargetBasePath); + } + + wchar_t path[MAX_PATH]; + swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter); + SaveTextureToPNG(p.deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]); + _dumpRenderTargetCounter++; +} +#endif + +void BackendD3D::_executeCustomShader(RenderingPayload& p) +{ + { + const CustomConstBuffer data{ + .time = std::chrono::duration(std::chrono::steady_clock::now() - _customShaderStartTime).count(), + .scale = static_cast(p.s->font->dpi) / static_cast(USER_DEFAULT_SCREEN_DPI), + .resolution = { + static_cast(_viewportCellCount.x * p.s->font->cellSize.x), + static_cast(_viewportCellCount.y * p.s->font->cellSize.y), + }, + .background = colorFromU32Premultiply(p.s->misc->backgroundColor), + }; + + D3D11_MAPPED_SUBRESOURCE mapped{}; + THROW_IF_FAILED(p.deviceContext->Map(_customShaderConstantBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + memcpy(mapped.pData, &data, sizeof(data)); + p.deviceContext->Unmap(_customShaderConstantBuffer.get(), 0); + } + + { + // Before we do anything else we have to unbound _renderTargetView from being + // a render target, otherwise we can't use it as a shader resource below. + p.deviceContext->OMSetRenderTargets(1, _renderTargetView.addressof(), nullptr); + + // IA: Input Assembler + p.deviceContext->IASetIndexBuffer(nullptr, DXGI_FORMAT_UNKNOWN, 0); + p.deviceContext->IASetInputLayout(nullptr); + p.deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + p.deviceContext->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); + + // VS: Vertex Shader + p.deviceContext->VSSetShader(_customVertexShader.get(), nullptr, 0); + p.deviceContext->VSSetConstantBuffers(0, 0, nullptr); + + // PS: Pixel Shader + p.deviceContext->PSSetShader(_customPixelShader.get(), nullptr, 0); + p.deviceContext->PSSetConstantBuffers(0, 1, _customShaderConstantBuffer.addressof()); + p.deviceContext->PSSetShaderResources(0, 1, _customOffscreenTextureView.addressof()); + p.deviceContext->PSSetSamplers(0, 1, _customShaderSamplerState.addressof()); + + // OM: Output Merger + p.deviceContext->OMSetBlendState(nullptr, nullptr, 0xffffffff); + } + + p.deviceContext->Draw(4, 0); + + { + // IA: Input Assembler + ID3D11Buffer* vertexBuffers[]{ _vertexBuffer.get(), _instanceBuffer.get() }; + static constexpr UINT strides[]{ sizeof(f32x2), sizeof(QuadInstance) }; + static constexpr UINT offsets[]{ 0, 0 }; + p.deviceContext->IASetIndexBuffer(_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0); + p.deviceContext->IASetInputLayout(_inputLayout.get()); + p.deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + p.deviceContext->IASetVertexBuffers(0, 2, &vertexBuffers[0], &strides[0], &offsets[0]); + + // VS: Vertex Shader + p.deviceContext->VSSetShader(_vertexShader.get(), nullptr, 0); + p.deviceContext->VSSetConstantBuffers(0, 1, _vsConstantBuffer.addressof()); + + // PS: Pixel Shader + ID3D11ShaderResourceView* resources[]{ _backgroundBitmapView.get(), _glyphAtlasView.get() }; + p.deviceContext->PSSetShader(_pixelShader.get(), nullptr, 0); + p.deviceContext->PSSetConstantBuffers(0, 1, _psConstantBuffer.addressof()); + p.deviceContext->PSSetShaderResources(0, 2, &resources[0]); + p.deviceContext->PSSetSamplers(0, 0, nullptr); + + // OM: Output Merger + p.deviceContext->OMSetBlendState(_blendState.get(), nullptr, 0xffffffff); + p.deviceContext->OMSetRenderTargets(1, _customRenderTargetView.addressof(), nullptr); + } + + // With custom shaders, everything might be invalidated, so we have to + // indirectly disable Present1() and its dirty rects this way. + p.dirtyRectInPx = { 0, 0, p.s->targetSize.x, p.s->targetSize.y }; +} + +TIL_FAST_MATH_END diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h new file mode 100644 index 0000000000..14f4ca2943 --- /dev/null +++ b/src/renderer/atlas/BackendD3D.h @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include + +#include "Backend.h" + +namespace Microsoft::Console::Render::Atlas +{ + struct BackendD3D : IBackend + { + BackendD3D(const RenderingPayload& p); + + void ReleaseResources() noexcept override; + void Render(RenderingPayload& payload) override; + bool RequiresContinuousRedraw() noexcept override; + + // NOTE: D3D constant buffers sizes must be a multiple of 16 bytes. + struct alignas(16) VSConstBuffer + { + // WARNING: Modify this carefully after understanding how HLSL struct packing works. The gist is: + // * Minimum alignment is 4 bytes + // * Members cannot straddle 16 byte boundaries + // This means a structure like {u32; u32; u32; u32x2} would require + // padding so that it is {u32; u32; u32; <4 byte padding>; u32x2}. + // * bool will probably not work the way you want it to, + // because HLSL uses 32-bit bools and C++ doesn't. + alignas(sizeof(f32x2)) f32x2 positionScale; +#pragma warning(suppress : 4324) // 'VSConstBuffer': structure was padded due to alignment specifier + }; + + // WARNING: Same rules as for VSConstBuffer above apply. + struct alignas(16) PSConstBuffer + { + alignas(sizeof(f32x4)) f32x4 backgroundColor; + alignas(sizeof(f32x2)) f32x2 backgroundCellSize; + alignas(sizeof(f32x2)) f32x2 backgroundCellCount; + alignas(sizeof(f32x4)) f32 gammaRatios[4]{}; + alignas(sizeof(f32)) f32 enhancedContrast = 0; + alignas(sizeof(f32)) f32 underlineWidth = 0; +#pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier + }; + + // WARNING: Same rules as for VSConstBuffer above apply. + struct alignas(16) CustomConstBuffer + { + alignas(sizeof(f32)) f32 time = 0; + alignas(sizeof(f32)) f32 scale = 0; + alignas(sizeof(f32x2)) f32x2 resolution; + alignas(sizeof(f32x4)) f32x4 background; +#pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier + }; + + enum class ShadingType : u32 + { + Default = 0, + Background = 0, + + // This block of values will be used for the TextDrawingFirst/Last range and need to stay together. + // This is used to quickly check if an instance is related to a "text drawing primitive". + TextGrayscale = 1, + TextClearType = 2, + TextPassthrough = 3, + DottedLine = 4, + DottedLineWide = 5, + // All items starting here will be drawing as a solid RGBA color + SolidLine = 6, + + Cursor = 7, + Selection = 8, + + TextDrawingFirst = TextGrayscale, + TextDrawingLast = SolidLine, + }; + + // NOTE: Don't initialize any members in this struct. This ensures that no + // zero-initialization needs to occur when we allocate large buffers of this object. + struct QuadInstance + { + // `position` might clip outside of the bounds of the viewport and so it needs to be a + // signed coordinate. i16x2 is used as the size of the instance buffer made the largest + // impact on performance and power draw. If (when?) displays with >32k resolution make their + // appearance in the future, this should be changed to f32x2. But if you do so, please change + // all other occurrences of i16x2 positions/offsets throughout the class to keep it consistent. + alignas(u32) ShadingType shadingType; + alignas(u32) i16x2 position; + alignas(u32) u16x2 size; + alignas(u32) u16x2 texcoord; + alignas(u32) u32 color; + }; + + struct alignas(u32) AtlasGlyphEntryData + { + u16 shadingType; + u16 overlapSplit; + i16x2 offset; + u16x2 size; + u16x2 texcoord; + + constexpr ShadingType GetShadingType() const noexcept + { + return static_cast(shadingType); + } + }; + + // NOTE: Don't initialize any members in this struct. This ensures that no + // zero-initialization needs to occur when we allocate large buffers of this object. + struct AtlasGlyphEntry + { + u16 glyphIndex; + // All data in QuadInstance is u32-aligned anyways, so this simultaneously serves as padding. + u16 _occupied; + + AtlasGlyphEntryData data; + + constexpr bool operator==(u16 key) const noexcept + { + return glyphIndex == key; + } + + constexpr operator bool() const noexcept + { + return _occupied != 0; + } + + constexpr AtlasGlyphEntry& operator=(u16 key) noexcept + { + glyphIndex = key; + _occupied = 1; + return *this; + } + }; + + // This exists so that we can look up a AtlasFontFaceEntry without AddRef()/Release()ing fontFace first. + struct AtlasFontFaceKey + { + IDWriteFontFace2* fontFace; + LineRendition lineRendition; + }; + + struct AtlasFontFaceEntryInner + { + // BODGY: At the time of writing IDWriteFontFallback::MapCharacters returns the same IDWriteFontFace instance + // for the same font face variant as long as someone is holding a reference to the instance (see ActiveFaceCache). + // This allows us to hash the value of the pointer as if it was uniquely identifying the font face variant. + wil::com_ptr fontFace; + LineRendition lineRendition = LineRendition::SingleWidth; + + til::linear_flat_set glyphs; + // boxGlyphs gets an increased growth rate of 2^2 = 4x, because presumably fonts either contain very + // few or almost all of the box glyphs. This reduces the cost of _initializeFontFaceEntry quite a bit. + til::linear_flat_set boxGlyphs; + }; + + struct AtlasFontFaceEntry + { + // This being a heap allocated allows us to insert into `glyphs` in `_splitDoubleHeightGlyph` + // (which might resize the hashmap!), while the caller `_drawText` is holding onto `glyphs`. + // If it wasn't heap allocated, all pointers into `linear_flat_set` would be invalidated. + std::unique_ptr inner; + + bool operator==(const AtlasFontFaceKey& key) const noexcept + { + const auto& i = *inner; + return i.fontFace.get() == key.fontFace && i.lineRendition == key.lineRendition; + } + + operator bool() const noexcept + { + return static_cast(inner); + } + + AtlasFontFaceEntry& operator=(const AtlasFontFaceKey& key) + { + inner = std::make_unique(); + auto& i = *inner; + i.fontFace = key.fontFace; + i.lineRendition = key.lineRendition; + return *this; + } + }; + + private: + struct CursorRect + { + i16x2 position; + u16x2 size; + u32 background; + u32 foreground; + }; + + ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); + void _updateFontDependents(const RenderingPayload& p); + void _d2dRenderTargetUpdateFontSettings(const RenderingPayload& p) const noexcept; + void _recreateCustomShader(const RenderingPayload& p); + void _recreateCustomRenderTargetView(const RenderingPayload& p); + void _recreateBackgroundColorBitmap(const RenderingPayload& p); + void _recreateConstBuffer(const RenderingPayload& p) const; + void _setupDeviceContextState(const RenderingPayload& p); + void _debugUpdateShaders(const RenderingPayload& p) noexcept; + void _debugShowDirty(const RenderingPayload& p); + void _debugDumpRenderTarget(const RenderingPayload& p); + void _d2dBeginDrawing() noexcept; + void _d2dEndDrawing(); + ATLAS_ATTR_COLD void _resetGlyphAtlas(const RenderingPayload& p); + ATLAS_ATTR_COLD void _resizeGlyphAtlas(const RenderingPayload& p, u16 u, u16 v); + QuadInstance& _getLastQuad() noexcept; + QuadInstance& _appendQuad(); + ATLAS_ATTR_COLD void _bumpInstancesSize(); + void _flushQuads(const RenderingPayload& p); + ATLAS_ATTR_COLD void _recreateInstanceBuffers(const RenderingPayload& p); + void _drawBackground(const RenderingPayload& p); + void _uploadBackgroundBitmap(const RenderingPayload& p); + void _drawText(RenderingPayload& p); + ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y); + ATLAS_ATTR_COLD static void _initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry); + [[nodiscard]] ATLAS_ATTR_COLD bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); + bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); + void _drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const; + void _drawGlyphPrepareRetry(const RenderingPayload& p); + void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry); + void _drawGridlines(const RenderingPayload& p, u16 y); + void _drawCursorBackground(const RenderingPayload& p); + ATLAS_ATTR_COLD void _drawCursorForeground(); + ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset); + void _drawSelection(const RenderingPayload& p); + void _executeCustomShader(RenderingPayload& p); + + wil::com_ptr _renderTargetView; + wil::com_ptr _inputLayout; + wil::com_ptr _vertexShader; + wil::com_ptr _pixelShader; + wil::com_ptr _blendState; + wil::com_ptr _vsConstantBuffer; + wil::com_ptr _psConstantBuffer; + wil::com_ptr _vertexBuffer; + wil::com_ptr _indexBuffer; + wil::com_ptr _instanceBuffer; + size_t _instanceBufferCapacity = 0; + Buffer _instances; + size_t _instancesCount = 0; + + wil::com_ptr _customRenderTargetView; + wil::com_ptr _customOffscreenTexture; + wil::com_ptr _customOffscreenTextureView; + wil::com_ptr _customVertexShader; + wil::com_ptr _customPixelShader; + wil::com_ptr _customShaderConstantBuffer; + wil::com_ptr _customShaderSamplerState; + std::chrono::steady_clock::time_point _customShaderStartTime; + + wil::com_ptr _backgroundBitmap; + wil::com_ptr _backgroundBitmapView; + til::generation_t _backgroundBitmapGeneration; + + wil::com_ptr _glyphAtlas; + wil::com_ptr _glyphAtlasView; + til::linear_flat_set _glyphAtlasMap; + Buffer _rectPackerData; + stbrp_context _rectPacker{}; + til::CoordType _ligatureOverhangTriggerLeft = 0; + til::CoordType _ligatureOverhangTriggerRight = 0; + + wil::com_ptr _d2dRenderTarget; + wil::com_ptr _d2dRenderTarget4; // Optional. Supported since Windows 10 14393. + wil::com_ptr _emojiBrush; + wil::com_ptr _brush; + wil::com_ptr _softFontBitmap; + bool _d2dBeganDrawing = false; + bool _fontChangedResetGlyphAtlas = false; + + float _gamma = 0; + float _cleartypeEnhancedContrast = 0; + float _grayscaleEnhancedContrast = 0; + wil::com_ptr _textRenderingParams; + + til::generation_t _generation; + til::generation_t _fontGeneration; + til::generation_t _miscGeneration; + u16x2 _targetSize{}; + u16x2 _viewportCellCount{}; + ShadingType _textShadingType = ShadingType::Default; + + // An empty-box cursor spanning a wide glyph that has different + // background colors on each side results in 6 lines being drawn. + til::small_vector _cursorRects; + // The bounding rect of _cursorRects in pixels. + til::rect _cursorPosition; + + bool _requiresContinuousRedraw = false; + +#if ATLAS_DEBUG_SHOW_DIRTY + i32r _presentRects[9]{}; + size_t _presentRectsPos = 0; +#endif + +#if ATLAS_DEBUG_DUMP_RENDER_TARGET + wchar_t _dumpRenderTargetBasePath[MAX_PATH]{}; + size_t _dumpRenderTargetCounter = 0; +#endif + +#if ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS + size_t _colorizeGlyphAtlasCounter = 0; +#endif + +#ifndef NDEBUG + std::filesystem::path _sourceDirectory; + wil::unique_folder_change_reader_nothrow _sourceCodeWatcher; + std::atomic _sourceCodeInvalidationTime{ INT64_MAX }; +#endif + }; +} diff --git a/src/renderer/atlas/DWriteTextAnalysis.cpp b/src/renderer/atlas/DWriteTextAnalysis.cpp index 8b4ba47734..4cc295d366 100644 --- a/src/renderer/atlas/DWriteTextAnalysis.cpp +++ b/src/renderer/atlas/DWriteTextAnalysis.cpp @@ -1,14 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + #include "pch.h" #include "DWriteTextAnalysis.h" #pragma warning(disable : 4100) // '...': unreferenced formal parameter #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). -using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Render::Atlas; -TextAnalysisSource::TextAnalysisSource(const wchar_t* _text, const UINT32 _textLength) noexcept : - _text{ _text }, - _textLength{ _textLength } +TextAnalysisSource::TextAnalysisSource(const wchar_t* locale, const wchar_t* text, const UINT32 textLength) noexcept : + _locale{ locale }, + _text{ text }, + _textLength{ textLength } { } @@ -91,7 +95,7 @@ HRESULT TextAnalysisSource::GetLocaleName(UINT32 textPosition, UINT32* textLengt __assume(localeName != nullptr); *textLength = _textLength - textPosition; - *localeName = nullptr; + *localeName = _locale; return S_OK; } @@ -151,7 +155,8 @@ HRESULT TextAnalysisSink::QueryInterface(const IID& riid, void** ppvObject) noex HRESULT __stdcall TextAnalysisSink::SetScriptAnalysis(UINT32 textPosition, UINT32 textLength, const DWRITE_SCRIPT_ANALYSIS* scriptAnalysis) noexcept try { - _results.emplace_back(TextAnalysisSinkResult{ textPosition, textLength, scriptAnalysis->script, static_cast(scriptAnalysis->shapes), 0 }); + __assume(scriptAnalysis != nullptr); + _results.emplace_back(textPosition, textLength, *scriptAnalysis); return S_OK; } CATCH_RETURN() diff --git a/src/renderer/atlas/DWriteTextAnalysis.h b/src/renderer/atlas/DWriteTextAnalysis.h index 15490ca868..2f28b2f839 100644 --- a/src/renderer/atlas/DWriteTextAnalysis.h +++ b/src/renderer/atlas/DWriteTextAnalysis.h @@ -3,24 +3,13 @@ #pragma once -namespace Microsoft::Console::Render +#include "common.h" + +namespace Microsoft::Console::Render::Atlas { - struct TextAnalysisSinkResult - { - uint32_t textPosition = 0; - uint32_t textLength = 0; - - // These 2 fields represent DWRITE_SCRIPT_ANALYSIS. - // Not using DWRITE_SCRIPT_ANALYSIS drops the struct size from 20 down to 12 bytes. - uint16_t script = 0; - uint8_t shapes = 0; - - uint8_t bidiLevel = 0; - }; - struct TextAnalysisSource final : IDWriteTextAnalysisSource { - TextAnalysisSource(const wchar_t* _text, const UINT32 _textLength) noexcept; + TextAnalysisSource(const wchar_t* locale, const wchar_t* text, const UINT32 textLength) noexcept; #ifndef NDEBUG ~TextAnalysisSource(); #endif @@ -35,6 +24,7 @@ namespace Microsoft::Console::Render HRESULT __stdcall GetNumberSubstitution(UINT32 textPosition, UINT32* textLength, IDWriteNumberSubstitution** numberSubstitution) noexcept override; private: + const wchar_t* _locale; const wchar_t* _text; const UINT32 _textLength; #ifndef NDEBUG diff --git a/src/renderer/atlas/README.md b/src/renderer/atlas/README.md new file mode 100644 index 0000000000..e5e87f2ca0 --- /dev/null +++ b/src/renderer/atlas/README.md @@ -0,0 +1,120 @@ +# AtlasEngine + +## General architecture overview + +```mermaid +graph TD + RenderThread["RenderThread (base/thread.cpp)\ncalls Renderer::PaintFrame() x times per sec"] + Renderer["Renderer (base/renderer.cpp)\nbreaks the text buffer down into GDI-oriented graphics\nprimitives (#quot;change brush to color X#quot;, #quot;draw string Y#quot;, ...)"] + RenderEngineBase[/"RenderEngineBase\n(base/RenderEngineBase.cpp)\nabstracts 24 LOC 👻"\] + GdiEngine["GdiEngine (gdi/...)"] + DxEngine["DxEngine (dx/...)"] + + subgraph AtlasEngine["AtlasEngine (atlas/...)"] + AtlasEngine.cpp["AtlasEngine.cpp\nImplements IRenderEngine text rendering API\nbreaks GDI graphics primitives down into DWRITE_GLYPH_RUNs"] + AtlasEngine.api.cpp["AtlasEngine.api.cpp\nImplements the parts run inside the console\nlock (many IRenderEngine setters)"] + AtlasEngine.r.cpp["AtlasEngine.r.cpp\nImplements the parts run\noutside of the console lock"] + Backend.cpp["Backend.cpp\nImplements common functionality/helpers"] + BackendD2D.cpp["BackendD2D.cpp\nPure Direct2D text renderer (for low latency\nremote desktop and older/no GPUs)"] + BackendD3D.cpp["BackendD3D.cpp\nCustom, performant text renderer\nwith our own glyph cache"] + end + + RenderThread --> Renderer + Renderer -->|owns| RenderThread + Renderer -.-> RenderEngineBase + %% Mermaid.js has no support for backwards arrow at the moment + RenderEngineBase <-.->|extends| GdiEngine + RenderEngineBase <-.->|extends| DxEngine + Renderer ----> AtlasEngine + AtlasEngine.cpp <--> AtlasEngine.api.cpp + AtlasEngine.cpp <--> AtlasEngine.r.cpp + AtlasEngine.r.cpp --> BackendD2D.cpp + AtlasEngine.r.cpp --> BackendD3D.cpp + BackendD2D.cpp -.- Backend.cpp + BackendD3D.cpp -.- Backend.cpp +``` + +As you can see, breaking the text buffer down into GDI-style primitives just to rebuild them into DirectWrite ones, is pretty wasteful. It's also incredibly bug prone. It would be beneficial if the TextBuffer and rendering settings were given directly to AtlasEngine so it can do its own bidding. + +## BackendD3D + +The primary entrypoint for rendering is `IBackend::Render` and `BackendD3D` implements it via the following functions, by calling them one by one in the order listed here. + +### `_handleSettingsUpdate` + +```mermaid +graph TD + Render --> _handleSettingsUpdate + _handleSettingsUpdate -->|font changes| _updateFontDependents --> _d2dRenderTargetUpdateFontSettings + _handleSettingsUpdate -->|misc changes| _recreateCustomShader + _handleSettingsUpdate --->|misc changes| _recreateCustomRenderTargetView + _handleSettingsUpdate ---->|size changes| _recreateBackgroundColorBitmap + _handleSettingsUpdate -----> _recreateConstBuffer + _handleSettingsUpdate ------> _setupDeviceContextState +``` + +### `_drawBackground` + +```mermaid +graph TD + Render --> _drawBackground + _drawBackground --> _uploadBackgroundBitmap +``` + +### `_drawCursorPart1` / `_drawCursorPart2` + +```mermaid +graph TD + Render --> _drawCursorPart1["_drawCursorPart1\nruns before _drawText\ndraws cursors that are behind the text"] + Render --> _drawCursorPart2["_drawCursorPart2\nruns after _drawText\ndraws inverted cursors"] + _drawCursorPart1 -.->|_cursorRects| _drawCursorPart2 +``` + +### `_drawText` + +```mermaid +graph TD + Render --> _drawText + + _drawText --> foreachRow(("for each row")) + foreachRow --> foreachRow + foreachRow --> foreachFont(("for each font face")) + foreachFont --> foreachFont + foreachFont --> foreachGlyph(("for each glyph")) + foreachGlyph --> foreachGlyph + + foreachGlyph --> _glyphAtlasMap[("font/glyph-pair lookup in\nglyph cache hashmap")] + _glyphAtlasMap --> drawGlyph + drawGlyph --> _appendQuad["_appendQuad\nstages the glyph for later drawing"] + _glyphAtlasMap --> _appendQuad + + subgraph drawGlyph["if glyph is missing"] + _drawGlyph["_drawGlyph\n(defers to _drawSoftFontGlyph for soft fonts)"] + + _drawGlyph -.->|if glpyh cache is full| _drawGlyphPrepareRetry + _drawGlyphPrepareRetry --> _flushQuads["_flushQuads\ndraws the current state\ninto the render target"] + _flushQuads --> _recreateInstanceBuffers["_recreateInstanceBuffers\nallocates a GPU buffer\nfor our glyph instances"] + _drawGlyphPrepareRetry --> _resetGlyphAtlas["_resetGlyphAtlas\nclears the glyph texture"] + _resetGlyphAtlas --> _resizeGlyphAtlas["_resizeGlyphAtlas\nresizes the glyph texture if it's still small"] + + _drawGlyph -.->|if it's a DECDHL glyph| _splitDoubleHeightGlyph["_splitDoubleHeightGlyph\nDECDHL glyphs are split up into their\ntop/bottom halves to emulate clip rects"] + end + + foreachGlyph -.-> _drawTextOverlapSplit["_drawTextOverlapSplit\nsplits overly wide glyphs up into smaller chunks to support\nforeground color changes within the ligature"] + + foreachRow -.->|if gridlines exist| _drawGridlineRow["_drawGridlineRow\ndraws underlines, etc."] +``` + +### `_drawSelection` + +```mermaid +graph TD + Render --> _drawSelection +``` + +### `_handleSettingsUpdate` + +```mermaid +graph TD + Render --> _executeCustomShader +``` diff --git a/src/renderer/atlas/atlas.vcxproj b/src/renderer/atlas/atlas.vcxproj index e9907d1549..689ec6ff8d 100644 --- a/src/renderer/atlas/atlas.vcxproj +++ b/src/renderer/atlas/atlas.vcxproj @@ -13,25 +13,35 @@ + + + Create + + + + + + + Pixel 4.0 true - custom_shader_ps + %(Filename) $(OutDir)$(ProjectName)\%(Filename).h true @@ -42,7 +52,7 @@ Vertex 4.0 true - custom_shader_vs + %(Filename) $(OutDir)$(ProjectName)\%(Filename).h true @@ -52,11 +62,14 @@ true + + true + Pixel 4.0 true - shader_ps + %(Filename) $(OutDir)$(ProjectName)\%(Filename).h true @@ -67,7 +80,7 @@ Vertex 4.0 true - shader_vs + %(Filename) $(OutDir)$(ProjectName)\%(Filename).h true @@ -75,12 +88,15 @@ /O3 /Qstrip_debug /Qstrip_reflect %(AdditionalOptions) + + + pch.h - $(OutDir)$(ProjectName)\;%(AdditionalIncludeDirectories) + $(SolutionDir)\oss\stb;$(OutDir)$(ProjectName);%(AdditionalIncludeDirectories) diff --git a/src/renderer/atlas/colorbrewer.h b/src/renderer/atlas/colorbrewer.h new file mode 100644 index 0000000000..be5cef6b8a --- /dev/null +++ b/src/renderer/atlas/colorbrewer.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace Microsoft::Console::Render::Atlas::colorbrewer +{ + // The following list of colors is only used as a debug aid and not part of the final product. + // They're licensed under: + // + // Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes + // + // Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. + // + // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // http://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software distributed + // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + // CONDITIONS OF ANY KIND, either express or implied. See the License for the + // specific language governing permissions and limitations under the License. + // + inline constexpr u32 pastel1[]{ + 0xfbb4ae, + 0xb3cde3, + 0xccebc5, + 0xdecbe4, + 0xfed9a6, + 0xffffcc, + 0xe5d8bd, + 0xfddaec, + 0xf2f2f2, + }; +} diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h new file mode 100644 index 0000000000..bb46ff6154 --- /dev/null +++ b/src/renderer/atlas/common.h @@ -0,0 +1,564 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "../../renderer/inc/IRenderEngine.hpp" + +namespace Microsoft::Console::Render::Atlas +{ +#define ATLAS_FLAG_OPS(type, underlying) \ + constexpr type operator~(type v) noexcept \ + { \ + return static_cast(~static_cast(v)); \ + } \ + constexpr type operator|(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) | static_cast(rhs)); \ + } \ + constexpr type operator&(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) & static_cast(rhs)); \ + } \ + constexpr type operator^(type lhs, type rhs) noexcept \ + { \ + return static_cast(static_cast(lhs) ^ static_cast(rhs)); \ + } \ + constexpr void operator|=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs | rhs; \ + } \ + constexpr void operator&=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs & rhs; \ + } \ + constexpr void operator^=(type& lhs, type rhs) noexcept \ + { \ + lhs = lhs ^ rhs; \ + } + +#define ATLAS_POD_OPS(type) \ + constexpr bool operator==(const type& rhs) const noexcept \ + { \ + return __builtin_memcmp(this, &rhs, sizeof(rhs)) == 0; \ + } \ + \ + constexpr bool operator!=(const type& rhs) const noexcept \ + { \ + return !(*this == rhs); \ + } + + // My best effort of replicating __attribute__((cold)) from gcc/clang. +#define ATLAS_ATTR_COLD __declspec(noinline) + + template + struct vec2 + { + // These members aren't zero-initialized to make these trivial types, + // and allow the compiler to quickly memset() allocations, etc. + T x; + T y; + + ATLAS_POD_OPS(vec2) + }; + + template + struct vec4 + { + // These members aren't zero-initialized to make these trivial types, + // and allow the compiler to quickly memset() allocations, etc. + T x; + T y; + T z; + T w; + + ATLAS_POD_OPS(vec4) + }; + + template + struct rect + { + // These members aren't zero-initialized to make these trivial types, + // and allow the compiler to quickly memset() allocations, etc. + T left; + T top; + T right; + T bottom; + + ATLAS_POD_OPS(rect) + + constexpr bool empty() const noexcept + { + return left >= right || top >= bottom; + } + + constexpr bool non_empty() const noexcept + { + return left < right && top < bottom; + } + }; + + template + struct range + { + T start; + T end; + + ATLAS_POD_OPS(range) + + constexpr bool empty() const noexcept + { + return start >= end; + } + + constexpr bool non_empty() const noexcept + { + return start < end; + } + + constexpr bool contains(T v) const noexcept + { + return v >= start && v < end; + } + }; + + using u8 = uint8_t; + + using u16 = uint16_t; + using u16x2 = vec2; + using u16r = rect; + + using i16 = int16_t; + using i16x2 = vec2; + using i16x4 = vec4; + using i16r = rect; + + using u32 = uint32_t; + using u32x2 = vec2; + using u32x4 = vec4; + using u32r = rect; + + using i32 = int32_t; + using i32x2 = vec2; + using i32x4 = vec4; + using i32r = rect; + + using f32 = float; + using f32x2 = vec2; + using f32x4 = vec4; + using f32r = rect; + + // I wrote `Buffer` instead of using `std::vector`, because I want to convey that these things + // explicitly _don't_ hold resizeable contents, but rather plain content of a fixed size. + // For instance I didn't want a resizeable vector with a `push_back` method for my fixed-size + // viewport arrays - that doesn't make sense after all. `Buffer` also doesn't initialize + // contents to zero, allowing rapid creation/destruction and you can easily specify a custom + // (over-)alignment which can improve rendering perf by up to ~20% over `std::vector`. + template + struct Buffer + { + constexpr Buffer() noexcept = default; + + explicit Buffer(size_t size) : + _data{ allocate(size) }, + _size{ size } + { + std::uninitialized_default_construct_n(_data, size); + } + + Buffer(const T* data, size_t size) : + _data{ allocate(size) }, + _size{ size } + { + // Changing the constructor arguments to accept std::span might + // be a good future extension, but not to improve security here. + // You can trivially construct std::span's from invalid ranges. + // Until then the raw-pointer style is more practical. +#pragma warning(suppress : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...]. + std::uninitialized_copy_n(data, size, _data); + } + + ~Buffer() + { + destroy(); + } + + Buffer(const Buffer& other) = delete; + Buffer& operator=(const Buffer& other) = delete; + + Buffer(Buffer&& other) noexcept : + _data{ std::exchange(other._data, nullptr) }, + _size{ std::exchange(other._size, 0) } + { + } + + Buffer& operator=(Buffer&& other) noexcept + { + destroy(); + _data = std::exchange(other._data, nullptr); + _size = std::exchange(other._size, 0); + return *this; + } + + explicit operator bool() const noexcept + { + return _data != nullptr; + } + + T& operator[](size_t index) noexcept + { + assert(index < _size); +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + return _data[index]; + } + + const T& operator[](size_t index) const noexcept + { + assert(index < _size); +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + return _data[index]; + } + + T* data() noexcept + { + return _data; + } + + const T* data() const noexcept + { + return _data; + } + + size_t size() const noexcept + { + return _size; + } + + T* begin() noexcept + { + return _data; + } + + const T* begin() const noexcept + { + return _data; + } + + T* end() noexcept + { +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + return _data + _size; + } + + const T* end() const noexcept + { +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + return _data + _size; + } + + private: + // These two functions don't need to use scoped objects or standard allocators, + // since this class is in fact an scoped allocator object itself. +#pragma warning(push) +#pragma warning(disable : 26402) // Return a scoped object instead of a heap-allocated if it has a move constructor (r.3). +#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique instead (r.11). + static T* allocate(size_t size) + { + if (!size) + { + return nullptr; + } + if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) + { + return static_cast(::operator new(size * sizeof(T))); + } + else + { + return static_cast(::operator new(size * sizeof(T), static_cast(Alignment))); + } + } + + static void deallocate(T* data) noexcept + { + if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) + { + ::operator delete(data); + } + else + { + ::operator delete(data, static_cast(Alignment)); + } + } +#pragma warning(pop) + + void destroy() noexcept + { + std::destroy_n(_data, _size); + deallocate(_data); + } + + T* _data = nullptr; + size_t _size = 0; + }; + + struct TextAnalysisSinkResult + { + uint32_t textPosition; + uint32_t textLength; + DWRITE_SCRIPT_ANALYSIS analysis; + }; + + struct TargetSettings + { + HWND hwnd = nullptr; + bool enableTransparentBackground = false; + bool useSoftwareRendering = false; + }; + + enum class AntialiasingMode : u8 + { + ClearType = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE, + Grayscale = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE, + Aliased = D2D1_TEXT_ANTIALIAS_MODE_ALIASED, + }; + + inline constexpr auto DefaultAntialiasingMode = AntialiasingMode::ClearType; + + struct FontDecorationPosition + { + u16 position = 0; + u16 height = 0; + }; + + struct FontSettings + { + wil::com_ptr fontCollection; + wil::com_ptr fontFamily; + std::wstring fontName; + std::vector fontFeatures; + std::vector fontAxisValues; + f32 fontSize = 0; + u16x2 cellSize; + u16 fontWeight = 0; + u16 advanceWidth = 0; + u16 baseline = 0; + u16 descender = 0; + u16 thinLineWidth = 0; + + FontDecorationPosition gridTop; + FontDecorationPosition gridBottom; + FontDecorationPosition gridLeft; + FontDecorationPosition gridRight; + + FontDecorationPosition underline; + FontDecorationPosition strikethrough; + FontDecorationPosition doubleUnderline[2]; + FontDecorationPosition overline; + + u16 dpi = 96; + AntialiasingMode antialiasingMode = DefaultAntialiasingMode; + + std::vector softFontPattern; + til::size softFontCellSize; + }; + + struct CursorSettings + { + ATLAS_POD_OPS(CursorSettings) + + u32 cursorColor = 0xffffffff; + u16 cursorType = 0; + u16 heightPercentage = 20; + }; + + struct MiscellaneousSettings + { + u32 backgroundColor = 0; + u32 selectionColor = 0x7fffffff; + std::wstring customPixelShaderPath; + bool useRetroTerminalEffect = false; + }; + + struct Settings + { + til::generational target; + til::generational font; + til::generational cursor; + til::generational misc; + // Size of the viewport / swap chain in pixel. + u16x2 targetSize{ 1, 1 }; + // Size of the portion of the text buffer that we're drawing on the screen. + u16x2 viewportCellCount{ 1, 1 }; + // The position of the viewport inside the text buffer (in cells). + u16x2 viewportOffset{ 0, 0 }; + }; + + using GenerationalSettings = til::generational; + + inline GenerationalSettings DirtyGenerationalSettings() noexcept + { + return GenerationalSettings{ + til::generation_t{ 1 }, + til::generational{ til::generation_t{ 1 } }, + til::generational{ til::generation_t{ 1 } }, + til::generational{ til::generation_t{ 1 } }, + til::generational{ til::generation_t{ 1 } }, + }; + } + + enum class FontRelevantAttributes : u8 + { + None = 0, + Bold = 0b01, + Italic = 0b10, + }; + ATLAS_FLAG_OPS(FontRelevantAttributes, u8) + + struct FontMapping + { + wil::com_ptr fontFace; + u32 glyphsFrom = 0; + u32 glyphsTo = 0; + }; + + struct GridLineRange + { + GridLineSet lines; + u32 color = 0; + u16 from = 0; + u16 to = 0; + }; + + struct ShapedRow + { + void Clear(u16 y, u16 cellHeight) noexcept + { + mappings.clear(); + glyphIndices.clear(); + glyphAdvances.clear(); + glyphOffsets.clear(); + colors.clear(); + gridLineRanges.clear(); + lineRendition = LineRendition::SingleWidth; + selectionFrom = 0; + selectionTo = 0; + dirtyTop = y * cellHeight; + dirtyBottom = dirtyTop + cellHeight; + } + + std::vector mappings; + std::vector glyphIndices; + std::vector glyphAdvances; // same size as glyphIndices + std::vector glyphOffsets; // same size as glyphIndices + std::vector colors; // same size as glyphIndices + std::vector gridLineRanges; + LineRendition lineRendition = LineRendition::SingleWidth; + u16 selectionFrom = 0; + u16 selectionTo = 0; + til::CoordType dirtyTop = 0; + til::CoordType dirtyBottom = 0; + }; + + struct RenderingPayload + { + //// Parameters which are constant across backends. + wil::com_ptr d2dFactory; + wil::com_ptr dwriteFactory; + wil::com_ptr dwriteFactory4; // optional, might be nullptr + wil::com_ptr systemFontFallback; + wil::com_ptr systemFontFallback1; // optional, might be nullptr + wil::com_ptr textAnalyzer; + wil::com_ptr renderingParams; + std::function warningCallback; + std::function swapChainChangedCallback; + + //// Parameters which are constant for the existence of the backend. + struct + { + wil::com_ptr factory; + wil::com_ptr adapter; + LUID adapterLuid{}; + UINT adapterFlags = 0; + } dxgi; + struct + { + wil::com_ptr swapChain; + wil::unique_handle handle; + wil::unique_handle frameLatencyWaitableObject; + til::generation_t generation; + til::generation_t targetGeneration; + til::generation_t fontGeneration; + u16x2 targetSize{}; + bool waitForPresentation = false; + } swapChain; + wil::com_ptr device; + wil::com_ptr deviceContext; + + //// Parameters which change seldom. + GenerationalSettings s; + + //// Parameters which change every frame. + // This is the backing buffer for `rows`. + Buffer unorderedRows; + // This is used as a scratch buffer during scrolling. + Buffer rowsScratch; + // This contains the rows in the right order from row 0 to N. + // They get rotated around when we scroll the buffer. Technically + // we could also implement scrolling by using a circular array. + Buffer rows; + // This contains two viewport-sized bitmaps back to back, sort of like a Texture2DArray. + // The first NxM (for instance 120x30 pixel) chunk contains background colors and the + // second chunk contains foreground colors. The distance in u32 items between the start + // and the begin of the foreground bitmap is equal to colorBitmapDepthStride. + // + // The background part is in premultiplied alpha, whereas the foreground part is in straight + // alpha. This is mostly because of Direct2D being annoying, as the former is the only thing + // it supports for bitmaps, whereas the latter is the only thing it supports for text. + // Since we implement Direct2D's text blending algorithm, we're equally dependent on + // straight alpha for BackendD3D, as straight alpha is used in the pixel shader there. + Buffer colorBitmap; + // This exists as a convenience access to colorBitmap and + // contains a view into the background color bitmap. + std::span backgroundBitmap; + // This exists as a convenience access to colorBitmap and + // contains a view into the foreground color bitmap. + std::span foregroundBitmap; + // This stride of the colorBitmap is a "count" of u32 and not in bytes. + size_t colorBitmapRowStride = 0; + // FYI depth refers to the `colorBitmapRowStride * height` size of each bitmap contained + // in colorBitmap. colorBitmap contains 2 bitmaps (background and foreground colors). + size_t colorBitmapDepthStride = 0; + // A generation of 1 ensures that the backends redraw the background on the first Present(). + // The 1st entry in this array corresponds to the background and the 2nd to the foreground bitmap. + std::array colorBitmapGenerations{ 1, 1 }; + // In columns/rows. + til::rect cursorRect; + // The viewport/SwapChain area to be presented. In pixel. + // NOTE: + // This cannot use til::rect, because til::rect generally expects positive coordinates only + // (`operator!()` checks for negative values), whereas this one can go out of bounds, + // whenever glyphs go out of bounds. `AtlasEngine::_present()` will clamp it. + i32r dirtyRectInPx{}; + // In rows. + range invalidatedRows{}; + // In pixel. + i16 scrollOffset = 0; + + void MarkAllAsDirty() noexcept + { + dirtyRectInPx = { 0, 0, s->targetSize.x, s->targetSize.y }; + invalidatedRows = { 0, s->viewportCellCount.y }; + scrollOffset = 0; + } + }; + + struct IBackend + { + virtual ~IBackend() = default; + virtual void ReleaseResources() noexcept = 0; + virtual void Render(RenderingPayload& payload) = 0; + virtual bool RequiresContinuousRedraw() noexcept = 0; + }; +} diff --git a/src/renderer/atlas/custom_shader_ps.hlsl b/src/renderer/atlas/custom_shader_ps.hlsl index 0073f2ca87..221be267b4 100644 --- a/src/renderer/atlas/custom_shader_ps.hlsl +++ b/src/renderer/atlas/custom_shader_ps.hlsl @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + // The original retro pixel shader Texture2D shaderTexture; SamplerState samplerState; diff --git a/src/renderer/atlas/custom_shader_vs.hlsl b/src/renderer/atlas/custom_shader_vs.hlsl index 5bb9fbff70..97b51c4c23 100644 --- a/src/renderer/atlas/custom_shader_vs.hlsl +++ b/src/renderer/atlas/custom_shader_vs.hlsl @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + struct VS_OUTPUT { float4 pos : SV_POSITION; diff --git a/src/renderer/atlas/pch.h b/src/renderer/atlas/pch.h index a436ae580a..a611147b66 100644 --- a/src/renderer/atlas/pch.h +++ b/src/renderer/atlas/pch.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -16,18 +18,17 @@ #include #include -#include -#include +#include +#include #include -#include #include +#include #include #include #include #include #include -#include #include #include #include diff --git a/src/renderer/atlas/shader_common.hlsl b/src/renderer/atlas/shader_common.hlsl new file mode 100644 index 0000000000..957b17ac6a --- /dev/null +++ b/src/renderer/atlas/shader_common.hlsl @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// clang-format off +#define SHADING_TYPE_TEXT_BACKGROUND 0 +#define SHADING_TYPE_TEXT_GRAYSCALE 1 +#define SHADING_TYPE_TEXT_CLEARTYPE 2 +#define SHADING_TYPE_TEXT_PASSTHROUGH 3 +#define SHADING_TYPE_DOTTED_LINE 4 +#define SHADING_TYPE_DOTTED_LINE_WIDE 5 +// clang-format on + +struct VSData +{ + float2 vertex : SV_Position; + uint shadingType : shadingType; + int2 position : position; + uint2 size : size; + uint2 texcoord : texcoord; + float4 color : color; +}; + +struct PSData +{ + float4 position : SV_Position; + float2 texcoord : texcoord; + nointerpolation uint shadingType : shadingType; + nointerpolation float4 color : color; +}; + +float4 premultiplyColor(float4 color) +{ + color.rgb *= color.a; + return color; +} + +float4 alphaBlendPremultiplied(float4 bottom, float4 top) +{ + bottom *= 1 - top.a; + return bottom + top; +} + +float4 decodeRGBA(uint i) +{ + return (i >> uint4(0, 8, 16, 24) & 0xff) / 255.0f; +} diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl index 867bcccf2f..2cb92947ce 100644 --- a/src/renderer/atlas/shader_ps.hlsl +++ b/src/renderer/atlas/shader_ps.hlsl @@ -2,200 +2,99 @@ // Licensed under the MIT license. #include "dwrite.hlsl" - -#define INVALID_COLOR 0xffffffff - -// These flags are shared with AtlasEngine::CellFlags. -// -// clang-format off -#define CellFlags_None 0x00000000 -#define CellFlags_Inlined 0x00000001 - -#define CellFlags_ColoredGlyph 0x00000002 - -#define CellFlags_Cursor 0x00000008 -#define CellFlags_Selected 0x00000010 - -#define CellFlags_BorderLeft 0x00000020 -#define CellFlags_BorderTop 0x00000040 -#define CellFlags_BorderRight 0x00000080 -#define CellFlags_BorderBottom 0x00000100 -#define CellFlags_Underline 0x00000200 -#define CellFlags_UnderlineDotted 0x00000400 -#define CellFlags_UnderlineDouble 0x00000800 -#define CellFlags_Strikethrough 0x00001000 -// clang-format on - -// According to Nvidia's "Understanding Structured Buffer Performance" guide -// one should aim for structures with sizes divisible by 128 bits (16 bytes). -// This prevents elements from spanning cache lines. -struct Cell -{ - uint glyphPos; - uint flags; - uint2 color; // x: foreground, y: background -}; +#include "shader_common.hlsl" cbuffer ConstBuffer : register(b0) { - float4 viewport; + float4 backgroundColor; + float2 backgroundCellSize; + float2 backgroundCellCount; float4 gammaRatios; float enhancedContrast; - uint cellCountX; - uint2 cellSize; - uint underlinePos; - uint underlineWidth; - uint strikethroughPos; - uint strikethroughWidth; - uint2 doubleUnderlinePos; - uint thinLineWidth; - uint backgroundColor; - uint cursorColor; - uint selectionColor; - uint useClearType; + float underlineWidth; +} + +Texture2D background : register(t0); +Texture2D glyphAtlas : register(t1); + +struct Output +{ + float4 color; + float4 weights; }; -StructuredBuffer cells : register(t0); -Texture2D glyphs : register(t1); - -float4 decodeRGBA(uint i) -{ - float4 c = (i >> uint4(0, 8, 16, 24) & 0xff) / 255.0f; - // Convert to premultiplied alpha for simpler alpha blending. - c.rgb *= c.a; - return c; -} - -uint2 decodeU16x2(uint i) -{ - return uint2(i & 0xffff, i >> 16); -} - -float4 alphaBlendPremultiplied(float4 bottom, float4 top) -{ - bottom *= 1 - top.a; - return bottom + top; -} // clang-format off -float4 main(float4 pos: SV_Position): SV_Target +Output main(PSData data) : SV_Target // clang-format on { - // We need to fill the entire render target with pixels, but only our "viewport" - // has cells we want to draw. The rest gets treated with the background color. - [branch] if (any(pos.xy < viewport.xy || pos.xy >= viewport.zw)) + float4 color; + float4 weights; + + switch (data.shadingType) { - return decodeRGBA(backgroundColor); + case SHADING_TYPE_TEXT_BACKGROUND: + { + const float2 cell = data.position.xy / backgroundCellSize; + color = all(cell < backgroundCellCount) ? background[cell] : backgroundColor; + weights = float4(1, 1, 1, 1); + break; + } + case SHADING_TYPE_TEXT_GRAYSCALE: + { + // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things. + const float4 foreground = premultiplyColor(data.color); + const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); + const float intensity = DWrite_CalcColorIntensity(data.color.rgb); + // These aren't. + const float4 glyph = glyphAtlas[data.texcoord]; + const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast); + const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios); + color = alphaCorrected * foreground; + weights = color.aaaa; + break; + } + case SHADING_TYPE_TEXT_CLEARTYPE: + { + // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things. + const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); + // These aren't. + const float4 glyph = glyphAtlas[data.texcoord]; + const float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast); + const float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios); + weights = float4(alphaCorrected * data.color.a, 1); + color = weights * data.color; + break; + } + case SHADING_TYPE_TEXT_PASSTHROUGH: + { + color = glyphAtlas[data.texcoord]; + weights = color.aaaa; + break; + } + case SHADING_TYPE_DOTTED_LINE: + { + const bool on = frac(data.position.x / (2.0f * underlineWidth)) < 0.5f; + color = on * premultiplyColor(data.color); + weights = color.aaaa; + break; + } + case SHADING_TYPE_DOTTED_LINE_WIDE: + { + const bool on = frac(data.position.x / (4.0f * underlineWidth)) < 0.5f; + color = on * premultiplyColor(data.color); + weights = color.aaaa; + break; + } + default: + { + color = premultiplyColor(data.color); + weights = color.aaaa; + break; + } } - uint2 viewportPos = pos.xy - viewport.xy; - uint2 cellIndex = viewportPos / cellSize; - uint2 cellPos = viewportPos % cellSize; - Cell cell = cells[cellIndex.y * cellCountX + cellIndex.x]; - - // Layer 0: - // The cell's background color - float4 color = decodeRGBA(cell.color.y); - float4 fg = decodeRGBA(cell.color.x); - - // Layer 1 (optional): - // Colored cursors are drawn "in between" the background color and the text of a cell. - [branch] if (cell.flags & CellFlags_Cursor) - { - [branch] if (cursorColor != INVALID_COLOR) - { - // The cursor texture is stored at the top-left-most glyph cell. - // Cursor pixels are either entirely transparent or opaque. - // --> We can just use .a as a mask to flip cursor pixels on or off. - color = alphaBlendPremultiplied(color, decodeRGBA(cursorColor) * glyphs[cellPos].a); - } - else if (glyphs[cellPos].a != 0) - { - // Make sure the cursor is always readable (see gh-3647) - // If we imagine the two colors to be in 0-255 instead of 0-1, - // this effectively XORs them with 63. This avoids a situation - // where a gray background color (0.5) gets inverted to the - // same gray making the cursor invisible. - float2x4 colors = { color, fg }; - float2x4 ip; // integral part - float2x4 frac = modf(colors * (255.0f / 64.0f), ip); - colors = (3.0f - ip + frac) * (64.0f / 255.0f); - color = float4(colors[0].rgb, 1); - fg = float4(colors[1].rgb, 1); - } - } - - // Layer 2: - // Step 1: The cell's glyph, potentially drawn in the foreground color - { - float4 glyph = glyphs[decodeU16x2(cell.glyphPos) + cellPos]; - - [branch] if (cell.flags & CellFlags_ColoredGlyph) - { - color = alphaBlendPremultiplied(color, glyph); - } - else - { - float3 foregroundStraight = DWrite_UnpremultiplyColor(fg); - float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, foregroundStraight); - - [branch] if (useClearType) - { - // See DWrite_ClearTypeBlend - float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast); - float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, foregroundStraight, gammaRatios); - color = float4(lerp(color.rgb, foregroundStraight, alphaCorrected * fg.a), 1.0f); - } - else - { - // See DWrite_GrayscaleBlend - float intensity = DWrite_CalcColorIntensity(foregroundStraight); - float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast); - float4 alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios); - color = alphaBlendPremultiplied(color, alphaCorrected * fg); - } - } - } - // Step 2: Lines - { - // What a nice coincidence that we have exactly 8 flags to handle right now! - // `mask` will mask away any positive results from checks we don't want. - // (I.e. even if we're in an underline, it doesn't matter if we don't want an underline.) - bool2x4 mask = { - cell.flags & CellFlags_BorderLeft, - cell.flags & CellFlags_BorderTop, - cell.flags & CellFlags_BorderRight, - cell.flags & CellFlags_BorderBottom, - cell.flags & CellFlags_Underline, - cell.flags & CellFlags_UnderlineDotted, - cell.flags & CellFlags_UnderlineDouble, - cell.flags & CellFlags_Strikethrough, - }; - // The following = lo && y < hi`. - bool2x4 checks = { - // These 2 expand to 4 bools, because cellPos is a - // uint2 vector which results in a bool2 result each. - cellPos < thinLineWidth, - (cellSize - cellPos) <= thinLineWidth, - // These 4 are 4 regular bools. - (cellPos.y - underlinePos) < underlineWidth, - (cellPos.y - underlinePos) < underlineWidth && (viewportPos.x / underlineWidth & 3) == 0, - any((cellPos.y - doubleUnderlinePos) < thinLineWidth), - (cellPos.y - strikethroughPos) < strikethroughWidth, - }; - [flatten] if (any(mask && checks)) - { - color = alphaBlendPremultiplied(color, fg); - } - } - - // Layer 4: - // The current selection is drawn semi-transparent on top. - [branch] if (cell.flags & CellFlags_Selected) - { - color = alphaBlendPremultiplied(color, decodeRGBA(selectionColor)); - } - - return color; + Output output; + output.color = color; + output.weights = weights; + return output; } diff --git a/src/renderer/atlas/shader_vs.hlsl b/src/renderer/atlas/shader_vs.hlsl index e4c75b3e98..49b9030b15 100644 --- a/src/renderer/atlas/shader_vs.hlsl +++ b/src/renderer/atlas/shader_vs.hlsl @@ -1,17 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#include "shader_common.hlsl" + +cbuffer ConstBuffer : register(b0) +{ + float2 positionScale; +} + // clang-format off -float4 main(uint id: SV_VERTEXID): SV_POSITION +PSData main(VSData data) // clang-format on { - // The algorithm below is a fast way to generate a full screen triangle, - // published by Bill Bilodeau "Vertex Shader Tricks" at GDC14. - // It covers the entire viewport and is faster for the GPU than a quad/rectangle. - return float4( - float(id / 2) * 4.0 - 1.0, - float(id % 2) * 4.0 - 1.0, - 0.0, - 1.0 - ); + PSData output; + output.color = data.color; + output.shadingType = data.shadingType; + // positionScale is expected to be float2(2.0f / sizeInPixel.x, -2.0f / sizeInPixel.y). Together with the + // addition below this will transform our "position" from pixel into normalized device coordinate (NDC) space. + output.position.xy = (data.position + data.vertex.xy * data.size) * positionScale + float2(-1.0f, 1.0f); + output.position.zw = float2(0, 1); + output.texcoord = data.texcoord + data.vertex.xy * data.size; + return output; } diff --git a/src/renderer/atlas/stb_rect_pack.cpp b/src/renderer/atlas/stb_rect_pack.cpp new file mode 100644 index 0000000000..e90d9b988f --- /dev/null +++ b/src/renderer/atlas/stb_rect_pack.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +// AtlasEngine doesn't use stb_rect_pack efficiently right now and packs rectangles one by one, +// because this simplifies the text rendering implementation quite a bit. On the flip side however, +// this allows us to skip sorting rectangles, because sorting arrays of size 1 is pointless. +#pragma warning(disable : 4505) // '...': unreferenced function with internal linkage has been removed +#define STBRP_SORT(_Base, _NumOfElements, _SizeOfElements, _CompareFunction) \ + assert(_NumOfElements == 1) + +#define STB_RECT_PACK_IMPLEMENTATION +#include "stb_rect_pack.h" diff --git a/src/renderer/atlas/wic.h b/src/renderer/atlas/wic.h new file mode 100644 index 0000000000..f9c2c81035 --- /dev/null +++ b/src/renderer/atlas/wic.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +inline void SaveTextureToPNG(ID3D11DeviceContext* deviceContext, ID3D11Resource* source, double dpi, const wchar_t* fileName) +{ + __assume(deviceContext != nullptr); + __assume(source != nullptr); + + wil::com_ptr texture; + THROW_IF_FAILED(source->QueryInterface(IID_PPV_ARGS(texture.addressof()))); + + wil::com_ptr d3dDevice; + deviceContext->GetDevice(d3dDevice.addressof()); + + D3D11_TEXTURE2D_DESC desc{}; + texture->GetDesc(&desc); + desc.BindFlags = 0; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.Usage = D3D11_USAGE_STAGING; + + wil::com_ptr staging; + THROW_IF_FAILED(d3dDevice->CreateTexture2D(&desc, nullptr, staging.put())); + + deviceContext->CopyResource(staging.get(), source); + + static const auto coUninitialize = wil::CoInitializeEx(); + static const auto wicFactory = wil::CoCreateInstance(CLSID_WICImagingFactory2); + + wil::com_ptr stream; + THROW_IF_FAILED(wicFactory->CreateStream(stream.addressof())); + THROW_IF_FAILED(stream->InitializeFromFilename(fileName, GENERIC_WRITE)); + + wil::com_ptr encoder; + THROW_IF_FAILED(wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.addressof())); + THROW_IF_FAILED(encoder->Initialize(stream.get(), WICBitmapEncoderNoCache)); + + wil::com_ptr frame; + wil::com_ptr props; + THROW_IF_FAILED(encoder->CreateNewFrame(frame.addressof(), props.addressof())); + THROW_IF_FAILED(frame->Initialize(props.get())); + THROW_IF_FAILED(frame->SetSize(desc.Width, desc.Height)); + THROW_IF_FAILED(frame->SetResolution(dpi, dpi)); + auto pixelFormat = GUID_WICPixelFormat32bppBGRA; + THROW_IF_FAILED(frame->SetPixelFormat(&pixelFormat)); + + D3D11_MAPPED_SUBRESOURCE mapped; + THROW_IF_FAILED(deviceContext->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped)); + THROW_IF_FAILED(frame->WritePixels(desc.Height, mapped.RowPitch, mapped.RowPitch * desc.Height, static_cast(mapped.pData))); + deviceContext->Unmap(staging.get(), 0); + + THROW_IF_FAILED(frame->Commit()); + THROW_IF_FAILED(encoder->Commit()); +} diff --git a/src/renderer/base/CSSLengthPercentage.cpp b/src/renderer/base/CSSLengthPercentage.cpp new file mode 100644 index 0000000000..a9bdf7b5d9 --- /dev/null +++ b/src/renderer/base/CSSLengthPercentage.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "../inc/CSSLengthPercentage.h" + +CSSLengthPercentage CSSLengthPercentage::FromString(const wchar_t* str) +{ + auto& errnoRef = errno; // Nonzero cost, pay it once. + errnoRef = 0; + + wchar_t* end; + auto value = std::wcstof(str, &end); + + if (str == end || errnoRef == ERANGE) + { + return {}; + } + + auto referenceFrame = ReferenceFrame::FontSize; + + if (*end) + { + if (wcscmp(end, L"%") == 0) + { + value /= 100.0f; + } + else if (wcscmp(end, L"px") == 0) + { + referenceFrame = ReferenceFrame::Absolute; + value /= 96.0f; + } + else if (wcscmp(end, L"pt") == 0) + { + referenceFrame = ReferenceFrame::Absolute; + value /= 72.0f; + } + else if (wcscmp(end, L"ch") == 0) + { + referenceFrame = ReferenceFrame::AdvanceWidth; + } + else + { + return {}; + } + } + + CSSLengthPercentage obj; + obj._value = value; + obj._referenceFrame = referenceFrame; + return obj; +} + +float CSSLengthPercentage::Resolve(float fallback, float dpi, float fontSize, float advanceWidth) const noexcept +{ + switch (_referenceFrame) + { + case ReferenceFrame::Absolute: + return _value * dpi; + case ReferenceFrame::FontSize: + return _value * fontSize; + case ReferenceFrame::AdvanceWidth: + return _value * advanceWidth; + default: + return fallback; + } +} diff --git a/src/renderer/base/FontCache.h b/src/renderer/base/FontCache.h index 163da0239d..0204693b00 100644 --- a/src/renderer/base/FontCache.h +++ b/src/renderer/base/FontCache.h @@ -3,14 +3,30 @@ #pragma once +#include + namespace Microsoft::Console::Render::FontCache { namespace details { - inline const std::vector>& getNearbyFontFiles(IDWriteFactory5* factory5) + inline wil::com_ptr getFontCollection() { - static const auto fontFiles = [=]() { - std::vector> files; + wil::com_ptr factory; + THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof()))); + + wil::com_ptr systemFontCollection; + THROW_IF_FAILED(factory->GetSystemFontCollection(systemFontCollection.addressof(), FALSE)); + + if constexpr (Feature_NearbyFontLoading::IsEnabled()) + { + // IDWriteFactory5 is supported since Windows 10, build 15021. + const auto factory5 = factory.try_query(); + if (!factory5) + { + return systemFontCollection; + } + + std::vector> nearbyFontFiles; const std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) }; const auto folder{ module.parent_path() }; @@ -22,35 +38,11 @@ namespace Microsoft::Console::Render::FontCache wil::com_ptr fontFile; if (SUCCEEDED_LOG(factory5->CreateFontFileReference(p.path().c_str(), nullptr, fontFile.addressof()))) { - files.emplace_back(std::move(fontFile)); + nearbyFontFiles.emplace_back(std::move(fontFile)); } } } - files.shrink_to_fit(); - return files; - }(); - return fontFiles; - } - - inline wil::com_ptr getFontCollection(bool forceUpdate) - { - wil::com_ptr factory; - THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof()))); - - wil::com_ptr systemFontCollection; - THROW_IF_FAILED(factory->GetSystemFontCollection(systemFontCollection.addressof(), forceUpdate)); - - if constexpr (Feature_NearbyFontLoading::IsEnabled()) - { - // IDWriteFactory5 is supported since Windows 10, build 15021. - const auto factory5 = factory.try_query(); - if (!factory5) - { - return systemFontCollection; - } - - const auto& nearbyFontFiles = getNearbyFontFiles(factory5.get()); if (nearbyFontFiles.empty()) { return systemFontCollection; @@ -89,11 +81,13 @@ namespace Microsoft::Console::Render::FontCache inline wil::com_ptr GetCached() { - return details::getFontCollection(false); - } + static til::shared_mutex> cachedCollection; - inline wil::com_ptr GetFresh() - { - return details::getFontCollection(true); + const auto guard = cachedCollection.lock(); + if (!*guard) + { + *guard = details::getFontCollection(); + } + return *guard; } } diff --git a/src/renderer/base/FontInfoDesired.cpp b/src/renderer/base/FontInfoDesired.cpp index 8340aeab12..742883e7c1 100644 --- a/src/renderer/base/FontInfoDesired.cpp +++ b/src/renderer/base/FontInfoDesired.cpp @@ -23,6 +23,22 @@ FontInfoDesired::FontInfoDesired(const FontInfo& fiFont) noexcept : { } +void FontInfoDesired::SetCellSize(const CSSLengthPercentage& cellWidth, const CSSLengthPercentage& cellHeight) noexcept +{ + _cellWidth = cellWidth; + _cellHeight = cellHeight; +} + +const CSSLengthPercentage& FontInfoDesired::GetCellWidth() const noexcept +{ + return _cellWidth; +} + +const CSSLengthPercentage& FontInfoDesired::GetCellHeight() const noexcept +{ + return _cellHeight; +} + float FontInfoDesired::GetFontSize() const noexcept { return _fontSize; @@ -33,7 +49,7 @@ til::size FontInfoDesired::GetEngineSize() const noexcept auto coordSize = _coordSizeDesired; if (IsTrueTypeFont()) { - coordSize.X = 0; // Don't tell the engine about the width for a TrueType font. It makes a mess. + coordSize.width = 0; // Don't tell the engine about the width for a TrueType font. It makes a mess. } return coordSize; diff --git a/src/renderer/base/FontResource.cpp b/src/renderer/base/FontResource.cpp index 465317e9ff..9a93582bc3 100644 --- a/src/renderer/base/FontResource.cpp +++ b/src/renderer/base/FontResource.cpp @@ -76,7 +76,7 @@ namespace #pragma pack(pop) } -FontResource::FontResource(const gsl::span bitPattern, +FontResource::FontResource(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, const size_t centeringHint) : @@ -149,7 +149,7 @@ void FontResource::_regenerateFont() // Raster fonts aren't generally scalable, so we need to resize the bit // patterns for the character glyphs to the requested target size, and // copy the results into the resource structure. - auto fontResourceSpan = gsl::span(fontResourceBuffer); + auto fontResourceSpan = std::span(fontResourceBuffer); _resizeBitPattern(fontResourceSpan.subspan(fontResource.dfBitsOffset)); DWORD fontCount = 0; @@ -169,7 +169,7 @@ void FontResource::_regenerateFont() LOG_HR_IF_NULL(E_FAIL, _fontHandle.get()); } -void FontResource::_resizeBitPattern(gsl::span targetBuffer) +void FontResource::_resizeBitPattern(std::span targetBuffer) { auto sourceWidth = _sourceSize.width; auto targetWidth = _targetSize.width; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index 988aaf6690..80c1f86cda 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -35,7 +35,7 @@ HRESULT RenderEngineBase::NotifyNewText(const std::wstring_view /*newText*/) noe return S_FALSE; } -HRESULT RenderEngineBase::UpdateSoftFont(const gsl::span /*bitPattern*/, +HRESULT RenderEngineBase::UpdateSoftFont(const std::span /*bitPattern*/, const til::size /*cellSize*/, const size_t /*centeringHint*/) noexcept { diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index 65a63a707d..03c4795635 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -11,10 +11,6 @@ using namespace Microsoft::Console::Render; using Microsoft::Console::Utils::InitializeColorTable; -static constexpr size_t AdjustedFgIndex{ 16 }; -static constexpr size_t AdjustedBgIndex{ 17 }; -static constexpr size_t AdjustedBrightFgIndex{ 18 }; - RenderSettings::RenderSettings() noexcept { InitializeColorTable(_colorTable); @@ -71,42 +67,6 @@ void RenderSettings::ResetColorTable() noexcept InitializeColorTable({ _colorTable.data(), 16 }); } -// Routine Description: -// - Creates the adjusted color array, which contains the possible foreground colors, -// adjusted for perceivability -// - The adjusted color array is 2-d, and effectively maps a background and foreground -// color pair to the adjusted foreground for that color pair -void RenderSettings::MakeAdjustedColorArray() noexcept -{ - // The color table has 16 colors, but the adjusted color table needs to be 19 - // to include the default background, default foreground and bright default foreground colors - std::array colorTableWithDefaults; - std::copy_n(std::begin(_colorTable), 16, std::begin(colorTableWithDefaults)); - colorTableWithDefaults[AdjustedFgIndex] = GetColorAlias(ColorAlias::DefaultForeground); - colorTableWithDefaults[AdjustedBgIndex] = GetColorAlias(ColorAlias::DefaultBackground); - - // We need to use TextColor to calculate the bright default fg - TextColor defaultFg; - colorTableWithDefaults[AdjustedBrightFgIndex] = defaultFg.GetColor(_colorTable, GetColorAliasIndex(ColorAlias::DefaultForeground), true); - - for (auto fgIndex = 0; fgIndex < 19; ++fgIndex) - { - const auto fg = til::at(colorTableWithDefaults, fgIndex); - for (auto bgIndex = 0; bgIndex < 19; ++bgIndex) - { - if (fgIndex == bgIndex) - { - _adjustedForegroundColors[bgIndex][fgIndex] = fg; - } - else - { - const auto bg = til::at(colorTableWithDefaults, bgIndex); - _adjustedForegroundColors[bgIndex][fgIndex] = ColorFix::GetPerceivableColor(fg, bg); - } - } - } -} - // Routine Description: // - Updates the given index in the color table to a new value. // Arguments: @@ -196,74 +156,37 @@ std::pair RenderSettings::GetAttributeColors(const TextAttri const auto dimFg = attr.IsFaint() || (_blinkShouldBeFaint && attr.IsBlinking()); const auto swapFgAndBg = attr.IsReverseVideo() ^ GetRenderMode(Mode::ScreenReversed); - // We want to nudge the foreground color to make it more perceivable only for the - // default color pairs within the color table - if (Feature_AdjustIndistinguishableText::IsEnabled() && - GetRenderMode(Mode::IndexedDistinguishableColors) && - !dimFg && - !attr.IsInvisible() && - (fgTextColor.IsDefault() || fgTextColor.IsLegacy()) && - (bgTextColor.IsDefault() || bgTextColor.IsLegacy())) + auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); + auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); + + if (dimFg) { - const auto bgIndex = bgTextColor.IsDefault() ? AdjustedBgIndex : bgTextColor.GetIndex(); - auto fgIndex = fgTextColor.IsDefault() ? AdjustedFgIndex : fgTextColor.GetIndex(); - - if (brightenFg) - { - // There is a special case for intense here - we need to get the bright version of the foreground color - if (fgTextColor.IsIndex16() && (fgIndex < 8)) - { - fgIndex += 8; - } - else if (fgTextColor.IsDefault()) - { - fgIndex = AdjustedBrightFgIndex; - } - } - - if (swapFgAndBg) - { - const auto fg = _adjustedForegroundColors[fgIndex][bgIndex]; - const auto bg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - return { fg, bg }; - } - else - { - const auto fg = _adjustedForegroundColors[bgIndex][fgIndex]; - const auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); - return { fg, bg }; - } + fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. } - else + if (swapFgAndBg) { - auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg); - auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex); + std::swap(fg, bg); + } + if (attr.IsInvisible()) + { + fg = bg; + } - if (dimFg) - { - fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. - } - if (swapFgAndBg) - { - std::swap(fg, bg); - } - if (attr.IsInvisible()) - { - fg = bg; - } - - // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to - // catch the cases where the fg was intentionally set to be the same as the bg. In either case, - // don't adjust the foreground. - if (Feature_AdjustIndistinguishableText::IsEnabled() && + // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to + // catch the cases where the fg was intentionally set to be the same as the bg. In either case, + // don't adjust the foreground. + if constexpr (Feature_AdjustIndistinguishableText::IsEnabled()) + { + if ( + _renderMode.any(Mode::IndexedDistinguishableColors, Mode::AlwaysDistinguishableColors) && fg != bg && - GetRenderMode(Mode::AlwaysDistinguishableColors)) + (_renderMode.test(Mode::AlwaysDistinguishableColors) || (fgTextColor.IsDefaultOrLegacy() && bgTextColor.IsDefaultOrLegacy()))) { - fg = ColorFix::GetPerceivableColor(fg, bg); + fg = ColorFix::GetPerceivableColor(fg, bg, 0.5f * 0.5f); } - - return { fg, bg }; } + + return { fg, bg }; } // Routine Description: diff --git a/src/renderer/base/fontinfo.cpp b/src/renderer/base/fontinfo.cpp index 8ef6d0b2c7..ff0bdcebd9 100644 --- a/src/renderer/base/fontinfo.cpp +++ b/src/renderer/base/fontinfo.cpp @@ -74,16 +74,16 @@ void FontInfo::_ValidateCoordSize() noexcept if (!IsDefaultRasterFontNoSize()) { // Initialize X to 1 so we don't divide by 0 - if (_coordSize.X == 0) + if (_coordSize.width == 0) { - _coordSize.X = 1; + _coordSize.width = 1; } // If we have no font size, we want to use 8x12 by default - if (_coordSize.Y == 0) + if (_coordSize.height == 0) { - _coordSize.X = 8; - _coordSize.Y = 12; + _coordSize.width = 8; + _coordSize.height = 12; _coordSizeUnscaled = _coordSize; } diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index 697934e855..3c177d955e 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -11,6 +11,7 @@ + @@ -25,6 +26,7 @@ + diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters index a678fb27cc..8887f1c610 100644 --- a/src/renderer/base/lib/base.vcxproj.filters +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -45,6 +45,9 @@ Source Files + + Source Files + @@ -89,8 +92,11 @@ Header Files + + Header Files\inc + - \ No newline at end of file + diff --git a/src/renderer/base/precomp.h b/src/renderer/base/precomp.h index e05716224a..40f7b95a01 100644 --- a/src/renderer/base/precomp.h +++ b/src/renderer/base/precomp.h @@ -22,10 +22,3 @@ Abstract: #include #include "../../types/inc/viewport.hpp" - -#ifndef _NTSTATUS_DEFINED -#define _NTSTATUS_DEFINED -typedef _Return_type_success_(return >= 0) long NTSTATUS; -#endif - -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 9ea4e9040b..917c5ba05f 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -223,11 +223,11 @@ void Renderer::TriggerRedraw(const Viewport& region) // If the dirty region has double width lines, we need to double the size of // the right margin to make sure all the affected cells are invalidated. const auto& buffer = _pData->GetTextBuffer(); - for (auto row = srUpdateRegion.Top; row < srUpdateRegion.Bottom; row++) + for (auto row = srUpdateRegion.top; row < srUpdateRegion.bottom; row++) { if (buffer.IsDoubleWidthLine(row)) { - srUpdateRegion.Right *= 2; + srUpdateRegion.right *= 2; break; } } @@ -274,17 +274,18 @@ void Renderer::TriggerRedrawCursor(const til::point* const pcoord) // We then calculate the region covered by the cursor. This requires // converting the buffer coordinates to an equivalent range of screen // cells for the cursor, taking line rendition into account. - const auto lineRendition = buffer.GetLineRendition(pcoord->Y); + const auto lineRendition = buffer.GetLineRendition(pcoord->y); const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; - const til::inclusive_rect cursorRect = { pcoord->X, pcoord->Y, pcoord->X + cursorWidth - 1, pcoord->Y }; - auto cursorView = Viewport::FromInclusive(BufferToScreenLine(cursorRect, lineRendition)); + til::inclusive_rect cursorRect = { pcoord->x, pcoord->y, pcoord->x + cursorWidth - 1, pcoord->y }; + cursorRect = BufferToScreenLine(cursorRect, lineRendition); - // The region is clamped within the viewport boundaries and we only - // trigger a redraw if the region is not empty. + // That region is then clipped within the viewport boundaries and we + // only trigger a redraw if the resulting region is not empty. auto view = _pData->GetViewport(); - if (view.IsInBounds(cursorView)) + auto updateRect = til::rect{ cursorRect }; + if (view.TrimToViewport(&updateRect)) { - const auto updateRect = view.ConvertToOrigin(cursorView).ToExclusive(); + view.ConvertToOrigin(&updateRect); FOREACH_ENGINE(pEngine) { LOG_IF_FAILED(pEngine->InvalidateCursor(&updateRect)); @@ -404,8 +405,8 @@ bool Renderer::_CheckViewportAndScroll() _forceUpdateViewport = false; til::point coordDelta; - coordDelta.X = srOldViewport.Left - srNewViewport.Left; - coordDelta.Y = srOldViewport.Top - srNewViewport.Top; + coordDelta.x = srOldViewport.left - srNewViewport.left; + coordDelta.y = srOldViewport.top - srNewViewport.top; FOREACH_ENGINE(engine) { @@ -543,13 +544,13 @@ void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfo // - centeringHint - The horizontal extent that glyphs are offset from center. // Return Value: // - -void Renderer::UpdateSoftFont(const gsl::span bitPattern, const til::size cellSize, const size_t centeringHint) +void Renderer::UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) { // We reserve PUA code points U+EF20 to U+EF7F for soft fonts, but the range // that we test for in _IsSoftFontChar will depend on the size of the active // bitPattern. If it's empty (i.e. no soft font is set), then nothing will // match, and those code points will be treated the same as everything else. - const auto softFontCharCount = cellSize.cy ? bitPattern.size() / cellSize.cy : 0; + const auto softFontCharCount = cellSize.height ? bitPattern.size() / cellSize.height : 0; _lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1; FOREACH_ENGINE(pEngine) @@ -637,6 +638,7 @@ void Renderer::EnablePainting() // When the renderer is constructed, the initial viewport won't be available yet, // but once EnablePainting is called it should be safe to retrieve. _viewport = _pData->GetViewport(); + _forceUpdateViewport = true; // When running the unit tests, we may be using a render without a render thread. if (_pThread) @@ -686,7 +688,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // This is effectively the number of cells on the visible screen that need to be redrawn. // The origin is always 0, 0 because it represents the screen itself, not the underlying buffer. - gsl::span dirtyAreas; + std::span dirtyAreas; LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas)); // This is to make sure any transforms are reset when this paint is finished. @@ -741,11 +743,11 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // 1. this row wrapped // 2. We're painting the last col of the row. // In that case, set lineWrapped=true for the _PaintBufferOutputHelper call. - const auto lineWrapped = (buffer.GetRowByOffset(bufferLine.Origin().Y).WasWrapForced()) && + const auto lineWrapped = (buffer.GetRowByOffset(bufferLine.Origin().y).WasWrapForced()) && (bufferLine.RightExclusive() == buffer.GetSize().Width()); // Prepare the appropriate line transform for the current row and viewport offset. - LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.Y, view.Left())); + LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.y, view.Left())); // Ask the helper to paint through this specific line. _PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped); @@ -801,7 +803,7 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, usingSoftFont, false)); // Advance the point by however many columns we've just outputted and reset the accumulator. - screenPoint.X += cols; + screenPoint.x += cols; cols = 0; // Hold onto the start of this run iterator and the target location where we started @@ -825,7 +827,7 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // We also accumulate clusters according to regex patterns do { - til::point thisPoint{ screenPoint.X + cols, screenPoint.Y }; + til::point thisPoint{ screenPoint.x + cols, screenPoint.y }; const auto thisPointPatterns = _pData->GetPatternId(thisPoint); const auto thisUsingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); const auto changedPatternOrFont = patternIds != thisPointPatterns || usingSoftFont != thisUsingSoftFont; @@ -849,10 +851,10 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // If we're on the first cluster to be added and it's marked as "trailing" // (a.k.a. the right half of a two column character), then we need some special handling. - if (_clusterBuffer.empty() && it->DbcsAttr().IsTrailing()) + if (_clusterBuffer.empty() && it->DbcsAttr() == DbcsAttribute::Trailing) { // Move left to the one so the whole character can be struck correctly. - --screenPoint.X; + --screenPoint.x; // And tell the next function to trim off the left half of it. trimLeft = true; // And add one to the number of columns we expect it to take as we insert it. @@ -895,7 +897,7 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // We could theoretically pre-pass for this in the loop above to be more efficient about walking // the iterator, but I fear it would make the code even more confusing than it already is. // Do that in the future if some WPR trace points you to this spot as super bad. - for (til::CoordType colsPainted = 0; colsPainted < cols; ++colsPainted, ++lineIt, ++lineTarget.X) + for (til::CoordType colsPainted = 0; colsPainted < cols; ++colsPainted, ++lineIt, ++lineTarget.x) { auto lines = lineIt->TextAttr(); _PaintBufferOutputGridLineHelper(pEngine, lines, 1, lineTarget); @@ -912,55 +914,62 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, } // Method Description: -// - Generates a IRenderEngine::GridLines structure from the values in the +// - Generates a GridLines structure from the values in the // provided textAttribute // Arguments: // - textAttribute: the TextAttribute to generate GridLines from. // Return Value: // - a GridLineSet containing all the gridline info from the TextAttribute -IRenderEngine::GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept +GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept { // Convert console grid line representations into rendering engine enum representations. - IRenderEngine::GridLineSet lines; + GridLineSet lines; if (textAttribute.IsTopHorizontalDisplayed()) { - lines.set(IRenderEngine::GridLines::Top); + lines.set(GridLines::Top); } if (textAttribute.IsBottomHorizontalDisplayed()) { - lines.set(IRenderEngine::GridLines::Bottom); + lines.set(GridLines::Bottom); } if (textAttribute.IsLeftVerticalDisplayed()) { - lines.set(IRenderEngine::GridLines::Left); + lines.set(GridLines::Left); } if (textAttribute.IsRightVerticalDisplayed()) { - lines.set(IRenderEngine::GridLines::Right); + lines.set(GridLines::Right); } if (textAttribute.IsCrossedOut()) { - lines.set(IRenderEngine::GridLines::Strikethrough); + lines.set(GridLines::Strikethrough); } - if (textAttribute.IsUnderlined()) + const auto underlineStyle = textAttribute.GetUnderlineStyle(); + switch (underlineStyle) { - lines.set(IRenderEngine::GridLines::Underline); - } - - if (textAttribute.IsDoublyUnderlined()) - { - lines.set(IRenderEngine::GridLines::DoubleUnderline); + 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()) { - lines.set(IRenderEngine::GridLines::HyperlinkUnderline); + lines.set(GridLines::HyperlinkUnderline); } return lines; } @@ -984,19 +993,10 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin auto lines = Renderer::s_GetGridlines(textAttribute); // For now, we dash underline patterns and switch to regular underline on hover - // Since we're only rendering pattern links on *hover*, there's no point in checking - // the pattern range if we aren't currently hovering. - if (_hoveredInterval.has_value()) + if (_isHoveredHyperlink(textAttribute) || _isInHoveredInterval(coordTarget)) { - const til::point coordTargetTil{ coordTarget }; - if (_hoveredInterval->start <= coordTargetTil && - coordTargetTil <= _hoveredInterval->stop) - { - if (_pData->GetPatternId(coordTarget).size() > 0) - { - lines.set(IRenderEngine::GridLines::Underline); - } - } + lines.reset(GridLines::HyperlinkUnderline); + lines.set(GridLines::Underline); } // Return early if there are no lines to paint. @@ -1009,6 +1009,18 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin } } +bool Renderer::_isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept +{ + return _hyperlinkHoveredId && _hyperlinkHoveredId == textAttribute.GetHyperlinkId(); +} + +bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept +{ + return _hoveredInterval && + _hoveredInterval->start <= coordTarget && coordTarget <= _hoveredInterval->stop && + _pData->GetPatternId(coordTarget).size() > 0; +} + // Routine Description: // - Retrieve information about the cursor, and pack it into a CursorOptions // which the render engine can use for painting the cursor. @@ -1034,7 +1046,7 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin // The cursor is never rendered as double height, so we don't care about // the exact line rendition - only whether it's double width or not. - const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.Y); + const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.y); const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth; // We need to convert the screen coordinates of the viewport to an @@ -1043,13 +1055,13 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin // Note that we allow the X coordinate to be outside the left border by 1 position, // because the cursor could still be visible if the focused character is double width. - const auto xInRange = coordCursor.X >= view.Left - 1 && coordCursor.X <= view.Right; - const auto yInRange = coordCursor.Y >= view.Top && coordCursor.Y <= view.Bottom; + const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right; + const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom; if (xInRange && yInRange) { // Adjust cursor Y offset to viewport. // The viewport X offset is saved in the options and handled with a transform. - coordCursor.Y -= view.Top; + coordCursor.y -= view.top; auto cursorColor = _renderSettings.GetColorTableEntry(TextColor::CURSOR_COLOR); auto useColor = cursorColor != INVALID_COLOR; @@ -1121,21 +1133,21 @@ void Renderer::_PaintOverlay(IRenderEngine& engine, { // Now get the overlay's viewport and adjust it to where it is supposed to be relative to the window. auto srCaView = overlay.region.ToExclusive(); - srCaView.Top += overlay.origin.Y; - srCaView.Bottom += overlay.origin.Y; - srCaView.Left += overlay.origin.X; - srCaView.Right += overlay.origin.X; + srCaView.top += overlay.origin.y; + srCaView.bottom += overlay.origin.y; + srCaView.left += overlay.origin.x; + srCaView.right += overlay.origin.x; - gsl::span dirtyAreas; + std::span dirtyAreas; LOG_IF_FAILED(engine.GetDirtyArea(dirtyAreas)); for (const auto& rect : dirtyAreas) { if (const auto viewDirty = rect & srCaView) { - for (auto iRow = viewDirty.Top; iRow < viewDirty.Bottom; iRow++) + for (auto iRow = viewDirty.top; iRow < viewDirty.bottom; iRow++) { - const til::point target{ viewDirty.Left, iRow }; + const til::point target{ viewDirty.left, iRow }; const auto source = target - overlay.origin; auto it = overlay.buffer.GetCellLineDataAt(source); @@ -1180,7 +1192,7 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) { try { - gsl::span dirtyAreas; + std::span dirtyAreas; LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas)); // Get selection rectangles @@ -1305,6 +1317,20 @@ void Renderer::AddRenderEngine(_In_ IRenderEngine* const pEngine) THROW_HR_MSG(E_UNEXPECTED, "engines array is full"); } +void Renderer::RemoveRenderEngine(_In_ IRenderEngine* const pEngine) +{ + THROW_HR_IF_NULL(E_INVALIDARG, pEngine); + + for (auto& p : _engines) + { + if (p == pEngine) + { + p = nullptr; + return; + } + } +} + // Method Description: // - Registers a callback for when the background color is changed // Arguments: @@ -1347,6 +1373,15 @@ void Renderer::ResetErrorStateAndResume() EnablePainting(); } +void Renderer::UpdateHyperlinkHoveredId(uint16_t id) noexcept +{ + _hyperlinkHoveredId = id; + FOREACH_ENGINE(pEngine) + { + pEngine->UpdateHyperlinkHoveredId(id); + } +} + void Renderer::UpdateLastHoveredInterval(const std::optional& newInterval) { _hoveredInterval = newInterval; diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index b6b051199b..ae3ed2cfda 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -22,7 +22,6 @@ Author(s): #include "thread.hpp" #include "../../buffer/out/textBuffer.hpp" -#include "../../buffer/out/CharRow.hpp" // fwdecl unittest classes #ifdef UNIT_TESTING @@ -68,7 +67,7 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo); - void UpdateSoftFont(const gsl::span bitPattern, + void UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint); @@ -83,16 +82,18 @@ namespace Microsoft::Console::Render void WaitUntilCanRender(); void AddRenderEngine(_In_ IRenderEngine* const pEngine); + void RemoveRenderEngine(_In_ IRenderEngine* const pEngine); void SetBackgroundColorChangedCallback(std::function pfn); void SetFrameColorChangedCallback(std::function pfn); void SetRendererEnteredErrorStateCallback(std::function pfn); void ResetErrorStateAndResume(); + void UpdateHyperlinkHoveredId(uint16_t id) noexcept; void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); private: - static IRenderEngine::GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; + static GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); [[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept; @@ -101,6 +102,7 @@ namespace Microsoft::Console::Render void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target, const bool lineWrapped); void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget); + bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept; void _PaintSelection(_In_ IRenderEngine* const pEngine); void _PaintCursor(_In_ IRenderEngine* const pEngine); void _PaintOverlays(_In_ IRenderEngine* const pEngine); @@ -110,6 +112,7 @@ namespace Microsoft::Console::Render std::vector _GetSelectionRects() const; void _ScrollPreviousSelection(const til::point delta); [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); + bool _isInHoveredInterval(til::point coordTarget) const noexcept; [[nodiscard]] std::optional _GetCursorInfo(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); @@ -119,6 +122,7 @@ namespace Microsoft::Console::Render std::unique_ptr _pThread; static constexpr size_t _firstSoftFontChar = 0xEF20; size_t _lastSoftFontChar = 0; + uint16_t _hyperlinkHoveredId = 0; std::optional::interval> _hoveredInterval; Microsoft::Console::Types::Viewport _viewport; std::vector _clusterBuffer; @@ -127,7 +131,7 @@ namespace Microsoft::Console::Render std::function _pfnFrameColorChanged; std::function _pfnRendererEnteredErrorState; bool _destructing = false; - bool _forceUpdateViewport = true; + bool _forceUpdateViewport = false; #ifdef UNIT_TESTING friend class ConptyOutputTests; diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc index 68150be017..7a3781a419 100644 --- a/src/renderer/base/sources.inc +++ b/src/renderer/base/sources.inc @@ -24,6 +24,7 @@ PRECOMPILED_CXX = 1 PRECOMPILED_INCLUDE = ..\precomp.h SOURCES = \ + ..\CSSLengthPercentage.cpp \ ..\FontInfo.cpp \ ..\FontInfoBase.cpp \ ..\FontInfoDesired.cpp \ diff --git a/src/renderer/base/thread.cpp b/src/renderer/base/thread.cpp index 056749f3e4..904cf2075a 100644 --- a/src/renderer/base/thread.cpp +++ b/src/renderer/base/thread.cpp @@ -169,6 +169,11 @@ DWORD WINAPI RenderThread::_ThreadProc() { while (_fKeepRunning) { + // Between waiting on _hEvent and calling PaintFrame() there should be a minimal delay, + // so that a key press progresses to a drawing operation as quickly as possible. + // As such, we wait for the renderer to complete _before_ waiting on _hEvent. + _pRenderer->WaitUntilCanRender(); + WaitForSingleObject(_hPaintEnabledEvent, INFINITE); if (!_fNextFrameRequested.exchange(false, std::memory_order_acq_rel)) @@ -209,10 +214,7 @@ DWORD WINAPI RenderThread::_ThreadProc() } ResetEvent(_hPaintCompletedEvent); - - _pRenderer->WaitUntilCanRender(); LOG_IF_FAILED(_pRenderer->PaintFrame()); - SetEvent(_hPaintCompletedEvent); } diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index c6f479dbc4..41c64a5a5c 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -65,7 +65,7 @@ CATCH_RETURN() // - clusters - From the backing buffer, the text to be displayed clustered by the columns it should consume. // Return Value: // - S_OK or suitable memory management issue. -[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::AppendClusters(const gsl::span clusters) +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::AppendClusters(const std::span clusters) try { _textClusterColumns.reserve(_textClusterColumns.size() + clusters.size()); @@ -406,7 +406,7 @@ CATCH_RETURN() // Get the features to apply to the font const auto& features = _fontRenderData->DefaultFontFeatures(); #pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3). - DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { const_cast(features.data()), gsl::narrow(features.size()) }; + const DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { const_cast(features.data()), gsl::narrow(features.size()) }; DWRITE_TYPOGRAPHIC_FEATURES const* typographicFeaturesPointer = &typographicFeatures; const uint32_t fontFeatureLengths[] = { textLength }; diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 1fe93810b5..4ea4da0f4d 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -23,7 +23,7 @@ namespace Microsoft::Console::Render CustomTextLayout(const gsl::not_null fontRenderData); - [[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const gsl::span clusters); + [[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const std::span clusters); [[nodiscard]] HRESULT STDMETHODCALLTYPE Reset() noexcept; diff --git a/src/renderer/dx/CustomTextRenderer.cpp b/src/renderer/dx/CustomTextRenderer.cpp index f316c96d51..c217cbfc35 100644 --- a/src/renderer/dx/CustomTextRenderer.cpp +++ b/src/renderer/dx/CustomTextRenderer.cpp @@ -275,8 +275,8 @@ try // Create rectangular block representing where the cursor can fill. D2D1_RECT_F rect; - rect.left = options.coordCursor.X * drawingContext.cellSize.width; - rect.top = options.coordCursor.Y * drawingContext.cellSize.height; + rect.left = options.coordCursor.x * drawingContext.cellSize.width; + rect.top = options.coordCursor.y * drawingContext.cellSize.height; rect.right = rect.left + drawingContext.cellSize.width; rect.bottom = rect.top + drawingContext.cellSize.height; @@ -509,7 +509,7 @@ CATCH_RETURN() clipRect.top = origin.y + drawingContext->topClipOffset; clipRect.bottom = origin.y + drawingContext->cellSize.height - drawingContext->bottomClipOffset; clipRect.left = 0; - clipRect.right = drawingContext->targetSize.width; + clipRect.right = FLT_MAX; // If we already have a clip rectangle, check if it different than the previous one. if (_clipRect.has_value()) @@ -540,7 +540,7 @@ CATCH_RETURN() // Draw the background // The rectangle needs to be deduced based on the origin and the BidiDirection - const auto advancesSpan = gsl::make_span(glyphRun->glyphAdvances, glyphRun->glyphCount); + const auto advancesSpan = std::span{ glyphRun->glyphAdvances, glyphRun->glyphCount }; const auto totalSpan = std::accumulate(advancesSpan.begin(), advancesSpan.end(), 0.0f); D2D1_RECT_F rect; diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index a4d4d802a1..4945939fd8 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -7,36 +7,31 @@ #include #include +#include "../base/FontCache.h" + static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; using namespace Microsoft::Console::Render; -DxFontInfo::DxFontInfo() noexcept : - _weight(DWRITE_FONT_WEIGHT_NORMAL), - _style(DWRITE_FONT_STYLE_NORMAL), - _stretch(DWRITE_FONT_STRETCH_NORMAL), - _didFallback(false) +DxFontInfo::DxFontInfo(IDWriteFactory1* dwriteFactory) : + DxFontInfo{ dwriteFactory, {}, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL } { } -DxFontInfo::DxFontInfo(std::wstring_view familyName, - unsigned int weight, - DWRITE_FONT_STYLE style, - DWRITE_FONT_STRETCH stretch) : - DxFontInfo(familyName, static_cast(weight), style, stretch) -{ -} - -DxFontInfo::DxFontInfo(std::wstring_view familyName, - DWRITE_FONT_WEIGHT weight, - DWRITE_FONT_STYLE style, - DWRITE_FONT_STRETCH stretch) : +DxFontInfo::DxFontInfo( + IDWriteFactory1* dwriteFactory, + std::wstring_view familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) : _familyName(familyName), _weight(weight), _style(style), _stretch(stretch), _didFallback(false) { + __assume(dwriteFactory != nullptr); + THROW_IF_FAILED(dwriteFactory->GetSystemFontCollection(_fontCollection.addressof(), FALSE)); } bool DxFontInfo::operator==(const DxFontInfo& other) const noexcept @@ -93,6 +88,11 @@ bool DxFontInfo::GetFallback() const noexcept return _didFallback; } +IDWriteFontCollection* DxFontInfo::GetFontCollection() const noexcept +{ + return _fontCollection.get(); +} + void DxFontInfo::SetFromEngine(const std::wstring_view familyName, const DWRITE_FONT_WEIGHT weight, const DWRITE_FONT_STYLE style, @@ -114,7 +114,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. -[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::ResolveFontFaceWithFallback(IDWriteFontCollection* fontCollection, std::wstring& localeName) +[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::ResolveFontFaceWithFallback(std::wstring& localeName) { // First attempt to find exactly what the user asked for. _didFallback = false; @@ -125,7 +125,16 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // method. We still want to fall back to a font that's reasonable, below. try { - face = _FindFontFace(fontCollection, localeName); + face = _FindFontFace(localeName); + + if constexpr (Feature_NearbyFontLoading::IsEnabled()) + { + if (!face) + { + _fontCollection = FontCache::GetCached(); + face = _FindFontFace(localeName); + } + } if (!face) { @@ -152,14 +161,14 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, _familyName = _familyName.substr(0, lastSpace); // Try to find it with the shortened family name - face = _FindFontFace(fontCollection, localeName); + face = _FindFontFace(localeName); } } } CATCH_LOG(); // Alright, if our quick shot at trimming didn't work either... - // move onto looking up a font from our hardcoded list of fonts + // move onto looking up a font from our hard-coded list of fonts // that should really always be available. if (!face) { @@ -167,7 +176,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, { _familyName = fallbackFace; - face = _FindFontFace(fontCollection, localeName); + face = _FindFontFace(localeName); if (face) { _didFallback = true; @@ -189,19 +198,19 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // Return Value: // - Smart pointer holding interface reference for queryable font data. #pragma warning(suppress : 26429) // C26429: Symbol 'fontCollection' is never tested for nullness, it can be marked as not_null (f.23). -[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::_FindFontFace(IDWriteFontCollection* fontCollection, std::wstring& localeName) +[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::_FindFontFace(std::wstring& localeName) { Microsoft::WRL::ComPtr fontFace; UINT32 familyIndex; BOOL familyExists; - THROW_IF_FAILED(fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); + THROW_IF_FAILED(_fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); if (familyExists) { Microsoft::WRL::ComPtr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + THROW_IF_FAILED(_fontCollection->GetFontFamily(familyIndex, &fontFamily)); Microsoft::WRL::ComPtr font; THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(GetWeight(), GetStretch(), GetStyle(), &font)); diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index dc8d7f85dd..c9ed0a41e2 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -13,17 +13,14 @@ namespace Microsoft::Console::Render class DxFontInfo { public: - DxFontInfo() noexcept; + DxFontInfo(IDWriteFactory1* dwriteFactory); - DxFontInfo(std::wstring_view familyName, - unsigned int weight, - DWRITE_FONT_STYLE style, - DWRITE_FONT_STRETCH stretch); - - DxFontInfo(std::wstring_view familyName, - DWRITE_FONT_WEIGHT weight, - DWRITE_FONT_STYLE style, - DWRITE_FONT_STRETCH stretch); + DxFontInfo( + IDWriteFactory1* dwriteFactory, + std::wstring_view familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); bool operator==(const DxFontInfo& other) const noexcept; @@ -40,16 +37,17 @@ namespace Microsoft::Console::Render void SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept; bool GetFallback() const noexcept; + IDWriteFontCollection* GetFontCollection() const noexcept; void SetFromEngine(const std::wstring_view familyName, const DWRITE_FONT_WEIGHT weight, const DWRITE_FONT_STYLE style, const DWRITE_FONT_STRETCH stretch); - [[nodiscard]] ::Microsoft::WRL::ComPtr ResolveFontFaceWithFallback(IDWriteFontCollection* fontCollection, std::wstring& localeName); + [[nodiscard]] ::Microsoft::WRL::ComPtr ResolveFontFaceWithFallback(std::wstring& localeName); private: - [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(IDWriteFontCollection* fontCollection, std::wstring& localeName); + [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(std::wstring& localeName); [[nodiscard]] std::wstring _GetFontFamilyName(const gsl::not_null fontFamily, std::wstring& localeName); @@ -66,7 +64,9 @@ namespace Microsoft::Console::Render // The stretch of the font is the spacing between each letter DWRITE_FONT_STRETCH _stretch; - // Indicates whether we couldn't match the user request and had to choose from a hardcoded default list. + wil::com_ptr _fontCollection; + + // Indicates whether we couldn't match the user request and had to choose from a hard-coded default list. bool _didFallback; }; } diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index 3f334dbedf..7765067d0b 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -6,8 +6,6 @@ #include -#include "../base/FontCache.h" - static constexpr float POINTS_PER_INCH = 72.0f; static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us"; static constexpr size_t TAG_LENGTH = 4; @@ -15,12 +13,11 @@ static constexpr size_t TAG_LENGTH = 4; using namespace Microsoft::Console::Render; DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) : - _dwriteFactory(dwriteFactory), - _nearbyCollection{ FontCache::GetCached() }, - _fontSize{}, - _glyphCell{}, + _dwriteFactory(std::move(dwriteFactory)), + _defaultFontInfo{ _dwriteFactory.Get() }, + _lineSpacing{}, _lineMetrics{}, - _lineSpacing{} + _fontSize{} { } @@ -164,7 +161,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr fontInfo.SetStretch(stretch); auto fontLocaleName = UserLocaleName(); - auto fontFace = fontInfo.ResolveFontFaceWithFallback(_nearbyCollection.get(), fontLocaleName); + auto fontFace = fontInfo.ResolveFontFaceWithFallback(fontLocaleName); _fontFaceMap.emplace(_ToMapKey(weight, style, stretch), fontFace); return fontFace; @@ -193,10 +190,12 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr _boxDrawingEffect.Reset(); // Initialize the default font info and build everything from here. - _defaultFontInfo = DxFontInfo(desired.GetFaceName(), - desired.GetWeight(), - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL); + _defaultFontInfo = DxFontInfo( + _dwriteFactory.Get(), + desired.GetFaceName(), + static_cast(desired.GetWeight()), + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL); _SetFeatures(features); _SetAxes(axes); @@ -573,7 +572,7 @@ void DxFontRenderData::_SetAxes(const std::unordered_map DxFontRenderData::GetAxisVector(const DWRITE // - None void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { + const auto dpiF = static_cast(dpi); auto fontLocaleName = UserLocaleName(); // This is the first attempt to resolve font face after `UpdateFont`. // Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist. // See the implementation of `ResolveFontFaceWithFallback` for details. - const auto face = _defaultFontInfo.ResolveFontFaceWithFallback(_nearbyCollection.get(), fontLocaleName); + const auto face = _defaultFontInfo.ResolveFontFaceWithFallback(fontLocaleName); DWRITE_FONT_METRICS1 fontMetrics; face->GetMetrics(&fontMetrics); @@ -737,27 +737,20 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%) // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%) // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%) - auto heightDesired = desired.GetEngineSize().Y * USER_DEFAULT_SCREEN_DPI / POINTS_PER_INCH; + const auto heightDesired = desired.GetEngineSize().height / POINTS_PER_INCH * dpiF; // The advance is the number of pixels left-to-right (X dimension) for the given font. // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement. - - // Now we play trickery with the font size. Scale by the DPI to get the height we expect. - heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); - const auto widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm; // Use the real pixel height desired by the "em" factor for the width to get the number of pixels // we will need per character in width. This will almost certainly result in fractional X-dimension pixels. - const auto widthApprox = heightDesired * widthAdvance; - - // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel. - const auto widthExact = round(widthApprox); + const auto widthAdvanceInPx = heightDesired * widthAdvance; // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional // height in pixels of each character. It's easier for us to pad out height and align vertically // than it is horizontally. - const auto fontSize = widthExact / widthAdvance; + const auto fontSize = roundf(widthAdvanceInPx) / widthAdvance; _fontSize = fontSize; // Now figure out the basic properties of the character height which include ascent and descent @@ -809,8 +802,12 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font const auto fullPixelAscent = ceil(ascent + halfGap); const auto fullPixelDescent = ceil(descent + halfGap); - lineSpacing.height = fullPixelAscent + fullPixelDescent; - lineSpacing.baseline = fullPixelAscent; + const auto defaultHeight = fullPixelAscent + fullPixelDescent; + const auto lineHeight = desired.GetCellHeight().Resolve(defaultHeight, dpiF, heightDesired, widthAdvanceInPx); + const auto baseline = fullPixelAscent + (lineHeight - defaultHeight) / 2.0f; + + lineSpacing.height = roundf(lineHeight); + lineSpacing.baseline = roundf(baseline); // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage) // Setting "ENABLED" means we've included the line gapping in the spacing numbers given. @@ -818,11 +815,14 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font _lineSpacing = lineSpacing; + const auto widthApprox = desired.GetCellWidth().Resolve(widthAdvanceInPx, dpiF, heightDesired, widthAdvanceInPx); + const auto widthExact = roundf(widthApprox); + // The scaled size needs to represent the pixel box that each character will fit within for the purposes // of hit testing math and other such multiplication/division. til::size coordSize; - coordSize.X = static_cast(widthExact); - coordSize.Y = static_cast(lineSpacing.height); + coordSize.width = static_cast(widthExact); + coordSize.height = static_cast(lineSpacing.height); // Unscaled is for the purposes of re-communicating this font back to the renderer again later. // As such, we need to give the same original size parameter back here without padding @@ -861,8 +861,8 @@ void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, Font // Offsets are relative to the base line of the font, so we subtract // from the ascent to get an offset relative to the top of the cell. - lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset; - lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset; + lineMetrics.underlineOffset = lineSpacing.baseline - lineMetrics.underlineOffset; + lineMetrics.strikethroughOffset = lineSpacing.baseline - lineMetrics.strikethroughOffset; // For double underlines we need a second offset, just below the first, // but with a bit of a gap (about double the grid line width). @@ -897,7 +897,7 @@ Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(con { Microsoft::WRL::ComPtr format; THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(), - _nearbyCollection.get(), + fontInfo.GetFontCollection(), fontInfo.GetWeight(), fontInfo.GetStyle(), fontInfo.GetStretch(), diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index b2a0e7053b..e12c6da91b 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -109,7 +109,7 @@ namespace Microsoft::Console::Render // The font axes to apply to the text std::vector _axesVector; - gsl::span _axesVectorWithoutWeight; + std::span _axesVectorWithoutWeight; // We use this to identify font variants with different attributes. static FontAttributeMapKey _ToMapKey(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) noexcept @@ -132,7 +132,6 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _dwriteFactory; ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; - wil::com_ptr _nearbyCollection; std::wstring _userLocaleName; DxFontInfo _defaultFontInfo; til::size _glyphCell; diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 83e09c7ddb..f74bf528dc 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -821,7 +821,6 @@ static constexpr D2D1_ALPHA_MODE _dxgiAlphaToD2d1Alpha(DXGI_ALPHA_MODE mode) noe // 1234123412341234 static constexpr std::array hyperlinkDashes{ 1.f, 3.f }; RETURN_IF_FAILED(_d2dFactory->CreateStrokeStyle(&_dashStrokeStyleProperties, hyperlinkDashes.data(), gsl::narrow_cast(hyperlinkDashes.size()), &_dashStrokeStyle)); - _hyperlinkStrokeStyle = _dashStrokeStyle; // If in composition mode, apply scaling factor matrix if (_chainMode == SwapChainMode::ForComposition) @@ -1229,10 +1228,10 @@ CATCH_RETURN(); // - - Updates reference void _ScaleByFont(til::rect& cellsToPixels, til::size fontSize) noexcept { - cellsToPixels.left *= fontSize.cx; - cellsToPixels.right *= fontSize.cx; - cellsToPixels.top *= fontSize.cy; - cellsToPixels.bottom *= fontSize.cy; + cellsToPixels.left *= fontSize.width; + cellsToPixels.right *= fontSize.width; + cellsToPixels.top *= fontSize.height; + cellsToPixels.bottom *= fontSize.height; } // Routine Description: @@ -1663,7 +1662,7 @@ CATCH_RETURN() // - fTrimLeft - Whether or not to trim off the left half of a double wide character // Return Value: // - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT DxEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept @@ -1715,7 +1714,7 @@ try _d2dBrushForeground->SetColor(_ColorFFromColorRef(color | 0xff000000)); const auto font = _fontRenderData->GlyphCell().to_d2d_size(); - const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height }; + const D2D_POINT_2F target = { coordTarget.x * font.width, coordTarget.y * font.height }; const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine); const auto DrawLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { @@ -1723,7 +1722,7 @@ try }; const auto DrawHyperlinkLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { - _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _hyperlinkStrokeStyle.Get()); + _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _dashStrokeStyle.Get()); }; // NOTE: Line coordinates are centered within the line, so they need to be @@ -1980,11 +1979,6 @@ try _drawingContext->useItalicFont = textAttributes.IsItalic(); } - if (textAttributes.IsHyperlink()) - { - _hyperlinkStrokeStyle = (textAttributes.GetHyperlinkId() == _hyperlinkHoveredId) ? _strokeStyle : _dashStrokeStyle; - } - // Update pixel shader settings as background color might have changed _ComputePixelShaderSettings(); @@ -2112,7 +2106,7 @@ CATCH_RETURN(); // - area - Rectangle describing dirty area in characters. // Return Value: // - S_OK -[[nodiscard]] HRESULT DxEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT DxEngine::GetDirtyArea(std::span& area) noexcept try { area = _invalidMap.runs(); @@ -2286,7 +2280,7 @@ void DxEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept // - centeringHint - The horizontal extent that glyphs are offset from center. // Return Value: // - S_OK if successful. E_FAIL if there was an error. -HRESULT DxEngine::UpdateSoftFont(const gsl::span bitPattern, +HRESULT DxEngine::UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept try diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index d3242d2a98..bfb11205a0 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -89,7 +89,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; - [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + [[nodiscard]] HRESULT UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept override; @@ -101,7 +101,7 @@ namespace Microsoft::Console::Render const til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; @@ -123,7 +123,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; @@ -203,7 +203,6 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _customRenderer; ::Microsoft::WRL::ComPtr _strokeStyle; ::Microsoft::WRL::ComPtr _dashStrokeStyle; - ::Microsoft::WRL::ComPtr _hyperlinkStrokeStyle; std::unique_ptr _fontRenderData; DxSoftFont _softFont; diff --git a/src/renderer/dx/DxSoftFont.cpp b/src/renderer/dx/DxSoftFont.cpp index f9c01155a5..2bfa932d87 100644 --- a/src/renderer/dx/DxSoftFont.cpp +++ b/src/renderer/dx/DxSoftFont.cpp @@ -23,18 +23,7 @@ constexpr size_t BITMAP_GRID_WIDTH = 12; constexpr size_t BITMAP_GRID_HEIGHT = 8; constexpr size_t PADDING = 2; -constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; -constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR; - -DxSoftFont::DxSoftFont() noexcept : - _centeringHint{}, - _interpolation{ ANTIALIASED_INTERPOLATION }, - _colorMatrix{} -{ - _colorMatrix.m[0][3] = 1; -} - -void DxSoftFont::SetFont(const gsl::span bitPattern, +void DxSoftFont::SetFont(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, const size_t centeringHint) @@ -141,7 +130,7 @@ HRESULT DxSoftFont::SetColor(const D2D1_COLOR_F& color) } HRESULT DxSoftFont::Draw(const DrawingContext& drawingContext, - const gsl::span clusters, + const std::span clusters, const float originX, const float originY) { diff --git a/src/renderer/dx/DxSoftFont.h b/src/renderer/dx/DxSoftFont.h index 718861902a..5dd83b3f4d 100644 --- a/src/renderer/dx/DxSoftFont.h +++ b/src/renderer/dx/DxSoftFont.h @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -18,8 +17,7 @@ namespace Microsoft::Console::Render class DxSoftFont { public: - DxSoftFont() noexcept; - void SetFont(const gsl::span bitPattern, + void SetFont(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, const size_t centeringHint); @@ -27,12 +25,15 @@ namespace Microsoft::Console::Render HRESULT SetAntialiasing(const bool antialiased); HRESULT SetColor(const D2D1_COLOR_F& color); HRESULT Draw(const DrawingContext& drawingContext, - const gsl::span clusters, + const std::span clusters, const float originX, const float originY); void Reset(); private: + static constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; + static constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR; + HRESULT _createResources(gsl::not_null d2dContext); D2D1_VECTOR_2F _scaleForTargetSize() const noexcept; template @@ -40,13 +41,13 @@ namespace Microsoft::Console::Render template T _yOffsetForGlyph(const size_t glyphNumber) const noexcept; - size_t _glyphCount; + size_t _glyphCount = 0; til::size _sourceSize; til::size _targetSize; - size_t _centeringHint; - D2D1_SCALE_INTERPOLATION_MODE _interpolation; - D2D1_MATRIX_5X4_F _colorMatrix; - D2D1_SIZE_U _bitmapSize; + size_t _centeringHint = 0; + D2D1_SCALE_INTERPOLATION_MODE _interpolation = ALIASED_INTERPOLATION; + D2D1_MATRIX_5X4_F _colorMatrix{ ._14 = 1 }; + D2D1_SIZE_U _bitmapSize{}; std::vector _bitmapBits; ::Microsoft::WRL::ComPtr _bitmap; ::Microsoft::WRL::ComPtr _scaleEffect; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 0eeb048622..a1ab2290a9 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -47,7 +47,7 @@ namespace Microsoft::Console::Render const til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; @@ -66,7 +66,7 @@ namespace Microsoft::Console::Render const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; - [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + [[nodiscard]] HRESULT UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; @@ -76,7 +76,7 @@ namespace Microsoft::Console::Render _Out_ FontInfo& Font, const int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/gdi/invalidate.cpp b/src/renderer/gdi/invalidate.cpp index 92186f6fef..ce2616be60 100644 --- a/src/renderer/gdi/invalidate.cpp +++ b/src/renderer/gdi/invalidate.cpp @@ -30,7 +30,7 @@ HRESULT GdiEngine::InvalidateSystem(const til::rect* const prcDirtyClient) noexc // - HRESULT S_OK, GDI-based error code, or safemath error HRESULT GdiEngine::InvalidateScroll(const til::point* const pcoordDelta) noexcept { - if (pcoordDelta->X != 0 || pcoordDelta->Y != 0) + if (pcoordDelta->x != 0 || pcoordDelta->y != 0) { const auto ptDelta = *pcoordDelta * _GetFontSize(); RETURN_IF_FAILED(_InvalidOffset(&ptDelta)); diff --git a/src/renderer/gdi/math.cpp b/src/renderer/gdi/math.cpp index 16824c0d2e..eb5c845ff5 100644 --- a/src/renderer/gdi/math.cpp +++ b/src/renderer/gdi/math.cpp @@ -16,7 +16,7 @@ using namespace Microsoft::Console::Render; // This is an Inclusive rect. // Return Value: // - S_OK or math failure -[[nodiscard]] HRESULT GdiEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT GdiEngine::GetDirtyArea(std::span& area) noexcept { _invalidCharacters = til::rect{ _psInvalidData.rcPaint }.scale_down(_GetFontSize()); @@ -47,7 +47,7 @@ using namespace Microsoft::Console::Render; { const int totalWidth = abc.abcA + abc.abcB + abc.abcC; - isFullWidth = totalWidth > _GetFontSize().X; + isFullWidth = totalWidth > _GetFontSize().width; } } else @@ -55,7 +55,7 @@ using namespace Microsoft::Console::Render; auto cpxWidth = 0; if (GetCharWidth32W(_hdcMemoryContext, wch, wch, &cpxWidth)) { - isFullWidth = cpxWidth > _GetFontSize().X; + isFullWidth = cpxWidth > _GetFontSize().width; } } } @@ -129,8 +129,8 @@ til::size GdiEngine::_GetInvalidRectSize() const til::size GdiEngine::_GetRectSize(const RECT* const pRect) const { til::size sz; - sz.cx = pRect->right - pRect->left; - sz.cy = pRect->bottom - pRect->top; + sz.width = pRect->right - pRect->left; + sz.height = pRect->bottom - pRect->top; return sz; } diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 47437502d3..17dc9bddd8 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -79,7 +79,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) [[nodiscard]] HRESULT GdiEngine::ScrollFrame() noexcept { // If we don't have any scrolling to do, return early. - RETURN_HR_IF(S_OK, 0 == _szInvalidScroll.cx && 0 == _szInvalidScroll.cy); + RETURN_HR_IF(S_OK, 0 == _szInvalidScroll.width && 0 == _szInvalidScroll.height); // If we have an inverted cursor, we have to see if we have to clean it before we scroll to prevent // left behind cursor copies in the scrolled region. @@ -115,20 +115,20 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) // We have to limit the region that can be scrolled to not include the gutters. // Gutters are defined as sub-character width pixels at the bottom or right of the screen. const auto coordFontSize = _GetFontSize(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0); til::size szGutter; - szGutter.cx = _szMemorySurface.cx % coordFontSize.X; - szGutter.cy = _szMemorySurface.cy % coordFontSize.Y; + szGutter.width = _szMemorySurface.width % coordFontSize.width; + szGutter.height = _szMemorySurface.height % coordFontSize.height; RECT rcScrollLimit{}; - RETURN_IF_FAILED(LongSub(_szMemorySurface.cx, szGutter.cx, &rcScrollLimit.right)); - RETURN_IF_FAILED(LongSub(_szMemorySurface.cy, szGutter.cy, &rcScrollLimit.bottom)); + RETURN_IF_FAILED(LongSub(_szMemorySurface.width, szGutter.width, &rcScrollLimit.right)); + RETURN_IF_FAILED(LongSub(_szMemorySurface.height, szGutter.height, &rcScrollLimit.bottom)); // Scroll real window and memory buffer in-sync. LOG_LAST_ERROR_IF(!ScrollWindowEx(_hwndTargetWindow, - _szInvalidScroll.cx, - _szInvalidScroll.cy, + _szInvalidScroll.width, + _szInvalidScroll.height, &rcScrollLimit, &rcScrollLimit, nullptr, @@ -136,7 +136,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) 0)); til::rect rcUpdate; - LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.cx, _szInvalidScroll.cy, &rcScrollLimit, &rcScrollLimit, nullptr, rcUpdate.as_win32_rect()))); + LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.width, _szInvalidScroll.height, &rcScrollLimit, &rcScrollLimit, nullptr, rcUpdate.as_win32_rect()))); LOG_IF_FAILED(_InvalidCombine(&rcUpdate)); @@ -161,7 +161,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) // Only do work if the existing memory surface is a different size from the client area. // Return quickly if they're the same. - RETURN_HR_IF(S_OK, _szMemorySurface.cx == szClient.cx && _szMemorySurface.cy == szClient.cy); + RETURN_HR_IF(S_OK, _szMemorySurface.width == szClient.width && _szMemorySurface.height == szClient.height); wil::unique_hdc hdcRealWindow(GetDC(_hwndTargetWindow)); RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow.get()); @@ -174,7 +174,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get()); // Make the new bitmap we'll use going forward with the new size. - wil::unique_hbitmap hbitmapNew(CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy)); + wil::unique_hbitmap hbitmapNew(CreateCompatibleBitmap(hdcRealWindow.get(), szClient.width, szClient.height)); RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get()); // Select it into the DC, but hold onto the junky one pixel bitmap (made by default) to give back when we need to Delete. @@ -183,7 +183,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) hbitmapNew.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object. // Blt from the DC/bitmap we're already holding onto into the new one. - RETURN_HR_IF(E_FAIL, !(BitBlt(hdcTemp.get(), 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY))); + RETURN_HR_IF(E_FAIL, !(BitBlt(hdcTemp.get(), 0, 0, _szMemorySurface.width, _szMemorySurface.height, _hdcMemoryContext, 0, 0, SRCCOPY))); // Put the junky bitmap back into the temp DC and get our new one out. hbitmapNew.reset(SelectBitmap(hdcTemp.get(), hbitmapOnePixelJunk.get())); @@ -199,7 +199,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) } else { - _hbitmapMemorySurface = CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy); + _hbitmapMemorySurface = CreateCompatibleBitmap(hdcRealWindow.get(), szClient.width, szClient.height); RETURN_HR_IF_NULL(E_FAIL, _hbitmapMemorySurface); wil::unique_hbitmap hOldBitmap(SelectBitmap(_hdcMemoryContext, _hbitmapMemorySurface)); // DC has a default junk bitmap, take it and delete it. @@ -228,7 +228,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) const auto pt = _GetInvalidRectPoint(); const auto sz = _GetInvalidRectSize(); - LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.cx, sz.cy, _hdcMemoryContext, pt.x, pt.y, SRCCOPY))); + LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.width, sz.height, _hdcMemoryContext, pt.x, pt.y, SRCCOPY))); WHEN_DBG(_DebugBltAll()); _rcInvalid = {}; @@ -322,7 +322,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) // See: Win7: 390673, 447839 and then superseded by http://osgvsowi/638274 when FE/non-FE rendering condensed. //#define CONSOLE_EXTTEXTOUT_FLAGS ETO_OPAQUE | ETO_CLIPPED //#define MAX_POLY_LINES 80 -[[nodiscard]] HRESULT GdiEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT GdiEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool trimLeft, const bool /*lineWrapped*/) noexcept @@ -361,7 +361,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) const auto text = cluster.GetText(); polyString += text; polyString.back() &= softFontCharMask; - polyWidth.push_back(gsl::narrow(cluster.GetColumns()) * coordFontSize.X); + polyWidth.push_back(gsl::narrow(cluster.GetColumns()) * coordFontSize.width); cchCharWidths += polyWidth.back(); polyWidth.append(text.size() - 1, 0); } @@ -407,7 +407,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) // If the line rendition is double height, we need to adjust the top or bottom // of the clipping rect to clip half the height of the rendered characters. - const auto halfHeight = coordFontSize.Y >> 1; + const auto halfHeight = coordFontSize.height >> 1; const auto topOffset = _currentLineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0; const auto bottomOffset = _currentLineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0; @@ -419,12 +419,12 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) pPolyTextLine->rcl.left = pPolyTextLine->x; pPolyTextLine->rcl.top = pPolyTextLine->y + topOffset; pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + (til::CoordType)cchCharWidths; - pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.Y - bottomOffset; + pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.height - bottomOffset; pPolyTextLine->pdx = polyWidth.data(); if (trimLeft) { - pPolyTextLine->rcl.left += coordFontSize.X; + pPolyTextLine->rcl.left += coordFontSize.width; } _cPolyText++; @@ -532,8 +532,8 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); }); // Get the font size so we know the size of the rectangle lines we'll be inscribing. - const auto fontWidth = _GetFontSize().X; - const auto fontHeight = _GetFontSize().Y; + const auto fontWidth = _GetFontSize().width; + const auto fontHeight = _GetFontSize().height; const auto widthOfAllCells = fontWidth * gsl::narrow_cast(cchLine); const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) { @@ -611,19 +611,19 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) LOG_IF_FAILED(_FlushBufferLines()); const auto coordFontSize = _GetFontSize(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0); // First set up a block cursor the size of the font. RECT rcBoundaries; - rcBoundaries.left = options.coordCursor.X * coordFontSize.X; - rcBoundaries.top = options.coordCursor.Y * coordFontSize.Y; - rcBoundaries.right = rcBoundaries.left + coordFontSize.X; - rcBoundaries.bottom = rcBoundaries.top + coordFontSize.Y; + rcBoundaries.left = options.coordCursor.x * coordFontSize.width; + rcBoundaries.top = options.coordCursor.y * coordFontSize.height; + rcBoundaries.right = rcBoundaries.left + coordFontSize.width; + rcBoundaries.bottom = rcBoundaries.top + coordFontSize.height; // If we're double-width cursor, make it an extra font wider. if (options.fIsDoubleWidth) { - rcBoundaries.right = rcBoundaries.right + coordFontSize.X; + rcBoundaries.right = rcBoundaries.right + coordFontSize.width; } // Make a set of RECTs to paint. @@ -641,7 +641,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) ulHeight = std::max(ulHeight, s_ulMinCursorHeightPercent); // No smaller than 25% ulHeight = std::min(ulHeight, s_ulMaxCursorHeightPercent); // No larger than 100% - ulHeight = MulDiv(coordFontSize.Y, ulHeight, 100); // divide by 100 because percent. + ulHeight = MulDiv(coordFontSize.height, ulHeight, 100); // divide by 100 because percent. // Reduce the height of the top to be relative to the bottom by the height we want. rcInvert.top = rcInvert.bottom - ulHeight; @@ -851,7 +851,7 @@ void GdiEngine::_DebugBltAll() const { if (_fDebug) { - BitBlt(_debugContext, 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY); + BitBlt(_debugContext, 0, 0, _szMemorySurface.width, _szMemorySurface.height, _hdcMemoryContext, 0, 0, SRCCOPY); Sleep(100); } } diff --git a/src/renderer/gdi/precomp.h b/src/renderer/gdi/precomp.h index 4b402939d5..12d9ade5b2 100644 --- a/src/renderer/gdi/precomp.h +++ b/src/renderer/gdi/precomp.h @@ -20,24 +20,6 @@ Abstract: #include #include -#ifndef _NTSTATUS_DEFINED -#define _NTSTATUS_DEFINED -typedef _Return_type_success_(return >= 0) long NTSTATUS; -#endif - -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) - -//#include -#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth -#define FACILITY_NTWIN32 0x7 -__inline int NTSTATUS_FROM_WIN32(long x) -{ - return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x)&0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); -} - -#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) -#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); - #if defined(DEBUG) || defined(_DEBUG) || defined(DBG) #define WHEN_DBG(x) x #else diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 4baa618082..556f2df02f 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -215,7 +215,7 @@ GdiEngine::~GdiEngine() { XFORM lineTransform = {}; // The X delta is to account for the horizontal viewport offset. - lineTransform.eDx = viewportLeft ? -1.0f * viewportLeft * _GetFontSize().X : 0.0f; + lineTransform.eDx = viewportLeft ? -1.0f * viewportLeft * _GetFontSize().width : 0.0f; switch (lineRendition) { case LineRendition::SingleWidth: @@ -230,14 +230,14 @@ GdiEngine::~GdiEngine() lineTransform.eM11 = 2; // double width lineTransform.eM22 = 2; // double height // The Y delta is to negate the offset caused by the scaled height. - lineTransform.eDy = -1.0f * targetRow * _GetFontSize().Y; + lineTransform.eDy = -1.0f * targetRow * _GetFontSize().height; break; case LineRendition::DoubleHeightBottom: lineTransform.eM11 = 2; // double width lineTransform.eM22 = 2; // double height // The Y delta is to negate the offset caused by the scaled height. // An extra row is added because we need the bottom half of the line. - lineTransform.eDy = -1.0f * (targetRow + 1) * _GetFontSize().Y; + lineTransform.eDy = -1.0f * (targetRow + 1) * _GetFontSize().height; break; } // Return early if the new matrix is the same as the current transform. @@ -387,7 +387,7 @@ GdiEngine::~GdiEngine() // However, we don't want the underline to extend past the bottom of the // cell, so we clamp the offset to fit just inside. - const auto maxUnderlineOffset = Font.GetSize().Y - _lineMetrics.underlineWidth; + const auto maxUnderlineOffset = Font.GetSize().height - _lineMetrics.underlineWidth; _lineMetrics.underlineOffset2 = std::min(_lineMetrics.underlineOffset2, maxUnderlineOffset); // But if the resulting gap isn't big enough even to register as a thicker @@ -440,7 +440,7 @@ GdiEngine::~GdiEngine() // - centeringHint - The horizontal extent that glyphs are offset from center. // Return Value: // - S_OK if successful. E_FAIL if there was an error. -[[nodiscard]] HRESULT GdiEngine::UpdateSoftFont(const gsl::span bitPattern, +[[nodiscard]] HRESULT GdiEngine::UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept { @@ -566,8 +566,8 @@ GdiEngine::~GdiEngine() // attention to the font previews to ensure that the font being selected by GDI is exactly the font requested -- // some monospace fonts look very similar. LOGFONTW lf = { 0 }; - lf.lfHeight = s_ScaleByDpi(coordFontRequested.Y, iDpi); - lf.lfWidth = s_ScaleByDpi(coordFontRequested.X, iDpi); + lf.lfHeight = s_ScaleByDpi(coordFontRequested.height, iDpi); + lf.lfWidth = s_ScaleByDpi(coordFontRequested.width, iDpi); lf.lfWeight = FontDesired.GetWeight(); // If we're searching for Terminal, our supported Raster Font, then we must use OEM_CHARSET. @@ -627,8 +627,8 @@ GdiEngine::~GdiEngine() RETURN_HR_IF(E_FAIL, !(GetTextExtentPoint32W(hdcTemp.get(), L"0", 1, &sz))); til::size coordFont; - coordFont.X = sz.cx; - coordFont.Y = sz.cy; + coordFont.width = sz.cx; + coordFont.height = sz.cy; // The extent point won't necessarily be perfect for the width, so get the ABC metrics for the 0 if possible to improve the measurement. // This will fail for non-TrueType fonts and we'll fall back to what GetTextExtentPoint said. @@ -641,7 +641,7 @@ GdiEngine::~GdiEngine() // No negatives or zeros or we'll have bad character-to-pixel math later. if (abcTotal > 0) { - coordFont.X = abcTotal; + coordFont.width = abcTotal; } } } @@ -662,9 +662,9 @@ GdiEngine::~GdiEngine() { coordFontRequested = coordFont; } - else if (coordFontRequested.X == 0) + else if (coordFontRequested.width == 0) { - coordFontRequested.X = s_ShrinkByDpi(coordFont.X, iDpi); + coordFontRequested.width = s_ShrinkByDpi(coordFont.width, iDpi); } Font.SetFromEngine(currentFaceName, diff --git a/src/renderer/inc/CSSLengthPercentage.h b/src/renderer/inc/CSSLengthPercentage.h new file mode 100644 index 0000000000..a9a82e13e5 --- /dev/null +++ b/src/renderer/inc/CSSLengthPercentage.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +struct CSSLengthPercentage +{ + enum class ReferenceFrame : uint8_t + { + // This indicates the object is empty/unset. + // No need to call Resolve(). + None, + // Call Resolve() with factor set to the target DPI (e.g. 96 for DIP). + // Returns an absolute length value scaled by that DPI. + Absolute, + // Call Resolve() with factor set to the font size + // in an arbitrary DPI. Returns a value relative to it. + FontSize, + // Call Resolve() with factor set to the "0" glyph advance width + // in an arbitrary DPI. Returns a value relative to it. + AdvanceWidth, + }; + + static CSSLengthPercentage FromString(const wchar_t* str); + + float Resolve(float fallback, float dpi, float fontSize, float advanceWidth) const noexcept; + +private: + float _value = 0; + ReferenceFrame _referenceFrame = ReferenceFrame::None; +}; diff --git a/src/renderer/inc/FontInfoBase.hpp b/src/renderer/inc/FontInfoBase.hpp index 2ef5478f84..70ba4bd120 100644 --- a/src/renderer/inc/FontInfoBase.hpp +++ b/src/renderer/inc/FontInfoBase.hpp @@ -20,8 +20,8 @@ Author(s): #include "IFontDefaultList.hpp" -static constexpr wchar_t DEFAULT_TT_FONT_FACENAME[]{ L"__DefaultTTFont__" }; -static constexpr wchar_t DEFAULT_RASTER_FONT_FACENAME[]{ L"Terminal" }; +inline constexpr wchar_t DEFAULT_TT_FONT_FACENAME[]{ L"__DefaultTTFont__" }; +inline constexpr wchar_t DEFAULT_RASTER_FONT_FACENAME[]{ L"Terminal" }; class FontInfoBase { diff --git a/src/renderer/inc/FontInfoDesired.hpp b/src/renderer/inc/FontInfoDesired.hpp index 5e77e466f9..8464328318 100644 --- a/src/renderer/inc/FontInfoDesired.hpp +++ b/src/renderer/inc/FontInfoDesired.hpp @@ -20,6 +20,7 @@ Author(s): #include "FontInfoBase.hpp" #include "FontInfo.hpp" +#include "CSSLengthPercentage.h" class FontInfoDesired : public FontInfoBase { @@ -33,6 +34,10 @@ public: bool operator==(const FontInfoDesired& other) = delete; + void SetCellSize(const CSSLengthPercentage& cellWidth, const CSSLengthPercentage& cellHeight) noexcept; + + const CSSLengthPercentage& GetCellWidth() const noexcept; + const CSSLengthPercentage& GetCellHeight() const noexcept; float GetFontSize() const noexcept; til::size GetEngineSize() const noexcept; bool IsDefaultRasterFont() const noexcept; @@ -40,4 +45,6 @@ public: private: til::size _coordSizeDesired; float _fontSize; + CSSLengthPercentage _cellWidth; + CSSLengthPercentage _cellHeight; }; diff --git a/src/renderer/inc/FontResource.hpp b/src/renderer/inc/FontResource.hpp index aa159ed346..b76fb7f0e2 100644 --- a/src/renderer/inc/FontResource.hpp +++ b/src/renderer/inc/FontResource.hpp @@ -21,7 +21,7 @@ namespace Microsoft::Console::Render class FontResource { public: - FontResource(const gsl::span bitPattern, + FontResource(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, const size_t centeringHint); @@ -33,7 +33,7 @@ namespace Microsoft::Console::Render private: void _regenerateFont(); - void _resizeBitPattern(gsl::span targetBuffer); + void _resizeBitPattern(std::span targetBuffer); std::vector _bitPattern; til::size _sourceSize; diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index 1477c2bd78..69e0dc2a0e 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -16,7 +16,6 @@ Author(s): #include "../../host/conimeinfo.h" #include "../../buffer/out/TextAttribute.hpp" -#include "../../types/IBaseData.h" class Cursor; @@ -37,15 +36,21 @@ namespace Microsoft::Console::Render const Microsoft::Console::Types::Viewport region; }; - class IRenderData : public Microsoft::Console::Types::IBaseData + class IRenderData { public: - ~IRenderData() = 0; - IRenderData(const IRenderData&) = default; - IRenderData(IRenderData&&) = default; - IRenderData& operator=(const IRenderData&) = default; - IRenderData& operator=(IRenderData&&) = default; + virtual ~IRenderData() = default; + // This block used to be IBaseData. + virtual Microsoft::Console::Types::Viewport GetViewport() noexcept = 0; + virtual til::point GetTextBufferEndPosition() const noexcept = 0; + virtual const TextBuffer& GetTextBuffer() const noexcept = 0; + virtual const FontInfo& GetFontInfo() const noexcept = 0; + virtual std::vector GetSelectionRects() noexcept = 0; + virtual void LockConsole() noexcept = 0; + virtual void UnlockConsole() noexcept = 0; + + // This block used to be the original IRenderData. virtual til::point GetCursorPosition() const noexcept = 0; virtual bool IsCursorVisible() const noexcept = 0; virtual bool IsCursorOn() const noexcept = 0; @@ -53,21 +58,21 @@ namespace Microsoft::Console::Render virtual CursorType GetCursorStyle() const noexcept = 0; virtual ULONG GetCursorPixelWidth() const noexcept = 0; virtual bool IsCursorDoubleWidth() const = 0; - virtual const std::vector GetOverlays() const noexcept = 0; - virtual const bool IsGridLineDrawingAllowed() noexcept = 0; virtual const std::wstring_view GetConsoleTitle() const noexcept = 0; - virtual const std::wstring GetHyperlinkUri(uint16_t id) const = 0; virtual const std::wstring GetHyperlinkCustomId(uint16_t id) const = 0; - virtual const std::vector GetPatternId(const til::point location) const = 0; - protected: - IRenderData() = default; + // This block used to be IUiaData. + virtual std::pair GetAttributeColors(const TextAttribute& attr) const noexcept = 0; + virtual const bool IsSelectionActive() const = 0; + virtual const bool IsBlockSelection() const = 0; + virtual void ClearSelection() = 0; + 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 const bool IsUiaDataInitialized() const noexcept = 0; }; - - // See docs/virtual-dtors.md for an explanation of why this is weird. - inline IRenderData::~IRenderData() = default; } diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 06a756c627..46221ae911 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -32,23 +32,23 @@ namespace Microsoft::Console::Render std::optional cursorInfo; }; + enum class GridLines + { + None, + Top, + Bottom, + Left, + Right, + Underline, + DoubleUnderline, + Strikethrough, + HyperlinkUnderline + }; + using GridLineSet = til::enumset; + class __declspec(novtable) IRenderEngine { public: - enum class GridLines - { - None, - Top, - Bottom, - Left, - Right, - Underline, - DoubleUnderline, - Strikethrough, - HyperlinkUnderline - }; - using GridLineSet = til::enumset; - #pragma warning(suppress : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21). virtual ~IRenderEngine() = default; @@ -72,17 +72,17 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0; [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0; [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0; - [[nodiscard]] virtual HRESULT PaintBufferLine(gsl::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0; + [[nodiscard]] virtual HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0; [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept = 0; [[nodiscard]] virtual HRESULT PaintSelection(const til::rect& rect) noexcept = 0; [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; - [[nodiscard]] virtual HRESULT UpdateSoftFont(gsl::span bitPattern, til::size cellSize, size_t centeringHint) noexcept = 0; + [[nodiscard]] virtual HRESULT UpdateSoftFont(std::span bitPattern, til::size cellSize, size_t centeringHint) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDpi(int iDpi) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept = 0; [[nodiscard]] virtual HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, int iDpi) noexcept = 0; - [[nodiscard]] virtual HRESULT GetDirtyArea(gsl::span& area) noexcept = 0; + [[nodiscard]] virtual HRESULT GetDirtyArea(std::span& area) noexcept = 0; [[nodiscard]] virtual HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept = 0; [[nodiscard]] virtual HRESULT IsGlyphWideByFont(std::wstring_view glyph, _Out_ bool* pResult) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateTitle(std::wstring_view newTitle) noexcept = 0; @@ -92,24 +92,24 @@ namespace Microsoft::Console::Render // DxRenderer - getter virtual HRESULT Enable() noexcept { return S_OK; } - virtual [[nodiscard]] std::wstring_view GetPixelShaderPath() noexcept { return {}; } - virtual [[nodiscard]] bool GetRetroTerminalEffect() const noexcept { return false; } - virtual [[nodiscard]] float GetScaling() const noexcept { return 1; } - virtual [[nodiscard]] Types::Viewport GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept { return Types::Viewport::Empty(); } - virtual [[nodiscard]] Types::Viewport GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept { return Types::Viewport::Empty(); } + [[nodiscard]] virtual std::wstring_view GetPixelShaderPath() noexcept { return {}; } + [[nodiscard]] virtual bool GetRetroTerminalEffect() const noexcept { return false; } + [[nodiscard]] virtual float GetScaling() const noexcept { return 1; } + [[nodiscard]] virtual Types::Viewport GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept { return Types::Viewport::Empty(); } + [[nodiscard]] virtual Types::Viewport GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept { return Types::Viewport::Empty(); } // DxRenderer - setter virtual void SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept {} virtual void SetCallback(std::function pfn) noexcept {} virtual void EnableTransparentBackground(const bool isTransparent) noexcept {} virtual void SetForceFullRepaintRendering(bool enable) noexcept {} - virtual [[nodiscard]] HRESULT SetHwnd(const HWND hwnd) noexcept { return E_NOTIMPL; } + [[nodiscard]] virtual HRESULT SetHwnd(const HWND hwnd) noexcept { return E_NOTIMPL; } virtual void SetPixelShaderPath(std::wstring_view value) noexcept {} virtual void SetRetroTerminalEffect(bool enable) noexcept {} virtual void SetSelectionBackground(const COLORREF color, const float alpha = 0.5f) noexcept {} virtual void SetSoftwareRendering(bool enable) noexcept {} virtual void SetWarningCallback(std::function pfn) noexcept {} - virtual [[nodiscard]] HRESULT SetWindowSize(const til::size pixels) noexcept { return E_NOTIMPL; } - virtual [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept { return E_NOTIMPL; } + [[nodiscard]] virtual HRESULT SetWindowSize(const til::size pixels) noexcept { return E_NOTIMPL; } + [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept { return E_NOTIMPL; } virtual void UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept {} }; } diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index eea9005102..8c33d59b1e 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -30,7 +30,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; - [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + [[nodiscard]] HRESULT UpdateSoftFont(const std::span bitPattern, const til::size cellSize, const size_t centeringHint) noexcept override; @@ -41,7 +41,7 @@ namespace Microsoft::Console::Render const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override; - [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override; + [[nodiscard]] bool RequiresContinuousRedraw() noexcept override; [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override; diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index 897d8e099d..4b6e7c3981 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -33,7 +33,6 @@ namespace Microsoft::Console::Render bool GetRenderMode(const Mode mode) const noexcept; const std::array& GetColorTable() const noexcept; void ResetColorTable() noexcept; - void MakeAdjustedColorArray() noexcept; void SetColorTableEntry(const size_t tableIndex, const COLORREF color); COLORREF GetColorTableEntry(const size_t tableIndex) const; void SetColorAlias(const ColorAlias alias, const size_t tableIndex, const COLORREF color); @@ -48,7 +47,6 @@ namespace Microsoft::Console::Render til::enumset _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright }; std::array _colorTable; std::array(ColorAlias::ENUM_COUNT)> _colorAliasIndices; - std::array, 19> _adjustedForegroundColors; size_t _blinkCycle = 0; mutable bool _blinkIsInUse = false; bool _blinkShouldBeFaint = false; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index 2ac51ffccc..e58c227c56 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -126,7 +126,7 @@ CATCH_RETURN(); const auto newRect = rectangles.at(i); // if any value is different, selection has changed - if (prevRect.Top != newRect.Top || prevRect.Right != newRect.Right || prevRect.Left != newRect.Left || prevRect.Bottom != newRect.Bottom) + if (prevRect.top != newRect.top || prevRect.right != newRect.right || prevRect.left != newRect.left || prevRect.bottom != newRect.bottom) { _selectionChanged = true; _prevSelection = rectangles; @@ -175,6 +175,7 @@ try { _newOutput.append(newText); _newOutput.push_back(L'\n'); + _textBufferChanged = true; } return S_OK; } @@ -333,7 +334,7 @@ void UiaEngine::WaitUntilCanRender() noexcept // - fTrimLeft - Whether or not to trim off the left half of a double wide character // Return Value: // - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintBufferLine(const gsl::span /*clusters*/, +[[nodiscard]] HRESULT UiaEngine::PaintBufferLine(const std::span /*clusters*/, const til::point /*coord*/, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept @@ -462,7 +463,7 @@ void UiaEngine::WaitUntilCanRender() noexcept // - area - Rectangle describing dirty area in characters. // Return Value: // - S_OK. -[[nodiscard]] HRESULT UiaEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT UiaEngine::GetDirtyArea(std::span& area) noexcept { // Magic static is only valid because any instance of this object has the same behavior. // Use member variable instead if this ever changes. diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 16a6ae6630..5e486b7e0e 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -48,7 +48,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; @@ -57,7 +57,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, const int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp index d5d45a07d8..1b728aae50 100644 --- a/src/renderer/vt/VtSequences.cpp +++ b/src/renderer/vt/VtSequences.cpp @@ -172,10 +172,10 @@ using namespace Microsoft::Console::Render; { // VT coords start at 1,1 auto coordVt = coord; - coordVt.X++; - coordVt.Y++; + coordVt.x++; + coordVt.y++; - return _WriteFormatted(FMT_COMPILE("\x1b[{};{}H"), coordVt.Y, coordVt.X); + return _WriteFormatted(FMT_COMPILE("\x1b[{};{}H"), coordVt.y, coordVt.x); } // Method Description: @@ -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: @@ -332,6 +371,34 @@ using namespace Microsoft::Console::Render; return _Write(isFaint ? "\x1b[2m" : "\x1b[22m"); } +// Method Description: +// - 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: +// - 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::_SetUnderlineExtended(const UnderlineStyle style) noexcept +{ + 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 underline of the following text. // Arguments: @@ -343,17 +410,6 @@ using namespace Microsoft::Console::Render; return _Write(isUnderlined ? "\x1b[4m" : "\x1b[24m"); } -// Method Description: -// - Formats and writes a sequence to change the double underline of the following text. -// Arguments: -// - isUnderlined: If true, we'll doubly 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 -{ - return _Write(isUnderlined ? "\x1b[21m" : "\x1b[24m"); -} - // Method Description: // - Formats and writes a sequence to change the overline of the following text. // Arguments: diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index dc506322cc..7527db3dee 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) + // 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(_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()) - { - 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 6852706aa7..1482235158 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -69,7 +69,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, } else { - gsl::span dirty; + std::span dirty; RETURN_IF_FAILED(GetDirtyArea(dirty)); // If we have 0 or 1 dirty pieces in the area, set as appropriate. @@ -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; @@ -201,7 +201,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // the last cell of the row. We're in a deferred wrap, but the host // thinks the cursor is actually in-frame. // See ConptyRoundtripTests::DontWrapMoveCursorInSingleFrame - const auto cursorIsInDeferredWrap = (nextCursorPosition.X == _lastText.X - 1) && (nextCursorPosition.Y == _lastText.Y); + const auto cursorIsInDeferredWrap = (nextCursorPosition.x == _lastText.x - 1) && (nextCursorPosition.y == _lastText.y); // If all three of these conditions are true, then: // * cursorIsInDeferredWrap: The cursor is in a position where the line // filled the last cell of the row, but the host tried to paint it in @@ -242,9 +242,9 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, const auto originalPos = _lastText; _trace.TraceMoveCursor(_lastText, coord); auto performedSoftWrap = false; - if (coord.X != _lastText.X || coord.Y != _lastText.Y) + if (coord.x != _lastText.x || coord.y != _lastText.y) { - if (coord.X == 0 && coord.Y == 0) + if (coord.x == 0 && coord.y == 0) { _needToDisableCursor = true; hr = _CursorHome(); @@ -253,7 +253,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, { hr = _CursorPosition(coord); } - else if (coord.X == 0 && coord.Y == (_lastText.Y + 1)) + else if (coord.x == 0 && coord.y == (_lastText.y + 1)) { // Down one line, at the start of the line. @@ -262,7 +262,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, auto previousLineWrapped = false; if (_wrappedRow.has_value()) { - previousLineWrapped = coord.Y == _wrappedRow.value() + 1; + previousLineWrapped = coord.y == _wrappedRow.value() + 1; } if (previousLineWrapped) @@ -291,28 +291,28 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // otherwise we might accidentally break wrapped lines (GH#405) hr = _CursorPosition(coord); } - else if (coord.X == 0 && coord.Y == _lastText.Y) + else if (coord.x == 0 && coord.y == _lastText.y) { // Start of this line std::string seq = "\r"; hr = _Write(seq); } - else if (coord.X == _lastText.X && coord.Y == (_lastText.Y + 1)) + else if (coord.x == _lastText.x && coord.y == (_lastText.y + 1)) { // Down one line, same X position std::string seq = "\n"; hr = _Write(seq); } - else if (coord.X == (_lastText.X - 1) && coord.Y == (_lastText.Y)) + else if (coord.x == (_lastText.x - 1) && coord.y == (_lastText.y)) { // Back one char, same Y position std::string seq = "\b"; hr = _Write(seq); } - else if (coord.Y == _lastText.Y && coord.X > _lastText.X) + else if (coord.y == _lastText.y && coord.x > _lastText.x) { // Same line, forward some distance - auto distance = coord.X - _lastText.X; + auto distance = coord.x - _lastText.x; hr = _CursorForward(distance); } else @@ -381,7 +381,7 @@ try // tell us to do is print the new line at the bottom of the viewport, // and _MoveCursor will automatically give us the newline we want. // When that's implemented, we'll probably want to make sure to add a - // _lastText.Y += dy; + // _lastText.y += dy; // statement here. // Move the cursor to the bottom of the current viewport @@ -504,7 +504,7 @@ CATCH_RETURN(); // will be false. // Return Value: // - S_OK or suitable HRESULT error from writing pipe. -[[nodiscard]] HRESULT XtermEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT XtermEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool /*trimLeft*/, const bool lineWrapped) noexcept @@ -526,18 +526,17 @@ CATCH_RETURN(); RETURN_IF_FAILED(_fUseAsciiOnly ? VtEngine::_WriteTerminalAscii(wstr) : VtEngine::_WriteTerminalUtf8(wstr)); - // GH#4106, GH#2011 - WriteTerminalW is only ever called by the + // GH#4106, GH#2011, GH#13710 - WriteTerminalW is only ever called by the // StateMachine, when we've encountered a string we don't understand. When - // this happens, we usually don't actually trigger another frame, but we - // _do_ want this string to immediately be sent to the terminal. Since we - // only flush our buffer on actual frames, this means that strings we've - // decided to pass through would have gotten buffered here until the next - // actual frame is triggered. - // - // To fix this, flush here, so this string is sent to the connected terminal - // application. + // this happens, we will trigger a new frame in the renderer, and + // immediately buffer this wstr (representing the sequence we didn't + // understand). We won't immediately _Flush to the terminal - that might + // cause flickering (where we've buffered some state but not the whole + // "frame" as specified by the app). We'll just immediately buffer this + // sequence, and flush it when the render thread comes around to paint the + // frame normally. - return _Flush(); + return S_OK; } // Method Description: @@ -556,7 +555,8 @@ CATCH_RETURN(); { RETURN_IF_FAILED(_Write("\x1b[2t")); } - return _Flush(); + _Flush(); + return S_OK; } // Method Description: diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index c3a1af5f8b..6f1b480b61 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -43,7 +43,7 @@ namespace Microsoft::Console::Render const gsl::not_null pData, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 08d2cf06f3..7ef59bc03b 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -69,9 +69,9 @@ CATCH_RETURN(); // We should ignore the first one, but after that, if the client application // is moving the cursor around in the viewport, move our virtual top // up to meet their changes. - if (!_skipCursor && _virtualTop > psrRegion->Top) + if (!_skipCursor && _virtualTop > psrRegion->top) { - _virtualTop = psrRegion->Top; + _virtualTop = psrRegion->top; } _skipCursor = false; @@ -106,22 +106,13 @@ CATCH_RETURN(); // - S_OK [[nodiscard]] HRESULT VtEngine::InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept { - // If we're in the middle of a resize request, don't try to immediately start a frame. - if (_inResizeRequest) - { - *pForcePaint = false; - } - else - { - *pForcePaint = true; + *pForcePaint = true; - // Keep track of the fact that we circled, we'll need to do some work on - // end paint to specifically handle this. - _circled = circled; - } + // Keep track of the fact that we circled, we'll need to do some work on + // end paint to specifically handle this. + _circled = circled; _trace.TraceTriggerCircling(*pForcePaint); - return S_OK; } diff --git a/src/renderer/vt/math.cpp b/src/renderer/vt/math.cpp index c608ad8fdd..d86e4644ca 100644 --- a/src/renderer/vt/math.cpp +++ b/src/renderer/vt/math.cpp @@ -17,7 +17,7 @@ using namespace Microsoft::Console::Types; // This is an Inclusive rect. // Return Value: // - S_OK. -[[nodiscard]] HRESULT VtEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT VtEngine::GetDirtyArea(std::span& area) noexcept { area = _invalidMap.runs(); return S_OK; @@ -47,10 +47,10 @@ using namespace Microsoft::Console::Types; // - void VtEngine::_OrRect(_Inout_ til::inclusive_rect* const pRectExisting, const til::inclusive_rect* const pRectToOr) const { - pRectExisting->Left = std::min(pRectExisting->Left, pRectToOr->Left); - pRectExisting->Top = std::min(pRectExisting->Top, pRectToOr->Top); - pRectExisting->Right = std::max(pRectExisting->Right, pRectToOr->Right); - pRectExisting->Bottom = std::max(pRectExisting->Bottom, pRectToOr->Bottom); + pRectExisting->left = std::min(pRectExisting->left, pRectToOr->left); + pRectExisting->top = std::min(pRectExisting->top, pRectToOr->top); + pRectExisting->right = std::max(pRectExisting->right, pRectToOr->right); + pRectExisting->bottom = std::max(pRectExisting->bottom, pRectToOr->bottom); } // Method Description: @@ -83,7 +83,7 @@ bool VtEngine::_WillWriteSingleChar() const // character should follow this code path // (The immediate previous character would suggest a backspace) auto invalidIsNext = invalidPoint == _lastText; - auto invalidIsLast = invalidPoint == til::point{ _lastText.X - 1, _lastText.Y }; + auto invalidIsLast = invalidPoint == til::point{ _lastText.x - 1, _lastText.y }; return invalidIsNext || invalidIsLast; } diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index 0e8639749b..801390085c 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -94,8 +94,7 @@ using namespace Microsoft::Console::Types; RETURN_IF_FAILED(_MoveCursor(_deferredCursorPos)); } - RETURN_IF_FAILED(_Flush()); - + _Flush(); return S_OK; } @@ -176,7 +175,7 @@ using namespace Microsoft::Console::Types; // will be false. // Return Value: // - S_OK or suitable HRESULT error from writing pipe. -[[nodiscard]] HRESULT VtEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT VtEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept @@ -243,11 +242,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 @@ -256,9 +257,11 @@ using namespace Microsoft::Console::Types; RETURN_IF_FAILED(_SetGraphicsDefault()); _lastTextAttributes.SetDefaultBackground(); _lastTextAttributes.SetDefaultForeground(); + _lastTextAttributes.SetDefaultUnderlineColor(); _lastTextAttributes.SetDefaultRenditionAttributes(); lastFg = {}; lastBg = {}; + lastUl = {}; } if (fg != lastFg) @@ -303,6 +306,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; } @@ -389,7 +413,7 @@ using namespace Microsoft::Console::Types; // - coord - character coordinate target to render within viewport // Return Value: // - S_OK or suitable HRESULT error from writing pipe. -[[nodiscard]] HRESULT VtEngine::_PaintAsciiBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT VtEngine::_PaintAsciiBufferLine(const std::span clusters, const til::point coord) noexcept { try @@ -409,7 +433,7 @@ using namespace Microsoft::Console::Types; RETURN_IF_FAILED(VtEngine::_WriteTerminalAscii(_bufferLine)); // Update our internal tracker of the cursor's position - _lastText.X += totalWidth; + _lastText.x += totalWidth; return S_OK; } @@ -424,11 +448,11 @@ using namespace Microsoft::Console::Types; // - coord - character coordinate target to render within viewport // Return Value: // - S_OK or suitable HRESULT error from writing pipe. -[[nodiscard]] HRESULT VtEngine::_PaintUtf8BufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT VtEngine::_PaintUtf8BufferLine(const std::span clusters, const til::point coord, const bool lineWrapped) noexcept { - if (coord.Y < _virtualTop) + if (coord.y < _virtualTop) { return S_OK; } @@ -484,7 +508,7 @@ using namespace Microsoft::Console::Types; (!_newBottomLine) && (!_clearedAllThisFrame) && (!_lastTextAttributes.HasAnyVisualAttributes()); - const auto printingBottomLine = coord.Y == _lastViewport.BottomInclusive(); + const auto printingBottomLine = coord.y == _lastViewport.BottomInclusive(); // GH#5502 - If the background color of the "new bottom line" is different // than when we emitted the line, we can't optimize out the spaces from it. @@ -555,15 +579,15 @@ using namespace Microsoft::Console::Types; // line. if (lineWrapped) { - _wrappedRow = coord.Y; - _trace.TraceSetWrapped(coord.Y); + _wrappedRow = coord.y; + _trace.TraceSetWrapped(coord.y); } // Update our internal tracker of the cursor's position. // See MSFT:20266233 (which is also GH#357) // If the cursor is at the rightmost column of the terminal, and we write a // space, the cursor won't actually move to the next cell (which would - // be {0, _lastText.Y++}). The cursor will stay visibly in that last + // be {0, _lastText.y++}). The cursor will stay visibly in that last // cell until then next character is output. // If in that case, we increment the cursor position here (such that the X // position would be one past the right of the terminal), when we come @@ -575,9 +599,9 @@ using namespace Microsoft::Console::Types; // GH#1245: This needs to be RightExclusive, _not_ inclusive. Otherwise, we // won't update our internal cursor position tracker correctly at the last // character of the row. - if (_lastText.X < _lastViewport.RightExclusive()) + if (_lastText.x < _lastViewport.RightExclusive()) { - _lastText.X += columnsActual; + _lastText.x += columnsActual; } // GH#1245: If we wrote the exactly last char of the row, then we're in the // "delayed EOL wrap" state. Different terminals (conhost, gnome-terminal, @@ -585,7 +609,7 @@ using namespace Microsoft::Console::Types; // Mark that we're in the delayed EOL wrap state - we don't want to be // clever about how we move the cursor in this state, since different // terminals will handle a backspace differently in this state. - if (_lastText.X >= _lastViewport.RightInclusive()) + if (_lastText.x >= _lastViewport.RightInclusive()) { _delayedEolWrap = true; } @@ -598,13 +622,17 @@ using namespace Microsoft::Console::Types; // cursor somewhere else before the end of the frame, we'll move the // cursor to the deferred position at the end of the frame, or right // before we need to print new text. - _deferredCursorPos = { _lastText.X + numSpaces, _lastText.Y }; + _deferredCursorPos = { _lastText.x + numSpaces, _lastText.y }; - if (_deferredCursorPos.X <= _lastViewport.RightInclusive()) + if (_deferredCursorPos.x <= _lastViewport.RightInclusive()) { RETURN_IF_FAILED(_EraseCharacter(numSpaces)); } - else + // If we're past the end of the row (i.e. in the "delayed EOL wrap" + // state), then there is no need to erase the rest of line. In fact + // if we did output an EL sequence at this point, it could reset the + // "delayed EOL wrap" state, breaking subsequent output. + else if (_lastText.x <= _lastViewport.RightInclusive()) { RETURN_IF_FAILED(_EraseLine()); } @@ -615,7 +643,7 @@ using namespace Microsoft::Console::Types; // line is already empty. if (optimalToUseECH) { - _deferredCursorPos = { _lastText.X + numSpaces, _lastText.Y }; + _deferredCursorPos = { _lastText.x + numSpaces, _lastText.y }; } else if (numSpaces > 0 && removeSpaces) // if we deleted the spaces... re-add them { @@ -623,7 +651,7 @@ using namespace Microsoft::Console::Types; auto spaces = std::wstring(numSpaces, L' '); RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(spaces)); - _lastText.X += numSpaces; + _lastText.x += numSpaces; } } diff --git a/src/renderer/vt/precomp.h b/src/renderer/vt/precomp.h index 9043621d11..b560c6e0eb 100644 --- a/src/renderer/vt/precomp.h +++ b/src/renderer/vt/precomp.h @@ -19,24 +19,6 @@ Abstract: #include #include -#ifndef _NTSTATUS_DEFINED -#define _NTSTATUS_DEFINED -typedef _Return_type_success_(return >= 0) long NTSTATUS; -#endif - -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) - -//#include -#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth -#define FACILITY_NTWIN32 0x7 -__inline int NTSTATUS_FROM_WIN32(long x) -{ - return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x)&0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); -} - -#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) -#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); - #if defined(DEBUG) || defined(_DEBUG) || defined(DBG) #define WHEN_DBG(x) x #else diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 0d10195b1e..8408ee3aeb 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), @@ -46,11 +46,9 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _circled(false), _firstPaint(true), _skipCursor(false), - _exitResult{ S_OK }, _terminalOwner{ nullptr }, _newBottomLine{ false }, _deferredCursorPos{ INVALID_COORDS }, - _inResizeRequest{ false }, _trace{}, _bufferLine{}, _buffer{}, @@ -136,25 +134,39 @@ CATCH_RETURN(); CATCH_RETURN(); } -[[nodiscard]] HRESULT VtEngine::_Flush() noexcept +void VtEngine::_Flush() noexcept +{ + if (!_corked && !_buffer.empty()) + { + _flushImpl(); + } +} + +// _corked is often true and separating _flushImpl() out allows _flush() to be inlined. +void VtEngine::_flushImpl() noexcept { if (_hFile) { - auto fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); + const auto fSuccess = WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast(_buffer.size()), nullptr, nullptr); _buffer.clear(); if (!fSuccess) { - _exitResult = HRESULT_FROM_WIN32(GetLastError()); + LOG_LAST_ERROR(); _hFile.reset(); if (_terminalOwner) { _terminalOwner->CloseOutput(); } - return _exitResult; } } +} - return S_OK; +// The name of this method is an analogy to TCP_CORK. It instructs +// the VT renderer to stop flushing its buffer to the output pipe. +// Don't forget to uncork it! +void VtEngine::Cork(bool corked) noexcept +{ + _corked = corked; } // Method Description: @@ -272,14 +284,14 @@ CATCH_RETURN(); // Don't emit a resize event if we've requested it be suppressed if (!_suppressResizeRepaint) { - hr = _ResizeWindow(newSize.X, newSize.Y); + hr = _ResizeWindow(newSize.width, newSize.height); } if (_resizeQuirk) { // GH#3490 - When the viewport width changed, don't do anything extra here. // If the buffer had areas that were invalid due to the resize, then the - // buffer will have triggered it's own invalidations for what it knows is + // buffer will have triggered its own invalidations for what it knows is // invalid. Previously, we'd invalidate everything if the width changed, // because we couldn't be sure if lines were reflowed. _invalidMap.resize(newSize); @@ -291,7 +303,7 @@ CATCH_RETURN(); _invalidMap.resize(newSize, true); // resize while filling in new space with repaint requests. // Viewport is smaller now - just update it all. - if (oldSize.Y > newSize.Y || oldSize.X > newSize.X) + if (oldSize.height > newSize.height || oldSize.width > newSize.width) { hr = InvalidateAll(); } @@ -401,7 +413,7 @@ bool VtEngine::_AllIsInvalid() const // - S_OK [[nodiscard]] HRESULT VtEngine::InheritCursor(const til::point coordCursor) noexcept { - _virtualTop = coordCursor.Y; + _virtualTop = coordCursor.y; _lastText = coordCursor; _skipCursor = true; // Prevent us from clearing the entire viewport on the first paint @@ -425,7 +437,7 @@ void VtEngine::SetTerminalOwner(Microsoft::Console::VirtualTerminal::VtIo* const HRESULT VtEngine::RequestCursor() noexcept { RETURN_IF_FAILED(_RequestCursor()); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } @@ -446,39 +458,11 @@ HRESULT VtEngine::RequestCursor() noexcept return S_OK; } -// Method Description: -// - Tell the vt renderer to begin a resize operation. During a resize -// operation, the vt renderer should _not_ request to be repainted during a -// text buffer circling event. Any callers of this method should make sure to -// call EndResize to make sure the renderer returns to normal behavior. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtEngine::BeginResizeRequest() -{ - _inResizeRequest = true; -} - -// Method Description: -// - Tell the vt renderer to end a resize operation. -// See BeginResize for more details. -// See GH#1795 for context on this method. -// Arguments: -// - -// Return Value: -// - -void VtEngine::EndResizeRequest() -{ - _inResizeRequest = false; -} - // Method Description: // - Configure the renderer for the resize quirk. This changes the behavior of // conpty to _not_ InvalidateAll the entire viewport on a resize operation. // This is used by the Windows Terminal, because it is prepared to be -// connected to a conpty, and handles it's own buffer specifically for a +// connected to a conpty, and handles its own buffer specifically for a // conpty scenario. // - See also: GH#3490, #4354, #4741 // Arguments: @@ -541,15 +525,25 @@ void VtEngine::SetTerminalCursorTextPosition(const til::point cursor) noexcept // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. HRESULT VtEngine::RequestWin32Input() noexcept { + // It's important that any additional modes set here are also mirrored in + // the AdaptDispatch::HardReset method, since that needs to re-enable them + // in the connected terminal after passing through an RIS sequence. RETURN_IF_FAILED(_RequestWin32Input()); RETURN_IF_FAILED(_RequestFocusEventMode()); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } HRESULT VtEngine::SwitchScreenBuffer(const bool useAltBuffer) noexcept { RETURN_IF_FAILED(_SwitchScreenBuffer(useAltBuffer)); - RETURN_IF_FAILED(_Flush()); + _Flush(); return S_OK; } + +HRESULT VtEngine::RequestMouseMode(const bool enable) noexcept +{ + const auto status = _WriteFormatted(FMT_COMPILE("\x1b[?1003;1006{}"), enable ? 'h' : 'l'); + _Flush(); + return status; +} diff --git a/src/renderer/vt/tracing.cpp b/src/renderer/vt/tracing.cpp index 99a8b85dda..21f2bc6709 100644 --- a/src/renderer/vt/tracing.cpp +++ b/src/renderer/vt/tracing.cpp @@ -18,14 +18,14 @@ RenderTracing::RenderTracing() { #ifndef UNIT_TESTING TraceLoggingRegister(g_hConsoleVtRendererTraceProvider); -#endif UNIT_TESTING +#endif // UNIT_TESTING } RenderTracing::~RenderTracing() { #ifndef UNIT_TESTING TraceLoggingUnregister(g_hConsoleVtRendererTraceProvider); -#endif UNIT_TESTING +#endif // UNIT_TESTING } // Function Description: @@ -79,7 +79,7 @@ void RenderTracing::TraceStringFill(const size_t n, const char c) const #else UNREFERENCED_PARAMETER(n); UNREFERENCED_PARAMETER(c); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceString(const std::string_view& instr) const { @@ -96,7 +96,7 @@ void RenderTracing::TraceString(const std::string_view& instr) const } #else UNREFERENCED_PARAMETER(instr); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidate(const til::rect& invalidRect) const @@ -114,7 +114,7 @@ void RenderTracing::TraceInvalidate(const til::rect& invalidRect) const } #else UNREFERENCED_PARAMETER(invalidRect); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidateAll(const til::rect& viewport) const @@ -132,7 +132,7 @@ void RenderTracing::TraceInvalidateAll(const til::rect& viewport) const } #else UNREFERENCED_PARAMETER(viewport); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceTriggerCircling(const bool newFrame) const @@ -145,7 +145,7 @@ void RenderTracing::TraceTriggerCircling(const bool newFrame) const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); #else UNREFERENCED_PARAMETER(newFrame); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceInvalidateScroll(const til::point scroll) const @@ -215,7 +215,7 @@ void RenderTracing::TraceStartPaint(const bool quickReturn, UNREFERENCED_PARAMETER(scrollDelt); UNREFERENCED_PARAMETER(cursorMoved); UNREFERENCED_PARAMETER(wrappedRow); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceEndPaint() const @@ -226,7 +226,7 @@ void RenderTracing::TraceEndPaint() const TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceLastText(const til::point lastTextPos) const @@ -244,7 +244,7 @@ void RenderTracing::TraceLastText(const til::point lastTextPos) const } #else UNREFERENCED_PARAMETER(lastTextPos); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceScrollFrame(const til::point scrollDeltaPos) const @@ -262,7 +262,7 @@ void RenderTracing::TraceScrollFrame(const til::point scrollDeltaPos) const } #else UNREFERENCED_PARAMETER(scrollDeltaPos); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceMoveCursor(const til::point lastTextPos, const til::point cursor) const @@ -286,7 +286,7 @@ void RenderTracing::TraceMoveCursor(const til::point lastTextPos, const til::poi #else UNREFERENCED_PARAMETER(lastTextPos); UNREFERENCED_PARAMETER(cursor); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceWrapped() const @@ -302,7 +302,7 @@ void RenderTracing::TraceWrapped() const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceSetWrapped(const til::CoordType wrappedRow) const @@ -318,7 +318,7 @@ void RenderTracing::TraceSetWrapped(const til::CoordType wrappedRow) const } #else UNREFERENCED_PARAMETER(wrappedRow); -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TraceClearWrapped() const @@ -334,7 +334,7 @@ void RenderTracing::TraceClearWrapped() const TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } #else -#endif UNIT_TESTING +#endif // UNIT_TESTING } void RenderTracing::TracePaintCursor(const til::point coordCursor) const @@ -352,5 +352,5 @@ void RenderTracing::TracePaintCursor(const til::point coordCursor) const } #else UNREFERENCED_PARAMETER(coordCursor); -#endif UNIT_TESTING +#endif // UNIT_TESTING } diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index a7cdb7326e..15de6e0e76 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -61,7 +61,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ResetLineTransform() noexcept override; [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(gsl::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; + [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; @@ -69,7 +69,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDpi(int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(std::wstring_view glyph, _Out_ bool* pResult) noexcept override; @@ -90,6 +90,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT RequestWin32Input() noexcept; [[nodiscard]] virtual HRESULT SetWindowVisibility(const bool showOrHide) noexcept = 0; [[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer) noexcept; + [[nodiscard]] HRESULT RequestMouseMode(bool enable) noexcept; + void Cork(bool corked) noexcept; protected: wil::unique_hfile _hFile; @@ -127,11 +129,9 @@ namespace Microsoft::Console::Render bool _newBottomLine; til::point _deferredCursorPos; - HRESULT _exitResult; Microsoft::Console::VirtualTerminal::VtIo* _terminalOwner; Microsoft::Console::VirtualTerminal::RenderTracing _trace; - bool _inResizeRequest{ false }; std::optional _wrappedRow{ std::nullopt }; @@ -139,11 +139,13 @@ namespace Microsoft::Console::Render bool _resizeQuirk{ false }; bool _passthrough{ false }; + bool _corked{ false }; std::optional _newBottomLineBG{ std::nullopt }; [[nodiscard]] HRESULT _WriteFill(const size_t n, const char c) noexcept; [[nodiscard]] HRESULT _Write(std::string_view const str) noexcept; - [[nodiscard]] HRESULT _Flush() noexcept; + void _Flush() noexcept; + void _flushImpl() noexcept; template [[nodiscard]] HRESULT _WriteFormatted(S&& format, Args&&... args) @@ -181,6 +183,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; @@ -188,7 +194,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; @@ -216,18 +222,18 @@ namespace Microsoft::Console::Render // buffer space for these two functions to build their lines // so they don't have to alloc/free in a tight loop std::wstring _bufferLine; - [[nodiscard]] HRESULT _PaintUtf8BufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT _PaintUtf8BufferLine(const std::span clusters, const til::point coord, const bool lineWrapped) noexcept; - [[nodiscard]] HRESULT _PaintAsciiBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT _PaintAsciiBufferLine(const std::span clusters, const til::point coord) noexcept; [[nodiscard]] HRESULT _WriteTerminalUtf8(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalAscii(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalDrcs(const std::wstring_view str) noexcept; - [[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; /////////////////////////// Unit Testing Helpers /////////////////////////// #ifdef UNIT_TESTING diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index eb246b8128..378b1dfd08 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -258,7 +258,7 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT WddmConEngine::PaintBufferLine(const gsl::span clusters, +[[nodiscard]] HRESULT WddmConEngine::PaintBufferLine(const std::span clusters, const til::point coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept @@ -269,8 +269,8 @@ CATCH_RETURN() for (size_t i = 0; i < clusters.size() && i < gsl::narrow_cast(_displayWidth); i++) { - const auto OldChar = &_displayState[coord.Y]->Old[coord.X + i]; - const auto NewChar = &_displayState[coord.Y]->New[coord.X + i]; + const auto OldChar = &_displayState[coord.y]->Old[coord.x + i]; + const auto NewChar = &_displayState[coord.y]->New[coord.x + i]; OldChar->Character = NewChar->Character; OldChar->Attribute = NewChar->Attribute; @@ -279,7 +279,7 @@ CATCH_RETURN() NewChar->Attribute = _currentLegacyColorAttribute; } - return WDDMConUpdateDisplay(_hWddmConCtx, _displayState[coord.Y], FALSE); + return WDDMConUpdateDisplay(_hWddmConCtx, _displayState[coord.y], FALSE); } CATCH_RETURN(); } @@ -353,7 +353,7 @@ CATCH_RETURN() return S_OK; } -[[nodiscard]] HRESULT WddmConEngine::GetDirtyArea(gsl::span& area) noexcept +[[nodiscard]] HRESULT WddmConEngine::GetDirtyArea(std::span& area) noexcept { _dirtyArea.bottom = std::max(0, _displayHeight); _dirtyArea.right = std::max(0, _displayWidth); diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 87c3a799bd..a658ef5118 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -40,7 +40,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(const gsl::span clusters, + [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; @@ -60,7 +60,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const int iDpi) noexcept override; - [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override; + [[nodiscard]] HRESULT GetDirtyArea(std::span& area) noexcept override; [[nodiscard]] HRESULT GetFontSize(_Out_ til::size* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/wddmcon/oss_shim.h b/src/renderer/wddmcon/oss_shim.h index 1734c3647f..0f3fea1562 100644 --- a/src/renderer/wddmcon/oss_shim.h +++ b/src/renderer/wddmcon/oss_shim.h @@ -3,8 +3,6 @@ #pragma once -typedef _Return_type_success_(return >= 0) LONG NTSTATUS; - #include "../host/conddkrefs.h" #include diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index b6dbe3b8af..2449b12874 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -12,6 +12,29 @@ #include "../host/telemetry.hpp" #include "../host/cmdline.h" +// Assumes that it will find in the calling environment. +#define TraceConsoleAPICallWithOrigin(ApiName, ...) \ + TraceLoggingWrite( \ + g_hConhostV2EventTraceProvider, \ + "API_" ApiName, \ + TraceLoggingPid(TraceGetProcessId(m), "OriginatingProcess"), \ + TraceLoggingTid(TraceGetThreadId(m), "OriginatingThread"), \ + __VA_ARGS__ __VA_OPT__(, ) \ + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), \ + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + +static DWORD TraceGetProcessId(CONSOLE_API_MSG* const m) +{ + const auto p = m->GetProcessHandle(); + return p ? p->dwProcessId : 0; +} + +static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m) +{ + const auto p = m->GetProcessHandle(); + return p ? p->dwThreadId : 0; +} + [[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleCP(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const /*pbReplyPending*/) { @@ -35,35 +58,22 @@ { Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleMode); const auto a = &m->u.consoleMsgL1.GetConsoleMode; - std::wstring_view handleType = L"unknown"; - - TraceLoggingWrite(g_hConhostV2EventTraceProvider, - "API_GetConsoleMode", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingOpcode(WINEVENT_OPCODE_START)); - - auto tracing = wil::scope_exit([&]() { - Tracing::s_TraceApi(a, handleType); - TraceLoggingWrite(g_hConhostV2EventTraceProvider, - "API_GetConsoleMode", - TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), - TraceLoggingKeyword(TIL_KEYWORD_TRACE), - TraceLoggingOpcode(WINEVENT_OPCODE_STOP)); - }); const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + TraceConsoleAPICallWithOrigin( + "GetConsoleMode", + TraceLoggingBool(pObjectHandle->IsInputHandle(), "InputHandle")); + if (pObjectHandle->IsInputHandle()) { - handleType = L"input"; InputBuffer* pObj; RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_READ, &pObj)); m->_pApiRoutines->GetConsoleInputModeImpl(*pObj, a->Mode); } else { - handleType = L"output"; SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj)); m->_pApiRoutines->GetConsoleOutputModeImpl(*pObj, a->Mode); @@ -79,6 +89,12 @@ const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + TraceConsoleAPICallWithOrigin( + "SetConsoleMode", + TraceLoggingBool(pObjectHandle->IsInputHandle(), "InputHandle"), + TraceLoggingHexULong(a->Mode, "Mode")); + if (pObjectHandle->IsInputHandle()) { InputBuffer* pObj; @@ -151,47 +167,15 @@ const auto pInputReadHandleData = pHandleData->GetClientInput(); std::unique_ptr waiter; - HRESULT hr; - std::deque> outEvents; - const auto eventsToRead = cRecords; - if (a->Unicode) - { - if (fIsPeek) - { - hr = m->_pApiRoutines->PeekConsoleInputWImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - else - { - hr = m->_pApiRoutines->ReadConsoleInputWImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - } - else - { - if (fIsPeek) - { - hr = m->_pApiRoutines->PeekConsoleInputAImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - else - { - hr = m->_pApiRoutines->ReadConsoleInputAImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - } + InputEventQueue outEvents; + auto hr = m->_pApiRoutines->GetConsoleInputImpl( + *pInputBuffer, + outEvents, + cRecords, + *pInputReadHandleData, + a->Unicode, + fIsPeek, + waiter); // We must return the number of records in the message payload (to alert the client) // as well as in the message headers (below in SetReplyInformation) to alert the driver. @@ -228,19 +212,7 @@ } 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)) @@ -270,13 +242,8 @@ // Get output parameter buffer. PVOID pvBuffer; ULONG cbBufferSize; - // TODO: This is dumb. We should find out how much we need, not guess. - // If the request is not in Unicode mode, we must allocate an output buffer that is twice as big as the actual caller buffer. - RETURN_IF_FAILED(m->GetAugmentedOutputBuffer((a->Unicode != FALSE) ? 1 : 2, - &pvBuffer, - &cbBufferSize)); + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBufferSize)); - // TODO: This is also rather strange and will also probably make more sense if we stop guessing that we need 2x buffer to convert. // This might need to go on the other side of the fence (inside host) because the server doesn't know what we're going to do with initial num bytes. // (This restriction exists because it's going to copy initial into the final buffer, but we don't know that.) RETURN_HR_IF(E_INVALIDARG, a->InitialNumBytes > cbBufferSize); @@ -297,17 +264,23 @@ 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(); @@ -322,37 +295,18 @@ std::unique_ptr waiter; size_t cbWritten; - HRESULT hr; - if (a->Unicode) - { - const std::string_view initialData(pbInitialData.get(), cbInitialData); - const gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); - hr = m->_pApiRoutines->ReadConsoleWImpl(*pInputBuffer, + 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); - } - else - { - const std::string_view initialData(pbInitialData.get(), cbInitialData); - const gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); - hr = m->_pApiRoutines->ReadConsoleAImpl(*pInputBuffer, - outputBuffer, - cbWritten, // We must set the reply length in bytes. - waiter, - initialData, - exeView, - *pInputReadHandleData, - hConsoleClient, - a->CtrlWakeupMask, - a->ControlKeyState); - } LOG_IF_FAILED(SizeTToULong(cbWritten, &a->NumBytes)); @@ -402,9 +356,6 @@ // Get input parameter buffer PVOID pvBuffer; ULONG cbBufferSize; - auto tracing = wil::scope_exit([&]() { - Tracing::s_TraceApi(pvBuffer, a); - }); RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize)); std::unique_ptr waiter; @@ -422,6 +373,11 @@ const std::wstring_view buffer(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(wchar_t)); size_t cchInputRead; + TraceConsoleAPICallWithOrigin( + "WriteConsoleW", + TraceLoggingUInt32(a->NumBytes, "NumBytes"), + TraceLoggingCountedWideString(buffer.data(), static_cast(buffer.size()), "Buffer")); + hr = m->_pApiRoutines->WriteConsoleWImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter); // We must set the reply length in bytes. Convert back from characters. @@ -432,6 +388,11 @@ const std::string_view buffer(reinterpret_cast(pvBuffer), cbBufferSize); size_t cchInputRead; + TraceConsoleAPICallWithOrigin( + "WriteConsoleA", + TraceLoggingUInt32(a->NumBytes, "NumBytes"), + TraceLoggingCountedString(buffer.data(), static_cast(buffer.size()), "Buffer")); + hr = m->_pApiRoutines->WriteConsoleAImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter); // Reply length is already in bytes (chars), don't need to convert. @@ -618,10 +579,6 @@ Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleScreenBufferInfoEx); const auto a = &m->u.consoleMsgL2.GetConsoleScreenBufferInfo; - auto tracing = wil::scope_exit([&]() { - Tracing::s_TraceApi(a); - }); - CONSOLE_SCREEN_BUFFER_INFOEX ex = { 0 }; ex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); @@ -677,6 +634,12 @@ ex.wAttributes = a->Attributes; ex.wPopupAttributes = a->PopupAttributes; + TraceConsoleAPICallWithOrigin( + "SetConsoleScreenBufferInfoEx", + TraceLoggingConsoleCoord(a->Size, "BufferSize"), + TraceLoggingConsoleCoord(a->CurrentWindowSize, "WindowSize"), + TraceLoggingConsoleCoord(a->MaximumWindowSize, "MaxWindowSize")); + return m->_pApiRoutines->SetConsoleScreenBufferInfoExImpl(*pObj, ex); } @@ -692,6 +655,10 @@ SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + TraceConsoleAPICallWithOrigin( + "SetConsoleScreenBufferSize", + TraceLoggingConsoleCoord(a->Size, "BufferSize")); + return m->_pApiRoutines->SetConsoleScreenBufferSizeImpl(*pObj, til::wrap_coord_size(a->Size)); } @@ -768,15 +735,16 @@ Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleTextAttribute); const auto a = &m->u.consoleMsgL2.SetConsoleTextAttribute; - auto tracing = wil::scope_exit([&]() { - Tracing::s_TraceApi(a); - }); - const auto pObjectHandle = m->GetObjectHandle(); RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + TraceConsoleAPICallWithOrigin( + "SetConsoleTextAttribute", + TraceLoggingHexUInt16(a->Attributes, "Attributes")); + RETURN_HR(m->_pApiRoutines->SetConsoleTextAttributeImpl(*pObj, a->Attributes)); } @@ -792,6 +760,14 @@ SCREEN_INFORMATION* pObj; RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + TraceConsoleAPICallWithOrigin( + "SetConsoleWindowInfo", + TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"), + TraceLoggingInt32(a->Window.Left, "WindowRectLeft"), + TraceLoggingInt32(a->Window.Right, "WindowRectRight"), + TraceLoggingInt32(a->Window.Top, "WindowRectTop"), + TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom")); + return m->_pApiRoutines->SetConsoleWindowInfoImpl(*pObj, a->Absolute, til::wrap_small_rect(a->Window)); } @@ -833,20 +809,20 @@ { case CONSOLE_ATTRIBUTE: { - const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(WORD)); + const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(WORD)); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputAttributeImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } case CONSOLE_REAL_UNICODE: case CONSOLE_FALSE_UNICODE: { - const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); + const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterWImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } case CONSOLE_ASCII: { - const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer); + const std::span buffer(reinterpret_cast(pvBuffer), cbBuffer); RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterAImpl(*pScreenInfo, til::wrap_coord(a->ReadCoord), buffer, written)); break; } @@ -884,7 +860,7 @@ RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_WRITE, &pInputBuffer)); size_t written; - gsl::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(INPUT_RECORD)); + std::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(INPUT_RECORD)); if (!a->Unicode) { RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleInputAImpl(*pInputBuffer, buffer, written, !!a->Append)); @@ -924,12 +900,12 @@ // Validate parameters size_t regionArea; - RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().X, originalRegion.Dimensions().Y, ®ionArea)); + RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().width, originalRegion.Dimensions().height, ®ionArea)); size_t regionBytes; RETURN_IF_FAILED(SizeTMult(regionArea, sizeof(CHAR_INFO), ®ionBytes)); RETURN_HR_IF(E_INVALIDARG, cbSize < regionBytes); // If given fewer bytes on input than we need to do this write, it's invalid. - const gsl::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(CHAR_INFO)); + const std::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(CHAR_INFO)); if (!a->Unicode) { RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleOutputAImpl(*pScreenInfo, buffer, originalRegion, writtenRegion)); @@ -948,10 +924,6 @@ { const auto a = &m->u.consoleMsgL2.WriteConsoleOutputString; - auto tracing = wil::scope_exit([&]() { - Tracing::s_TraceApi(a); - }); - switch (a->StringType) { case CONSOLE_ATTRIBUTE: @@ -988,6 +960,11 @@ { const std::string_view text(reinterpret_cast(pvBuffer), cbBufferSize); + TraceConsoleAPICallWithOrigin( + "WriteConsoleOutputCharacterA", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(a->NumRecords, "NumRecords")); + hr = m->_pApiRoutines->WriteConsoleOutputCharacterAImpl(*pScreenInfo, text, til::wrap_coord(a->WriteCoord), @@ -1000,6 +977,11 @@ { const std::wstring_view text(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(wchar_t)); + TraceConsoleAPICallWithOrigin( + "WriteConsoleOutputCharacterW", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(a->NumRecords, "NumRecords")); + hr = m->_pApiRoutines->WriteConsoleOutputCharacterWImpl(*pScreenInfo, text, til::wrap_coord(a->WriteCoord), @@ -1009,7 +991,12 @@ } case CONSOLE_ATTRIBUTE: { - const gsl::span text(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(WORD)); + const std::span text(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(WORD)); + + TraceConsoleAPICallWithOrigin( + "WriteConsoleOutputAttribute", + TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"), + TraceLoggingUInt32(a->NumRecords, "NumRecords")); hr = m->_pApiRoutines->WriteConsoleOutputAttributeImpl(*pScreenInfo, text, @@ -1054,12 +1041,12 @@ // Validate parameters size_t regionArea; - RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().X, originalRegion.Dimensions().Y, ®ionArea)); + RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().width, originalRegion.Dimensions().height, ®ionArea)); size_t regionBytes; RETURN_IF_FAILED(SizeTMult(regionArea, sizeof(CHAR_INFO), ®ionBytes)); RETURN_HR_IF(E_INVALIDARG, regionArea > 0 && ((regionArea > ULONG_MAX / sizeof(CHAR_INFO)) || (cbBuffer < regionBytes))); - gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(CHAR_INFO)); + std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(CHAR_INFO)); auto finalRegion = Microsoft::Console::Types::Viewport::Empty(); // the actual region read out of the buffer if (!a->Unicode) { @@ -1098,7 +1085,7 @@ auto hr = S_OK; if (a->Unicode) { - gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); + std::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); size_t written; size_t needed; if (a->Original) @@ -1120,7 +1107,7 @@ } else { - gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer); + std::span buffer(reinterpret_cast(pvBuffer), cbBuffer); size_t written; size_t needed; if (a->Original) @@ -1258,38 +1245,38 @@ 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); } @@ -1305,20 +1292,22 @@ 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; @@ -1328,9 +1317,9 @@ size_t cbWritten; if (a->Unicode) { - const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); - const std::wstring_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe / sizeof(wchar_t)); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize / sizeof(wchar_t)); + 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); @@ -1340,9 +1329,9 @@ } else { - const std::string_view inputSource(reinterpret_cast(pvInputSource), cbInputSource); - const std::string_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize); + 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); @@ -1443,7 +1432,7 @@ if (a->Unicode) { const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength / sizeof(wchar_t)); + std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength / sizeof(wchar_t)); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesWImpl(inputExeName, outputBuffer, cchWritten)); @@ -1454,7 +1443,7 @@ else { const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength); + std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesAImpl(inputExeName, outputBuffer, cchWritten)); @@ -1482,7 +1471,7 @@ size_t cbWritten; if (a->Unicode) { - gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength / sizeof(wchar_t)); + std::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength / sizeof(wchar_t)); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesWImpl(outputBuffer, cchWritten)); @@ -1490,7 +1479,7 @@ } else { - gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength); + std::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesAImpl(outputBuffer, cchWritten)); @@ -1604,7 +1593,7 @@ if (a->Unicode) { const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer / sizeof(wchar_t)); + std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer / sizeof(wchar_t)); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryWImpl(inputExeName, outputBuffer, cchWritten)); @@ -1614,7 +1603,7 @@ else { const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); - gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer); + std::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer); size_t cchWritten; RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryAImpl(inputExeName, outputBuffer, cchWritten)); diff --git a/src/server/ApiDispatchersInternal.cpp b/src/server/ApiDispatchersInternal.cpp index caea87f527..062cb5e5aa 100644 --- a/src/server/ApiDispatchersInternal.cpp +++ b/src/server/ApiDispatchersInternal.cpp @@ -93,7 +93,6 @@ using Microsoft::Console::Interactivity::ServiceLocator; RETURN_IF_FAILED(gci.ProcessHandleList.AllocProcessData(a->ProcessGroupId, 0, a->ProcessGroupId, - ProcessHandle, nullptr)); } } diff --git a/src/server/ApiMessage.cpp b/src/server/ApiMessage.cpp index f30420c854..ff7b5ff768 100644 --- a/src/server/ApiMessage.cpp +++ b/src/server/ApiMessage.cpp @@ -131,17 +131,15 @@ CATCH_RETURN(); // Routine Description: // - This routine retrieves the output buffer associated with this message. It will allocate one if needed. -// The allocated will be bigger than the actual output size by the requested factor. // - Before completing the message, ReleaseMessageBuffers must be called to free any allocation performed by this routine. // Arguments: -// - Factor - Supplies the factor to multiply the allocated buffer by. +// - Message - Supplies the message whose output buffer will be retrieved. // - Buffer - Receives a pointer to the output buffer. // - Size - Receives the size, in bytes, of the output buffer. -// Return Value: +// Return Value: // - HRESULT indicating if the output buffer was successfully retrieved. -[[nodiscard]] HRESULT _CONSOLE_API_MSG::GetAugmentedOutputBuffer(const ULONG cbFactor, - _Outptr_result_bytebuffer_(*pcbSize) PVOID* const ppvBuffer, - _Out_ PULONG pcbSize) +[[nodiscard]] HRESULT _CONSOLE_API_MSG::GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, + _Out_ ULONG* const pcbSize) try { // Initialize the buffer if it hasn't been initialized yet. @@ -150,7 +148,6 @@ try RETURN_HR_IF(E_FAIL, State.WriteOffset > Descriptor.OutputSize); auto cbWriteSize = Descriptor.OutputSize - State.WriteOffset; - RETURN_IF_FAILED(ULongMult(cbWriteSize, cbFactor, &cbWriteSize)); // If we were previously called with a huge buffer we have an equally large _outputBuffer. // We shouldn't just keep this huge buffer around, if no one needs it anymore. @@ -176,21 +173,6 @@ try } CATCH_RETURN(); -// Routine Description: -// - This routine retrieves the output buffer associated with this message. It will allocate one if needed. -// - Before completing the message, ReleaseMessageBuffers must be called to free any allocation performed by this routine. -// Arguments: -// - Message - Supplies the message whose output buffer will be retrieved. -// - Buffer - Receives a pointer to the output buffer. -// - Size - Receives the size, in bytes, of the output buffer. -// Return Value: -// - HRESULT indicating if the output buffer was successfully retrieved. -[[nodiscard]] HRESULT _CONSOLE_API_MSG::GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, - _Out_ ULONG* const pcbSize) -{ - return GetAugmentedOutputBuffer(1, ppvBuffer, pcbSize); -} - // Routine Description: // - This routine releases output or input buffers that might have been allocated // during the processing of the given message. If the current completion status @@ -213,7 +195,7 @@ CATCH_RETURN(); if (State.OutputBuffer != nullptr) { - if (NT_SUCCESS(Complete.IoStatus.Status)) + if (SUCCEEDED_NTSTATUS(Complete.IoStatus.Status)) { CD_IO_OPERATION IoOperation; IoOperation.Identifier = Descriptor.Identifier; diff --git a/src/server/ApiMessage.h b/src/server/ApiMessage.h index 9e5ec97ee8..d730cff9e0 100644 --- a/src/server/ApiMessage.h +++ b/src/server/ApiMessage.h @@ -41,9 +41,6 @@ typedef struct _CONSOLE_API_MSG ConsoleHandleData* GetObjectHandle() const; [[nodiscard]] HRESULT ReadMessageInput(const ULONG cbOffset, _Out_writes_bytes_(cbSize) PVOID pvBuffer, const ULONG cbSize); - [[nodiscard]] HRESULT GetAugmentedOutputBuffer(const ULONG cbFactor, - _Outptr_result_bytebuffer_(*pcbSize) PVOID* ppvBuffer, - _Out_ PULONG pcbSize); [[nodiscard]] HRESULT GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); [[nodiscard]] HRESULT GetInputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); diff --git a/src/server/ApiSorter.cpp b/src/server/ApiSorter.cpp index 2593eb5806..39cf676e49 100644 --- a/src/server/ApiSorter.cpp +++ b/src/server/ApiSorter.cpp @@ -174,7 +174,6 @@ PCONSOLE_API_MSG ApiSorter::ConsoleDispatchRequest(_Inout_ PCONSOLE_API_MSG Mess // alias API. NTSTATUS Status = S_OK; { - const auto trace = Tracing::s_TraceApiCall(Status, Descriptor->TraceName); Status = (*Descriptor->Routine)(Message, &ReplyPending); } if (Status != STATUS_BUFFER_TOO_SMALL) diff --git a/src/server/IApiRoutines.h b/src/server/IApiRoutines.h index 9621b895d2..87830795c6 100644 --- a/src/server/IApiRoutines.h +++ b/src/server/IApiRoutines.h @@ -66,51 +66,25 @@ public: [[nodiscard]] virtual HRESULT GetNumberOfConsoleInputEventsImpl(const IConsoleInputObject& context, ULONG& events) noexcept = 0; - [[nodiscard]] virtual HRESULT PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; + [[nodiscard]] virtual HRESULT GetConsoleInputImpl(IConsoleInputObject& context, + InputEventQueue& outEvents, + const size_t eventReadCount, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool IsUnicode, + const bool IsPeek, + std::unique_ptr& waiter) noexcept = 0; - [[nodiscard]] virtual HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleAImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleWImpl(IConsoleInputObject& context, - gsl::span buffer, - size_t& written, - std::unique_ptr& waiter, - const std::string_view initialData, - const std::wstring_view exeName, - INPUT_READ_HANDLE_DATA& readHandleState, - const HANDLE clientHandle, - const DWORD controlWakeupMask, - DWORD& controlKeyState) noexcept = 0; + [[nodiscard]] virtual HRESULT 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, @@ -207,41 +181,41 @@ public: [[nodiscard]] virtual HRESULT ReadConsoleOutputAttributeImpl(const IConsoleOutputObject& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept = 0; [[nodiscard]] virtual HRESULT ReadConsoleOutputCharacterAImpl(const IConsoleOutputObject& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept = 0; [[nodiscard]] virtual HRESULT ReadConsoleOutputCharacterWImpl(const IConsoleOutputObject& context, const til::point origin, - gsl::span buffer, + std::span buffer, size_t& written) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleInputAImpl(IConsoleInputObject& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleInputWImpl(IConsoleInputObject& context, - const gsl::span buffer, + const std::span buffer, size_t& written, const bool append) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleOutputAImpl(IConsoleOutputObject& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleOutputWImpl(IConsoleOutputObject& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& requestRectangle, Microsoft::Console::Types::Viewport& writtenRectangle) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, - const gsl::span attrs, + const std::span attrs, const til::point target, size_t& used) noexcept = 0; @@ -256,28 +230,28 @@ public: size_t& used) noexcept = 0; [[nodiscard]] virtual HRESULT ReadConsoleOutputAImpl(const IConsoleOutputObject& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept = 0; [[nodiscard]] virtual HRESULT ReadConsoleOutputWImpl(const IConsoleOutputObject& context, - gsl::span buffer, + std::span buffer, const Microsoft::Console::Types::Viewport& sourceRectangle, Microsoft::Console::Types::Viewport& readRectangle) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleTitleAImpl(gsl::span title, + [[nodiscard]] virtual HRESULT GetConsoleTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleTitleWImpl(gsl::span title, + [[nodiscard]] virtual HRESULT GetConsoleTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleOriginalTitleAImpl(gsl::span title, + [[nodiscard]] virtual HRESULT GetConsoleOriginalTitleAImpl(std::span title, size_t& written, size_t& needed) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleOriginalTitleWImpl(gsl::span title, + [[nodiscard]] virtual HRESULT GetConsoleOriginalTitleWImpl(std::span title, size_t& written, size_t& needed) noexcept = 0; @@ -314,12 +288,12 @@ public: const std::wstring_view exeName) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleAliasAImpl(const std::string_view source, - gsl::span target, + std::span target, size_t& written, const std::string_view exeName) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleAliasWImpl(const std::wstring_view source, - gsl::span target, + std::span target, size_t& written, const std::wstring_view exeName) noexcept = 0; @@ -334,17 +308,17 @@ public: [[nodiscard]] virtual HRESULT GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleAliasesAImpl(const std::string_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleAliasesWImpl(const std::wstring_view exeName, - gsl::span alias, + std::span alias, size_t& written) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleAliasExesAImpl(gsl::span aliasExes, + [[nodiscard]] virtual HRESULT GetConsoleAliasExesAImpl(std::span aliasExes, size_t& written) noexcept = 0; - [[nodiscard]] virtual HRESULT GetConsoleAliasExesWImpl(gsl::span aliasExes, + [[nodiscard]] virtual HRESULT GetConsoleAliasExesWImpl(std::span aliasExes, size_t& written) noexcept = 0; #pragma region CMDext Private API @@ -366,11 +340,11 @@ public: size_t& length) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleCommandHistoryAImpl(const std::string_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept = 0; [[nodiscard]] virtual HRESULT GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, - gsl::span commandHistory, + std::span commandHistory, size_t& written) noexcept = 0; #pragma endregion diff --git a/src/server/IWaitRoutine.h b/src/server/IWaitRoutine.h index b73ea768c0..59ffa905bd 100644 --- a/src/server/IWaitRoutine.h +++ b/src/server/IWaitRoutine.h @@ -37,6 +37,11 @@ public: virtual ~IWaitRoutine() = default; + IWaitRoutine(const IWaitRoutine&) = delete; + IWaitRoutine(IWaitRoutine&&) = delete; + IWaitRoutine& operator=(const IWaitRoutine&) & = delete; + IWaitRoutine& operator=(IWaitRoutine&&) & = delete; + virtual void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) = 0; virtual bool Notify(const WaitTerminationReason TerminationReason, diff --git a/src/server/IoDispatchers.cpp b/src/server/IoDispatchers.cpp index 81be061f76..fb3f025c95 100644 --- a/src/server/IoDispatchers.cpp +++ b/src/server/IoDispatchers.cpp @@ -90,7 +90,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleCreateObject(_In_ PCONSOLE_API_MSG pMessa Status = STATUS_INVALID_PARAMETER; } - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { UnlockConsole(); pMessage->SetReplyStatus(Status); @@ -421,7 +421,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API LockConsole(); const auto cleanup = wil::scope_exit([&]() noexcept { - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { pReceiveMsg->SetReplyStatus(Status); if (ProcessData != nullptr) @@ -440,7 +440,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API CONSOLE_API_CONNECTINFO Cac; Status = ConsoleInitializeConnectInfo(pReceiveMsg, &Cac); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return pReceiveMsg; } @@ -452,10 +452,9 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API Status = NTSTATUS_FROM_HRESULT(gci.ProcessHandleList.AllocProcessData(dwProcessId, dwThreadId, Cac.ProcessGroupId, - nullptr, &ProcessData)); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return pReceiveMsg; } @@ -477,7 +476,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API if (WI_IsFlagClear(gci.Flags, CONSOLE_INITIALIZED)) { Status = ConsoleAllocateConsole(&Cac); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return pReceiveMsg; } @@ -519,7 +518,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API FILE_SHARE_READ | FILE_SHARE_WRITE, ProcessData->pInputHandle)); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return pReceiveMsg; } @@ -530,7 +529,7 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API FILE_SHARE_READ | FILE_SHARE_WRITE, ProcessData->pOutputHandle)); - if (!NT_SUCCESS(Status)) + if (FAILED_NTSTATUS(Status)) { return pReceiveMsg; } diff --git a/src/server/ProcessHandle.cpp b/src/server/ProcessHandle.cpp index b9cf2f1b35..51b6f221c6 100644 --- a/src/server/ProcessHandle.cpp +++ b/src/server/ProcessHandle.cpp @@ -28,7 +28,7 @@ ConsoleProcessHandle::ConsoleProcessHandle(const DWORD dwProcessId, dwProcessId))), _policy(ConsoleProcessPolicy::s_CreateInstance(_hProcess.get())), _shimPolicy(_hProcess.get()), - _processCreationTime(0) + _processCreationTime{} { if (nullptr != _hProcess.get()) { @@ -77,24 +77,17 @@ const HANDLE ConsoleProcessHandle::GetRawHandle() const // Routine Description: // - Retrieves the process creation time (currently used in telemetry traces) // - The creation time is lazily populated on first call -const ULONG64 ConsoleProcessHandle::GetProcessCreationTime() const +const FILETIME ConsoleProcessHandle::GetProcessCreationTime() const { - if (_processCreationTime == 0 && _hProcess != nullptr) + if (_processCreationTime.dwHighDateTime == 0 && _processCreationTime.dwLowDateTime == 0 && _hProcess != nullptr) { - FILETIME ftCreationTime, ftDummyTime = { 0 }; - ULARGE_INTEGER creationTime = { 0 }; + FILETIME ftDummyTime = { 0 }; - if (::GetProcessTimes(_hProcess.get(), - &ftCreationTime, - &ftDummyTime, - &ftDummyTime, - &ftDummyTime)) - { - creationTime.HighPart = ftCreationTime.dwHighDateTime; - creationTime.LowPart = ftCreationTime.dwLowDateTime; - } - - _processCreationTime = creationTime.QuadPart; + ::GetProcessTimes(_hProcess.get(), + &_processCreationTime, + &ftDummyTime, + &ftDummyTime, + &ftDummyTime); } return _processCreationTime; diff --git a/src/server/ProcessHandle.h b/src/server/ProcessHandle.h index 50084f4543..58f9f9c52d 100644 --- a/src/server/ProcessHandle.h +++ b/src/server/ProcessHandle.h @@ -28,6 +28,15 @@ Revision History: class ConsoleProcessHandle { public: + ConsoleProcessHandle(const DWORD dwProcessId, + const DWORD dwThreadId, + const ULONG ulProcessGroupId); + ~ConsoleProcessHandle() = default; + ConsoleProcessHandle(const ConsoleProcessHandle&) = delete; + ConsoleProcessHandle(ConsoleProcessHandle&&) = delete; + ConsoleProcessHandle& operator=(const ConsoleProcessHandle&) & = delete; + ConsoleProcessHandle& operator=(ConsoleProcessHandle&&) & = delete; + const std::unique_ptr pWaitBlockQueue; std::unique_ptr pInputHandle; std::unique_ptr pOutputHandle; @@ -44,23 +53,14 @@ public: CD_CONNECTION_INFORMATION GetConnectionInformation(IDeviceComm* deviceComm) const; - const ULONG64 GetProcessCreationTime() const; + const FILETIME GetProcessCreationTime() const; private: - ConsoleProcessHandle(const DWORD dwProcessId, - const DWORD dwThreadId, - const ULONG ulProcessGroupId); - ~ConsoleProcessHandle() = default; - ConsoleProcessHandle(const ConsoleProcessHandle&) = delete; - ConsoleProcessHandle(ConsoleProcessHandle&&) = delete; - ConsoleProcessHandle& operator=(const ConsoleProcessHandle&) & = delete; - ConsoleProcessHandle& operator=(ConsoleProcessHandle&&) & = delete; - ULONG _ulTerminateCount; ULONG const _ulProcessGroupId; wil::unique_handle const _hProcess; - mutable ULONG64 _processCreationTime; + mutable FILETIME _processCreationTime; const ConsoleProcessPolicy _policy; const ConsoleShimPolicy _shimPolicy; diff --git a/src/server/ProcessList.cpp b/src/server/ProcessList.cpp index ab9c529a76..e304556692 100644 --- a/src/server/ProcessList.cpp +++ b/src/server/ProcessList.cpp @@ -5,10 +5,7 @@ #include "ProcessList.h" -#include "../host/conwinuserrefs.h" #include "../host/globals.h" -#include "../host/telemetry.hpp" - #include "../interactivity/inc/ServiceLocator.hpp" using namespace Microsoft::Console::Interactivity; @@ -25,56 +22,31 @@ using namespace Microsoft::Console::Interactivity; // - If not used, return code will specify whether this process is known to the list or not. // Return Value: // - S_OK if the process was recorded in the list successfully or already existed. -// - E_FAIL if we're running into an LPC port conflict by nature of the process chain. +// - S_FALSE if we're running into an LPC port conflict by nature of the process chain. // - E_OUTOFMEMORY if there wasn't space to allocate a handle or push it into the list. [[nodiscard]] HRESULT ConsoleProcessList::AllocProcessData(const DWORD dwProcessId, const DWORD dwThreadId, const ULONG ulProcessGroupId, - _In_opt_ ConsoleProcessHandle* const pParentProcessData, _Outptr_opt_ ConsoleProcessHandle** const ppProcessData) { - FAIL_FAST_IF(!(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked())); + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - auto pProcessData = FindProcessInList(dwProcessId); - if (nullptr != pProcessData) + if (FindProcessInList(dwProcessId)) { - // In the GenerateConsoleCtrlEvent it's OK for this process to already have a ProcessData object. However, the other case is someone - // connecting to our LPC port and they should only do that once, so we fail subsequent connection attempts. - if (nullptr == pParentProcessData) - { - return E_FAIL; - // TODO: MSFT: 9574803 - This fires all the time. Did it always do that? - //RETURN_HR(E_FAIL); - } - else - { - if (nullptr != ppProcessData) - { - *ppProcessData = pProcessData; - } - RETURN_HR(S_OK); - } + return S_FALSE; } + std::unique_ptr pProcessData; try { - pProcessData = new ConsoleProcessHandle(dwProcessId, - dwThreadId, - ulProcessGroupId); - - // Some applications, when reading the process list through the GetConsoleProcessList API, are expecting - // the returned list of attached process IDs to be from newest to oldest. - // As such, we have to put the newest process into the head of the list. - _processes.push_front(pProcessData); - - if (nullptr != ppProcessData) - { - *ppProcessData = pProcessData; - } + pProcessData = std::make_unique(dwProcessId, dwThreadId, ulProcessGroupId); + _processes.emplace_back(pProcessData.get()); } CATCH_RETURN(); - RETURN_HR(S_OK); + wil::assign_to_opt_param(ppProcessData, pProcessData.release()); + + return S_OK; } // Routine Description: @@ -85,47 +57,39 @@ using namespace Microsoft::Console::Interactivity; // - void ConsoleProcessList::FreeProcessData(_In_ ConsoleProcessHandle* const pProcessData) { - FAIL_FAST_IF(!(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked())); + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - // Assert that the item exists in the list. If it doesn't exist, the end/last will be returned. - FAIL_FAST_IF(!(_processes.cend() != std::find(_processes.cbegin(), _processes.cend(), pProcessData))); - - _processes.remove(pProcessData); - - delete pProcessData; + const auto it = std::ranges::find(_processes, pProcessData); + if (it != _processes.end()) + { + _processes.erase(it); + delete pProcessData; + } + else + { + // The pointer not existing in the process list would be similar to a heap corruption, + // as the only code allowed to allocate a `ConsoleProcessHandle` is us, in AllocProcessData(). + // An assertion here would indicate a double-free or similar. + assert(false); + } } // Routine Description: // - Locates a process handle in this list. -// - NOTE: Calling FindProcessInList(0) means you want the root process. // Arguments: -// - dwProcessId - ID of the process to search for or ROOT_PROCESS_ID to find the root process. +// - dwProcessId - ID of the process to search for. // Return Value: // - Pointer to the process handle information or nullptr if no match was found. ConsoleProcessHandle* ConsoleProcessList::FindProcessInList(const DWORD dwProcessId) const { - auto it = _processes.cbegin(); + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - while (it != _processes.cend()) + for (const auto& p : _processes) { - const auto pProcessHandleRecord = *it; - - if (ROOT_PROCESS_ID != dwProcessId) + if (p->dwProcessId == dwProcessId) { - if (pProcessHandleRecord->dwProcessId == dwProcessId) - { - return pProcessHandleRecord; - } + return p; } - else - { - if (pProcessHandleRecord->fRootProcess) - { - return pProcessHandleRecord; - } - } - - it = std::next(it); } return nullptr; @@ -139,17 +103,53 @@ ConsoleProcessHandle* ConsoleProcessList::FindProcessInList(const DWORD dwProces // - Pointer to first matching process handle with given group ID. nullptr if no match was found. ConsoleProcessHandle* ConsoleProcessList::FindProcessByGroupId(_In_ ULONG ulProcessGroupId) const { - auto it = _processes.cbegin(); + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - while (it != _processes.cend()) + for (const auto& p : _processes) { - const auto pProcessHandleRecord = *it; - if (pProcessHandleRecord->_ulProcessGroupId == ulProcessGroupId) + if (p->_ulProcessGroupId == ulProcessGroupId) { - return pProcessHandleRecord; + return p; } + } - it = std::next(it); + return nullptr; +} + +// Routine Description: +// - Locates the root process handle in this list. +// Return Value: +// - Pointer to the process handle information or nullptr if no match was found. +ConsoleProcessHandle* ConsoleProcessList::GetRootProcess() const +{ + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + + for (const auto& p : _processes) + { + if (p->fRootProcess) + { + return p; + } + } + + return nullptr; +} + +// Routine Description: +// - Gets the first process in the list. +// - Used for reassigning a new root process. +// TODO: MSFT 9450737 - encapsulate root process logic. https://osgvsowi/9450737 +// Arguments: +// - +// Return Value: +// - Pointer to the first item in the list or nullptr if there are no items. +ConsoleProcessHandle* ConsoleProcessList::GetOldestProcess() const +{ + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + + if (!_processes.empty()) + { + return _processes.front(); } return nullptr; @@ -157,7 +157,6 @@ ConsoleProcessHandle* ConsoleProcessList::FindProcessByGroupId(_In_ ULONG ulProc // Routine Description: // - Retrieves the entire list of process IDs that is known to this list. -// - Requires caller to allocate space. If not enough space, pcProcessList will be filled with count of array necessary. // Arguments: // - pProcessList - Pointer to buffer to store process IDs. Caller allocated. // - pcProcessList - On the way in, the length of the buffer given. On the way out, the amount of the buffer used. @@ -165,36 +164,30 @@ ConsoleProcessHandle* ConsoleProcessList::FindProcessByGroupId(_In_ ULONG ulProc // Return Value: // - S_OK if buffer was filled successfully and resulting count of items is in pcProcessList. // - E_NOT_SUFFICIENT_BUFFER if the buffer given was too small. Refer to pcProcessList for size requirement. -[[nodiscard]] HRESULT ConsoleProcessList::GetProcessList(_Inout_updates_(*pcProcessList) DWORD* const pProcessList, +[[nodiscard]] HRESULT ConsoleProcessList::GetProcessList(_Inout_updates_(*pcProcessList) DWORD* pProcessList, _Inout_ size_t* const pcProcessList) const { - auto hr = S_OK; + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - const auto cProcesses = _processes.size(); - - // If we can fit inside the given list space, copy out the data. - if (cProcesses <= *pcProcessList) + if (*pcProcessList < _processes.size()) { - size_t cFilled = 0; - - // Loop over the list of processes and fill in the caller's buffer. - auto it = _processes.cbegin(); - while (it != _processes.cend() && cFilled < *pcProcessList) - { - pProcessList[cFilled] = (*it)->dwProcessId; - cFilled++; - it = std::next(it); - } - } - else - { - hr = E_NOT_SUFFICIENT_BUFFER; + *pcProcessList = _processes.size(); + return E_NOT_SUFFICIENT_BUFFER; } - // Return how many items were copied (or how many values we would need to fit). - *pcProcessList = cProcesses; + // Some applications, when reading the process list through the GetConsoleProcessList API, + // are expecting the returned list of attached process IDs to be from newest to oldest. + // As such, we have to put the newest process into the head of the list. + auto it = _processes.crbegin(); + const auto end = _processes.crend(); - return hr; + for (; it != end; ++it) + { + *pProcessList++ = (*it)->dwProcessId; + } + + *pcProcessList = _processes.size(); + return S_OK; } // Routine Description @@ -210,85 +203,48 @@ ConsoleProcessHandle* ConsoleProcessList::FindProcessByGroupId(_In_ ULONG ulProc // - E_OUTOFMEMORY in a low memory situation. [[nodiscard]] HRESULT ConsoleProcessList::GetTerminationRecordsByGroupId(const DWORD dwLimitingProcessId, const bool fCtrlClose, - _Outptr_result_buffer_all_(*pcRecords) ConsoleProcessTerminationRecord** prgRecords, - _Out_ size_t* const pcRecords) const + _Out_ std::vector& termRecords) const { - *pcRecords = 0; + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); try { - std::deque> TermRecords; + termRecords.clear(); // Dig through known processes looking for a match - auto it = _processes.cbegin(); - while (it != _processes.cend()) + for (const auto& p : _processes) { - const auto pProcessHandleRecord = *it; - // If no limit was specified OR if we have a match, generate a new termination record. - if (0 == dwLimitingProcessId || - pProcessHandleRecord->_ulProcessGroupId == dwLimitingProcessId) + if (!dwLimitingProcessId || + p->_ulProcessGroupId == dwLimitingProcessId) { - auto pNewRecord = std::make_unique(); + // If we're hard closing the window, increment the counter. + if (fCtrlClose) + { + p->_ulTerminateCount++; + } + wil::unique_handle process; // If the duplicate failed, the best we can do is to skip including the process in the list and hope it goes away. LOG_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), - pProcessHandleRecord->_hProcess.get(), + p->_hProcess.get(), GetCurrentProcess(), - &pNewRecord->hProcess, + &process, 0, 0, DUPLICATE_SAME_ACCESS)); - pNewRecord->dwProcessID = pProcessHandleRecord->dwProcessId; - - // If we're hard closing the window, increment the counter. - if (fCtrlClose) - { - pProcessHandleRecord->_ulTerminateCount++; - } - - pNewRecord->ulTerminateCount = pProcessHandleRecord->_ulTerminateCount; - - TermRecords.push_back(std::move(pNewRecord)); + termRecords.emplace_back(ConsoleProcessTerminationRecord{ + .hProcess = std::move(process), + .dwProcessID = p->dwProcessId, + .ulTerminateCount = p->_ulTerminateCount, + }); } - - it = std::next(it); } - // From all found matches, convert to C-style array to return - const auto cchRetVal = TermRecords.size(); - auto pRetVal = new ConsoleProcessTerminationRecord[cchRetVal]; - - for (size_t i = 0; i < cchRetVal; i++) - { - pRetVal[i] = *TermRecords.at(i); - } - - *prgRecords = pRetVal; - *pcRecords = cchRetVal; + return S_OK; } CATCH_RETURN(); - - return S_OK; -} - -// Routine Description: -// - Gets the first process in the list. -// - Used for reassigning a new root process. -// TODO: MSFT 9450737 - encapsulate root process logic. https://osgvsowi/9450737 -// Arguments: -// - -// Return Value: -// - Pointer to the first item in the list or nullptr if there are no items. -ConsoleProcessHandle* ConsoleProcessList::GetFirstProcess() const -{ - if (!_processes.empty()) - { - return _processes.front(); - } - - return nullptr; } // Routine Description: @@ -300,17 +256,14 @@ ConsoleProcessHandle* ConsoleProcessList::GetFirstProcess() const // - NOTE: Will attempt to request a change, but it's non fatal if it doesn't work. Failures will be logged to debug channel. void ConsoleProcessList::ModifyConsoleProcessFocus(const bool fForeground) { - auto it = _processes.cbegin(); - while (it != _processes.cend()) - { - const auto pProcessHandle = *it; + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + for (const auto& pProcessHandle : _processes) + { if (pProcessHandle->_hProcess) { _ModifyProcessForegroundRights(pProcessHandle->_hProcess.get(), fForeground); } - - it = std::next(it); } // Do this for conhost.exe itself, too. @@ -326,6 +279,7 @@ void ConsoleProcessList::ModifyConsoleProcessFocus(const bool fForeground) // - True if the list is empty. False if we have known processes. bool ConsoleProcessList::IsEmpty() const { + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); return _processes.empty(); } diff --git a/src/server/ProcessList.h b/src/server/ProcessList.h index 2b3e2af699..e790bc42c5 100644 --- a/src/server/ProcessList.h +++ b/src/server/ProcessList.h @@ -23,7 +23,10 @@ Revision History: // holding the console lock. struct ConsoleProcessTerminationRecord { - HANDLE hProcess; + // Unfortunately the reason for this was lost in time, but presumably a process + // handle is held so that we can refer to a process via PID (dwProcessID) without + // holding the console lock and fearing that the PID might get reused by the OS. + wil::unique_handle hProcess; DWORD dwProcessID; ULONG ulTerminateCount; }; @@ -31,25 +34,21 @@ struct ConsoleProcessTerminationRecord class ConsoleProcessList { public: - static const DWORD ROOT_PROCESS_ID = 0; - [[nodiscard]] HRESULT AllocProcessData(const DWORD dwProcessId, const DWORD dwThreadId, const ULONG ulProcessGroupId, - _In_opt_ ConsoleProcessHandle* const pParentProcessData, _Outptr_opt_ ConsoleProcessHandle** const ppProcessData); void FreeProcessData(_In_ ConsoleProcessHandle* const ProcessData); ConsoleProcessHandle* FindProcessInList(const DWORD dwProcessId) const; ConsoleProcessHandle* FindProcessByGroupId(_In_ ULONG ulProcessGroupId) const; + ConsoleProcessHandle* GetRootProcess() const; + ConsoleProcessHandle* GetOldestProcess() const; [[nodiscard]] HRESULT GetTerminationRecordsByGroupId(const DWORD dwLimitingProcessId, const bool fCtrlClose, - _Outptr_result_buffer_all_(*pcRecords) ConsoleProcessTerminationRecord** prgRecords, - _Out_ size_t* const pcRecords) const; - - ConsoleProcessHandle* GetFirstProcess() const; + std::vector& termRecords) const; [[nodiscard]] HRESULT GetProcessList(_Inout_updates_(*pcProcessList) DWORD* const pProcessList, _Inout_ size_t* const pcProcessList) const; @@ -59,7 +58,7 @@ public: bool IsEmpty() const; private: - std::list _processes; + std::vector _processes; void _ModifyProcessForegroundRights(const HANDLE hProcess, const bool fForeground) const; }; diff --git a/src/server/WaitBlock.cpp b/src/server/WaitBlock.cpp index 74ef76b536..49a4663f1c 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/server/precomp.h b/src/server/precomp.h index 9b9b2de382..50963fe6fa 100644 --- a/src/server/precomp.h +++ b/src/server/precomp.h @@ -24,42 +24,16 @@ Abstract: #define NOMINMAX // Windows Header Files: +#define WIN32_NO_STATUS #include +#undef WIN32_NO_STATUS -typedef long NTSTATUS; -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) -#define STATUS_SUCCESS ((DWORD)0x0) -#define STATUS_UNSUCCESSFUL ((DWORD)0xC0000001L) -#define STATUS_SHARING_VIOLATION ((NTSTATUS)0xC0000043L) -#define STATUS_INSUFFICIENT_RESOURCES ((DWORD)0xC000009AL) -#define STATUS_ILLEGAL_FUNCTION ((DWORD)0xC00000AFL) -#define STATUS_PIPE_DISCONNECTED ((DWORD)0xC00000B0L) -#define STATUS_BUFFER_TOO_SMALL ((DWORD)0xC0000023L) -#define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) +#include -// -// Map a WIN32 error value into an NTSTATUS -// Note: This assumes that WIN32 errors fall in the range -32k to 32k. -// - -#define FACILITY_NTWIN32 0x7 - -#define __NTSTATUS_FROM_WIN32(x) ((NTSTATUS)(x) <= 0 ? ((NTSTATUS)(x)) : ((NTSTATUS)(((x)&0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR))) - -#ifdef INLINE_NTSTATUS_FROM_WIN32 -#ifndef __midl -__inline NTSTATUS_FROM_WIN32(long x) -{ - return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x)&0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); -} -#else -#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) -#endif -#else -#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) -#endif - -//#include +#pragma warning(push) +#pragma warning(disable : 4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. +#include +#pragma warning(pop) #include #include diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 44f3de5dbb..612f82c093 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -7,9 +7,11 @@ namespace Microsoft::Console::VirtualTerminal { using VTInt = int32_t; - class VTID + union VTID { public: + VTID() = default; + template constexpr VTID(const char (&s)[Length]) : _value{ _FromString(s) } @@ -17,7 +19,7 @@ namespace Microsoft::Console::VirtualTerminal } constexpr VTID(const uint64_t value) : - _value{ value } + _value{ value & 0x00FFFFFFFFFFFFFF } { } @@ -26,6 +28,11 @@ namespace Microsoft::Console::VirtualTerminal return _value; } + constexpr const std::string_view ToString() const + { + return &_string[0]; + } + constexpr char operator[](const size_t offset) const { return SubSequence(offset)._value & 0xFF; @@ -40,7 +47,7 @@ namespace Microsoft::Console::VirtualTerminal template static constexpr uint64_t _FromString(const char (&s)[Length]) { - static_assert(Length - 1 <= sizeof(_value)); + static_assert(Length <= sizeof(_value)); uint64_t value = 0; for (auto i = Length - 1; i-- > 0;) { @@ -49,7 +56,12 @@ namespace Microsoft::Console::VirtualTerminal return value; } - uint64_t _value; + // In order for the _string to hold the correct representation of the + // ID stored in _value, we must be on a little endian architecture. + static_assert(std::endian::native == std::endian::little); + + uint64_t _value = 0; + char _string[sizeof(_value)]; }; class VTIDBuilder @@ -63,13 +75,13 @@ namespace Microsoft::Console::VirtualTerminal void AddIntermediate(const wchar_t intermediateChar) noexcept { - if (_idShift + CHAR_BIT >= sizeof(_idAccumulator) * CHAR_BIT) + if (_idShift + CHAR_BIT * 2 >= sizeof(_idAccumulator) * CHAR_BIT) { // If there is not enough space in the accumulator to add - // the intermediate and still have room left for the final, - // then we reset the accumulator to zero. This will result - // in an id with all zero intermediates, which shouldn't - // match anything. + // the intermediate and still have room left for the final + // and null terminator, then we reset the accumulator to zero. + // This will result in an id with all zero intermediates, + // which shouldn't match anything. _idAccumulator = 0; } else @@ -140,6 +152,51 @@ namespace Microsoft::Console::VirtualTerminal VTInt _value; }; + class VTSubParameters + { + public: + constexpr VTSubParameters() noexcept + { + } + + constexpr VTSubParameters(const std::span subParams) noexcept : + _subParams{ subParams } + { + } + + constexpr VTParameter at(const size_t index) const noexcept + { + // If the index is out of range, we return a sub parameter with no value. + return index < _subParams.size() ? til::at(_subParams, index) : defaultParameter; + } + + VTSubParameters subspan(const size_t offset, const size_t count) const noexcept + { + const auto subParamsSpan = _subParams.subspan(offset, count); + return { subParamsSpan }; + } + + bool empty() const noexcept + { + return _subParams.empty(); + } + + size_t size() const noexcept + { + return _subParams.size(); + } + + constexpr operator std::span() const noexcept + { + return _subParams; + } + + private: + static constexpr VTParameter defaultParameter{}; + + std::span _subParams; + }; + class VTParameters { public: @@ -147,50 +204,108 @@ namespace Microsoft::Console::VirtualTerminal { } - constexpr VTParameters(const VTParameter* ptr, const size_t count) noexcept : - _values{ ptr, count } + constexpr VTParameters(const VTParameter* paramsPtr, const size_t paramsCount) noexcept : + _params{ paramsPtr, paramsCount }, + _subParams{}, + _subParamRanges{} + { + } + + constexpr VTParameters(const std::span params, + const std::span subParams, + const std::span> subParamRanges) noexcept : + _params{ params }, + _subParams{ subParams }, + _subParamRanges{ subParamRanges } { } constexpr VTParameter at(const size_t index) const noexcept { // If the index is out of range, we return a parameter with no value. - return index < _values.size() ? _values[index] : VTParameter{}; + return index < _params.size() ? til::at(_params, index) : defaultParameter; } constexpr bool empty() const noexcept { - return _values.empty(); + return _params.empty(); } constexpr size_t size() const noexcept { // We always return a size of at least 1, since an empty parameter // list is the equivalent of a single "default" parameter. - return std::max(_values.size(), 1); + return std::max(_params.size(), 1); } VTParameters subspan(const size_t offset) const noexcept { - const auto subValues = _values.subspan(offset); - return { subValues.data(), subValues.size() }; + // We need sub parameters to always be in their original index + // because we store their indexes in subParamRanges. So we pass + // _subParams as is and create new span for others. + const auto newParamsSpan = _params.subspan(std::min(offset, _params.size())); + const auto newSubParamRangesSpan = _subParamRanges.subspan(std::min(offset, _subParamRanges.size())); + return { newParamsSpan, _subParams, newSubParamRangesSpan }; + } + + VTSubParameters subParamsFor(const size_t index) const noexcept + { + if (index < _subParamRanges.size()) + { + const auto& range = til::at(_subParamRanges, index); + return _subParams.subspan(range.first, range.second - range.first); + } + else + { + return VTSubParameters{}; + } + } + + bool hasSubParams() const noexcept + { + return !_subParams.empty(); + } + + bool hasSubParamsFor(const size_t index) const noexcept + { + if (index < _subParamRanges.size()) + { + const auto& range = til::at(_subParamRanges, index); + return range.second > range.first; + } + else + { + return false; + } } template bool for_each(const T&& predicate) const { + auto params = _params; + // We always return at least 1 value here, since an empty parameter // list is the equivalent of a single "default" parameter. - auto success = predicate(at(0)); - for (auto i = 1u; i < _values.size(); i++) + if (params.empty()) { - success = predicate(_values[i]) && success; + params = defaultParameters; + } + + auto success = true; + for (const auto& v : params) + { + success = predicate(v) && success; } return success; } private: - gsl::span _values; + static constexpr VTParameter defaultParameter{}; + static constexpr std::span defaultParameters{ &defaultParameter, 1 }; + + std::span _params; + VTSubParameters _subParams; + std::span> _subParamRanges; }; // FlaggedEnumValue is a convenience class that produces enum values (of a specified size) @@ -283,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, @@ -319,6 +434,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes BackgroundDefault = 49, Overline = 53, NoOverline = 55, + UnderlineColor = 58, + UnderlineColorDefault = 59, BrightForegroundBlack = 90, BrightForegroundRed = 91, BrightForegroundGreen = 92, @@ -386,6 +503,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes OS_OperatingStatus = ANSIStandardStatus(5), CPR_CursorPositionReport = ANSIStandardStatus(6), ExCPR_ExtendedCursorPositionReport = DECPrivateStatus(6), + MSR_MacroSpaceReport = DECPrivateStatus(62), + MEM_MemoryChecksum = DECPrivateStatus(63), }; using ANSIStandardMode = FlaggedEnumValue<0x00000000>; @@ -393,6 +512,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes enum ModeParams : VTInt { + IRM_InsertReplaceMode = ANSIStandardMode(4), + LNM_LineFeedNewLineMode = ANSIStandardMode(20), DECCKM_CursorKeysMode = DECPrivateMode(1), DECANM_AnsiMode = DECPrivateMode(2), DECCOLM_SetNumberOfColumns = DECPrivateMode(3), @@ -403,7 +524,10 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes ATT610_StartCursorBlink = DECPrivateMode(12), DECTCEM_TextCursorEnableMode = DECPrivateMode(25), XTERM_EnableDECCOLMSupport = DECPrivateMode(40), + DECNKM_NumericKeypadMode = DECPrivateMode(66), DECBKM_BackarrowKeyMode = DECPrivateMode(67), + DECLRMM_LeftRightMarginMode = DECPrivateMode(69), + DECECM_EraseColorMode = DECPrivateMode(117), VT200_MOUSE_MODE = DECPrivateMode(1000), BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002), ANY_EVENT_MOUSE_MODE = DECPrivateMode(1003), @@ -441,6 +565,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes IconifyWindow = 2, RefreshWindow = 7, ResizeWindowInCharacters = 8, + ReportTextSizeInCharacters = 18 }; enum class CursorStyle : VTInt @@ -507,31 +632,31 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes Size96 = 1 }; + enum class MacroDeleteControl : VTInt + { + DeleteId = 0, + DeleteAll = 1 + }; + + enum class MacroEncoding : VTInt + { + Text = 0, + HexPair = 1 + }; + enum class ReportFormat : VTInt { TerminalStateReport = 1, ColorTableReport = 2 }; + enum class PresentationReportFormat : VTInt + { + CursorInformationReport = 1, + TabulationStopReport = 2 + }; + constexpr VTInt s_sDECCOLMSetColumns = 132; constexpr VTInt s_sDECCOLMResetColumns = 80; - enum class MarkCategory : size_t - { - Prompt = 0, - Error = 1, - Warning = 2, - Success = 3, - Info = 4 - }; - - struct ScrollMark - { - std::optional color; - til::point start; - til::point end; // exclusive - MarkCategory category{ MarkCategory::Info }; - // Other things we may want to think about in the future are listed in - // GH#11000 - }; } diff --git a/src/terminal/adapter/FontBuffer.cpp b/src/terminal/adapter/FontBuffer.cpp index 8bac5d3f9d..c7f205f5a1 100644 --- a/src/terminal/adapter/FontBuffer.cpp +++ b/src/terminal/adapter/FontBuffer.cpp @@ -217,7 +217,7 @@ bool FontBuffer::FinalizeSixelData() return true; } -gsl::span FontBuffer::GetBitPattern() const noexcept +std::span FontBuffer::GetBitPattern() const noexcept { return { _buffer.data(), gsl::narrow_cast(MAX_CHARS * _fullHeight) }; } diff --git a/src/terminal/adapter/FontBuffer.hpp b/src/terminal/adapter/FontBuffer.hpp index a945f9a75e..f520c7bc95 100644 --- a/src/terminal/adapter/FontBuffer.hpp +++ b/src/terminal/adapter/FontBuffer.hpp @@ -30,7 +30,7 @@ namespace Microsoft::Console::VirtualTerminal void AddSixelData(const wchar_t ch); bool FinalizeSixelData(); - gsl::span GetBitPattern() const noexcept; + std::span GetBitPattern() const noexcept; til::size GetCellSize() const noexcept; size_t GetTextCenteringHint() const noexcept; VTID GetDesignation() const noexcept; diff --git a/src/terminal/adapter/IInteractDispatch.hpp b/src/terminal/adapter/IInteractDispatch.hpp index 33b9659c84..36278bd2e1 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/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index eab7c1b400..9221ea3cb4 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -45,43 +45,29 @@ public: virtual bool CursorPosition(const VTInt line, const VTInt column) = 0; // CUP, HVP virtual bool CursorSaveState() = 0; // DECSC virtual bool CursorRestoreState() = 0; // DECRC - virtual bool CursorVisibility(const bool isVisible) = 0; // DECTCEM virtual bool InsertCharacter(const VTInt count) = 0; // ICH virtual bool DeleteCharacter(const VTInt count) = 0; // DCH virtual bool ScrollUp(const VTInt distance) = 0; // SU virtual bool ScrollDown(const VTInt distance) = 0; // SD virtual bool InsertLine(const VTInt distance) = 0; // IL virtual bool DeleteLine(const VTInt distance) = 0; // DL - virtual bool SetColumns(const VTInt columns) = 0; // DECCOLM - virtual bool SetCursorKeysMode(const bool applicationMode) = 0; // DECCKM + virtual bool InsertColumn(const VTInt distance) = 0; // DECIC + virtual bool DeleteColumn(const VTInt distance) = 0; // DECDC virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM - virtual bool EnableWin32InputMode(const bool win32InputMode) = 0; // win32-input-mode - virtual bool EnableCursorBlinking(const bool enable) = 0; // ATT610 virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM - virtual bool SetScreenMode(const bool reverseMode) = 0; // DECSCNM - virtual bool SetOriginMode(const bool relativeMode) = 0; // DECOM - virtual bool SetAutoWrapMode(const bool wrapAtEOL) = 0; // DECAWM virtual bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM + virtual bool SetLeftRightScrollingMargins(const VTInt leftMargin, const VTInt rightMargin) = 0; // DECSLRM virtual bool WarningBell() = 0; // BEL virtual bool CarriageReturn() = 0; // CR virtual bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) = 0; // IND, NEL, LF, FF, VT virtual bool ReverseLineFeed() = 0; // RI + virtual bool BackIndex() = 0; // DECBI + virtual bool ForwardIndex() = 0; // DECFI virtual bool SetWindowTitle(std::wstring_view title) = 0; // OscWindowTitle - virtual bool UseAlternateScreenBuffer() = 0; // ASBSET - virtual bool UseMainScreenBuffer() = 0; // ASBRST virtual bool HorizontalTabSet() = 0; // HTS virtual bool ForwardTab(const VTInt numTabs) = 0; // CHT, HT virtual bool BackwardsTab(const VTInt numTabs) = 0; // CBT virtual bool TabClear(const DispatchTypes::TabClearType clearType) = 0; // TBC - virtual bool EnableDECCOLMSupport(const bool enabled) = 0; // ?40 - virtual bool EnableVT200MouseMode(const bool enabled) = 0; // ?1000 - virtual bool EnableUTF8ExtendedMouseMode(const bool enabled) = 0; // ?1005 - virtual bool EnableSGRExtendedMouseMode(const bool enabled) = 0; // ?1006 - virtual bool EnableButtonEventMouseMode(const bool enabled) = 0; // ?1002 - virtual bool EnableAnyEventMouseMode(const bool enabled) = 0; // ?1003 - virtual bool EnableFocusEventMode(const bool enabled) = 0; // ?1004 - virtual bool EnableAlternateScroll(const bool enabled) = 0; // ?1007 - virtual bool EnableXtermBracketedPasteMode(const bool enabled) = 0; // ?2004 virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD color) = 0; // OSCColorTable virtual bool SetDefaultForeground(const DWORD color) = 0; // OSCDefaultForeground virtual bool SetDefaultBackground(const DWORD color) = 0; // OSCDefaultBackground @@ -100,6 +86,7 @@ public: virtual bool EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECERA virtual bool SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECSERA virtual bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) = 0; // DECSACE + virtual bool RequestChecksumRectangularArea(const VTInt id, const VTInt page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECRQCRA virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR virtual bool SetLineRendition(const LineRendition rendition) = 0; // DECSWL, DECDWL, DECDHL @@ -108,11 +95,11 @@ public: virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR virtual bool PopGraphicsRendition() = 0; // XTPOPSGR - virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // DECSET + virtual bool SetMode(const DispatchTypes::ModeParams param) = 0; // SM, DECSET + virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // RM, DECRST + virtual bool RequestMode(const DispatchTypes::ModeParams param) = 0; // DECRQM - virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST - - virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR + virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) = 0; // DSR virtual bool DeviceAttributes() = 0; // DA1 virtual bool SecondaryDeviceAttributes() = 0; // DA2 virtual bool TertiaryDeviceAttributes() = 0; // DA3 @@ -150,6 +137,8 @@ public: virtual bool DoFinalTermAction(const std::wstring_view string) = 0; + virtual bool DoVsCodeAction(const std::wstring_view string) = 0; + virtual StringHandler DownloadDRCS(const VTInt fontNumber, const VTParameter startChar, const DispatchTypes::DrcsEraseControl eraseControl, @@ -159,10 +148,18 @@ public: const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD + virtual StringHandler DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) = 0; // DECDMAC + virtual bool InvokeMacro(const VTInt macroId) = 0; // DECINVM + virtual StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) = 0; // DECRSTS virtual StringHandler RequestSetting() = 0; // DECRQSS + virtual bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format) = 0; // DECRQPSR + virtual StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat format) = 0; // DECRSPS + virtual bool PlaySounds(const VTParameters parameters) = 0; // DECPS }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() = default; diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 3291013356..3375a968b0 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -37,7 +37,6 @@ namespace Microsoft::Console::VirtualTerminal ITerminalApi& operator=(const ITerminalApi&) = delete; ITerminalApi& operator=(ITerminalApi&&) = delete; - virtual void PrintString(const std::wstring_view string) = 0; virtual void ReturnResponse(const std::wstring_view response) = 0; virtual StateMachine& GetStateMachine() = 0; @@ -49,14 +48,19 @@ namespace Microsoft::Console::VirtualTerminal virtual void SetTextAttributes(const TextAttribute& attrs) = 0; - virtual void SetAutoWrapMode(const bool wrapAtEOL) = 0; + enum class Mode : size_t + { + AutoWrap, + LineFeed, + BracketedPaste + }; + + virtual void SetSystemMode(const Mode mode, const bool enabled) = 0; + virtual bool GetSystemMode(const Mode mode) const = 0; - virtual void SetScrollingRegion(const til::inclusive_rect& scrollMargins) = 0; virtual void WarningBell() = 0; - virtual bool GetLineFeedMode() const = 0; - virtual void LineFeed(const bool withReturn) = 0; virtual void SetWindowTitle(const std::wstring_view title) = 0; - virtual void UseAlternateScreenBuffer() = 0; + virtual void UseAlternateScreenBuffer(const TextAttribute& attrs) = 0; virtual void UseMainScreenBuffer() = 0; virtual CursorType GetUserDefaultCursorStyle() const = 0; @@ -66,7 +70,6 @@ namespace Microsoft::Console::VirtualTerminal virtual void SetConsoleOutputCP(const unsigned int codepage) = 0; virtual unsigned int GetConsoleOutputCP() const = 0; - virtual void EnableXtermBracketedPasteMode(const bool enabled) = 0; virtual void CopyToClipboard(const std::wstring_view content) = 0; virtual void SetTaskbarProgress(const DispatchTypes::TaskbarState state, const size_t progress) = 0; virtual void SetWorkingDirectory(const std::wstring_view uri) = 0; @@ -76,7 +79,13 @@ namespace Microsoft::Console::VirtualTerminal virtual bool IsConsolePty() const = 0; virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0; + virtual void NotifyBufferRotation(const int delta) = 0; - virtual void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) = 0; + virtual void MarkPrompt(const ScrollMark& mark) = 0; + virtual void MarkCommandStart() = 0; + virtual void MarkOutputStart() = 0; + virtual void MarkCommandFinish(std::optional error) = 0; + + virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0; }; } diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index 8f622704b6..cb06c985c9 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); @@ -147,9 +139,9 @@ bool InteractDispatch::MoveCursor(const VTInt row, const VTInt col) // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. - til::point coordCursor{ col - 1 + viewport.Left, row - 1 + viewport.Top }; - coordCursor.Y = std::clamp(coordCursor.Y, viewport.Top, viewport.Bottom); - coordCursor.X = std::clamp(coordCursor.X, viewport.Left, viewport.Right); + til::point coordCursor{ col - 1 + viewport.left, row - 1 + viewport.top }; + coordCursor.y = std::clamp(coordCursor.y, viewport.top, viewport.bottom); + coordCursor.x = std::clamp(coordCursor.x, viewport.left, viewport.right); // Finally, attempt to set the adjusted cursor position back into the console. const auto api = gsl::not_null{ ServiceLocator::LocateGlobals().api }; @@ -240,7 +232,7 @@ bool InteractDispatch::FocusChanged(const bool focused) const WI_UpdateFlag(gci.Flags, CONSOLE_HAS_FOCUS, shouldActuallyFocus); gci.ProcessHandleList.ModifyConsoleProcessFocus(shouldActuallyFocus); - gci.pInputBuffer->Write(std::make_unique(focused)); + gci.pInputBuffer->WriteFocusEvent(focused); } // Does nothing outside of ConPTY. If there's a real HWND, then the HWND is solely in charge. diff --git a/src/terminal/adapter/InteractDispatch.hpp b/src/terminal/adapter/InteractDispatch.hpp index 8fd3486821..56fd9b2dd8 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/MacroBuffer.cpp b/src/terminal/adapter/MacroBuffer.cpp new file mode 100644 index 0000000000..7ae1f05e5b --- /dev/null +++ b/src/terminal/adapter/MacroBuffer.cpp @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "MacroBuffer.hpp" +#include "../parser/ascii.hpp" +#include "../parser/stateMachine.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +size_t MacroBuffer::GetSpaceAvailable() const noexcept +{ + return MAX_SPACE - _spaceUsed; +} + +uint16_t MacroBuffer::CalculateChecksum() const noexcept +{ + // The algorithm that we're using here is intended to match the checksums + // produced by the original DEC VT420 terminal. Although note that a real + // VT420 would have included the entire macro memory area in the checksum, + // which could still contain remnants of previous macro definitions that + // are no longer active. We don't replicate that behavior, since that's of + // no benefit to applications that might want to use the checksum. + uint16_t checksum = 0; + for (auto& macro : _macros) + { + for (auto ch : macro) + { + checksum -= ch; + } + } + return checksum; +} + +void MacroBuffer::InvokeMacro(const size_t macroId, StateMachine& stateMachine) +{ + if (macroId < _macros.size()) + { + const auto& macroSequence = til::at(_macros, macroId); + // Macros can invoke other macros up to a depth of 16, but we don't allow + // the total sequence length to exceed the maximum buffer size, since that's + // likely to facilitate a denial-of-service attack. + const auto allowedLength = MAX_SPACE - _invokedSequenceLength; + if (_invokedDepth < 16 && macroSequence.length() < allowedLength) + { + _invokedSequenceLength += macroSequence.length(); + _invokedDepth++; + auto resetInvokeDepth = wil::scope_exit([&] { + // Once the invoke depth reaches zero, we know we've reached the end + // of the root invoke, so we can reset the sequence length tracker. + if (--_invokedDepth == 0) + { + _invokedSequenceLength = 0; + } + }); + stateMachine.ProcessString(macroSequence); + } + } +} + +void MacroBuffer::ClearMacrosIfInUse() +{ + // If we receive an RIS from within a macro invocation, we can't release the + // buffer because it's still being used. Instead we'll just replace all the + // macro definitions with NUL characters to prevent any further output. The + // buffer will eventually be released once the invocation finishes. + if (_invokedDepth > 0) + { + for (auto& macro : _macros) + { + std::fill(macro.begin(), macro.end(), AsciiChars::NUL); + } + } +} + +bool MacroBuffer::InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding) +{ + // We're checking the invoked depth here to make sure we aren't defining + // a macro from within a macro invocation. + if (macroId < _macros.size() && _invokedDepth == 0) + { + _activeMacroId = macroId; + _decodedChar = 0; + _repeatPending = false; + + switch (encoding) + { + case DispatchTypes::MacroEncoding::HexPair: + _parseState = State::ExpectingHexDigit; + break; + case DispatchTypes::MacroEncoding::Text: + _parseState = State::ExpectingText; + break; + default: + return false; + } + + switch (deleteControl) + { + case DispatchTypes::MacroDeleteControl::DeleteId: + _deleteMacro(_activeMacro()); + return true; + case DispatchTypes::MacroDeleteControl::DeleteAll: + for (auto& macro : _macros) + { + _deleteMacro(macro); + } + return true; + default: + return false; + } + } + return false; +} + +bool MacroBuffer::ParseDefinition(const wchar_t ch) +{ + // Once we receive an ESC, that marks the end of the definition, but if + // an unterminated repeat is still pending, we should apply that now. + if (ch == AsciiChars::ESC) + { + if (_repeatPending && !_applyPendingRepeat()) + { + _deleteMacro(_activeMacro()); + } + return false; + } + + // Any other control characters are just ignored. + if (ch < L' ') + { + return true; + } + + // For "text encoded" macros, we'll always be in the ExpectingText state. + // For "hex encoded" macros, we'll typically be alternating between the + // ExpectingHexDigit and ExpectingSecondHexDigit states as we parse the two + // digits of each hex pair. But we also need to deal with repeat sequences, + // which start with `!`, followed by a numeric repeat count, and then a + // range of hex pairs between two `;` characters. When parsing the repeat + // count, we use the ExpectingRepeatCount state, but when parsing the hex + // pairs of the repeat, we just use the regular ExpectingHexDigit states. + + auto success = true; + switch (_parseState) + { + case State::ExpectingText: + success = _appendToActiveMacro(ch); + break; + case State::ExpectingHexDigit: + if (_decodeHexDigit(ch)) + { + _parseState = State::ExpectingSecondHexDigit; + } + else if (ch == L'!' && !_repeatPending) + { + _parseState = State::ExpectingRepeatCount; + _repeatCount = 0; + } + else if (ch == L';' && _repeatPending) + { + success = _applyPendingRepeat(); + } + else + { + success = false; + } + break; + case State::ExpectingSecondHexDigit: + success = _decodeHexDigit(ch) && _appendToActiveMacro(_decodedChar); + _decodedChar = 0; + _parseState = State::ExpectingHexDigit; + break; + case State::ExpectingRepeatCount: + if (ch >= L'0' && ch <= L'9') + { + _repeatCount = _repeatCount * 10 + (ch - L'0'); + _repeatCount = std::min(_repeatCount, MAX_PARAMETER_VALUE); + } + else if (ch == L';') + { + _repeatPending = true; + _repeatStart = _activeMacro().length(); + _parseState = State::ExpectingHexDigit; + } + else + { + success = false; + } + break; + default: + success = false; + break; + } + + // If there is an error in the definition, clear everything received so far. + if (!success) + { + _deleteMacro(_activeMacro()); + } + return success; +} + +bool MacroBuffer::_decodeHexDigit(const wchar_t ch) noexcept +{ + _decodedChar <<= 4; + if (ch >= L'0' && ch <= L'9') + { + _decodedChar += (ch - L'0'); + return true; + } + else if (ch >= L'A' && ch <= L'F') + { + _decodedChar += (ch - L'A' + 10); + return true; + } + else if (ch >= L'a' && ch <= L'f') + { + _decodedChar += (ch - L'a' + 10); + return true; + } + return false; +} + +bool MacroBuffer::_appendToActiveMacro(const wchar_t ch) +{ + if (GetSpaceAvailable() > 0) + { + _activeMacro().push_back(ch); + _spaceUsed++; + return true; + } + return false; +} + +std::wstring& MacroBuffer::_activeMacro() +{ + return _macros.at(_activeMacroId); +} + +void MacroBuffer::_deleteMacro(std::wstring& macro) noexcept +{ + _spaceUsed -= macro.length(); + std::wstring{}.swap(macro); +} + +bool MacroBuffer::_applyPendingRepeat() +{ + if (_repeatCount > 1) + { + auto& activeMacro = _activeMacro(); + const auto sequenceLength = activeMacro.length() - _repeatStart; + // Note that the repeat sequence has already been written to the buffer + // once while it was being parsed, so we only need to append additional + // copies for repeat counts that are greater than one. If there is not + // enough space for the additional content, we'll just abort the macro. + const auto spaceRequired = (_repeatCount - 1) * sequenceLength; + if (spaceRequired > GetSpaceAvailable()) + { + return false; + } + for (size_t i = 1; i < _repeatCount; i++) + { + activeMacro.append(activeMacro.substr(_repeatStart, sequenceLength)); + _spaceUsed += sequenceLength; + } + } + _repeatPending = false; + return true; +} diff --git a/src/terminal/adapter/MacroBuffer.hpp b/src/terminal/adapter/MacroBuffer.hpp new file mode 100644 index 0000000000..92c1c07c73 --- /dev/null +++ b/src/terminal/adapter/MacroBuffer.hpp @@ -0,0 +1,79 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- MacrosBuffer.hpp + +Abstract: +- This manages the parsing and storage of macros defined by the DECDMAC control sequence. +--*/ + +#pragma once + +#include "DispatchTypes.hpp" +#include +#include +#include + +// fwdecl unittest classes +#ifdef UNIT_TESTING +class AdapterTest; +#endif + +namespace Microsoft::Console::VirtualTerminal +{ + class StateMachine; + + class MacroBuffer + { + public: + // The original DEC terminals only supported 6K of memory, which is + // probably a bit low for modern usage. But we also don't want to make + // this value too large, otherwise it could be used in a denial-of- + // service attack. So for now this is probably a sufficient limit, but + // we may need to increase it in the future if we intend to support + // macros containing sixel sequences. + static constexpr size_t MAX_SPACE = 0x40000; + + MacroBuffer() = default; + ~MacroBuffer() = default; + + size_t GetSpaceAvailable() const noexcept; + uint16_t CalculateChecksum() const noexcept; + void InvokeMacro(const size_t macroId, StateMachine& stateMachine); + void ClearMacrosIfInUse(); + bool InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding); + bool ParseDefinition(const wchar_t ch); + + private: + bool _decodeHexDigit(const wchar_t ch) noexcept; + bool _appendToActiveMacro(const wchar_t ch); + std::wstring& _activeMacro(); + void _deleteMacro(std::wstring& macro) noexcept; + bool _applyPendingRepeat(); + + enum class State + { + ExpectingText, + ExpectingHexDigit, + ExpectingSecondHexDigit, + ExpectingRepeatCount + }; + + State _parseState{ State::ExpectingText }; + wchar_t _decodedChar{ 0 }; + bool _repeatPending{ false }; + size_t _repeatCount{ 0 }; + size_t _repeatStart{ 0 }; + std::array _macros; + size_t _activeMacroId{ 0 }; + size_t _spaceUsed{ 0 }; + size_t _invokedDepth{ 0 }; + size_t _invokedSequenceLength{ 0 }; + +#ifdef UNIT_TESTING + friend class AdapterTest; +#endif + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 72c07b07b1..8558254298 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -15,15 +15,14 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Render; using namespace Microsoft::Console::VirtualTerminal; +static constexpr std::wstring_view whitespace{ L" " }; + AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) : _api{ api }, _renderer{ renderer }, _renderSettings{ renderSettings }, _terminalInput{ terminalInput }, _usingAltBuffer(false), - _isOriginModeRelative(false), // by default, the DECOM origin mode is absolute. - _isDECCOLMAllowed(false), // by default, DECCOLM is not allowed. - _isChangeExtentRectangular(false), _termOutput() { } @@ -43,7 +42,7 @@ void AdaptDispatch::Print(const wchar_t wchPrintable) // a character is only output if the DEL is translated to something else. if (wchTranslated != AsciiChars::DEL) { - _api.PrintString({ &wchTranslated, 1 }); + _WriteToBuffer({ &wchTranslated, 1 }); } } @@ -64,14 +63,138 @@ void AdaptDispatch::PrintString(const std::wstring_view string) { buffer.push_back(_termOutput.TranslateKey(wch)); } - _api.PrintString(buffer); + _WriteToBuffer(buffer); } else { - _api.PrintString(string); + _WriteToBuffer(string); } } +void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) +{ + auto& textBuffer = _api.GetTextBuffer(); + auto& cursor = textBuffer.GetCursor(); + auto cursorPosition = cursor.GetPosition(); + const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); + const auto& attributes = textBuffer.GetCurrentAttributes(); + + const auto viewport = _api.GetViewport(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(textBuffer.GetSize().Width()); + + auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y); + if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) + { + lineWidth = std::min(lineWidth, rightMargin + 1); + } + + // Turn off the cursor until we're done, so it isn't refreshed unnecessarily. + cursor.SetIsOn(false); + + RowWriteState state{ + .text = string, + .columnLimit = lineWidth, + }; + + while (!state.text.empty()) + { + if (cursor.IsDelayedEOLWrap() && wrapAtEOL) + { + const auto delayedCursorPosition = cursor.GetDelayedAtPosition(); + cursor.ResetDelayEOLWrap(); + // Only act on a delayed EOL if we didn't move the cursor to a + // different position from where the EOL was marked. + if (delayedCursorPosition == cursorPosition) + { + _DoLineFeed(textBuffer, true, true); + cursorPosition = cursor.GetPosition(); + // We need to recalculate the width when moving to a new line. + lineWidth = textBuffer.GetLineWidth(cursorPosition.y); + if (cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) + { + lineWidth = std::min(lineWidth, rightMargin + 1); + } + state.columnLimit = lineWidth; + } + } + + if (_modes.test(Mode::InsertReplace)) + { + // If insert-replace mode is enabled, we first measure how many cells + // the string will occupy, and scroll the target area right by that + // amount to make space for the incoming text. + const OutputCellIterator it(state.text, attributes); + auto measureIt = it; + while (measureIt && measureIt.GetCellDistance(it) < state.columnLimit) + { + ++measureIt; + } + const auto row = cursorPosition.y; + const auto cellCount = measureIt.GetCellDistance(it); + _ScrollRectHorizontally(textBuffer, { cursorPosition.x, row, state.columnLimit, row + 1 }, cellCount); + } + + state.columnBegin = cursorPosition.x; + + const auto textPositionBefore = state.text.data(); + textBuffer.Write(cursorPosition.y, attributes, state); + const auto textPositionAfter = state.text.data(); + + // TODO: A row should not be marked as wrapped just because we wrote the last column. + // It should be marked whenever we write _past_ it (above, _DoLineFeed call). See GH#15602. + if (wrapAtEOL && state.columnEnd >= state.columnLimit) + { + textBuffer.SetWrapForced(cursorPosition.y, true); + } + + if (state.columnBeginDirty != state.columnEndDirty) + { + const til::rect changedRect{ state.columnBeginDirty, cursorPosition.y, state.columnEndDirty, cursorPosition.y + 1 }; + _api.NotifyAccessibilityChange(changedRect); + } + + // If we're past the end of the line, we need to clamp the cursor + // back into range, and if wrapping is enabled, set the delayed wrap + // flag. The wrapping only occurs once another character is output. + const auto isWrapping = state.columnEnd >= state.columnLimit; + cursorPosition.x = isWrapping ? state.columnLimit - 1 : state.columnEnd; + cursor.SetPosition(cursorPosition); + + if (isWrapping) + { + // We want to wrap, but we failed to write even a single character into the row. + // ROW::Write() returns the lineWidth and leaves stringIterator untouched. To prevent a + // deadlock, because stringIterator never advances, we need to throw that glyph away. + // + // This can happen under two circumstances: + // * The glyph is wider than the buffer and can never be inserted in + // the first place. There's no good way to detect this, so we check + // whether the begin column is the left margin, which is the column + // at which any legit insertion should work at a minimum. + // * The DECAWM Autowrap mode is disabled ("\x1b[?7l", !wrapAtEOL) and + // we tried writing a wide glyph into the last column which can't work. + if (textPositionBefore == textPositionAfter && (state.columnBegin == 0 || !wrapAtEOL)) + { + state.text = state.text.substr(textBuffer.GraphemeNext(state.text, 0)); + } + + if (wrapAtEOL) + { + cursor.DelayEOLWrap(); + } + } + } + + _ApplyCursorMovementFlags(cursor); + + // Notify UIA of new text. + // It's important to do this here instead of in TextBuffer, because here you + // have access to the entire line of text, whereas TextBuffer writes it one + // character at a time via the OutputCellIterator. + textBuffer.TriggerNewTextNotification(string); +} + // Routine Description: // - CUU - Handles cursor upward movement by given distance. // CUU and CUD are handled separately from other CUP sequences, because they are @@ -157,19 +280,18 @@ bool AdaptDispatch::CursorPrevLine(const VTInt distance) // - absolute - Should coordinates be absolute or relative to the viewport. // Return Value: // - A std::pair containing the top and bottom coordinates (inclusive). -std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) +std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept { // If the top is out of range, reset the margins completely. const auto bottommostRow = viewport.bottom - viewport.top - 1; - if (_scrollMargins.Top >= bottommostRow) + if (_scrollMargins.top >= bottommostRow) { - _scrollMargins.Top = _scrollMargins.Bottom = 0; - _api.SetScrollingRegion(_scrollMargins); + _scrollMargins.top = _scrollMargins.bottom = 0; } // If margins aren't set, use the full extent of the viewport. - const auto marginsSet = _scrollMargins.Top < _scrollMargins.Bottom; - auto topMargin = marginsSet ? _scrollMargins.Top : 0; - auto bottomMargin = marginsSet ? _scrollMargins.Bottom : bottommostRow; + const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom; + auto topMargin = marginsSet ? _scrollMargins.top : 0; + auto bottomMargin = marginsSet ? _scrollMargins.bottom : bottommostRow; // If the bottom is out of range, clamp it to the bottommost row. bottomMargin = std::min(bottomMargin, bottommostRow); if (absolute) @@ -180,6 +302,29 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport return { topMargin, bottomMargin }; } +// Routine Description: +// - Returns the coordinates of the horizontal scroll margins. +// Arguments: +// - bufferWidth - The width of the buffer +// Return Value: +// - A std::pair containing the left and right coordinates (inclusive). +std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bufferWidth) noexcept +{ + // If the left is out of range, reset the margins completely. + const auto rightmostColumn = bufferWidth - 1; + if (_scrollMargins.left >= rightmostColumn) + { + _scrollMargins.left = _scrollMargins.right = 0; + } + // If margins aren't set, use the full extent of the buffer. + const auto marginsSet = _scrollMargins.left < _scrollMargins.right; + auto leftMargin = marginsSet ? _scrollMargins.left : 0; + auto rightMargin = marginsSet ? _scrollMargins.right : rightmostColumn; + // If the right is out of range, clamp it to the rightmost column. + rightMargin = std::min(rightMargin, rightmostColumn); + return { leftMargin, rightMargin }; +} + // Routine Description: // - Generalizes cursor movement to a specific position, which can be absolute or relative. // Arguments: @@ -194,54 +339,71 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); + const auto bufferWidth = textBuffer.GetSize().Width(); const auto cursorPosition = cursor.GetPosition(); const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); // For relative movement, the given offsets will be relative to // the current cursor position. - auto row = cursorPosition.Y; - auto col = cursorPosition.X; + auto row = cursorPosition.y; + auto col = cursorPosition.x; // But if the row is absolute, it will be relative to the top of the // viewport, or the top margin, depending on the origin mode. if (rowOffset.IsAbsolute) { - row = _isOriginModeRelative ? topMargin : viewport.top; + row = _modes.test(Mode::Origin) ? topMargin : viewport.top; } - // And if the column is absolute, it'll be relative to column 0. + // And if the column is absolute, it'll be relative to column 0, + // or the left margin, depending on the origin mode. // Horizontal positions are not affected by the viewport. if (colOffset.IsAbsolute) { - col = 0; + col = _modes.test(Mode::Origin) ? leftMargin : 0; } // Adjust the base position by the given offsets and clamp the results. // The row is constrained within the viewport's vertical boundaries, // while the column is constrained by the buffer width. row = std::clamp(row + rowOffset.Value, viewport.top, viewport.bottom - 1); - col = std::clamp(col + colOffset.Value, 0, textBuffer.GetSize().Width() - 1); + col = std::clamp(col + colOffset.Value, 0, bufferWidth - 1); // If the operation needs to be clamped inside the margins, or the origin // mode is relative (which always requires margin clamping), then the row - // may need to be adjusted further. - if (clampInMargins || _isOriginModeRelative) + // and column may need to be adjusted further. + if (clampInMargins || _modes.test(Mode::Origin)) { - // See microsoft/terminal#2929 - If the cursor is _below_ the top - // margin, it should stay below the top margin. If it's _above_ the - // bottom, it should stay above the bottom. Cursor movements that stay - // outside the margins shouldn't necessarily be affected. For example, - // moving up while below the bottom margin shouldn't just jump straight - // to the bottom margin. See - // ScreenBufferTests::CursorUpDownOutsideMargins for a test of that - // behavior. - if (cursorPosition.Y >= topMargin) + // Vertical margins only apply if the original position is inside the + // horizontal margins. Also, the cursor will only be clamped inside the + // top margin if it was already below the top margin to start with, and + // it will only be clamped inside the bottom margin if it was already + // above the bottom margin to start with. + if (cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin) { - row = std::max(row, topMargin); + if (cursorPosition.y >= topMargin) + { + row = std::max(row, topMargin); + } + if (cursorPosition.y <= bottomMargin) + { + row = std::min(row, bottomMargin); + } } - if (cursorPosition.Y <= bottomMargin) + // Similarly, horizontal margins only apply if the new row is inside the + // vertical margins. And the cursor is only clamped inside the horizontal + // margins if it was already inside to start with. + if (row >= topMargin && row <= bottomMargin) { - row = std::min(row, bottomMargin); + if (cursorPosition.x >= leftMargin) + { + col = std::max(col, leftMargin); + } + if (cursorPosition.x <= rightMargin) + { + col = std::min(col, rightMargin); + } } } @@ -340,21 +502,29 @@ 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. auto cursorPosition = textBuffer.GetCursor().GetPosition(); - cursorPosition.Y -= viewport.top; + cursorPosition.y -= viewport.top; + + // Although if origin mode is set, the cursor is relative to the margin origin. + if (_modes.test(Mode::Origin)) + { + cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; + cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + } // VT is also 1 based, not 0 based, so correct by 1. auto& savedCursorState = _savedCursorState.at(_usingAltBuffer); - savedCursorState.Column = cursorPosition.X + 1; - savedCursorState.Row = cursorPosition.Y + 1; - savedCursorState.IsOriginModeRelative = _isOriginModeRelative; + savedCursorState.Column = cursorPosition.x + 1; + savedCursorState.Row = cursorPosition.y + 1; + savedCursorState.IsDelayedEOLWrap = textBuffer.GetCursor().IsDelayedEOLWrap(); + savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin); savedCursorState.Attributes = attributes; savedCursorState.TermOutput = _termOutput; - savedCursorState.C1ControlsAccepted = _GetParserMode(StateMachine::Mode::AcceptC1); + savedCursorState.C1ControlsAccepted = _api.GetStateMachine().GetParserMode(StateMachine::Mode::AcceptC1); savedCursorState.CodePage = _api.GetConsoleOutputCP(); return true; @@ -372,26 +542,18 @@ bool AdaptDispatch::CursorRestoreState() { auto& savedCursorState = _savedCursorState.at(_usingAltBuffer); - auto row = savedCursorState.Row; - const auto col = savedCursorState.Column; + // Restore the origin mode first, since the cursor coordinates may be relative. + _modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative); - // If the origin mode is relative, we need to make sure the restored - // position is clamped within the margins. - if (savedCursorState.IsOriginModeRelative) + // We can then restore the position with a standard CUP operation. + CursorPosition(savedCursorState.Row, savedCursorState.Column); + + // If the delayed wrap flag was set when the cursor was saved, we need to restore that now. + if (savedCursorState.IsDelayedEOLWrap) { - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false); - // VT origin is at 1,1 so we need to add 1 to these margins. - row = std::clamp(row, topMargin + 1, bottomMargin + 1); + _api.GetTextBuffer().GetCursor().DelayEOLWrap(); } - // The saved coordinates are always absolute, so we need reset the origin mode temporarily. - _isOriginModeRelative = false; - CursorPosition(row, col); - - // Once the cursor position is restored, we can then restore the actual origin mode. - _isOriginModeRelative = savedCursorState.IsOriginModeRelative; - // Restore text attributes. _api.SetTextAttributes(savedCursorState.Attributes); @@ -411,15 +573,25 @@ bool AdaptDispatch::CursorRestoreState() } // Routine Description: -// - DECTCEM - Sets the show/hide visibility status of the cursor. +// - Returns the attributes that should be used when erasing the buffer. When +// the Erase Color mode is set, we use the default attributes, but when reset, +// we use the active color attributes with the character attributes cleared. // Arguments: -// - fIsVisible - Turns the cursor rendering on (TRUE) or off (FALSE). +// - textBuffer - Target buffer that is being erased. // Return Value: -// - True. -bool AdaptDispatch::CursorVisibility(const bool fIsVisible) +// - The erase TextAttribute value. +TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept { - _api.GetTextBuffer().GetCursor().SetIsVisible(fIsVisible); - return true; + if (_modes.test(Mode::EraseColor)) + { + return {}; + } + else + { + auto eraseAttributes = textBuffer.GetCurrentAttributes(); + eraseAttributes.SetStandardErase(); + return eraseAttributes; + } } // Routine Description: @@ -430,28 +602,48 @@ bool AdaptDispatch::CursorVisibility(const bool fIsVisible) // - delta - Distance to move (positive is down, negative is up). // Return Value: // - -void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const int32_t delta) +void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) { const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height()); if (absoluteDelta < scrollRect.height()) { - // For now we're assuming the scrollRect is always the full width of the - // buffer, but this will likely need to be extended to support scrolling - // of arbitrary widths at some point in the future. const auto top = delta > 0 ? scrollRect.top : scrollRect.top + absoluteDelta; + const auto width = scrollRect.width(); const auto height = scrollRect.height() - absoluteDelta; const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta; - textBuffer.ScrollRows(top, height, actualDelta); - textBuffer.TriggerRedraw(Viewport::FromExclusive(scrollRect)); + if (width == textBuffer.GetSize().Width()) + { + // If the scrollRect is the full width of the buffer, we can scroll + // more efficiently by rotating the row storage. + textBuffer.ScrollRows(top, height, actualDelta); + textBuffer.TriggerRedraw(Viewport::FromExclusive(scrollRect)); + } + else + { + // Otherwise we have to move the content up or down by copying the + // requested buffer range one cell at a time. + const auto srcOrigin = til::point{ scrollRect.left, top }; + const auto dstOrigin = til::point{ scrollRect.left, top + actualDelta }; + const auto srcView = Viewport::FromDimensions(srcOrigin, width, height); + const auto dstView = Viewport::FromDimensions(dstOrigin, width, height); + const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView); + auto srcPos = srcView.GetWalkOrigin(walkDirection); + auto dstPos = dstView.GetWalkOrigin(walkDirection); + do + { + const auto current = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); + srcView.WalkInBounds(srcPos, walkDirection); + } while (dstView.WalkInBounds(dstPos, walkDirection)); + } } // Rows revealed by the scroll are filled with standard erase attributes. auto eraseRect = scrollRect; eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta); eraseRect.bottom = eraseRect.top + absoluteDelta; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom); @@ -465,7 +657,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec // - delta - Distance to move (positive is right, negative is left). // Return Value: // - -void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const int32_t delta) +void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) { const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width()); if (absoluteDelta < scrollRect.width()) @@ -481,11 +673,16 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r const auto walkDirection = Viewport::DetermineWalkDirection(source, target); auto sourcePos = source.GetWalkOrigin(walkDirection); auto targetPos = target.GetWalkOrigin(walkDirection); + // Note that we read two cells from the source before we start writing + // to the target, so a two-cell DBCS character can't accidentally delete + // itself when moving one cell horizontally. + auto next = OutputCell(*textBuffer.GetCellDataAt(sourcePos)); do { - const auto data = OutputCell(*textBuffer.GetCellDataAt(sourcePos)); - textBuffer.Write(OutputCellIterator({ &data, 1 }), targetPos); + const auto current = next; source.WalkInBounds(sourcePos, walkDirection); + next = OutputCell(*textBuffer.GetCellDataAt(sourcePos)); + textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), targetPos); } while (target.WalkInBounds(targetPos, walkDirection)); } @@ -493,9 +690,8 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r auto eraseRect = scrollRect; eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta); eraseRect.right = eraseRect.left + absoluteDelta; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); } // Routine Description: @@ -507,11 +703,21 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r // - void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta) { + const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto startCol = textBuffer.GetCursor().GetPosition().X; - const auto endCol = textBuffer.GetLineWidth(row); - _ScrollRectHorizontally(textBuffer, { startCol, row, endCol, row + 1 }, delta); + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto col = textBuffer.GetCursor().GetPosition().x; + const auto lineWidth = textBuffer.GetLineWidth(row); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ? + _GetHorizontalMargins(lineWidth) : + std::make_pair(0, lineWidth - 1); + if (col >= leftMargin && col <= rightMargin) + { + _ScrollRectHorizontally(textBuffer, { col, row, rightMargin + 1, row + 1 }, delta); + // The ICH and DCH controls are expected to reset the delayed wrap flag. + textBuffer.GetCursor().ResetDelayEOLWrap(); + } } // Routine Description: @@ -549,19 +755,10 @@ bool AdaptDispatch::DeleteCharacter(const VTInt count) // - fillAttrs - Attributes to be written to the buffer. // Return Value: // - -void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs) +void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const { - if (fillRect.left < fillRect.right && fillRect.top < fillRect.bottom) - { - const auto fillWidth = gsl::narrow_cast(fillRect.right - fillRect.left); - const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillWidth }; - const auto col = fillRect.left; - for (auto row = fillRect.top; row < fillRect.bottom; row++) - { - textBuffer.WriteLine(fillData, { col, row }, false); - } - _api.NotifyAccessibilityChange(fillRect); - } + textBuffer.FillRect(fillRect, fillChar, fillAttrs); + _api.NotifyAccessibilityChange(fillRect); } // Routine Description: @@ -576,13 +773,15 @@ void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, bool AdaptDispatch::EraseCharacters(const VTInt numChars) { auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto startCol = textBuffer.GetCursor().GetPosition().X; + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto startCol = textBuffer.GetCursor().GetPosition().x; const auto endCol = std::min(startCol + numChars, textBuffer.GetLineWidth(row)); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, L' ', eraseAttributes); + // The ECH control is expected to reset the delayed wrap flag. + textBuffer.GetCursor().ResetDelayEOLWrap(); + + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); return true; } @@ -608,33 +807,25 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // by moving the current contents of the viewport into the scrollback. if (eraseType == DispatchTypes::EraseType::Scrollback) { - _EraseScrollback(); - // GH#2715 - If this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this ED sequence to the connected - // terminal application. While we're in conpty mode, we don't really - // have a scrollback, but the attached terminal might. - return !_api.IsConsolePty(); + return _EraseScrollback(); } else if (eraseType == DispatchTypes::EraseType::All) { - // GH#5683 - If this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this ED sequence to the connected - // terminal application. While we're in conpty mode, when the client - // requests a Erase All operation, we need to manually tell the - // connected terminal to do the same thing, so that the terminal will - // move it's own buffer contents into the scrollback. - _EraseAll(); - return !_api.IsConsolePty(); + return _EraseAll(); } const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto col = textBuffer.GetCursor().GetPosition().X; + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto col = textBuffer.GetCursor().GetPosition().x; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); + // The ED control is expected to reset the delayed wrap flag. + // The special case variants above ("erase all" and "erase scrollback") + // take care of that themselves when they set the cursor position. + textBuffer.GetCursor().ResetDelayEOLWrap(); + + const auto eraseAttributes = _GetEraseAttributes(textBuffer); // When erasing the display, every line that is erased in full should be // reset to single width. When erasing to the end, this could include @@ -645,14 +836,14 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) if (eraseType == DispatchTypes::EraseType::FromBeginning) { textBuffer.ResetLineRenditionRange(viewport.top, row); - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, L' ', eraseAttributes); - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, whitespace, eraseAttributes); + _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); } if (eraseType == DispatchTypes::EraseType::ToEnd) { textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, viewport.bottom); - _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, L' ', eraseAttributes); - _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, L' ', eraseAttributes); + _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, whitespace, eraseAttributes); + _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, whitespace, eraseAttributes); } return true; @@ -667,21 +858,23 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) { auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto col = textBuffer.GetCursor().GetPosition().X; + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto col = textBuffer.GetCursor().GetPosition().x; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); + // The EL control is expected to reset the delayed wrap flag. + textBuffer.GetCursor().ResetDelayEOLWrap(); + + const auto eraseAttributes = _GetEraseAttributes(textBuffer); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::ToEnd: - _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::All: - _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; default: return false; @@ -701,16 +894,14 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& { for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetRowByOffset(row); - const auto& attrs = rowBuffer.GetAttrRow(); - auto& chars = rowBuffer.GetCharRow(); + auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); for (auto col = eraseRect.left; col < eraseRect.right; col++) { // Only unprotected cells are affected. - if (!attrs.GetAttrByColumn(col).IsProtected()) + if (!rowBuffer.GetAttrByColumn(col).IsProtected()) { // The text is cleared but the attributes are left as is. - chars.ClearGlyph(col); + rowBuffer.ClearCell(col); textBuffer.TriggerRedraw(Viewport::FromCoord({ col, row })); } } @@ -733,8 +924,11 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto col = textBuffer.GetCursor().GetPosition().X; + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto col = textBuffer.GetCursor().GetPosition().x; + + // The DECSED control is expected to reset the delayed wrap flag. + textBuffer.GetCursor().ResetDelayEOLWrap(); switch (eraseType) { @@ -766,8 +960,11 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) { auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().Y; - const auto col = textBuffer.GetCursor().GetPosition().X; + const auto row = textBuffer.GetCursor().GetPosition().y; + const auto col = textBuffer.GetCursor().GetPosition().x; + + // The DECSEL control is expected to reset the delayed wrap flag. + textBuffer.GetCursor().ResetDelayEOLWrap(); switch (eraseType) { @@ -799,11 +996,10 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec { for (auto row = changeRect.top; row < changeRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetRowByOffset(row); - auto& attrs = rowBuffer.GetAttrRow(); + auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); for (auto col = changeRect.left; col < changeRect.right; col++) { - auto attr = attrs.GetAttrByColumn(col); + auto attr = rowBuffer.GetAttrByColumn(col); auto characterAttributes = attr.GetCharacterAttributes(); characterAttributes &= changeOps.andAttrMask; characterAttributes ^= changeOps.xorAttrMask; @@ -816,7 +1012,11 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec { attr.SetBackground(*changeOps.background); } - attrs.Replace(col, col + 1, attr); + if (changeOps.underlineColor) + { + attr.SetUnderlineColor(*changeOps.underlineColor); + } + rowBuffer.ReplaceAttributes(col, col + 1, attr); } } textBuffer.TriggerRedraw(Viewport::FromExclusive(changeRect)); @@ -829,7 +1029,7 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec // Arguments: // - changeArea - Area of the buffer that will be affected. This may be // interpreted as a rectangle or a stream depending on the state of the -// _isChangeExtentRectangular field. +// RectangularChangeExtent mode. // - changeOps - Changes that will be applied to each of the attributes. // Return Value: // - @@ -842,14 +1042,14 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // If the change extent is rectangular, we can apply the change with a // single call. The same is true for a stream extent that is only one line. - if (_isChangeExtentRectangular || lineCount == 1) + if (_modes.test(Mode::RectangularChangeExtent) || lineCount == 1) { _ChangeRectAttributes(textBuffer, changeRect, changeOps); } // If the stream extent is more than one line we require three passes. The // top line is altered from the left offset up to the end of the line. The // bottom line is altered from the start up to the right offset. All the - // lines inbetween have their entire length altered. The right coordinate + // lines in-between have their entire length altered. The right coordinate // must be greater than the left, otherwise the operation is ignored. else if (lineCount > 1 && changeRect.right > changeRect.left) { @@ -861,7 +1061,7 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c } // Routine Description: -// - Helper method to caculate the applicable buffer coordinates for use with +// - Helper method to calculate the applicable buffer coordinates for use with // the various rectangular area operations. // Arguments: // - top - The first row of the area. @@ -878,15 +1078,17 @@ til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, c // We start by calculating the margin offsets and maximum dimensions. // If the origin mode isn't set, we use the viewport extent. const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false); - const auto yOffset = _isOriginModeRelative ? topMargin : 0; - const auto yMaximum = _isOriginModeRelative ? bottomMargin + 1 : viewport.height(); - const auto xMaximum = bufferSize.width; + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferSize.width); + const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0; + const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : viewport.height(); + const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0; + const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : bufferSize.width; auto fillRect = til::inclusive_rect{}; - fillRect.left = left; + fillRect.left = left + xOffset; fillRect.top = top + yOffset; // Right and bottom default to the maximum dimensions. - fillRect.right = (right ? right : xMaximum); + fillRect.right = (right ? right + xOffset : xMaximum); fillRect.bottom = (bottom ? bottom + yOffset : yMaximum); // We also clamp everything to the maximum dimensions, and subtract 1 @@ -923,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); @@ -946,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; @@ -954,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. @@ -970,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();) @@ -980,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 @@ -1002,7 +1213,7 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn } // Routine Description: -// - DECCRA - Copys a rectangular area from one part of the buffer to another. +// - DECCRA - Copies a rectangular area from one part of the buffer to another. // Arguments: // - top - The first row of the source area. // - left - The first column of the source area. @@ -1035,16 +1246,22 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView); auto srcPos = srcView.GetWalkOrigin(walkDirection); auto dstPos = dstView.GetWalkOrigin(walkDirection); + // Note that we read two cells from the source before we start writing + // to the target, so a two-cell DBCS character can't accidentally delete + // itself when moving one cell horizontally. + auto next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); do { + const auto current = next; + const auto currentSrcPos = srcPos; + srcView.WalkInBounds(srcPos, walkDirection); + next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); // If the source position is offscreen (which can occur on double // width lines), then we shouldn't copy anything to the destination. - if (srcPos.x < textBuffer.GetLineWidth(srcPos.y)) + if (currentSrcPos.x < textBuffer.GetLineWidth(currentSrcPos.y)) { - const auto data = OutputCell(*textBuffer.GetCellDataAt(srcPos)); - textBuffer.Write(OutputCellIterator({ &data, 1 }), dstPos); + textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); } - srcView.WalkInBounds(srcPos, walkDirection); } while (dstView.WalkInBounds(dstPos, walkDirection)); _api.NotifyAccessibilityChange(dstRect); } @@ -1066,7 +1283,7 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { auto& textBuffer = _api.GetTextBuffer(); - auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); // The standard only allows for characters in the range of the GL and GR // character set tables, but we also support additional Unicode characters @@ -1078,14 +1295,8 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c if (glChar || grChar || unicodeChar) { const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast(charValue)); - const auto fillAttributes = textBuffer.GetCurrentAttributes(); - if (IsGlyphFullWidth(fillChar)) - { - // If the fill char is full width, we need to halve the width of the - // fill area, otherwise it'll occupy twice as much space as expected. - fillRect.right = fillRect.left + fillRect.width() / 2; - } - _FillRect(textBuffer, fillRect, fillChar, fillAttributes); + const auto& fillAttributes = textBuffer.GetCurrentAttributes(); + _FillRect(textBuffer, fillRect, { &fillChar, 1 }, fillAttributes); } return true; @@ -1105,9 +1316,8 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons { auto& textBuffer = _api.GetTextBuffer(); const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); return true; } @@ -1142,16 +1352,94 @@ bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExten { case DispatchTypes::ChangeExtent::Default: case DispatchTypes::ChangeExtent::Stream: - _isChangeExtentRectangular = false; + _modes.reset(Mode::RectangularChangeExtent); return true; case DispatchTypes::ChangeExtent::Rectangle: - _isChangeExtentRectangular = true; + _modes.set(Mode::RectangularChangeExtent); return true; default: return false; } } +// Routine Description: +// - DECRQCRA - Computes and reports a checksum of the specified area of +// the buffer memory. +// Arguments: +// - id - a numeric label used to identify the request. +// - page - The page number (unused for now). +// - top - The first row of the area. +// - left - The first column of the area. +// - bottom - The last row of the area (inclusive). +// - right - The last column of the area (inclusive). +// Return value: +// - True. +bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) +{ + uint16_t checksum = 0; + // If this feature is not enabled, we'll just report a zero checksum. + if constexpr (Feature_VtChecksumReport::IsEnabled()) + { + if (page == 1) + { + // As part of the checksum, we need to include the color indices of each + // cell, and in the case of default colors, those indices come from the + // color alias table. But if they're not in the bottom 16 range, we just + // fallback to using white on black (7 and 0). + auto defaultFgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground); + auto defaultBgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground); + defaultFgIndex = defaultFgIndex < 16 ? defaultFgIndex : 7; + defaultBgIndex = defaultBgIndex < 16 ? defaultBgIndex : 0; + + const auto& textBuffer = _api.GetTextBuffer(); + const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + for (auto row = eraseRect.top; row < eraseRect.bottom; row++) + { + for (auto col = eraseRect.left; col < eraseRect.right; col++) + { + // The algorithm we're using here should match the DEC terminals + // for the ASCII and Latin-1 range. Their other character sets + // predate Unicode, though, so we'd need a custom mapping table + // to lookup the correct checksums. Considering this is only for + // testing at the moment, that doesn't seem worth the effort. + const auto cell = textBuffer.GetCellDataAt({ col, row }); + for (auto ch : cell->Chars()) + { + // That said, I've made a special allowance for U+2426, + // since that is widely used in a lot of character sets. + checksum -= (ch == L'\u2426' ? 0x1B : ch); + } + + // Since we're attempting to match the DEC checksum algorithm, + // the only attributes affecting the checksum are the ones that + // were supported by DEC terminals. + const auto attr = cell->TextAttr(); + checksum -= attr.IsProtected() ? 0x04 : 0; + checksum -= attr.IsInvisible() ? 0x08 : 0; + checksum -= attr.IsUnderlined() ? 0x10 : 0; + checksum -= attr.IsReverseVideo() ? 0x20 : 0; + checksum -= attr.IsBlinking() ? 0x40 : 0; + checksum -= attr.IsIntense() ? 0x80 : 0; + + // For the same reason, we only care about the eight basic ANSI + // colors, although technically we also report the 8-16 index + // range. Everything else gets mapped to the default colors. + const auto colorIndex = [](const auto color, const auto defaultIndex) { + return color.IsLegacy() ? color.GetIndex() : defaultIndex; + }; + const auto fgIndex = colorIndex(attr.GetForeground(), defaultFgIndex); + const auto bgIndex = colorIndex(attr.GetBackground(), defaultBgIndex); + checksum -= gsl::narrow_cast(fgIndex << 4); + checksum -= gsl::narrow_cast(bgIndex); + } + } + } + } + const auto response = wil::str_printf(L"\033P%d!~%04X\033\\", id, checksum); + _api.ReturnResponse(response); + return true; +} + // Routine Description: // - DECSWL/DECDWL/DECDHL - Sets the line rendition attribute for the current line. // Arguments: @@ -1161,7 +1449,18 @@ bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExten // - True. bool AdaptDispatch::SetLineRendition(const LineRendition rendition) { - _api.GetTextBuffer().SetCurrentLineRendition(rendition); + // The line rendition can't be changed if left/right margins are allowed. + if (!_modes.test(Mode::AllowDECSLRM)) + { + auto& textBuffer = _api.GetTextBuffer(); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + textBuffer.SetCurrentLineRendition(rendition, eraseAttributes); + // There is some variation in how this was handled by the different DEC + // terminals, but the STD 070 reference (on page D-13) makes it clear that + // the delayed wrap (aka the Last Column Flag) was expected to be reset when + // line rendition controls were executed. + textBuffer.GetCursor().ResetDelayEOLWrap(); + } return true; } @@ -1169,9 +1468,10 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) // - DSR - Reports status of a console property back to the STDIN based on the type of status requested. // Arguments: // - statusType - status type indicating what property we should report back +// - id - a numeric label used to identify the request in DECCKSR reports // Return Value: // - True if handled successfully. False otherwise. -bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType) +bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) { switch (statusType) { @@ -1184,29 +1484,56 @@ bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusTyp case DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport: _CursorPositionReport(true); return true; + case DispatchTypes::StatusType::MSR_MacroSpaceReport: + _MacroSpaceReport(); + return true; + case DispatchTypes::StatusType::MEM_MemoryChecksum: + _MacroChecksumReport(id); + return true; default: return false; } } // Routine Description: -// - DA - Reports the identity of this Virtual Terminal machine to the caller. -// - In our case, we'll report back to acknowledge we understand, but reveal no "hardware" upgrades like physical terminals of old. +// - DA - Reports the service class or conformance level that the terminal +// supports, and the set of implemented extensions. // Arguments: // - // Return Value: // - True. bool AdaptDispatch::DeviceAttributes() { - // See: http://vt100.net/docs/vt100-ug/chapter3.html#DA - _api.ReturnResponse(L"\x1b[?1;0c"); + // This first parameter of the response is 61, representing a conformance + // level of 1. The subsequent parameters identify the supported feature + // extensions. + // + // 1 = 132 column mode (ConHost only) + // 6 = Selective erase + // 7 = Soft fonts + // 21 = Horizontal scrolling + // 22 = Color text + // 23 = Greek character sets + // 24 = Turkish character sets + // 28 = Rectangular area operations + // 32 = Text macros + // 42 = ISO Latin - 2 character set + + if (_api.IsConsolePty()) + { + _api.ReturnResponse(L"\x1b[?61;6;7;21;22;23;24;28;32;42c"); + } + else + { + _api.ReturnResponse(L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c"); + } return true; } // Routine Description: // - DA2 - Reports the terminal type, firmware version, and hardware options. // For now we're following the XTerm practice of using 0 to represent a VT100 -// terminal, the version is hardcoded as 10 (1.0), and the hardware option +// terminal, the version is hard-coded as 10 (1.0), and the hardware option // is set to 1 (indicating a PC Keyboard). // Arguments: // - @@ -1220,7 +1547,7 @@ bool AdaptDispatch::SecondaryDeviceAttributes() // Routine Description: // - DA3 - Reports the terminal unit identification code. Terminal emulators -// typically return a hardcoded value, the most common being all zeros. +// typically return a hard-coded value, the most common being all zeros. // Arguments: // - // Return Value: @@ -1248,7 +1575,7 @@ bool AdaptDispatch::Vt52DeviceAttributes() // Routine Description: // - DECREQTPARM - This sequence was originally used on the VT100 terminal to // report the serial communication parameters (baud rate, data bits, parity, -// etc.). On modern terminal emulators, the response is simply hardcoded. +// etc.). On modern terminal emulators, the response is simply hard-coded. // Arguments: // - permission - This would originally have determined whether the terminal // was allowed to send unsolicited reports or not. @@ -1258,7 +1585,7 @@ bool AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPerm { // We don't care whether unsolicited reports are allowed or not, but the // requested permission does determine the value of the first response - // parameter. The remaining parameters are just hardcoded to indicate a + // parameter. The remaining parameters are just hard-coded to indicate a // 38400 baud connection, which matches the XTerm response. The full // parameter sequence is as follows: // - response type: 2 or 3 (unsolicited or solicited) @@ -1309,36 +1636,64 @@ void AdaptDispatch::_CursorPositionReport(const bool extendedReport) til::point cursorPosition{ textBuffer.GetCursor().GetPosition() }; // Now adjust it for its position in respect to the current viewport top. - cursorPosition.Y -= viewport.top; + cursorPosition.y -= viewport.top; // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. - cursorPosition.X++; - cursorPosition.Y++; + cursorPosition.x++; + cursorPosition.y++; - // If the origin mode is relative, line numbers start at top margin of the scrolling region. - if (_isOriginModeRelative) + // If the origin mode is set, the cursor is relative to the margin origin. + if (_modes.test(Mode::Origin)) { - const auto topMargin = _GetVerticalMargins(viewport, false).first; - cursorPosition.Y -= topMargin; + cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; + cursorPosition.y -= _GetVerticalMargins(viewport, false).first; } // Now send it back into the input channel of the console. if (extendedReport) { // An extended report should also include the page number, but for now - // we hardcode it to 1, since we don't yet support paging (GH#13892). + // we hard-code it to 1, since we don't yet support paging (GH#13892). const auto pageNumber = 1; - const auto response = wil::str_printf(L"\x1b[?%d;%d;%dR", cursorPosition.Y, cursorPosition.X, pageNumber); + const auto response = wil::str_printf(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber); _api.ReturnResponse(response); } else { // The standard report only returns the cursor position. - const auto response = wil::str_printf(L"\x1b[%d;%dR", cursorPosition.Y, cursorPosition.X); + const auto response = wil::str_printf(L"\x1b[%d;%dR", cursorPosition.y, cursorPosition.x); _api.ReturnResponse(response); } } +// Routine Description: +// - DECMSR - Reports the amount of space available for macro definitions. +// Arguments: +// - +// Return Value: +// - +void AdaptDispatch::_MacroSpaceReport() const +{ + const auto spaceInBytes = _macroBuffer ? _macroBuffer->GetSpaceAvailable() : MacroBuffer::MAX_SPACE; + // The available space is measured in blocks of 16 bytes, so we need to divide by 16. + const auto response = wil::str_printf(L"\x1b[%zu*{", spaceInBytes / 16); + _api.ReturnResponse(response); +} + +// Routine Description: +// - DECCKSR - Reports a checksum of the current macro definitions. +// Arguments: +// - id - a numeric label used to identify the DSR request +// Return Value: +// - +void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const +{ + const auto requestId = id.value_or(0); + const auto checksum = _macroBuffer ? _macroBuffer->CalculateChecksum() : 0; + const auto response = wil::str_printf(L"\033P%d!~%04X\033\\", requestId, checksum); + _api.ReturnResponse(response); +} + // Routine Description: // - Generalizes scrolling movement for up/down // Arguments: @@ -1351,7 +1706,8 @@ void AdaptDispatch::_ScrollMovement(const VTInt delta) auto& textBuffer = _api.GetTextBuffer(); const auto bufferWidth = textBuffer.GetSize().Width(); const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - _ScrollRectVertically(textBuffer, { 0, topMargin, bufferWidth, bottomMargin + 1 }, delta); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } // Routine Description: @@ -1362,7 +1718,7 @@ void AdaptDispatch::_ScrollMovement(const VTInt delta) // - True. bool AdaptDispatch::ScrollUp(const VTInt uiDistance) { - _ScrollMovement(-gsl::narrow_cast(uiDistance)); + _ScrollMovement(-uiDistance); return true; } @@ -1374,23 +1730,7 @@ bool AdaptDispatch::ScrollUp(const VTInt uiDistance) // - True. bool AdaptDispatch::ScrollDown(const VTInt uiDistance) { - _ScrollMovement(gsl::narrow_cast(uiDistance)); - return true; -} - -// Routine Description: -// - DECSCPP / DECCOLM Sets the number of columns "per page" AKA sets the console width. -// DECCOLM also clear the screen (like a CSI 2 J sequence), while DECSCPP just sets the width. -// (DECCOLM will do this separately of this function) -// Arguments: -// - columns - Number of columns -// Return Value: -// - True. -bool AdaptDispatch::SetColumns(const VTInt columns) -{ - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - _api.ResizeWindow(columns, viewportHeight); + _ScrollMovement(uiDistance); return true; } @@ -1398,71 +1738,71 @@ bool AdaptDispatch::SetColumns(const VTInt columns) // - DECCOLM not only sets the number of columns, but also clears the screen buffer, // resets the page margins and origin mode, and places the cursor at 1,1 // Arguments: -// - columns - Number of columns +// - enable - the number of columns is set to 132 if true, 80 if false. // Return Value: -// - True. -bool AdaptDispatch::_DoDECCOLMHelper(const VTInt columns) +// - +void AdaptDispatch::_SetColumnMode(const bool enable) { // Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling. - if (_isDECCOLMAllowed) + if (_modes.test(Mode::AllowDECCOLM) && !_api.IsConsolePty()) { - SetColumns(columns); - SetOriginMode(false); + const auto viewport = _api.GetViewport(); + const auto viewportHeight = viewport.bottom - viewport.top; + const auto viewportWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); + _api.ResizeWindow(viewportWidth, viewportHeight); + _modes.set(Mode::Column, enable); + _modes.reset(Mode::Origin, Mode::AllowDECSLRM); CursorPosition(1, 1); EraseInDisplay(DispatchTypes::EraseType::All); _DoSetTopBottomScrollingMargins(0, 0); + _DoSetLeftRightScrollingMargins(0, 0); } - return true; -} - -// Routine Description : -// - Retrieves the various StateMachine parser modes. -// Arguments: -// - mode - the parser mode to query. -// Return Value: -// - true if the mode is enabled. false if disabled. -bool AdaptDispatch::_GetParserMode(const StateMachine::Mode mode) const -{ - return _api.GetStateMachine().GetParserMode(mode); } // Routine Description: -// - Sets the various StateMachine parser modes. +// - Set the alternate screen buffer mode. In virtual terminals, there exists +// both a "main" screen buffer and an alternate. This mode is used to switch +// between the two. // Arguments: -// - mode - the parser mode to change. -// - enable - set to true to enable the mode, false to disable it. +// - enable - true selects the alternate buffer, false returns to the main buffer. // Return Value: // - -void AdaptDispatch::_SetParserMode(const StateMachine::Mode mode, const bool enable) +void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable) { - _api.GetStateMachine().SetParserMode(mode, enable); + if (enable) + { + CursorSaveState(); + const auto& textBuffer = _api.GetTextBuffer(); + _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer)); + _usingAltBuffer = true; + } + else + { + _api.UseMainScreenBuffer(); + _usingAltBuffer = false; + CursorRestoreState(); + } } // Routine Description: -// - Sets the various terminal input modes. -// Arguments: -// - mode - the input mode to change. -// - enable - set to true to enable the mode, false to disable it. +// - Determines whether we need to pass through input mode requests. +// If we're a conpty, AND WE'RE IN VT INPUT MODE, always pass input mode requests +// The VT Input mode check is to work around ssh.exe v7.7, which uses VT +// output, but not Input. +// The original comment said, "Once the conpty supports these types of input, +// this check can be removed. See GH#4911". Unfortunately, time has shown +// us that SSH 7.7 _also_ requests mouse input and that can have a user interface +// impact on the actual connected terminal. We can't remove this check, +// because SSH <=7.7 is out in the wild on all versions of Windows <=2004. // Return Value: -// - true if successful. false otherwise. -bool AdaptDispatch::_SetInputMode(const TerminalInput::Mode mode, const bool enable) +// - True if we should pass through. False otherwise. +bool AdaptDispatch::_PassThroughInputModes() { - _terminalInput.SetInputMode(mode, enable); - - // If we're a conpty, AND WE'RE IN VT INPUT MODE, always pass input mode requests - // The VT Input mode check is to work around ssh.exe v7.7, which uses VT - // output, but not Input. - // The original comment said, "Once the conpty supports these types of input, - // this check can be removed. See GH#4911". Unfortunately, time has shown - // us that SSH 7.7 _also_ requests mouse input and that can have a user interface - // impact on the actual connected terminal. We can't remove this check, - // because SSH <=7.7 is out in the wild on all versions of Windows <=2004. - - return !_api.IsConsolePty() || !_api.IsVtInputEnabled(); + return _api.IsConsolePty() && _api.IsVtInputEnabled(); } // Routine Description: -// - Support routine for routing private mode parameters to be set/reset as flags +// - Support routine for routing mode parameters to be set/reset as flags // Arguments: // - param - mode parameter to set/reset // - enable - True for set, false for unset. @@ -1470,84 +1810,122 @@ bool AdaptDispatch::_SetInputMode(const TerminalInput::Mode mode, const bool ena // - True if handled successfully. False otherwise. bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, const bool enable) { - auto success = false; switch (param) { + case DispatchTypes::ModeParams::IRM_InsertReplaceMode: + _modes.set(Mode::InsertReplace, enable); + return true; + case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode: + // VT apps expect that the system and input modes are the same, so if + // they become out of sync, we just act as if LNM mode isn't supported. + if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed)) + { + _api.SetSystemMode(ITerminalApi::Mode::LineFeed, enable); + _terminalInput.SetInputMode(TerminalInput::Mode::LineFeed, enable); + } + return true; case DispatchTypes::ModeParams::DECCKM_CursorKeysMode: - // set - Enable Application Mode, reset - Normal mode - success = SetCursorKeysMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::DECANM_AnsiMode: - success = SetAnsiMode(enable); - break; + return SetAnsiMode(enable); case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns: - success = _DoDECCOLMHelper(enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); - break; + _SetColumnMode(enable); + return true; case DispatchTypes::ModeParams::DECSCNM_ScreenMode: - success = SetScreenMode(enable); - break; + _renderSettings.SetRenderMode(RenderSettings::Mode::ScreenReversed, enable); + // No need to force a redraw in pty mode. + if (_api.IsConsolePty()) + { + return false; + } + _renderer.TriggerRedrawAll(); + return true; case DispatchTypes::ModeParams::DECOM_OriginMode: + _modes.set(Mode::Origin, enable); // The cursor is also moved to the new home position when the origin mode is set or reset. - success = SetOriginMode(enable) && CursorPosition(1, 1); - break; + CursorPosition(1, 1); + return true; case DispatchTypes::ModeParams::DECAWM_AutoWrapMode: - success = SetAutoWrapMode(enable); - break; + _api.SetSystemMode(ITerminalApi::Mode::AutoWrap, enable); + // Resetting DECAWM should also reset the delayed wrap flag. + if (!enable) + { + _api.GetTextBuffer().GetCursor().ResetDelayEOLWrap(); + } + return true; case DispatchTypes::ModeParams::DECARM_AutoRepeatMode: - success = _SetInputMode(TerminalInput::Mode::AutoRepeat, enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - success = EnableCursorBlinking(enable); - break; + _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(enable); + return !_api.IsConsolePty(); case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - success = CursorVisibility(enable); - break; + _api.GetTextBuffer().GetCursor().SetIsVisible(enable); + return true; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: - success = EnableDECCOLMSupport(enable); - break; + _modes.set(Mode::AllowDECCOLM, enable); + return true; + case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: + _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode: - success = _SetInputMode(TerminalInput::Mode::BackarrowKey, enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::BackarrowKey, enable); + return !_PassThroughInputModes(); + case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode: + _modes.set(Mode::AllowDECSLRM, enable); + _DoSetLeftRightScrollingMargins(0, 0); + if (enable) + { + // If we've allowed left/right margins, we can't have line renditions. + const auto viewport = _api.GetViewport(); + auto& textBuffer = _api.GetTextBuffer(); + textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + } + return true; + case DispatchTypes::ModeParams::DECECM_EraseColorMode: + _modes.set(Mode::EraseColor, enable); + return true; case DispatchTypes::ModeParams::VT200_MOUSE_MODE: - success = EnableVT200MouseMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::BUTTON_EVENT_MOUSE_MODE: - success = EnableButtonEventMouseMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ANY_EVENT_MOUSE_MODE: - success = EnableAnyEventMouseMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::UTF8_EXTENDED_MODE: - success = EnableUTF8ExtendedMouseMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::SGR_EXTENDED_MODE: - success = EnableSGRExtendedMouseMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::FOCUS_EVENT_MODE: - success = EnableFocusEventMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::FocusEvent, enable); + // GH#12799 - If the app requested that we disable focus events, DON'T pass + // that through. ConPTY would _always_ like to know about focus events. + return !_PassThroughInputModes() || !enable; case DispatchTypes::ModeParams::ALTERNATE_SCROLL: - success = EnableAlternateScroll(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::AlternateScroll, enable); + return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer: - success = enable ? UseAlternateScreenBuffer() : UseMainScreenBuffer(); - break; + _SetAlternateScreenBufferMode(enable); + return true; case DispatchTypes::ModeParams::XTERM_BracketedPasteMode: - success = EnableXtermBracketedPasteMode(enable); - break; + _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, enable); + return !_api.IsConsolePty(); case DispatchTypes::ModeParams::W32IM_Win32InputMode: - success = EnableWin32InputMode(enable); - break; + _terminalInput.SetInputMode(TerminalInput::Mode::Win32, enable); + return !_PassThroughInputModes(); default: // If no functions to call, overall dispatch was a failure. - success = false; - break; + return false; } - return success; } // Routine Description: -// - DECSET - Enables the given DEC private mode params. +// - SM/DECSET - Enables the given mode parameter (both ANSI and private). // Arguments: // - param - mode parameter to set // Return Value: @@ -1558,7 +1936,7 @@ bool AdaptDispatch::SetMode(const DispatchTypes::ModeParams param) } // Routine Description: -// - DECRST - Disables the given DEC private mode params. +// - RM/DECRST - Disables the given mode parameter (both ANSI and private). // Arguments: // - param - mode parameter to reset // Return Value: @@ -1568,6 +1946,125 @@ bool AdaptDispatch::ResetMode(const DispatchTypes::ModeParams param) return _ModeParamsHelper(param, false); } +// Routine Description: +// - DECRQM - Requests the current state of a given mode number. The result +// is reported back with a DECRPM escape sequence. +// Arguments: +// - param - the mode number being queried +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) +{ + auto enabled = std::optional{}; + + switch (param) + { + case DispatchTypes::ModeParams::IRM_InsertReplaceMode: + enabled = _modes.test(Mode::InsertReplace); + break; + case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode: + // VT apps expect that the system and input modes are the same, so if + // they become out of sync, we just act as if LNM mode isn't supported. + if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed)) + { + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed); + } + break; + case DispatchTypes::ModeParams::DECCKM_CursorKeysMode: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::CursorKey); + break; + case DispatchTypes::ModeParams::DECANM_AnsiMode: + enabled = _api.GetStateMachine().GetParserMode(StateMachine::Mode::Ansi); + break; + case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns: + // DECCOLM is not supported in conpty mode + if (!_api.IsConsolePty()) + { + enabled = _modes.test(Mode::Column); + } + break; + case DispatchTypes::ModeParams::DECSCNM_ScreenMode: + enabled = _renderSettings.GetRenderMode(RenderSettings::Mode::ScreenReversed); + break; + case DispatchTypes::ModeParams::DECOM_OriginMode: + enabled = _modes.test(Mode::Origin); + break; + case DispatchTypes::ModeParams::DECAWM_AutoWrapMode: + enabled = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); + break; + case DispatchTypes::ModeParams::DECARM_AutoRepeatMode: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat); + break; + case DispatchTypes::ModeParams::ATT610_StartCursorBlink: + enabled = _api.GetTextBuffer().GetCursor().IsBlinkingAllowed(); + break; + case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: + enabled = _api.GetTextBuffer().GetCursor().IsVisible(); + break; + case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: + // DECCOLM is not supported in conpty mode + if (!_api.IsConsolePty()) + { + enabled = _modes.test(Mode::AllowDECCOLM); + } + break; + case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Keypad); + break; + case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::BackarrowKey); + break; + case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode: + enabled = _modes.test(Mode::AllowDECSLRM); + break; + case DispatchTypes::ModeParams::DECECM_EraseColorMode: + enabled = _modes.test(Mode::EraseColor); + break; + case DispatchTypes::ModeParams::VT200_MOUSE_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking); + break; + case DispatchTypes::ModeParams::BUTTON_EVENT_MOUSE_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::ButtonEventMouseTracking); + break; + case DispatchTypes::ModeParams::ANY_EVENT_MOUSE_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AnyEventMouseTracking); + break; + case DispatchTypes::ModeParams::UTF8_EXTENDED_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Utf8MouseEncoding); + break; + case DispatchTypes::ModeParams::SGR_EXTENDED_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::SgrMouseEncoding); + break; + case DispatchTypes::ModeParams::FOCUS_EVENT_MODE: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::FocusEvent); + break; + case DispatchTypes::ModeParams::ALTERNATE_SCROLL: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AlternateScroll); + break; + case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer: + enabled = _usingAltBuffer; + break; + case DispatchTypes::ModeParams::XTERM_BracketedPasteMode: + enabled = _api.GetSystemMode(ITerminalApi::Mode::BracketedPaste); + break; + case DispatchTypes::ModeParams::W32IM_Win32InputMode: + enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Win32); + break; + default: + enabled = std::nullopt; + break; + } + + // 1 indicates the mode is enabled, 2 it's disabled, and 0 it's unsupported + const auto state = enabled.has_value() ? (enabled.value() ? 1 : 2) : 0; + const auto isPrivate = param >= DispatchTypes::DECPrivateMode(0); + const auto prefix = isPrivate ? L"?" : L""; + const auto mode = isPrivate ? param - DispatchTypes::DECPrivateMode(0) : param; + const auto response = wil::str_printf(L"\x1b[%s%d;%d$y", prefix, mode, state); + _api.ReturnResponse(response); + return true; +} + // - DECKPAM, DECKPNM - Sets the keypad input mode to either Application mode or Numeric mode (true, false respectively) // Arguments: // - applicationMode - set to true to enable Application Mode Input, false for Numeric Mode Input. @@ -1575,50 +2072,8 @@ bool AdaptDispatch::ResetMode(const DispatchTypes::ModeParams param) // - True if handled successfully. False otherwise. bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) { - return _SetInputMode(TerminalInput::Mode::Keypad, fApplicationMode); -} - -// Method Description: -// - win32-input-mode: Enable sending full input records encoded as a string of -// characters to the client application. -// Arguments: -// - win32InputMode - set to true to enable win32-input-mode, false to disable. -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::EnableWin32InputMode(const bool win32InputMode) -{ - return _SetInputMode(TerminalInput::Mode::Win32, win32InputMode); -} - -// - DECCKM - Sets the cursor keys input mode to either Application mode or Normal mode (true, false respectively) -// Arguments: -// - applicationMode - set to true to enable Application Mode Input, false for Normal Mode Input. -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::SetCursorKeysMode(const bool applicationMode) -{ - return _SetInputMode(TerminalInput::Mode::CursorKey, applicationMode); -} - -// - att610 - Enables or disables the cursor blinking. -// Arguments: -// - enable - set to true to enable blinking, false to disable -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::EnableCursorBlinking(const bool enable) -{ - auto& cursor = _api.GetTextBuffer().GetCursor(); - cursor.SetBlinkingAllowed(enable); - - // GH#2642 - From what we've gathered from other terminals, when blinking is - // disabled, the cursor should remain On always, and have the visibility - // controlled by the IsVisible property. So when you do a printf "\e[?12l" - // to disable blinking, the cursor stays stuck On. At this point, only the - // cursor visibility property controls whether the user can see it or not. - // (Yes, the cursor can be On and NOT Visible) - cursor.SetIsOn(true); - - return !_api.IsConsolePty(); + _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, fApplicationMode); + return !_PassThroughInputModes(); } // Routine Description: @@ -1628,24 +2083,25 @@ bool AdaptDispatch::EnableCursorBlinking(const bool enable) // - delta - Number of lines to modify (positive if inserting, negative if deleting). // Return Value: // - -void AdaptDispatch::_InsertDeleteLineHelper(const int32_t delta) +void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta) { const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); const auto bufferWidth = textBuffer.GetSize().Width(); auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().Y; + const auto col = cursor.GetPosition().x; + const auto row = cursor.GetPosition().y; const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - if (row >= topMargin && row <= bottomMargin) + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin. - _ScrollRectVertically(textBuffer, { 0, row, bufferWidth, bottomMargin + 1 }, delta); + _ScrollRectVertically(textBuffer, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); // The IL and DL controls are also expected to move the cursor to the left margin. - // For now this is just column 0, since we don't yet support DECSLRM. - cursor.SetXPosition(0); + cursor.SetXPosition(leftMargin); _ApplyCursorMovementFlags(cursor); } } @@ -1660,7 +2116,7 @@ void AdaptDispatch::_InsertDeleteLineHelper(const int32_t delta) // - True. bool AdaptDispatch::InsertLine(const VTInt distance) { - _InsertDeleteLineHelper(gsl::narrow_cast(distance)); + _InsertDeleteLineHelper(distance); return true; } @@ -1678,7 +2134,58 @@ bool AdaptDispatch::InsertLine(const VTInt distance) // - True. bool AdaptDispatch::DeleteLine(const VTInt distance) { - _InsertDeleteLineHelper(-gsl::narrow_cast(distance)); + _InsertDeleteLineHelper(-distance); + return true; +} + +// Routine Description: +// - Internal logic for adding or removing columns in the active screen buffer. +// Parameters: +// - delta - Number of columns to modify (positive if inserting, negative if deleting). +// Return Value: +// - +void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta) +{ + const auto viewport = _api.GetViewport(); + auto& textBuffer = _api.GetTextBuffer(); + const auto bufferWidth = textBuffer.GetSize().Width(); + + const auto& cursor = textBuffer.GetCursor(); + const auto col = cursor.GetPosition().x; + const auto row = cursor.GetPosition().y; + + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) + { + // We emulate inserting and deleting by scrolling the area between the cursor and the right margin. + _ScrollRectHorizontally(textBuffer, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + } +} + +// Routine Description: +// - DECIC - This control function inserts one or more blank columns in the +// scrolling region, starting at the column that has the cursor. +// Arguments: +// - distance - number of columns to insert +// Return Value: +// - True. +bool AdaptDispatch::InsertColumn(const VTInt distance) +{ + _InsertDeleteColumnHelper(distance); + return true; +} + +// Routine Description: +// - DECDC - This control function deletes one or more columns in the scrolling +// region, starting with the column that has the cursor. +// Arguments: +// - distance - number of columns to delete +// Return Value: +// - True. +bool AdaptDispatch::DeleteColumn(const VTInt distance) +{ + _InsertDeleteColumnHelper(-distance); return true; } @@ -1693,58 +2200,11 @@ bool AdaptDispatch::SetAnsiMode(const bool ansiMode) // need to be reset to defaults, even if the mode doesn't actually change. _termOutput = {}; - _SetParserMode(StateMachine::Mode::Ansi, ansiMode); - _SetInputMode(TerminalInput::Mode::Ansi, ansiMode); + _api.GetStateMachine().SetParserMode(StateMachine::Mode::Ansi, ansiMode); + _terminalInput.SetInputMode(TerminalInput::Mode::Ansi, ansiMode); - // We don't check the _SetInputMode return value, because we'll never want - // to forward a DECANM mode change over conpty. - return true; -} - -// Routine Description: -// - DECSCNM - Sets the screen mode to either normal or reverse. -// When in reverse screen mode, the background and foreground colors are switched. -// Arguments: -// - reverseMode - set to true to enable reverse screen mode, false for normal mode. -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::SetScreenMode(const bool reverseMode) -{ - // If we're a conpty, always return false - if (_api.IsConsolePty()) - { - return false; - } - _renderSettings.SetRenderMode(RenderSettings::Mode::ScreenReversed, reverseMode); - _renderer.TriggerRedrawAll(); - return true; -} - -// Routine Description: -// - DECOM - Sets the cursor addressing origin mode to relative or absolute. -// When relative, line numbers start at top margin of the user-defined scrolling region. -// When absolute, line numbers are independent of the scrolling region. -// Arguments: -// - relativeMode - set to true to use relative addressing, false for absolute addressing. -// Return Value: -// - True. -bool AdaptDispatch::SetOriginMode(const bool relativeMode) noexcept -{ - _isOriginModeRelative = relativeMode; - return true; -} - -// Routine Description: -// - DECAWM - Sets the Auto Wrap Mode. -// This controls whether the cursor moves to the beginning of the next row -// when it reaches the end of the current row. -// Arguments: -// - wrapAtEOL - set to true to wrap, false to overwrite the last character. -// Return Value: -// - True. -bool AdaptDispatch::SetAutoWrapMode(const bool wrapAtEOL) -{ - _api.SetAutoWrapMode(wrapAtEOL); + // While input mode changes are often forwarded over conpty, we never want + // to do that for the DECANM mode. return true; } @@ -1756,10 +2216,12 @@ bool AdaptDispatch::SetAutoWrapMode(const bool wrapAtEOL) // Arguments: // - topMargin - the line number for the top margin. // - bottomMargin - the line number for the bottom margin. +// - homeCursor - move the cursor to the home position. // Return Value: // - void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, - const VTInt bottomMargin) + const VTInt bottomMargin, + const bool homeCursor) { // so notes time: (input -> state machine out -> adapter out -> conhost internal) // having only a top param is legal ([3;r -> 3,0 -> 3,h -> 3,h,true) @@ -1800,9 +2262,14 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, actualTop -= 1; actualBottom -= 1; } - _scrollMargins.Top = actualTop; - _scrollMargins.Bottom = actualBottom; - _api.SetScrollingRegion(_scrollMargins); + _scrollMargins.top = actualTop; + _scrollMargins.bottom = actualBottom; + // If requested, we may also need to move the cursor to the home + // position, but only if the requested margins were valid. + if (homeCursor) + { + CursorPosition(1, 1); + } } } @@ -1819,10 +2286,90 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, bool AdaptDispatch::SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) { - // When this is called, the cursor should also be moved to home. - // Other functions that only need to set/reset the margins should call _DoSetTopBottomScrollingMargins - _DoSetTopBottomScrollingMargins(topMargin, bottomMargin); - CursorPosition(1, 1); + _DoSetTopBottomScrollingMargins(topMargin, bottomMargin, true); + return true; +} + +// Routine Description: +// - DECSLRM - Set Scrolling Region +// This control function sets the left and right margins for the current page. +// You cannot perform scrolling outside the margins. +// Default: Margins are at the page limits. +// Arguments: +// - leftMargin - the column number for the left margin. +// - rightMargin - the column number for the right margin. +// - homeCursor - move the cursor to the home position. +// Return Value: +// - +void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin, + const VTInt rightMargin, + const bool homeCursor) +{ + til::CoordType actualLeft = leftMargin; + til::CoordType actualRight = rightMargin; + + const auto& textBuffer = _api.GetTextBuffer(); + const auto bufferWidth = textBuffer.GetSize().Width(); + // The default left margin is column 1 + if (actualLeft == 0) + { + actualLeft = 1; + } + // The default right margin is the buffer width + if (actualRight == 0) + { + actualRight = bufferWidth; + } + // The left margin must be less than the right margin, and the + // right margin must be less than or equal to the buffer width + if (actualLeft < actualRight && actualRight <= bufferWidth) + { + if (actualLeft == 1 && actualRight == bufferWidth) + { + // Client requests setting margins to the entire screen + // - clear them instead of setting them. + actualLeft = 0; + actualRight = 0; + } + else + { + // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. + actualLeft -= 1; + actualRight -= 1; + } + _scrollMargins.left = actualLeft; + _scrollMargins.right = actualRight; + // If requested, we may also need to move the cursor to the home + // position, but only if the requested margins were valid. + if (homeCursor) + { + CursorPosition(1, 1); + } + } +} + +// Routine Description: +// - DECSLRM - Set Scrolling Region +// This control function sets the left and right margins for the current page. +// You cannot perform scrolling outside the margins. +// Default: Margins are at the page limits. +// Arguments: +// - leftMargin - the column number for the left margin. +// - rightMargin - the column number for the right margin. +// Return Value: +// - True. +bool AdaptDispatch::SetLeftRightScrollingMargins(const VTInt leftMargin, + const VTInt rightMargin) +{ + if (_modes.test(Mode::AllowDECSLRM)) + { + _DoSetLeftRightScrollingMargins(leftMargin, rightMargin, true); + } + else + { + // When DECSLRM isn't allowed, `CSI s` is interpreted as ANSISYSSC. + CursorSaveState(); + } return true; } @@ -1851,6 +2398,104 @@ bool AdaptDispatch::CarriageReturn() return _CursorMovePosition(Offset::Unchanged(), Offset::Absolute(1), true); } +// Routine Description: +// - Helper method for executing a line feed, possibly preceded by carriage return. +// Arguments: +// - textBuffer - Target buffer on which the line feed is executed. +// - withReturn - Set to true if a carriage return should be performed as well. +// - wrapForced - Set to true is the line feed was the result of the line wrapping. +// Return Value: +// - +void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced) +{ + const auto viewport = _api.GetViewport(); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto bufferHeight = textBuffer.GetSize().Height(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + + auto& cursor = textBuffer.GetCursor(); + const auto currentPosition = cursor.GetPosition(); + auto newPosition = currentPosition; + + // If the line was forced to wrap, set the wrap status. + // When explicitly moving down a row, clear the wrap status. + 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. + if (withReturn) + { + const auto clampToMargin = currentPosition.y >= topMargin && + currentPosition.y <= bottomMargin && + currentPosition.x >= leftMargin; + newPosition.x = clampToMargin ? leftMargin : 0; + } + + if (currentPosition.y != bottomMargin || newPosition.x < leftMargin || newPosition.x > rightMargin) + { + // If we're not at the bottom margin, or outside the horizontal margins, + // then there's no scrolling, so we make sure we don't move past the + // bottom of the viewport. + newPosition.y = std::min(currentPosition.y + 1, viewport.bottom - 1); + newPosition = textBuffer.ClampPositionWithinLine(newPosition); + } + else if (topMargin > viewport.top || leftMargin > 0 || rightMargin < bufferWidth - 1) + { + // If the top margin isn't at the top of the viewport, or the + // horizontal margins are set, then we're just scrolling the margin + // area and the cursor stays where it is. + _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + } + else if (viewport.bottom < bufferHeight) + { + // If the top margin is at the top of the viewport, then we'll scroll + // the content up by panning the viewport down, and also move the cursor + // down a row. But we only do this if the viewport hasn't yet reached + // the end of the buffer. + _api.SetViewportPosition({ viewport.left, viewport.top + 1 }); + newPosition.y++; + + // And if the bottom margin didn't cover the full viewport, we copy the + // lower part of the viewport down so it remains static. But for a full + // pan we reset the newly revealed row with the erase attributes. + if (bottomMargin < viewport.bottom - 1) + { + _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); + } + else + { + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes); + } + } + else + { + // If the viewport has reached the end of the buffer, we can't pan down, + // so we cycle the row coordinates, which effectively scrolls the buffer + // content up. In this case we don't need to move the cursor down. + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + textBuffer.IncrementCircularBuffer(eraseAttributes); + _api.NotifyBufferRotation(1); + + // We trigger a scroll rather than a redraw, since that's more efficient, + // but we need to turn the cursor off before doing so, otherwise a ghost + // cursor can be left behind in the previous position. + cursor.SetIsOn(false); + textBuffer.TriggerScroll({ 0, -1 }); + + // And again, if the bottom margin didn't cover the full viewport, we + // copy the lower part of the viewport down so it remains static. + if (bottomMargin < viewport.bottom - 1) + { + _ScrollRectVertically(textBuffer, { 0, bottomMargin, bufferWidth, bufferHeight }, 1); + } + } + + cursor.SetPosition(newPosition); + _ApplyCursorMovementFlags(cursor); +} + // Routine Description: // - IND/NEL - Performs a line feed, possibly preceded by carriage return. // Moves the cursor down one line, and possibly also to the leftmost column. @@ -1860,16 +2505,17 @@ bool AdaptDispatch::CarriageReturn() // - True if handled successfully. False otherwise. bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) { + auto& textBuffer = _api.GetTextBuffer(); switch (lineFeedType) { case DispatchTypes::LineFeedType::DependsOnMode: - _api.LineFeed(_api.GetLineFeedMode()); + _DoLineFeed(textBuffer, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); return true; case DispatchTypes::LineFeedType::WithoutReturn: - _api.LineFeed(false); + _DoLineFeed(textBuffer, false, false); return true; case DispatchTypes::LineFeedType::WithReturn: - _api.LineFeed(true); + _DoLineFeed(textBuffer, true, false); return true; default: return false; @@ -1889,19 +2535,82 @@ bool AdaptDispatch::ReverseLineFeed() auto& textBuffer = _api.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); const auto cursorPosition = cursor.GetPosition(); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); // If the cursor is at the top of the margin area, we shift the buffer // contents down, to emulate inserting a line at that point. - if (cursorPosition.Y == topMargin) + if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin) { - const auto bufferWidth = textBuffer.GetSize().Width(); - _ScrollRectVertically(textBuffer, { 0, topMargin, bufferWidth, bottomMargin + 1 }, 1); + _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } - else if (cursorPosition.Y > viewport.top) + else if (cursorPosition.y > viewport.top) { // Otherwise we move the cursor up, but not past the top of the viewport. - cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.X, cursorPosition.Y - 1 })); + cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.x, cursorPosition.y - 1 })); + _ApplyCursorMovementFlags(cursor); + } + return true; +} + +// Routine Description: +// - DECBI - Moves the cursor back one column, scrolling the screen +// horizontally if it reaches the left margin. +// Arguments: +// - None +// Return Value: +// - True. +bool AdaptDispatch::BackIndex() +{ + const auto viewport = _api.GetViewport(); + auto& textBuffer = _api.GetTextBuffer(); + auto& cursor = textBuffer.GetCursor(); + const auto cursorPosition = cursor.GetPosition(); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + + // If the cursor is at the left of the margin area, we shift the buffer right. + if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) + { + _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + } + // Otherwise we move the cursor left, but not past the start of the line. + else if (cursorPosition.x > 0) + { + cursor.SetXPosition(cursorPosition.x - 1); + _ApplyCursorMovementFlags(cursor); + } + return true; +} + +// Routine Description: +// - DECFI - Moves the cursor forward one column, scrolling the screen +// horizontally if it reaches the right margin. +// Arguments: +// - None +// Return Value: +// - True. +bool AdaptDispatch::ForwardIndex() +{ + const auto viewport = _api.GetViewport(); + auto& textBuffer = _api.GetTextBuffer(); + auto& cursor = textBuffer.GetCursor(); + const auto cursorPosition = cursor.GetPosition(); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + + // If the cursor is at the right of the margin area, we shift the buffer left. + if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) + { + _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + } + // Otherwise we move the cursor right, but not past the end of the line. + else if (cursorPosition.x < textBuffer.GetLineWidth(cursorPosition.y) - 1) + { + cursor.SetXPosition(cursorPosition.x + 1); _ApplyCursorMovementFlags(cursor); } return true; @@ -1919,36 +2628,6 @@ bool AdaptDispatch::SetWindowTitle(std::wstring_view title) return true; } -// - ASBSET - Creates and swaps to the alternate screen buffer. In virtual terminals, there exists both a "main" -// screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already -// existing alternate, it is discarded. -// Arguments: -// - None -// Return Value: -// - True. -bool AdaptDispatch::UseAlternateScreenBuffer() -{ - CursorSaveState(); - _api.UseAlternateScreenBuffer(); - _usingAltBuffer = true; - return true; -} - -// Routine Description: -// - ASBRST - From the alternate buffer, returns to the main screen buffer. -// From the main screen buffer, does nothing. The alternate is discarded. -// Arguments: -// - None -// Return Value: -// - True. -bool AdaptDispatch::UseMainScreenBuffer() -{ - _api.UseMainScreenBuffer(); - _usingAltBuffer = false; - CursorRestoreState(); - return true; -} - //Routine Description: // HTS - sets a VT tab stop in the cursor's current column. //Arguments: @@ -1958,8 +2637,8 @@ bool AdaptDispatch::UseMainScreenBuffer() bool AdaptDispatch::HorizontalTabSet() { const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().X; - const auto column = textBuffer.GetCursor().GetPosition().X; + const auto width = textBuffer.GetSize().Dimensions().width; + const auto column = textBuffer.GetCursor().GetPosition().x; _InitTabStopsForWidth(width); _tabStopColumns.at(column) = true; @@ -1980,12 +2659,19 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs) { auto& textBuffer = _api.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); - const auto width = textBuffer.GetLineWidth(cursor.GetPosition().Y); - auto column = cursor.GetPosition().X; + auto column = cursor.GetPosition().x; + const auto row = cursor.GetPosition().y; + const auto width = textBuffer.GetLineWidth(row); auto tabsPerformed = 0; + const auto viewport = _api.GetViewport(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); + const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin; + const auto maxColumn = clampToMargin ? rightMargin : width - 1; + _InitTabStopsForWidth(width); - while (column + 1 < width && tabsPerformed < numTabs) + while (column < maxColumn && tabsPerformed < numTabs) { column++; if (til::at(_tabStopColumns, column)) @@ -1994,8 +2680,20 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs) } } + // While the STD 070 reference suggests that horizontal tabs should reset + // the delayed wrap, almost none of the DEC terminals actually worked that + // way, and most modern terminal emulators appear to have taken the same + // approach (i.e. they don't reset). For us this is a bit messy, since all + // cursor movement resets the flag automatically, so we need to save the + // original state here, and potentially reapply it after the move. + const auto delayedWrapOriginallySet = cursor.IsDelayedEOLWrap(); cursor.SetXPosition(column); _ApplyCursorMovementFlags(cursor); + if (delayedWrapOriginallySet) + { + cursor.DelayEOLWrap(); + } + return true; } @@ -2010,12 +2708,19 @@ bool AdaptDispatch::BackwardsTab(const VTInt numTabs) { auto& textBuffer = _api.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); - const auto width = textBuffer.GetLineWidth(cursor.GetPosition().Y); - auto column = cursor.GetPosition().X; + auto column = cursor.GetPosition().x; + const auto row = cursor.GetPosition().y; + const auto width = textBuffer.GetLineWidth(row); auto tabsPerformed = 0; + const auto viewport = _api.GetViewport(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); + const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin; + const auto minColumn = clampToMargin ? leftMargin : 0; + _InitTabStopsForWidth(width); - while (column > 0 && tabsPerformed < numTabs) + while (column > minColumn && tabsPerformed < numTabs) { column--; if (til::at(_tabStopColumns, column)) @@ -2061,8 +2766,8 @@ bool AdaptDispatch::TabClear(const DispatchTypes::TabClearType clearType) void AdaptDispatch::_ClearSingleTabStop() { const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().X; - const auto column = textBuffer.GetCursor().GetPosition().X; + const auto width = textBuffer.GetSize().Dimensions().width; + const auto column = textBuffer.GetCursor().GetPosition().x; _InitTabStopsForWidth(width); _tabStopColumns.at(column) = false; @@ -2216,7 +2921,7 @@ bool AdaptDispatch::LockingShiftRight(const VTInt gsetNumber) // - gsetNumber - The G-set that will be invoked. // Return value: // True if handled successfully. False otherwise. -bool AdaptDispatch::SingleShift(const VTInt gsetNumber) +bool AdaptDispatch::SingleShift(const VTInt gsetNumber) noexcept { return _termOutput.SingleShift(gsetNumber); } @@ -2229,7 +2934,7 @@ bool AdaptDispatch::SingleShift(const VTInt gsetNumber) // - True. bool AdaptDispatch::AcceptC1Controls(const bool enabled) { - _SetParserMode(StateMachine::Mode::AcceptC1, enabled); + _api.GetStateMachine().SetParserMode(StateMachine::Mode::AcceptC1, enabled); return true; } @@ -2239,7 +2944,7 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled) // we actually perform. As the appropriate functionality is added to our ANSI support, // we should update this. // X Text cursor enable DECTCEM Cursor enabled. -// Insert/replace IRM Replace mode. +// X Insert/replace IRM Replace mode. // X Origin DECOM Absolute (cursor origin at upper-left of screen.) // X Autowrap DECAWM Autowrap enabled (matches XTerm behavior). // National replacement DECNRCM Multinational set. @@ -2266,14 +2971,19 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled) // True if handled successfully. False otherwise. bool AdaptDispatch::SoftReset() { - CursorVisibility(true); // Cursor enabled. - SetOriginMode(false); // Absolute cursor addressing. - SetAutoWrapMode(true); // Wrap at end of line. - SetCursorKeysMode(false); // Normal characters. - SetKeypadMode(false); // Numeric characters. + _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled. + + // Replace mode; Absolute cursor addressing; Disallow left/right margins. + _modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM); + + _api.SetSystemMode(ITerminalApi::Mode::AutoWrap, true); // Wrap at end of line. + _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters. + _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters. // Top margin = 1; bottom margin = page length. _DoSetTopBottomScrollingMargins(0, 0); + // Left margin = 1; right margin = page width. + _DoSetLeftRightScrollingMargins(0, 0); _termOutput = {}; // Reset all character set designations. if (_initialCodePage.has_value()) @@ -2336,20 +3046,28 @@ bool AdaptDispatch::HardReset() EraseInDisplay(DispatchTypes::EraseType::Scrollback); // Set the DECSCNM screen mode back to normal. - SetScreenMode(false); + _renderSettings.SetRenderMode(RenderSettings::Mode::ScreenReversed, false); // Cursor to 1,1 - the Soft Reset guarantees this is absolute CursorPosition(1, 1); - // Reset the mouse mode - EnableSGRExtendedMouseMode(false); - EnableAnyEventMouseMode(false); + // We only reset the system line feed mode if the input mode is set. If it + // isn't set, that either means they're both reset, and there's nothing for + // us to do, or they're out of sync, which implies the system mode was set + // via the console API, so it's not our responsibility. + if (_terminalInput.GetInputMode(TerminalInput::Mode::LineFeed)) + { + _api.SetSystemMode(ITerminalApi::Mode::LineFeed, false); + } - // Reset the Backarrow Key mode - _SetInputMode(TerminalInput::Mode::BackarrowKey, false); + // Reset input modes to their initial state + _terminalInput.ResetInputModes(); - // Set the keyboard Auto Repeat mode - _SetInputMode(TerminalInput::Mode::AutoRepeat, true); + // Reset bracketed paste mode + _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false); + + // Restore cursor blinking mode. + _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(true); // Delete all current tab stops and reapply _ResetTabStops(); @@ -2358,14 +3076,31 @@ bool AdaptDispatch::HardReset() _renderer.UpdateSoftFont({}, {}, false); _fontBuffer = nullptr; - // Reset the attribute change extent (DECSACE) to a stream. - _isChangeExtentRectangular = false; + // Reset internal modes to their initial state + _modes = {}; - // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this RIS sequence to the connected - // terminal application. We've reset our state, but the connected terminal - // might need to do more. - return !_api.IsConsolePty(); + // Clear and release the macro buffer. + if (_macroBuffer) + { + _macroBuffer->ClearMacrosIfInUse(); + _macroBuffer = nullptr; + } + + // If we're in a conpty, we need flush this RIS sequence to the connected + // terminal application, but we also need to follow that up with a DECSET + // sequence to re-enable the modes that we require (namely win32 input mode + // and focus event mode). It's important that this is kept in sync with the + // VtEngine::RequestWin32Input method which requests the modes on startup. + if (_api.IsConsolePty()) + { + auto& stateMachine = _api.GetStateMachine(); + if (stateMachine.FlushToTerminal()) + { + auto& engine = stateMachine.Engine(); + engine.ActionPassThroughString(L"\033[?9001;1004h"); + } + } + return true; } // Routine Description: @@ -2380,20 +3115,21 @@ bool AdaptDispatch::ScreenAlignmentPattern() { const auto viewport = _api.GetViewport(); auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Dimensions().X; + const auto bufferWidth = textBuffer.GetSize().Dimensions().width; // Fill the screen with the letter E using the default attributes. - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L'E', {}); + _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L"E", {}); // Reset the line rendition for all of these rows. textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); // Reset the meta/extended attributes (but leave the colors unchanged). auto attr = textBuffer.GetCurrentAttributes(); attr.SetStandardErase(); _api.SetTextAttributes(attr); - // Reset the origin mode to absolute. - SetOriginMode(false); + // Reset the origin mode to absolute, and disallow left/right margins. + _modes.reset(Mode::Origin, Mode::AllowDECSLRM); // Clear the scrolling margins. _DoSetTopBottomScrollingMargins(0, 0); + _DoSetLeftRightScrollingMargins(0, 0); // Set the cursor position to home. CursorPosition(1, 1); @@ -2411,8 +3147,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() // Arguments: // - // Return value: -// - -void AdaptDispatch::_EraseScrollback() +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseScrollback() { const auto viewport = _api.GetViewport(); const auto top = viewport.top; @@ -2420,19 +3156,31 @@ void AdaptDispatch::_EraseScrollback() auto& textBuffer = _api.GetTextBuffer(); const auto bufferSize = textBuffer.GetSize().Dimensions(); auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().Y; + const auto row = cursor.GetPosition().y; + + // Clear all the marks below the new viewport position. + textBuffer.ClearMarksInRange(til::point{ 0, height }, + til::point{ bufferSize.width, bufferSize.height }); + // Then scroll all the remaining marks up. This will trim ones that are now "outside" the buffer + textBuffer.ScrollMarks(-top); // Scroll the viewport content to the top of the buffer. textBuffer.ScrollRows(top, height, -top); // Clear everything after the viewport. - _FillRect(textBuffer, { 0, height, bufferSize.X, bufferSize.Y }, L' ', {}); + _FillRect(textBuffer, { 0, height, bufferSize.width, bufferSize.height }, whitespace, {}); // Also reset the line rendition for all of the cleared rows. - textBuffer.ResetLineRenditionRange(height, bufferSize.Y); + textBuffer.ResetLineRenditionRange(height, bufferSize.height); // Move the viewport _api.SetViewportPosition({ viewport.left, 0 }); // Move the cursor to the same relative location. cursor.SetYPosition(row - top); cursor.SetHasMoved(true); + + // GH#2715 - If this succeeded, but we're in a conpty, return `false` to + // make the state machine propagate this ED sequence to the connected + // terminal application. While we're in conpty mode, we don't really + // have a scrollback, but the attached terminal might. + return !_api.IsConsolePty(); } //Routine Description: @@ -2446,31 +3194,44 @@ void AdaptDispatch::_EraseScrollback() // Arguments: // - // Return value: -// - -void AdaptDispatch::_EraseAll() +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseAll() { const auto viewport = _api.GetViewport(); const auto viewportHeight = viewport.bottom - viewport.top; auto& textBuffer = _api.GetTextBuffer(); const auto bufferSize = textBuffer.GetSize(); + const auto inPtyMode = _api.IsConsolePty(); // Stash away the current position of the cursor within the viewport. // We'll need to restore the cursor to that same relative position, after // we move the viewport. auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().Y - viewport.top; + const auto row = cursor.GetPosition().y - viewport.top; // Calculate new viewport position. Typically we want to move one line below // the last non-space row, but if the last non-space character is the very // start of the buffer, then we shouldn't move down at all. const auto lastChar = textBuffer.GetLastNonSpaceCharacter(); - auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.Y + 1; - const auto newViewportBottom = newViewportTop + viewportHeight; + auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; + auto newViewportBottom = newViewportTop + viewportHeight; const auto delta = newViewportBottom - (bufferSize.Height()); - for (auto i = 0; i < delta; i++) + if (delta > 0) { - textBuffer.IncrementCircularBuffer(); - newViewportTop--; + for (auto i = 0; i < delta; i++) + { + textBuffer.IncrementCircularBuffer(); + } + _api.NotifyBufferRotation(delta); + newViewportTop -= delta; + newViewportBottom -= delta; + // We don't want to trigger a scroll in pty mode, because we're going to + // pass through the ED sequence anyway, and this will just result in the + // buffer being scrolled up by two pages instead of one. + if (!inPtyMode) + { + textBuffer.TriggerScroll({ 0, -delta }); + } } // Move the viewport _api.SetViewportPosition({ viewport.left, newViewportTop }); @@ -2479,128 +3240,23 @@ void AdaptDispatch::_EraseAll() cursor.SetHasMoved(true); // Erase all the rows in the current viewport. - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); -} -// Routine Description: -// - Enables or disables support for the DECCOLM escape sequence. -// Arguments: -// - enabled - set to true to allow DECCOLM to be used, false to disallow. -// Return Value: -// - True. -bool AdaptDispatch::EnableDECCOLMSupport(const bool enabled) noexcept -{ - _isDECCOLMAllowed = enabled; - return true; -} + // Clear any marks that remain below the start of the + textBuffer.ClearMarksInRange(til::point{ 0, newViewportTop }, + til::point{ bufferSize.Width(), bufferSize.Height() }); -//Routine Description: -// Enable VT200 Mouse Mode - Enables/disables the mouse input handler in default tracking mode. -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableVT200MouseMode(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enabled); -} - -//Routine Description: -// Enable UTF-8 Extended Encoding - this changes the encoding scheme for sequences -// emitted by the mouse input handler. Does not enable/disable mouse mode on its own. -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableUTF8ExtendedMouseMode(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, enabled); -} - -//Routine Description: -// Enable SGR Extended Encoding - this changes the encoding scheme for sequences -// emitted by the mouse input handler. Does not enable/disable mouse mode on its own. -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableSGRExtendedMouseMode(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::SgrMouseEncoding, enabled); -} - -//Routine Description: -// Enable Button Event mode - send mouse move events WITH A BUTTON PRESSED to the input. -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableButtonEventMouseMode(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, enabled); -} - -//Routine Description: -// Enable Any Event mode - send all mouse events to the input. -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableAnyEventMouseMode(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, enabled); -} - -// Method Description: -// - Enables/disables focus event mode. A client may enable this if they want to -// receive focus events. -// - ConPTY always enables this mode and never disables it. Internally, we'll -// always set this mode, but conpty will never request this to be disabled by -// the hosting terminal. -// Arguments: -// - enabled - true to enable, false to disable. -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::EnableFocusEventMode(const bool enabled) -{ - // GH#12799 - If the app requested that we disable focus events, DON'T pass - // that through. ConPTY would _always_ like to know about focus events. - return _SetInputMode(TerminalInput::Mode::FocusEvent, enabled) || !enabled; -} - -//Routine Description: -// Enable Alternate Scroll Mode - When in the Alt Buffer, send CUP and CUD on -// scroll up/down events instead of the usual sequences -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableAlternateScroll(const bool enabled) -{ - return _SetInputMode(TerminalInput::Mode::AlternateScroll, enabled); -} - -//Routine Description: -// Enable "bracketed paste mode". -//Arguments: -// - enabled - true to enable, false to disable. -// Return value: -// True if handled successfully. False otherwise. -bool AdaptDispatch::EnableXtermBracketedPasteMode(const bool enabled) -{ - // Return false to forward the operation to the hosting terminal, - // since ConPTY can't handle this itself. - if (_api.IsConsolePty()) - { - return false; - } - _api.EnableXtermBracketedPasteMode(enabled); - return true; + // GH#5683 - If this succeeded, but we're in a conpty, return `false` to + // make the state machine propagate this ED sequence to the connected + // terminal application. While we're in conpty mode, when the client + // requests a Erase All operation, we need to manually tell the + // connected terminal to do the same thing, so that the terminal will + // move it's own buffer contents into the scrollback. + return !inPtyMode; } //Routine Description: @@ -2656,7 +3312,6 @@ bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) auto& cursor = _api.GetTextBuffer().GetCursor(); cursor.SetType(actualType); cursor.SetBlinkingAllowed(fEnableBlinking); - cursor.SetIsOn(true); // If we're a conpty, always return false, so that this cursor state will be // sent to the connected terminal @@ -2703,11 +3358,6 @@ bool AdaptDispatch::SetClipboard(const std::wstring_view content) bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) { _renderSettings.SetColorTableEntry(tableIndex, dwColor); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that one of the entries has been changed - _renderSettings.MakeAdjustedColorArray(); - } // If we're a conpty, always return false, so that we send the updated color // value to the terminal. Still handle the sequence so apps that use @@ -2773,11 +3423,6 @@ bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt case DispatchTypes::ColorItem::NormalText: _renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex); _renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex); - if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors)) - { - // Re-calculate the adjusted colors now that these aliases have been changed - _renderSettings.MakeAdjustedColorArray(); - } break; case DispatchTypes::ColorItem::WindowFrame: _renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex); @@ -2831,6 +3476,9 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: _api.ResizeWindow(parameter2.value_or(0), parameter1.value_or(0)); return true; + case DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters: + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), _api.GetViewport().height(), _api.GetTextBuffer().GetSize().Width())); + return true; default: return false; } @@ -2956,6 +3604,18 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string) return true; } } + // 12: "Let ConEmu treat current cursor position as prompt start" + // + // Based on the official conemu docs: + // * https://conemu.github.io/en/ShellWorkDir.html#connector-ps1 + // * https://conemu.github.io/en/ShellWorkDir.html#PowerShell + // + // This seems like basically the same as 133;B - the end of the prompt, the start of the commandline. + else if (subParam == 12) + { + _api.MarkCommandStart(); + return true; + } return false; } @@ -2996,9 +3656,9 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) if (action == L"SetMark") { - DispatchTypes::ScrollMark mark; - mark.category = DispatchTypes::MarkCategory::Prompt; - _api.AddMark(mark); + ScrollMark mark; + mark.category = MarkCategory::Prompt; + _api.MarkPrompt(mark); return true; } return false; @@ -3037,14 +3697,52 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) } const auto action = til::at(parts, 0); - - if (action == L"A") // FTCS_PROMPT + if (action.size() == 1) { - // Simply just mark this line as a prompt line. - DispatchTypes::ScrollMark mark; - mark.category = DispatchTypes::MarkCategory::Prompt; - _api.AddMark(mark); - return true; + switch (til::at(action, 0)) + { + case L'A': // FTCS_PROMPT + { + // Simply just mark this line as a prompt line. + ScrollMark mark; + mark.category = MarkCategory::Prompt; + _api.MarkPrompt(mark); + return true; + } + case L'B': // FTCS_COMMAND_START + { + _api.MarkCommandStart(); + return true; + } + case L'C': // FTCS_COMMAND_EXECUTED + { + _api.MarkOutputStart(); + return true; + } + case L'D': // FTCS_COMMAND_FINISHED + { + std::optional error = std::nullopt; + if (parts.size() >= 2) + { + const auto errorString = til::at(parts, 1); + + // If we fail to parse the code, then it was gibberish, or it might + // have just started with "-". Either way, let's just treat it as an + // error and move on. + // + // We know that "0" will be successfully parsed, and that's close enough. + unsigned int parsedError = 0; + error = Utils::StringToUint(errorString, parsedError) ? parsedError : + UINT_MAX; + } + _api.MarkCommandFinish(error); + return true; + } + default: + { + return false; + } + } } // When we add the rest of the FTCS sequences (GH#11000), we should add a @@ -3053,6 +3751,86 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) // modify the state of that mark as we go. return false; } +// Method Description: +// - Performs a VsCode action +// - Currently, the actions we support are: +// * Completions: An experimental protocol for passing shell completion +// information from the shell to the terminal. This sequence is still under +// active development, and subject to change. +// - Not actually used in conhost +// Arguments: +// - string: contains the parameters that define which action we do +// Return Value: +// - false in conhost, true for the SetMark action, otherwise false. +bool AdaptDispatch::DoVsCodeAction(const std::wstring_view string) +{ + // This is not implemented in conhost. + if (_api.IsConsolePty()) + { + // Flush the frame manually to make sure this action happens at the right time. + _renderer.TriggerFlush(false); + return false; + } + + if constexpr (!Feature_ShellCompletions::IsEnabled()) + { + return false; + } + + const auto parts = Utils::SplitString(string, L';'); + + if (parts.size() < 1) + { + return false; + } + + const auto action = til::at(parts, 0); + + if (action == L"Completions") + { + // The structure of the message is as follows: + // `e]633; + // 0: Completions; + // 1: $($completions.ReplacementIndex); + // 2: $($completions.ReplacementLength); + // 3: $($cursorIndex); + // 4: $completions.CompletionMatches | ConvertTo-Json + unsigned int replacementIndex = 0; + unsigned int replacementLength = 0; + unsigned int cursorIndex = 0; + + bool succeeded = (parts.size() >= 2) && + (Utils::StringToUint(til::at(parts, 1), replacementIndex)); + succeeded &= (parts.size() >= 3) && + (Utils::StringToUint(til::at(parts, 2), replacementLength)); + succeeded &= (parts.size() >= 4) && + (Utils::StringToUint(til::at(parts, 3), cursorIndex)); + + // VsCode is using cursorIndex and replacementIndex, but we aren't currently. + if (succeeded) + { + // Get the combined lengths of parts 0-3, plus the semicolons. We + // need this so that we can just pass the remainder of the string. + const auto prefixLength = til::at(parts, 0).size() + 1 + + til::at(parts, 1).size() + 1 + + til::at(parts, 2).size() + 1 + + til::at(parts, 3).size() + 1; + if (prefixLength > string.size()) + { + return true; + } + // Get the remainder of the string + const auto remainder = string.substr(prefixLength); + + _api.InvokeCompletions(parts.size() < 5 ? L"" : remainder, + replacementLength); + } + + // If it's poorly formatted, just eat it + return true; + } + return false; +} // Method Description: // - DECDLD - Downloads one or more characters of a dynamically redefinable @@ -3098,7 +3876,7 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber, } // If we're a conpty, we create a special passthrough handler that will - // forward the DECDLD sequence to the conpty terminal with a hardcoded ID. + // forward the DECDLD sequence to the conpty terminal with a hard-coded ID. // That ID is also pre-mapped into the G1 table, so the VT engine can just // switch to G1 when it needs to output any DRCS characters. But note that // we still need to process the DECDLD sequence locally, so the character @@ -3143,7 +3921,7 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber, // Routine Description: // - Helper method to create a string handler that can be used to pass through // DECDLD sequences when in conpty mode. This patches the original sequence -// with a hardcoded character set ID, and pre-maps that ID into the G1 table. +// with a hard-coded character set ID, and pre-maps that ID into the G1 table. // Arguments: // - // Return value: @@ -3158,7 +3936,7 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const // The character set ID is contained in the first characters of the // sequence, so we just ignore that initial content until we receive // a "final" character (i.e. in range 30 to 7E). At that point we - // pass through a hardcoded ID of "@". + // pass through a hard-coded ID of "@". if (!gotId) { if (ch >= 0x30 && ch <= 0x7E) @@ -3180,6 +3958,60 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const return nullptr; } +// Method Description: +// - DECDMAC - Defines a string of characters as a macro that can later be +// invoked with a DECINVM sequence. +// Arguments: +// - macroId - a number to identify the macro when invoked. +// - deleteControl - what gets deleted before loading the new macro data. +// - encoding - whether the data is encoded as plain text or hex digits. +// Return Value: +// - a function to receive the macro data or nullptr if parameters are invalid. +ITermDispatch::StringHandler AdaptDispatch::DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) +{ + if (!_macroBuffer) + { + _macroBuffer = std::make_shared(); + } + + if (_macroBuffer->InitParser(macroId, deleteControl, encoding)) + { + return [&](const auto ch) { + return _macroBuffer->ParseDefinition(ch); + }; + } + + return nullptr; +} + +// Method Description: +// - DECINVM - Invokes a previously defined macro, executing the macro content +// as if it had been received directly from the host. +// Arguments: +// - macroId - the id number of the macro to be invoked. +// Return Value: +// - True +bool AdaptDispatch::InvokeMacro(const VTInt macroId) +{ + if (_macroBuffer) + { + // In order to inject our macro sequence into the state machine + // we need to register a callback that will be executed only + // once it has finished processing the current operation, and + // has returned to the ground state. Note that we're capturing + // a copy of the _macroBuffer pointer here to make sure it won't + // be deleted (e.g. from an invoked RIS) while still in use. + const auto macroBuffer = _macroBuffer; + auto& stateMachine = _api.GetStateMachine(); + stateMachine.OnCsiComplete([=, &stateMachine]() { + macroBuffer->InvokeMacro(macroId, stateMachine); + }); + } + return true; +} + // Method Description: // - DECRSTS - Restores the terminal state from a stream of data previously // saved with a DECRQTSR query. @@ -3276,11 +4108,11 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting() // this is the opposite of what is documented in most DEC manuals, which // say that 0 is for a valid response, and 1 is for an error. The correct // interpretation is documented in the DEC STD 070 reference. - const auto idBuilder = std::make_shared(); - return [=](const auto ch) { - if (ch >= '\x40' && ch <= '\x7e') + return [this, parameter = VTInt{}, idBuilder = VTIDBuilder{}](const auto ch) mutable { + const auto isFinal = ch >= L'\x40' && ch <= L'\x7e'; + if (isFinal) { - const auto id = idBuilder->Finalize(ch); + const auto id = idBuilder.Finalize(ch); switch (id) { case VTID("m"): @@ -3289,12 +4121,18 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting() case VTID("r"): _ReportDECSTBMSetting(); break; + case VTID("s"): + _ReportDECSLRMSetting(); + break; case VTID("\"q"): _ReportDECSCASetting(); break; case VTID("*x"): _ReportDECSACESetting(); break; + case VTID(",|"): + _ReportDECACSetting(VTParameter{ parameter }); + break; default: _api.ReturnResponse(L"\033P0$r\033\\"); break; @@ -3303,9 +4141,22 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting() } else { - if (ch >= '\x20' && ch <= '\x2f') + // Although we don't yet support any operations with parameter + // prefixes, it's important that we still parse the prefix and + // include it in the ID. Otherwise we'll mistakenly respond to + // prefixed queries that we don't actually recognise. + const auto isParameterPrefix = ch >= L'<' && ch <= L'?'; + const auto isParameter = ch >= L'0' && ch < L'9'; + const auto isIntermediate = ch >= L'\x20' && ch <= L'\x2f'; + if (isParameterPrefix || isIntermediate) { - idBuilder->AddIntermediate(ch); + idBuilder.AddIntermediate(ch); + } + else if (isParameter) + { + parameter *= 10; + parameter += (ch - L'0'); + parameter = std::min(parameter, MAX_PARAMETER_VALUE); } return true; } @@ -3327,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) { @@ -3339,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 @@ -3359,18 +4214,19 @@ void AdaptDispatch::_ReportSGRSetting() const else if (color.IsIndex256()) { const auto index = color.GetIndex(); - fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};5;{}"), base + 8, index); + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}:5:{}"), base + 8, index); } else if (color.IsRgb()) { const auto r = GetRValue(color.GetRGB()); const auto g = GetGValue(color.GetRGB()); const auto b = GetBValue(color.GetRGB()); - fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};2;{};{};{}"), base + 8, r, g, b); + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}:2::{}:{}:{}"), base + 8, r, g, b); } }; 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); @@ -3401,6 +4257,30 @@ void AdaptDispatch::_ReportDECSTBMSetting() _api.ReturnResponse({ response.data(), response.size() }); } +// Method Description: +// - Reports the DECSLRM margin range in response to a DECRQSS query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportDECSLRMSetting() +{ + using namespace std::string_view_literals; + + // A valid response always starts with DCS 1 $ r. + fmt::basic_memory_buffer response; + response.append(L"\033P1$r"sv); + + const auto bufferWidth = _api.GetTextBuffer().GetSize().Width(); + const auto [marginLeft, marginRight] = _GetHorizontalMargins(bufferWidth); + // VT origin is at 1,1 so we need to add 1 to these margins. + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1); + + // The 's' indicates this is an DECSLRM response, and ST ends the sequence. + response.append(L"s\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); +} + // Method Description: // - Reports the DECSCA protected attribute in response to a DECRQSS query. // Arguments: @@ -3415,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. @@ -3437,14 +4317,408 @@ void AdaptDispatch::_ReportDECSACESetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto attr = _api.GetTextBuffer().GetCurrentAttributes(); - response.append(_isChangeExtentRectangular ? L"2"sv : L"1"sv); + 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. response.append(L"*x\033\\"sv); _api.ReturnResponse({ response.data(), response.size() }); } +// Method Description: +// - Reports the DECAC color assignments in response to a DECRQSS query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportDECACSetting(const VTInt itemNumber) const +{ + using namespace std::string_view_literals; + + size_t fgIndex = 0; + size_t bgIndex = 0; + switch (static_cast(itemNumber)) + { + case DispatchTypes::ColorItem::NormalText: + fgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground); + bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground); + break; + case DispatchTypes::ColorItem::WindowFrame: + fgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameForeground); + bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground); + break; + default: + _api.ReturnResponse(L"\033P0$r\033\\"); + return; + } + + // A valid response always starts with DCS 1 $ r. + fmt::basic_memory_buffer response; + response.append(L"\033P1$r"sv); + + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{};{}"), itemNumber, fgIndex, bgIndex); + + // The ',|' indicates this is a DECAC response, and ST ends the sequence. + response.append(L",|\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); +} + +// Routine Description: +// - DECRQPSR - Queries the presentation state of the terminal. This can either +// be in the form of a cursor information report, or a tabulation stop report, +// depending on the requested format. +// Arguments: +// - format - the format of the report being requested. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format) +{ + switch (format) + { + case DispatchTypes::PresentationReportFormat::CursorInformationReport: + _ReportCursorInformation(); + return true; + case DispatchTypes::PresentationReportFormat::TabulationStopReport: + _ReportTabStops(); + return true; + default: + return false; + } +} + +// Method Description: +// - DECRSPS - Restores the presentation state from a stream of data previously +// saved with a DECRQPSR query. +// Arguments: +// - format - the format of the report being restored. +// Return Value: +// - a function to receive the data or nullptr if the format is unsupported. +ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const DispatchTypes::PresentationReportFormat format) +{ + switch (format) + { + case DispatchTypes::PresentationReportFormat::CursorInformationReport: + return _RestoreCursorInformation(); + case DispatchTypes::PresentationReportFormat::TabulationStopReport: + return _RestoreTabStops(); + default: + return nullptr; + } +} + +// Method Description: +// - DECCIR - Returns the Cursor Information Report in response to a DECRQPSR query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportCursorInformation() +{ + const auto viewport = _api.GetViewport(); + const auto& textBuffer = _api.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto& attributes = textBuffer.GetCurrentAttributes(); + + // First pull the cursor position relative to the entire buffer out of the console. + til::point cursorPosition{ cursor.GetPosition() }; + + // Now adjust it for its position in respect to the current viewport top. + cursorPosition.y -= viewport.top; + + // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + cursorPosition.x++; + cursorPosition.y++; + + // If the origin mode is set, the cursor is relative to the margin origin. + if (_modes.test(Mode::Origin)) + { + cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; + cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + } + + // Paging is not supported yet (GH#13892). + const auto pageNumber = 1; + + // Only some of the rendition attributes are reported. + // 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); + renditionAttributes += (attributes.IsReverseVideo() ? 8 : 0); + renditionAttributes += (attributes.IsInvisible() ? 16 : 0); + + // There is only one character attribute. + const auto characterAttributes = attributes.IsProtected() ? L'A' : L'@'; + + // Miscellaneous flags and modes. + auto flags = L'@'; + flags += (_modes.test(Mode::Origin) ? 1 : 0); + flags += (_termOutput.IsSingleShiftPending(2) ? 2 : 0); + flags += (_termOutput.IsSingleShiftPending(3) ? 4 : 0); + flags += (cursor.IsDelayedEOLWrap() ? 8 : 0); + + // Character set designations. + const auto leftSetNumber = _termOutput.GetLeftSetNumber(); + const auto rightSetNumber = _termOutput.GetRightSetNumber(); + auto charsetSizes = L'@'; + charsetSizes += (_termOutput.GetCharsetSize(0) == 96 ? 1 : 0); + charsetSizes += (_termOutput.GetCharsetSize(1) == 96 ? 2 : 0); + charsetSizes += (_termOutput.GetCharsetSize(2) == 96 ? 4 : 0); + charsetSizes += (_termOutput.GetCharsetSize(3) == 96 ? 8 : 0); + const auto charset0 = _termOutput.GetCharsetId(0); + const auto charset1 = _termOutput.GetCharsetId(1); + const auto charset2 = _termOutput.GetCharsetId(2); + const auto charset3 = _termOutput.GetCharsetId(3); + + // A valid response always starts with DCS 1 $ u and ends with ST. + const auto response = fmt::format( + FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"), + cursorPosition.y, + cursorPosition.x, + pageNumber, + renditionAttributes, + characterAttributes, + flags, + leftSetNumber, + rightSetNumber, + charsetSizes, + charset0.ToString(), + charset1.ToString(), + charset2.ToString(), + charset3.ToString()); + _api.ReturnResponse({ response.data(), response.size() }); +} + +// Method Description: +// - DECCIR - This is a parser for the Cursor Information Report received via DECRSPS. +// Arguments: +// - +// Return Value: +// - a function to parse the report data. +ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() +{ + // clang-format off + enum Field { Row, Column, Page, SGR, Attr, Flags, GL, GR, Sizes, G0, G1, G2, G3 }; + // clang-format on + constexpr til::enumset numeric{ Field::Row, Field::Column, Field::Page, Field::GL, Field::GR }; + constexpr til::enumset flags{ Field::SGR, Field::Attr, Field::Flags, Field::Sizes }; + constexpr til::enumset charset{ Field::G0, Field::G1, Field::G2, Field::G3 }; + struct State + { + Field field{ Field::Row }; + VTInt value{ 0 }; + VTIDBuilder charsetId{}; + std::array charset96{}; + VTParameter row{}; + VTParameter column{}; + }; + auto& textBuffer = _api.GetTextBuffer(); + return [&, state = State{}](const auto ch) mutable { + if (numeric.test(state.field)) + { + if (ch >= '0' && ch <= '9') + { + state.value *= 10; + state.value += (ch - L'0'); + state.value = std::min(state.value, MAX_PARAMETER_VALUE); + } + else if (ch == L';' || ch == AsciiChars::ESC) + { + if (state.field == Field::Row) + { + state.row = state.value; + } + else if (state.field == Field::Column) + { + state.column = state.value; + } + else if (state.field == Field::Page) + { + // Paging is not supported yet (GH#13892). + } + else if (state.field == Field::GL && state.value <= 3) + { + LockingShift(state.value); + } + else if (state.field == Field::GR && state.value <= 3) + { + LockingShiftRight(state.value); + } + state.value = {}; + state.field = static_cast(state.field + 1); + } + } + else if (flags.test(state.field)) + { + // Note that there could potentially be multiple characters in a + // flag field, so we process the flags as soon as they're received. + // But for now we're only interested in the first one, so once the + // state.value is set, we ignore everything else until the `;`. + if (ch >= L'@' && ch <= '~' && !state.value) + { + state.value = ch; + if (state.field == Field::SGR) + { + auto attr = textBuffer.GetCurrentAttributes(); + attr.SetIntense(state.value & 1); + attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline); + attr.SetBlinking(state.value & 4); + attr.SetReverseVideo(state.value & 8); + attr.SetInvisible(state.value & 16); + textBuffer.SetCurrentAttributes(attr); + } + else if (state.field == Field::Attr) + { + auto attr = textBuffer.GetCurrentAttributes(); + attr.SetProtected(state.value & 1); + textBuffer.SetCurrentAttributes(attr); + } + else if (state.field == Field::Sizes) + { + state.charset96.at(0) = state.value & 1; + state.charset96.at(1) = state.value & 2; + state.charset96.at(2) = state.value & 4; + state.charset96.at(3) = state.value & 8; + } + else if (state.field == Field::Flags) + { + const bool originMode = state.value & 1; + const bool ss2 = state.value & 2; + const bool ss3 = state.value & 4; + const bool delayedEOLWrap = state.value & 8; + // The cursor position is parsed at the start of the sequence, + // but we only set the position once we know the origin mode. + _modes.set(Mode::Origin, originMode); + CursorPosition(state.row, state.column); + // There can only be one single shift applied at a time, so + // we'll just apply the last one that is enabled. + _termOutput.SingleShift(ss3 ? 3 : (ss2 ? 2 : 0)); + // The EOL flag will always be reset by the cursor movement + // above, so we only need to worry about setting it. + if (delayedEOLWrap) + { + textBuffer.GetCursor().DelayEOLWrap(); + } + } + } + else if (ch == L';') + { + state.value = 0; + state.field = static_cast(state.field + 1); + } + } + else if (charset.test(state.field)) + { + if (ch >= L' ' && ch <= L'/') + { + state.charsetId.AddIntermediate(ch); + } + else if (ch >= L'0' && ch <= L'~') + { + const auto id = state.charsetId.Finalize(ch); + const auto gset = state.field - Field::G0; + if (state.charset96.at(gset)) + { + Designate96Charset(gset, id); + } + else + { + Designate94Charset(gset, id); + } + state.charsetId.Clear(); + state.field = static_cast(state.field + 1); + } + } + return (ch != AsciiChars::ESC); + }; +} + +// Method Description: +// - DECTABSR - Returns the Tabulation Stop Report in response to a DECRQPSR query. +// Arguments: +// - None +// Return Value: +// - None +void AdaptDispatch::_ReportTabStops() +{ + // In order to be compatible with the original hardware terminals, we only + // report tab stops up to the current buffer width, even though there may + // be positions recorded beyond that limit. + const auto width = _api.GetTextBuffer().GetSize().Dimensions().width; + _InitTabStopsForWidth(width); + + using namespace std::string_view_literals; + + // A valid response always starts with DCS 2 $ u. + fmt::basic_memory_buffer response; + response.append(L"\033P2$u"sv); + + auto need_separator = false; + for (auto column = 0; column < width; column++) + { + if (til::at(_tabStopColumns, column)) + { + response.append(need_separator ? L"/"sv : L""sv); + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{}"), column + 1); + need_separator = true; + } + } + + // An ST ends the sequence. + response.append(L"\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); +} + +// Method Description: +// - DECTABSR - This is a parser for the Tabulation Stop Report received via DECRSPS. +// Arguments: +// - +// Return Value: +// - a function to parse the report data. +ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops() +{ + // In order to be compatible with the original hardware terminals, we need + // to be able to set tab stops up to at least 132 columns, even though the + // current buffer width may be less than that. + const auto width = std::max(_api.GetTextBuffer().GetSize().Dimensions().width, 132); + _ClearAllTabStops(); + _InitTabStopsForWidth(width); + + return [this, width, column = size_t{}](const auto ch) mutable { + if (ch >= L'0' && ch <= L'9') + { + column *= 10; + column += (ch - L'0'); + column = std::min(column, MAX_PARAMETER_VALUE); + } + else if (ch == L'/' || ch == AsciiChars::ESC) + { + // Note that column 1 is always a tab stop, so there is no + // need to record an entry at that offset. + if (column > 1u && column <= static_cast(width)) + { + _tabStopColumns.at(column - 1) = true; + } + column = 0; + } + else + { + // If we receive an unexpected character, we don't try and + // process any more of the input - we just abort. + return false; + } + return (ch != AsciiChars::ESC); + }; +} + // Routine Description: // - DECPS - Plays a sequence of musical notes. // Arguments: diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index aa233ce145..4b145f1b8c 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -17,10 +17,16 @@ Author(s): #include "termDispatch.hpp" #include "ITerminalApi.hpp" #include "FontBuffer.hpp" +#include "MacroBuffer.hpp" #include "terminalOutput.hpp" #include "../input/terminalInput.hpp" #include "../../types/inc/sgrStack.hpp" +// fwdecl unittest classes +#ifdef UNIT_TESTING +class AdapterTest; +#endif + namespace Microsoft::Console::VirtualTerminal { class AdaptDispatch : public ITermDispatch @@ -47,7 +53,6 @@ namespace Microsoft::Console::VirtualTerminal bool CursorPosition(const VTInt line, const VTInt column) override; // CUP, HVP bool CursorSaveState() override; // DECSC bool CursorRestoreState() override; // DECRC - bool CursorVisibility(const bool isVisible) override; // DECTCEM bool EraseInDisplay(const DispatchTypes::EraseType eraseType) override; // ED bool EraseInLine(const DispatchTypes::EraseType eraseType) override; // EL bool EraseCharacters(const VTInt numChars) override; // ECH @@ -62,12 +67,13 @@ namespace Microsoft::Console::VirtualTerminal bool EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECERA bool SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECSERA bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) noexcept override; // DECSACE + bool RequestChecksumRectangularArea(const VTInt id, const VTInt page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) override; // DECRQCRA bool SetGraphicsRendition(const VTParameters options) override; // SGR bool SetLineRendition(const LineRendition rendition) override; // DECSWL, DECDWL, DECDHL bool SetCharacterProtectionAttribute(const VTParameters options) override; // DECSCA bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR bool PopGraphicsRendition() override; // XTPOPSGR - bool DeviceStatusReport(const DispatchTypes::StatusType statusType) override; // DSR, DSR-OS, DSR-CPR + bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) override; // DSR bool DeviceAttributes() override; // DA1 bool SecondaryDeviceAttributes() override; // DA2 bool TertiaryDeviceAttributes() override; // DA3 @@ -77,26 +83,24 @@ namespace Microsoft::Console::VirtualTerminal bool ScrollDown(const VTInt distance) override; // SD bool InsertLine(const VTInt distance) override; // IL bool DeleteLine(const VTInt distance) override; // DL - bool SetColumns(const VTInt columns) override; // DECCOLM - bool SetMode(const DispatchTypes::ModeParams param) override; // DECSET - bool ResetMode(const DispatchTypes::ModeParams param) override; // DECRST - bool SetCursorKeysMode(const bool applicationMode) override; // DECCKM + bool InsertColumn(const VTInt distance) override; // DECIC + bool DeleteColumn(const VTInt distance) override; // DECDC + bool SetMode(const DispatchTypes::ModeParams param) override; // SM, DECSET + bool ResetMode(const DispatchTypes::ModeParams param) override; // RM, DECRST + bool RequestMode(const DispatchTypes::ModeParams param) override; // DECRQM bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM - bool EnableWin32InputMode(const bool win32InputMode) override; // win32-input-mode - bool EnableCursorBlinking(const bool enable) override; // ATT610 bool SetAnsiMode(const bool ansiMode) override; // DECANM - bool SetScreenMode(const bool reverseMode) override; // DECSCNM - bool SetOriginMode(const bool relativeMode) noexcept override; // DECOM - bool SetAutoWrapMode(const bool wrapAtEOL) override; // DECAWM bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) override; // DECSTBM + bool SetLeftRightScrollingMargins(const VTInt leftMargin, + const VTInt rightMargin) override; // DECSLRM bool WarningBell() override; // BEL bool CarriageReturn() override; // CR bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) override; // IND, NEL, LF, FF, VT bool ReverseLineFeed() override; // RI + bool BackIndex() override; // DECBI + bool ForwardIndex() override; // DECFI bool SetWindowTitle(const std::wstring_view title) override; // OSCWindowTitle - bool UseAlternateScreenBuffer() override; // ASBSET - bool UseMainScreenBuffer() override; // ASBRST bool HorizontalTabSet() override; // HTS bool ForwardTab(const VTInt numTabs) override; // CHT, HT bool BackwardsTab(const VTInt numTabs) override; // CBT @@ -106,20 +110,11 @@ namespace Microsoft::Console::VirtualTerminal bool Designate96Charset(const VTInt gsetNumber, const VTID charset) override; // SCS bool LockingShift(const VTInt gsetNumber) override; // LS0, LS1, LS2, LS3 bool LockingShiftRight(const VTInt gsetNumber) override; // LS1R, LS2R, LS3R - bool SingleShift(const VTInt gsetNumber) override; // SS2, SS3 + bool SingleShift(const VTInt gsetNumber) noexcept override; // SS2, SS3 bool AcceptC1Controls(const bool enabled) override; // DECAC1 bool SoftReset() override; // DECSTR bool HardReset() override; // RIS bool ScreenAlignmentPattern() override; // DECALN - bool EnableDECCOLMSupport(const bool enabled) noexcept override; // ?40 - bool EnableVT200MouseMode(const bool enabled) override; // ?1000 - bool EnableUTF8ExtendedMouseMode(const bool enabled) override; // ?1005 - bool EnableSGRExtendedMouseMode(const bool enabled) override; // ?1006 - bool EnableButtonEventMouseMode(const bool enabled) override; // ?1002 - bool EnableAnyEventMouseMode(const bool enabled) override; // ?1003 - bool EnableFocusEventMode(const bool enabled) override; // ?1004 - bool EnableAlternateScroll(const bool enabled) override; // ?1007 - bool EnableXtermBracketedPasteMode(const bool enabled) override; // ?2004 bool SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) override; // DECSCUSR bool SetCursorColor(const COLORREF cursorColor) override; @@ -144,6 +139,8 @@ namespace Microsoft::Console::VirtualTerminal bool DoFinalTermAction(const std::wstring_view string) override; + bool DoVsCodeAction(const std::wstring_view string) override; + StringHandler DownloadDRCS(const VTInt fontNumber, const VTParameter startChar, const DispatchTypes::DrcsEraseControl eraseControl, @@ -153,13 +150,31 @@ namespace Microsoft::Console::VirtualTerminal const VTParameter cellHeight, const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + StringHandler DefineMacro(const VTInt macroId, + const DispatchTypes::MacroDeleteControl deleteControl, + const DispatchTypes::MacroEncoding encoding) override; // DECDMAC + bool InvokeMacro(const VTInt macroId) override; // DECINVM + StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) override; // DECRSTS StringHandler RequestSetting() override; // DECRQSS + bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat format) override; // DECRQPSR + StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat format) override; // DECRSPS + bool PlaySounds(const VTParameters parameters) override; // DECPS private: + enum class Mode + { + InsertReplace, + Origin, + Column, + AllowDECCOLM, + AllowDECSLRM, + EraseColor, + RectangularChangeExtent + }; enum class ScrollDirection { Up, @@ -169,6 +184,7 @@ namespace Microsoft::Console::VirtualTerminal { VTInt Row = 1; VTInt Column = 1; + bool IsDelayedEOLWrap = false; bool IsOriginModeRelative = false; TextAttribute Attributes = {}; TerminalOutput TermOutput = {}; @@ -191,34 +207,47 @@ namespace Microsoft::Console::VirtualTerminal CharacterAttributes xorAttrMask = CharacterAttributes::Normal; std::optional foreground; std::optional background; + std::optional underlineColor; }; - std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute); + void _WriteToBuffer(const std::wstring_view string); + std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept; + std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; - void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs); + void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); - void _EraseScrollback(); - void _EraseAll(); + bool _EraseScrollback(); + bool _EraseAll(); + TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept; void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); void _InsertDeleteLineHelper(const VTInt delta); + void _InsertDeleteColumnHelper(const VTInt delta); void _ScrollMovement(const VTInt delta); void _DoSetTopBottomScrollingMargins(const VTInt topMargin, - const VTInt bottomMargin); + const VTInt bottomMargin, + const bool homeCursor = false); + void _DoSetLeftRightScrollingMargins(const VTInt leftMargin, + const VTInt rightMargin, + const bool homeCursor = false); + + void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced); + void _OperatingStatus() const; void _CursorPositionReport(const bool extendedReport); + void _MacroSpaceReport() const; + void _MacroChecksumReport(const VTParameter id) const; - bool _GetParserMode(const StateMachine::Mode mode) const; - void _SetParserMode(const StateMachine::Mode mode, const bool enable); - bool _SetInputMode(const TerminalInput::Mode mode, const bool enable); + void _SetColumnMode(const bool enable); + void _SetAlternateScreenBufferMode(const bool enable); + bool _PassThroughInputModes(); bool _ModeParamsHelper(const DispatchTypes::ModeParams param, const bool enable); - bool _DoDECCOLMHelper(const VTInt columns); void _ClearSingleTabStop(); void _ClearAllTabStops() noexcept; @@ -229,8 +258,15 @@ namespace Microsoft::Console::VirtualTerminal void _ReportSGRSetting() const; void _ReportDECSTBMSetting(); + void _ReportDECSLRMSetting(); void _ReportDECSCASetting() const; void _ReportDECSACESetting() const; + void _ReportDECACSetting(const VTInt itemNumber) const; + + void _ReportCursorInformation(); + StringHandler _RestoreCursorInformation(); + void _ReportTabStops(); + StringHandler _RestoreTabStops(); StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize); StringHandler _CreatePassthroughHandler(); @@ -244,6 +280,7 @@ namespace Microsoft::Console::VirtualTerminal TerminalInput& _terminalInput; TerminalOutput _termOutput; std::unique_ptr _fontBuffer; + std::shared_ptr _macroBuffer; std::optional _initialCodePage; // We have two instances of the saved cursor state, because we need @@ -256,19 +293,28 @@ namespace Microsoft::Console::VirtualTerminal til::inclusive_rect _scrollMargins; - bool _isOriginModeRelative; - bool _isDECCOLMAllowed; - bool _isChangeExtentRectangular; + til::enumset _modes; SgrStack _sgrStack; + void _SetUnderlineStyleHelper(const VTParameter option, TextAttribute& attr) noexcept; size_t _SetRgbColorsHelper(const VTParameters options, TextAttribute& attr, const bool isForeground) noexcept; + void _SetRgbColorsHelperFromSubParams(const VTParameter colorItem, + const VTSubParameters options, + TextAttribute& attr) noexcept; size_t _ApplyGraphicsOption(const VTParameters options, const size_t optionIndex, TextAttribute& attr) noexcept; + void _ApplyGraphicsOptionWithSubParams(const VTParameter option, + const VTSubParameters subParams, + TextAttribute& attr) noexcept; void _ApplyGraphicsOptions(const VTParameters options, TextAttribute& attr) noexcept; + +#ifdef UNIT_TESTING + friend class AdapterTest; +#endif }; } diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index 1f351089b3..ac1b0180ef 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include +#include "precomp.h" #include "adaptDispatch.hpp" #include "../../types/inc/utils.hpp" @@ -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) @@ -35,7 +52,8 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options, const size_t red = options.at(1).value_or(0); const size_t green = options.at(2).value_or(0); const size_t blue = options.at(3).value_or(0); - // ensure that each value fits in a byte + // We only apply the color if the R, G, B values fit within a byte. + // This is to match XTerm's and VTE's behavior. if (red <= 255 && green <= 255 && blue <= 255) { const auto rgbColor = RGB(red, green, blue); @@ -46,6 +64,9 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options, { optionsConsumed = 2; const size_t tableIndex = options.at(1).value_or(0); + + // We only apply the color if the index value fit within a byte. + // This is to match XTerm's and VTE's behavior. if (tableIndex <= 255) { const auto adjustedIndex = gsl::narrow_cast(tableIndex); @@ -62,8 +83,89 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options, return optionsConsumed; } +// Routine Description: +// - 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 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: +// - +void AdaptDispatch::_SetRgbColorsHelperFromSubParams(const VTParameter colorItem, + const VTSubParameters options, + TextAttribute& attr) noexcept +{ + 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 DispatchTypes::GraphicsOptions typeOpt = options.at(0); + switch (typeOpt) + { + case DispatchTypes::GraphicsOptions::RGBColorOrFaint: + { + // sub params are in the order: + // :2:::: + + // 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(); + + 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); + + // We only apply the color if the R, G, B values fit within a byte. + // This is to match XTerm's and VTE's behavior. + if (!hasColorSpaceId && red <= 255 && green <= 255 && blue <= 255) + { + applyColor(TextColor{ RGB(red, green, blue) }); + } + break; + } + case DispatchTypes::GraphicsOptions::BlinkOrXterm256Index: + { + // sub params are in the order: + // :5: + // where 'n' is the index into the xterm color table. + const size_t tableIndex = options.at(1).value_or(0); + + // We only apply the color if the index value fit within a byte. + // This is to match XTerm's and VTE's behavior. + if (tableIndex <= 255) + { + const auto adjustedIndex = gsl::narrow_cast(tableIndex); + applyColor(TextColor{ adjustedIndex, true }); + } + break; + } + default: + break; + }; +} + // Routine Description: // - Helper to apply a single graphic rendition option to an attribute. +// - Calls appropriate helper to apply the option with sub parameters when necessary. // Arguments: // - options - An array of options. // - optionIndex - The start index of the option that will be applied. @@ -75,11 +177,20 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options, TextAttribute& attr) noexcept { const GraphicsOptions opt = options.at(optionIndex); + + if (options.hasSubParamsFor(optionIndex)) + { + const auto subParams = options.subParamsFor(optionIndex); + _ApplyGraphicsOptionWithSubParams(opt, subParams, attr); + return 1; + } + switch (opt) { case Off: attr.SetDefaultForeground(); attr.SetDefaultBackground(); + attr.SetDefaultUnderlineColor(); attr.SetDefaultRenditionAttributes(); return 1; case ForegroundDefault: @@ -88,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; @@ -129,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); @@ -250,6 +363,37 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options, } } +// Routine Description: +// - Helper to apply a single graphic rendition option with sub parameters to an attribute. +// Arguments: +// - option - An option to apply. +// - subParams - Sub parameters associated with the option. +// - attr - The attribute that will be updated with the applied option. +// Return Value: +// - +void AdaptDispatch::_ApplyGraphicsOptionWithSubParams(const VTParameter option, + const VTSubParameters subParams, + TextAttribute& attr) noexcept +{ + // here, we apply our "best effort" rule, while handling sub params if we don't + // recognise the parameter substring (parameter and it's sub parameters) then + // 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: + /* do nothing */ + break; + } +} + // Routine Description: // - Helper to apply a number of graphic rendition options to an attribute. // Arguments: @@ -326,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; } @@ -340,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/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index 5bc8488f89..3ecefa5e83 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -14,6 +14,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 10a5aa55ff..21524f61cd 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -80,6 +83,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 87c5507685..d1e63430bb 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -33,6 +33,7 @@ SOURCES= \ ..\adaptDispatch.cpp \ ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ + ..\MacroBuffer.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ ..\telemetry.cpp \ diff --git a/src/terminal/adapter/telemetry.cpp b/src/terminal/adapter/telemetry.cpp index 75de2c1c21..f4a63025a0 100644 --- a/src/terminal/adapter/telemetry.cpp +++ b/src/terminal/adapter/telemetry.cpp @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include +#include "precomp.h" #include "telemetry.hpp" diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 683bd14e62..4f11ae8bb9 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -38,43 +38,29 @@ public: bool CursorPosition(const VTInt /*line*/, const VTInt /*column*/) override { return false; } // CUP, HVP bool CursorSaveState() override { return false; } // DECSC bool CursorRestoreState() override { return false; } // DECRC - bool CursorVisibility(const bool /*isVisible*/) override { return false; } // DECTCEM bool InsertCharacter(const VTInt /*count*/) override { return false; } // ICH bool DeleteCharacter(const VTInt /*count*/) override { return false; } // DCH bool ScrollUp(const VTInt /*distance*/) override { return false; } // SU bool ScrollDown(const VTInt /*distance*/) override { return false; } // SD bool InsertLine(const VTInt /*distance*/) override { return false; } // IL bool DeleteLine(const VTInt /*distance*/) override { return false; } // DL - bool SetColumns(const VTInt /*columns*/) override { return false; } // DECCOLM - bool SetCursorKeysMode(const bool /*applicationMode*/) override { return false; } // DECCKM + bool InsertColumn(const VTInt /*distance*/) override { return false; } // DECIC + bool DeleteColumn(const VTInt /*distance*/) override { return false; } // DECDC bool SetKeypadMode(const bool /*applicationMode*/) override { return false; } // DECKPAM, DECKPNM - bool EnableWin32InputMode(const bool /*win32InputMode*/) override { return false; } // win32-input-mode - bool EnableCursorBlinking(const bool /*enable*/) override { return false; } // ATT610 bool SetAnsiMode(const bool /*ansiMode*/) override { return false; } // DECANM - bool SetScreenMode(const bool /*reverseMode*/) override { return false; } // DECSCNM - bool SetOriginMode(const bool /*relativeMode*/) override { return false; }; // DECOM - bool SetAutoWrapMode(const bool /*wrapAtEOL*/) override { return false; }; // DECAWM bool SetTopBottomScrollingMargins(const VTInt /*topMargin*/, const VTInt /*bottomMargin*/) override { return false; } // DECSTBM + bool SetLeftRightScrollingMargins(const VTInt /*leftMargin*/, const VTInt /*rightMargin*/) override { return false; } // DECSLRM bool WarningBell() override { return false; } // BEL bool CarriageReturn() override { return false; } // CR bool LineFeed(const DispatchTypes::LineFeedType /*lineFeedType*/) override { return false; } // IND, NEL, LF, FF, VT bool ReverseLineFeed() override { return false; } // RI + bool BackIndex() override { return false; } // DECBI + bool ForwardIndex() override { return false; } // DECFI bool SetWindowTitle(std::wstring_view /*title*/) override { return false; } // OscWindowTitle - bool UseAlternateScreenBuffer() override { return false; } // ASBSET - bool UseMainScreenBuffer() override { return false; } // ASBRST bool HorizontalTabSet() override { return false; } // HTS bool ForwardTab(const VTInt /*numTabs*/) override { return false; } // CHT, HT bool BackwardsTab(const VTInt /*numTabs*/) override { return false; } // CBT bool TabClear(const DispatchTypes::TabClearType /*clearType*/) override { return false; } // TBC - bool EnableDECCOLMSupport(const bool /*enabled*/) override { return false; } // ?40 - bool EnableVT200MouseMode(const bool /*enabled*/) override { return false; } // ?1000 - bool EnableUTF8ExtendedMouseMode(const bool /*enabled*/) override { return false; } // ?1005 - bool EnableSGRExtendedMouseMode(const bool /*enabled*/) override { return false; } // ?1006 - bool EnableButtonEventMouseMode(const bool /*enabled*/) override { return false; } // ?1002 - bool EnableAnyEventMouseMode(const bool /*enabled*/) override { return false; } // ?1003 - bool EnableFocusEventMode(const bool /*enabled*/) override { return false; } // ?1004 - bool EnableAlternateScroll(const bool /*enabled*/) override { return false; } // ?1007 - bool EnableXtermBracketedPasteMode(const bool /*enabled*/) override { return false; } // ?2004 bool SetColorTableEntry(const size_t /*tableIndex*/, const DWORD /*color*/) override { return false; } // OSCColorTable bool SetDefaultForeground(const DWORD /*color*/) override { return false; } // OSCDefaultForeground bool SetDefaultBackground(const DWORD /*color*/) override { return false; } // OSCDefaultBackground @@ -93,6 +79,7 @@ public: bool EraseRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECERA bool SelectiveEraseRectangularArea(const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECSERA bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent /*changeExtent*/) override { return false; } // DECSACE + bool RequestChecksumRectangularArea(const VTInt /*id*/, const VTInt /*page*/, const VTInt /*top*/, const VTInt /*left*/, const VTInt /*bottom*/, const VTInt /*right*/) override { return false; } // DECRQCRA bool SetGraphicsRendition(const VTParameters /*options*/) override { return false; } // SGR bool SetLineRendition(const LineRendition /*rendition*/) override { return false; } // DECSWL, DECDWL, DECDHL @@ -101,11 +88,11 @@ public: bool PushGraphicsRendition(const VTParameters /*options*/) override { return false; } // XTPUSHSGR bool PopGraphicsRendition() override { return false; } // XTPOPSGR - bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECSET + bool SetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // SM, DECSET + bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // RM, DECRST + bool RequestMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRQM - bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRST - - bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/) override { return false; } // DSR, DSR-OS, DSR-CPR + bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/, const VTParameter /*id*/) override { return false; } // DSR bool DeviceAttributes() override { return false; } // DA1 bool SecondaryDeviceAttributes() override { return false; } // DA2 bool TertiaryDeviceAttributes() override { return false; } // DA3 @@ -143,6 +130,8 @@ public: bool DoFinalTermAction(const std::wstring_view /*string*/) override { return false; } + bool DoVsCodeAction(const std::wstring_view /*string*/) override { return false; } + StringHandler DownloadDRCS(const VTInt /*fontNumber*/, const VTParameter /*startChar*/, const DispatchTypes::DrcsEraseControl /*eraseControl*/, @@ -152,10 +141,18 @@ public: const VTParameter /*cellHeight*/, const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD + StringHandler DefineMacro(const VTInt /*macroId*/, + const DispatchTypes::MacroDeleteControl /*deleteControl*/, + const DispatchTypes::MacroEncoding /*encoding*/) override { return nullptr; } // DECDMAC + bool InvokeMacro(const VTInt /*macroId*/) override { return false; } // DECINVM + StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat /*format*/) override { return nullptr; }; // DECRSTS StringHandler RequestSetting() override { return nullptr; }; // DECRQSS + bool RequestPresentationStateReport(const DispatchTypes::PresentationReportFormat /*format*/) override { return false; } // DECRQPSR + StringHandler RestorePresentationState(const DispatchTypes::PresentationReportFormat /*format*/) override { return nullptr; } // DECRSPS + bool PlaySounds(const VTParameters /*parameters*/) override { return false; }; // DECPS }; diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp index 8a5d492399..f18f3512c6 100644 --- a/src/terminal/adapter/terminalOutput.cpp +++ b/src/terminal/adapter/terminalOutput.cpp @@ -20,12 +20,17 @@ TerminalOutput::TerminalOutput() noexcept _gsetTranslationTables.at(1) = Ascii; _gsetTranslationTables.at(2) = Ascii; _gsetTranslationTables.at(3) = Ascii; + _gsetIds.at(0) = VTID("B"); + _gsetIds.at(1) = VTID("B"); + _gsetIds.at(2) = VTID("B"); + _gsetIds.at(3) = VTID("B"); } bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset) { const auto translationTable = _LookupTranslationTable94(charset); RETURN_BOOL_IF_FALSE(!translationTable.empty()); + _gsetIds.at(gsetNumber) = charset; return _SetTranslationTable(gsetNumber, translationTable); } @@ -33,6 +38,7 @@ bool TerminalOutput::Designate96Charset(size_t gsetNumber, const VTID charset) { const auto translationTable = _LookupTranslationTable96(charset); RETURN_BOOL_IF_FALSE(!translationTable.empty()); + _gsetIds.at(gsetNumber) = charset; return _SetTranslationTable(gsetNumber, translationTable); } @@ -50,6 +56,16 @@ void TerminalOutput::SetDrcs96Designation(const VTID charset) _drcsTranslationTable = Drcs96; } +VTID TerminalOutput::GetCharsetId(const size_t gsetNumber) const +{ + return _gsetIds.at(gsetNumber); +} + +size_t TerminalOutput::GetCharsetSize(const size_t gsetNumber) const +{ + return _gsetTranslationTables.at(gsetNumber).size() == 96 ? 96 : 94; +} + #pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning bool TerminalOutput::LockingShift(const size_t gsetNumber) { @@ -76,13 +92,27 @@ bool TerminalOutput::LockingShiftRight(const size_t gsetNumber) return true; } -#pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning -bool TerminalOutput::SingleShift(const size_t gsetNumber) +bool TerminalOutput::SingleShift(const size_t gsetNumber) noexcept { - _ssTranslationTable = _gsetTranslationTables.at(gsetNumber); + _ssSetNumber = gsetNumber; return true; } +size_t TerminalOutput::GetLeftSetNumber() const noexcept +{ + return _glSetNumber; +} + +size_t TerminalOutput::GetRightSetNumber() const noexcept +{ + return _grSetNumber; +} + +bool TerminalOutput::IsSingleShiftPending(const size_t gsetNumber) const noexcept +{ + return _ssSetNumber == gsetNumber; +} + // Routine Description: // - Returns true if there is an active translation table, indicating that text has to come through here // Arguments: @@ -91,7 +121,7 @@ bool TerminalOutput::SingleShift(const size_t gsetNumber) // - True if translation is required. bool TerminalOutput::NeedToTranslate() const noexcept { - return !_glTranslationTable.empty() || !_grTranslationTable.empty() || !_ssTranslationTable.empty(); + return !_glTranslationTable.empty() || !_grTranslationTable.empty() || _ssSetNumber != 0; } void TerminalOutput::EnableGrTranslation(boolean enabled) @@ -110,17 +140,18 @@ void TerminalOutput::EnableGrTranslation(boolean enabled) wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const noexcept { auto wchFound = wch; - if (!_ssTranslationTable.empty()) + if (_ssSetNumber == 2 || _ssSetNumber == 3) { - if (wch - 0x20u < _ssTranslationTable.size()) + const auto ssTranslationTable = _gsetTranslationTables.at(_ssSetNumber); + if (wch - 0x20u < ssTranslationTable.size()) { - wchFound = _ssTranslationTable.at(wch - 0x20u); + wchFound = ssTranslationTable.at(wch - 0x20u); } - else if (wch - 0xA0u < _ssTranslationTable.size()) + else if (wch - 0xA0u < ssTranslationTable.size()) { - wchFound = _ssTranslationTable.at(wch - 0xA0u); + wchFound = ssTranslationTable.at(wch - 0xA0u); } - _ssTranslationTable = {}; + _ssSetNumber = 0; } else { diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp index aabc12657b..3b77a3da61 100644 --- a/src/terminal/adapter/terminalOutput.hpp +++ b/src/terminal/adapter/terminalOutput.hpp @@ -30,9 +30,14 @@ namespace Microsoft::Console::VirtualTerminal bool Designate96Charset(const size_t gsetNumber, const VTID charset); void SetDrcs94Designation(const VTID charset); void SetDrcs96Designation(const VTID charset); + VTID GetCharsetId(const size_t gsetNumber) const; + size_t GetCharsetSize(const size_t gsetNumber) const; bool LockingShift(const size_t gsetNumber); bool LockingShiftRight(const size_t gsetNumber); - bool SingleShift(const size_t gsetNumber); + bool SingleShift(const size_t gsetNumber) noexcept; + size_t GetLeftSetNumber() const noexcept; + size_t GetRightSetNumber() const noexcept; + bool IsSingleShiftPending(const size_t gsetNumber) const noexcept; bool NeedToTranslate() const noexcept; void EnableGrTranslation(boolean enabled); @@ -43,11 +48,12 @@ namespace Microsoft::Console::VirtualTerminal void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable); std::array _gsetTranslationTables; + std::array _gsetIds; size_t _glSetNumber = 0; size_t _grSetNumber = 2; std::wstring_view _glTranslationTable; std::wstring_view _grTranslationTable; - mutable std::wstring_view _ssTranslationTable; + mutable size_t _ssSetNumber = 0; boolean _grTranslationEnabled = false; VTID _drcsId = 0; std::wstring_view _drcsTranslationTable; diff --git a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp index da91455f4a..f3df50a0a6 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; @@ -23,12 +22,6 @@ namespace Microsoft }; using namespace Microsoft::Console::VirtualTerminal; -// For magic reasons, this has to live outside the class. Something wonderful about TAEF macros makes it -// invisible to the linker when inside the class. -static const wchar_t* s_pwszInputExpected; - -static wchar_t s_pwszExpectedBuffer[BYTE_MAX]; // big enough for anything - static til::point s_rgTestCoords[] = { { 0, 0 }, { 0, 1 }, @@ -85,71 +78,29 @@ class MouseInputTest public: TEST_CLASS(MouseInputTest); - static void s_MouseInputTestCallback(_Inout_ std::deque>& events) - { - Log::Comment(L"MouseInput successfully generated a sequence for the input, and sent it."); - - size_t cInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(s_pwszInputExpected, STRSAFE_MAX_CCH, &cInputExpected)); - - if (VERIFY_ARE_EQUAL(cInputExpected, events.size(), L"Verify expected and actual input array lengths matched.")) - { - Log::Comment(L"We are expecting always key events and always key down. All other properties should not be written by simulated keys."); - - for (size_t i = 0; i < events.size(); ++i) - { - KeyEvent expectedKeyEvent(TRUE, 1, 0, 0, s_pwszInputExpected[i], 0); - auto testKeyEvent = *static_cast(events[i].get()); - VERIFY_ARE_EQUAL(expectedKeyEvent, testKeyEvent, NoThrowString().Format(L"Chars='%c','%c'", s_pwszInputExpected[i], testKeyEvent.GetCharData())); - } - } - } - - void ClearTestBuffer() - { - memset(s_pwszExpectedBuffer, 0, ARRAYSIZE(s_pwszExpectedBuffer) * sizeof(wchar_t)); - } - // Routine Description: // Constructs a string from s_rgDefaultTestOutput with the third char // correctly filled in to match uiButton. - wchar_t* BuildDefaultTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + TerminalInput::OutputType BuildDefaultTestOutput(const std::wstring_view& pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) { - Log::Comment(NoThrowString().Format(L"Input Test Output:\'%s\'", pwchTestOutput)); - // Copy the expected output into the buffer - size_t cchInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(pwchTestOutput, STRSAFE_MAX_CCH, &cchInputExpected)); - VERIFY_ARE_EQUAL(cchInputExpected, 6ul); - - ClearTestBuffer(); - memcpy(s_pwszExpectedBuffer, pwchTestOutput, cchInputExpected * sizeof(wchar_t)); - - // Change the expected button value - auto wch = GetDefaultCharFromButton(uiButton, sModifierKeystate, sScrollDelta); - Log::Comment(NoThrowString().Format(L"Button Char was:\'%d\' for uiButton '%d", (int)wch, uiButton)); - - s_pwszExpectedBuffer[3] = wch; - Log::Comment(NoThrowString().Format(L"Expected Input:\'%s\'", s_pwszExpectedBuffer)); - return s_pwszExpectedBuffer; + TerminalInput::StringType str; + str.append(pwchTestOutput); + str[3] = GetDefaultCharFromButton(uiButton, sModifierKeystate, sScrollDelta); + return str; } // Routine Description: // Constructs a string from s_rgSgrTestOutput with the third and last chars // correctly filled in to match uiButton. - wchar_t* BuildSGRTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + TerminalInput::OutputType BuildSGRTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) { - ClearTestBuffer(); + wchar_t buffer[256]; + swprintf_s(buffer, pwchTestOutput, GetSgrCharFromButton(uiButton, sModifierKeystate, sScrollDelta)); - // Copy the expected output into the buffer - swprintf_s(s_pwszExpectedBuffer, BYTE_MAX, pwchTestOutput, GetSgrCharFromButton(uiButton, sModifierKeystate, sScrollDelta)); - - size_t cchInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(s_pwszExpectedBuffer, STRSAFE_MAX_CCH, &cchInputExpected)); - - s_pwszExpectedBuffer[cchInputExpected - 1] = IsButtonDown(uiButton) ? L'M' : L'm'; - - Log::Comment(NoThrowString().Format(L"Expected Input:\'%s\'", s_pwszExpectedBuffer)); - return s_pwszExpectedBuffer; + TerminalInput::StringType str; + str.append(std::wstring_view{ buffer }); + str[str.size() - 1] = IsButtonDown(uiButton) ? L'M' : L'm'; + return str; } wchar_t GetDefaultCharFromButton(unsigned int uiButton, short sModifierKeystate, short sScrollDelta) @@ -284,7 +235,7 @@ public: Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); @@ -294,66 +245,70 @@ public: unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= 94 && Coord.Y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= 94 && Coord.Y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= 94 && Coord.Y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } TEST_METHOD(Utf8ModeTests) @@ -367,7 +322,7 @@ public: Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); @@ -377,69 +332,73 @@ public: unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - mouseInput->SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); + mouseInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); auto MaxCoord = SHORT_MAX - 33; - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= MaxCoord && Coord.Y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= MaxCoord && Coord.Y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= MaxCoord && Coord.Y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -454,7 +413,7 @@ public: Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); auto sModifierKeystate = (SHORT)uiModifierKeystate; @@ -463,65 +422,66 @@ public: unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - mouseInput->SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); + mouseInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); - // SGR Mode should be able to handle any arbitrary coords. - // However, mouse moves are only handled in Any Event mode - fExpectedKeyHandled = uiButton != WM_MOUSEMOVE; - - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + // SGR Mode should be able to handle any arbitrary coords. + // However, mouse moves are only handled in Any Event mode + TerminalInput::OutputType expected; + if (uiButton != WM_MOUSEMOVE) + { + expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta, {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta, {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + // SGR Mode should be able to handle any arbitrary coords. + // However, mouse moves are only handled in Any Event mode + TerminalInput::OutputType expected; + if (uiButton != WM_MOUSEMOVE) + { + expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - fExpectedKeyHandled = true; - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + const auto Coord = s_rgTestCoords[i]; + const auto expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -535,7 +495,7 @@ public: Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); auto sModifierKeystate = (SHORT)uiModifierKeystate; @@ -545,110 +505,105 @@ public: VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"sScrollDelta", iScrollDelta)); auto sScrollDelta = (short)(iScrollDelta); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); // Default Tracking, Default Encoding - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= 94 && Coord.Y <= 94); + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } // Default Tracking, UTF8 Encoding - mouseInput->SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); + mouseInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); auto MaxCoord = SHORT_MAX - 33; for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.X <= MaxCoord && Coord.Y <= MaxCoord); + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } // Default Tracking, SGR Encoding - mouseInput->SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); - fExpectedKeyHandled = true; // SGR Mode should be able to handle any arbitrary coords. + mouseInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.X, Coord.Y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + const auto Coord = s_rgTestCoords[i]; + const auto expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), - NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } TEST_METHOD(AlternateScrollModeTests) { Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; const short noModifierKeys = 0; Log::Comment(L"Enable alternate scroll mode in the alt screen buffer"); - mouseInput->UseAlternateScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, true); + mouseInput.UseAlternateScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, true); Log::Comment(L"Test mouse wheel scrolling up"); - s_pwszInputExpected = L"\x1B[A"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[A"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Test mouse wheel scrolling down"); - s_pwszInputExpected = L"\x1B[B"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[B"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); Log::Comment(L"Enable cursor keys mode"); - mouseInput->SetInputMode(TerminalInput::Mode::CursorKey, true); + mouseInput.SetInputMode(TerminalInput::Mode::CursorKey, true); Log::Comment(L"Test mouse wheel scrolling up"); - s_pwszInputExpected = L"\x1BOA"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOA"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Test mouse wheel scrolling down"); - s_pwszInputExpected = L"\x1BOB"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOB"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); Log::Comment(L"Confirm no effect when scroll mode is disabled"); - mouseInput->UseAlternateScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, false); - VERIFY_IS_FALSE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + mouseInput.UseAlternateScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, false); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Confirm no effect when using the main buffer"); - mouseInput->UseMainScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, true); - VERIFY_IS_FALSE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + mouseInput.UseMainScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, true); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); } }; diff --git a/src/terminal/adapter/ut_adapter/WexHelpers.hpp b/src/terminal/adapter/ut_adapter/WexHelpers.hpp index 3ae563f77a..c3edc4718b 100644 --- a/src/terminal/adapter/ut_adapter/WexHelpers.hpp +++ b/src/terminal/adapter/ut_adapter/WexHelpers.hpp @@ -11,7 +11,7 @@ namespace WEX public: static WEX::Common::NoThrowString ToString(const til::inclusive_rect& sr) { - return WEX::Common::NoThrowString().Format(L"(L:%d, R:%d, T:%d, B:%d)", sr.Left, sr.Right, sr.Top, sr.Bottom); + return WEX::Common::NoThrowString().Format(L"(L:%d, R:%d, T:%d, B:%d)", sr.left, sr.right, sr.top, sr.bottom); } }; @@ -21,10 +21,10 @@ namespace WEX public: static bool AreEqual(const til::inclusive_rect& expected, const til::inclusive_rect& actual) { - return expected.Left == actual.Left && - expected.Right == actual.Right && - expected.Top == actual.Top && - expected.Bottom == actual.Bottom; + return expected.left == actual.left && + expected.right == actual.right && + expected.top == actual.top && + expected.bottom == actual.bottom; } static bool AreSame(const til::inclusive_rect& expected, const til::inclusive_rect& actual) @@ -46,7 +46,7 @@ namespace WEX static bool IsNull(const til::inclusive_rect& object) { - return object.Left == 0 && object.Right == 0 && object.Top == 0 && object.Bottom == 0; + return object.left == 0 && object.right == 0 && object.top == 0 && object.bottom == 0; } }; @@ -56,7 +56,7 @@ namespace WEX public: static WEX::Common::NoThrowString ToString(const til::point coord) { - return WEX::Common::NoThrowString().Format(L"(X:%d, Y:%d)", coord.X, coord.Y); + return WEX::Common::NoThrowString().Format(L"(X:%d, Y:%d)", coord.x, coord.y); } }; @@ -77,22 +77,22 @@ namespace WEX static bool IsLessThan(const til::point expectedLess, const til::point expectedGreater) { // less is on a line above greater (Y values less than) - return (expectedLess.Y < expectedGreater.Y) || + return (expectedLess.y < expectedGreater.y) || // or on the same lines and less is left of greater (X values less than) - ((expectedLess.Y == expectedGreater.Y) && (expectedLess.X < expectedGreater.X)); + ((expectedLess.y == expectedGreater.y) && (expectedLess.x < expectedGreater.x)); } static bool IsGreaterThan(const til::point expectedGreater, const til::point expectedLess) { // greater is on a line below less (Y value greater than) - return (expectedGreater.Y > expectedLess.Y) || + return (expectedGreater.y > expectedLess.y) || // or on the same lines and greater is right of less (X values greater than) - ((expectedGreater.Y == expectedLess.Y) && (expectedGreater.X > expectedLess.X)); + ((expectedGreater.y == expectedLess.y) && (expectedGreater.x > expectedLess.x)); } static bool IsNull(const til::point object) { - return object.X == 0 && object.Y == 0; + return object.x == 0 && object.y == 0; } }; @@ -322,8 +322,8 @@ namespace WEX VERIFY_SUCCEEDED(StringCchPrintf(szBufferSize, ARRAYSIZE(szBufferSize), L"WINDOW_BUFFER_SIZE (%d, %d)", - ir.Event.WindowBufferSizeEvent.dwSize.X, - ir.Event.WindowBufferSizeEvent.dwSize.Y)); + ir.Event.WindowBufferSizeEvent.dwSize.width, + ir.Event.WindowBufferSizeEvent.dwSize.height)); VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szBufferSize)); break; } @@ -382,8 +382,8 @@ namespace WEX case WINDOW_BUFFER_SIZE_EVENT: { - fEqual = (expected.Event.WindowBufferSizeEvent.dwSize.X == actual.Event.WindowBufferSizeEvent.dwSize.X && - expected.Event.WindowBufferSizeEvent.dwSize.Y == actual.Event.WindowBufferSizeEvent.dwSize.Y); + fEqual = (expected.Event.WindowBufferSizeEvent.dwSize.width == actual.Event.WindowBufferSizeEvent.dwSize.width && + expected.Event.WindowBufferSizeEvent.dwSize.height == actual.Event.WindowBufferSizeEvent.dwSize.height); break; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 74e1381328..c319246205 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -60,10 +60,6 @@ using namespace Microsoft::Console::VirtualTerminal; class TestGetSet final : public ITerminalApi { public: - void PrintString(const std::wstring_view /*string*/) override - { - } - void ReturnResponse(const std::wstring_view response) override { Log::Comment(L"ReturnResponse MOCK called..."); @@ -92,7 +88,7 @@ public: til::rect GetViewport() const override { - return { _viewport.Left, _viewport.Top, _viewport.Right, _viewport.Bottom }; + return { _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; } void SetViewportPosition(const til::point /*position*/) override @@ -100,11 +96,6 @@ public: Log::Comment(L"SetViewportPosition MOCK called..."); } - void SetAutoWrapMode(const bool /*wrapAtEOL*/) override - { - Log::Comment(L"SetAutoWrapMode MOCK called..."); - } - bool IsVtInputEnabled() const override { return false; @@ -119,15 +110,16 @@ public: _textBuffer->SetCurrentAttributes(attrs); } - void SetScrollingRegion(const til::inclusive_rect& scrollMargins) override + void SetSystemMode(const Mode mode, const bool enabled) { - Log::Comment(L"SetScrollingRegion MOCK called..."); + Log::Comment(L"SetSystemMode MOCK called..."); + _systemMode.set(mode, enabled); + } - if (_setScrollingRegionResult) - { - VERIFY_ARE_EQUAL(_expectedScrollRegion, scrollMargins); - _activeScrollRegion = scrollMargins; - } + bool GetSystemMode(const Mode mode) const + { + Log::Comment(L"GetSystemMode MOCK called..."); + return _systemMode.test(mode); } void WarningBell() override @@ -135,20 +127,6 @@ public: Log::Comment(L"WarningBell MOCK called..."); } - bool GetLineFeedMode() const override - { - Log::Comment(L"GetLineFeedMode MOCK called..."); - return _getLineFeedModeResult; - } - - void LineFeed(const bool withReturn) override - { - Log::Comment(L"LineFeed MOCK called..."); - - THROW_HR_IF(E_FAIL, !_lineFeedResult); - VERIFY_ARE_EQUAL(_expectedLineFeedWithReturn, withReturn); - } - void SetWindowTitle(const std::wstring_view title) { Log::Comment(L"SetWindowTitle MOCK called..."); @@ -161,7 +139,7 @@ public: } } - void UseAlternateScreenBuffer() override + void UseAlternateScreenBuffer(const TextAttribute& /*attr*/) override { Log::Comment(L"UseAlternateScreenBuffer MOCK called..."); } @@ -201,11 +179,6 @@ public: return _expectedOutputCP; } - void EnableXtermBracketedPasteMode(const bool /*enabled*/) - { - Log::Comment(L"EnableXtermBracketedPasteMode MOCK called..."); - } - void CopyToClipboard(const std::wstring_view /*content*/) { Log::Comment(L"CopyToClipboard MOCK called..."); @@ -237,9 +210,32 @@ public: Log::Comment(L"NotifyAccessibilityChange MOCK called..."); } - void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) override + void NotifyBufferRotation(const int /*delta*/) override { - Log::Comment(L"AddMark MOCK called..."); + Log::Comment(L"NotifyBufferRotation MOCK called..."); + } + + void MarkPrompt(const ScrollMark& /*mark*/) override + { + Log::Comment(L"MarkPrompt MOCK called..."); + } + void MarkCommandStart() override + { + Log::Comment(L"MarkCommandStart MOCK called..."); + } + void MarkOutputStart() override + { + Log::Comment(L"MarkOutputStart MOCK called..."); + } + void MarkCommandFinish(std::optional /*error*/) override + { + Log::Comment(L"MarkCommandFinish MOCK called..."); + } + void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override + { + Log::Comment(L"InvokeCompletions MOCK called..."); + VERIFY_ARE_EQUAL(_expectedMenuJson, menuJson); + VERIFY_ARE_EQUAL(_expectedReplaceLength, replaceLength); } void PrepData() @@ -277,10 +273,10 @@ public: _textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, _renderer); // Viewport sitting in the "middle" of the buffer somewhere (so all sides have excess buffer around them) - _viewport.Top = 20; - _viewport.Bottom = 49; - _viewport.Left = 30; - _viewport.Right = 59; + _viewport.top = 20; + _viewport.bottom = 49; + _viewport.left = 30; + _viewport.right = 59; // Call cursor positions separately PrepCursor(xact, yact); @@ -304,15 +300,15 @@ public: { case CursorX::LEFT: Log::Comment(L"Cursor set to left edge of buffer."); - cursorPos.X = 0; + cursorPos.x = 0; break; case CursorX::RIGHT: Log::Comment(L"Cursor set to right edge of buffer."); - cursorPos.X = bufferSize.X - 1; + cursorPos.x = bufferSize.width - 1; break; case CursorX::XCENTER: Log::Comment(L"Cursor set to centered X of buffer."); - cursorPos.X = bufferSize.X / 2; + cursorPos.x = bufferSize.width / 2; break; } @@ -320,15 +316,15 @@ public: { case CursorY::TOP: Log::Comment(L"Cursor set to top edge of viewport."); - cursorPos.Y = _viewport.Top; + cursorPos.y = _viewport.top; break; case CursorY::BOTTOM: Log::Comment(L"Cursor set to bottom edge of viewport."); - cursorPos.Y = _viewport.Bottom - 1; + cursorPos.y = _viewport.bottom - 1; break; case CursorY::YCENTER: Log::Comment(L"Cursor set to centered Y of viewport."); - cursorPos.Y = _viewport.Top + ((_viewport.Bottom - _viewport.Top) / 2); + cursorPos.y = _viewport.top + ((_viewport.bottom - _viewport.top) / 2); break; } @@ -336,6 +332,24 @@ public: _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()); @@ -346,15 +360,6 @@ public: VERIFY_ARE_EQUAL(pwszExpectedResponse, _response); } - void _SetMarginsHelper(til::inclusive_rect* rect, til::CoordType top, til::CoordType bottom) - { - rect->Top = top; - rect->Bottom = bottom; - //The rectangle is going to get converted from VT space to conhost space - _expectedScrollRegion.Top = (top > 0) ? rect->Top - 1 : rect->Top; - _expectedScrollRegion.Bottom = (bottom > 0) ? rect->Bottom - 1 : rect->Bottom; - } - ~TestGetSet() = default; static const WCHAR s_wchErase = (WCHAR)0x20; @@ -379,8 +384,6 @@ public: DummyRenderer _renderer; std::unique_ptr _textBuffer; til::inclusive_rect _viewport; - til::inclusive_rect _expectedScrollRegion; - til::inclusive_rect _activeScrollRegion; til::point _expectedCursorPos; @@ -391,10 +394,7 @@ public: bool _setTextAttributesResult = false; bool _returnResponseResult = false; - bool _setScrollingRegionResult = false; - bool _getLineFeedModeResult = false; - bool _lineFeedResult = false; - bool _expectedLineFeedWithReturn = false; + til::enumset _systemMode{ Mode::AutoWrap }; bool _setWindowTitleResult = false; std::wstring_view _expectedWindowTitle{}; @@ -402,6 +402,9 @@ public: bool _getConsoleOutputCPResult = false; bool _expectedShowWindow = false; + std::wstring _expectedMenuJson{}; + unsigned int _expectedReplaceLength = 0; + private: HANDLE _hCon; }; @@ -420,7 +423,7 @@ public: if (fSuccess) { _testGetSet = std::move(api); - _terminalInput = TerminalInput{ nullptr }; + _terminalInput = TerminalInput{}; auto& renderer = _testGetSet->_renderer; auto& renderSettings = renderer._renderSettings; auto adapter = std::make_unique(*_testGetSet, renderer, renderSettings, _terminalInput); @@ -523,7 +526,7 @@ public: if (fDoTest1b) { - _testGetSet->_expectedCursorPos.X = 0; + _testGetSet->_expectedCursorPos.x = 0; VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(1)); _testGetSet->ValidateExpectedCursorPos(); } @@ -539,24 +542,24 @@ public: switch (direction) { case CursorDirection::UP: - _testGetSet->_expectedCursorPos.Y--; + _testGetSet->_expectedCursorPos.y--; break; case CursorDirection::DOWN: - _testGetSet->_expectedCursorPos.Y++; + _testGetSet->_expectedCursorPos.y++; break; case CursorDirection::RIGHT: - _testGetSet->_expectedCursorPos.X++; + _testGetSet->_expectedCursorPos.x++; break; case CursorDirection::LEFT: - _testGetSet->_expectedCursorPos.X--; + _testGetSet->_expectedCursorPos.x--; break; case CursorDirection::NEXTLINE: - _testGetSet->_expectedCursorPos.Y++; - _testGetSet->_expectedCursorPos.X = 0; + _testGetSet->_expectedCursorPos.y++; + _testGetSet->_expectedCursorPos.x = 0; break; case CursorDirection::PREVLINE: - _testGetSet->_expectedCursorPos.Y--; - _testGetSet->_expectedCursorPos.X = 0; + _testGetSet->_expectedCursorPos.y--; + _testGetSet->_expectedCursorPos.x = 0; break; } @@ -572,24 +575,24 @@ public: switch (direction) { case CursorDirection::UP: - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Top; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.top; break; case CursorDirection::DOWN: - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Bottom - 1; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.bottom - 1; break; case CursorDirection::RIGHT: - _testGetSet->_expectedCursorPos.X = _testGetSet->_textBuffer->GetSize().Dimensions().X - 1; + _testGetSet->_expectedCursorPos.x = _testGetSet->_textBuffer->GetSize().Dimensions().width - 1; break; case CursorDirection::LEFT: - _testGetSet->_expectedCursorPos.X = 0; + _testGetSet->_expectedCursorPos.x = 0; break; case CursorDirection::NEXTLINE: - _testGetSet->_expectedCursorPos.X = 0; - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Bottom - 1; + _testGetSet->_expectedCursorPos.x = 0; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.bottom - 1; break; case CursorDirection::PREVLINE: - _testGetSet->_expectedCursorPos.X = 0; - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Top; + _testGetSet->_expectedCursorPos.x = 0; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.top; break; } @@ -604,12 +607,12 @@ public: Log::Comment(L"Test 1: Place cursor within the viewport. Start from top left, move to middle."); _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); - auto sCol = (_testGetSet->_viewport.Right - _testGetSet->_viewport.Left) / 2; - auto sRow = (_testGetSet->_viewport.Bottom - _testGetSet->_viewport.Top) / 2; + auto sCol = (_testGetSet->_viewport.right - _testGetSet->_viewport.left) / 2; + auto sRow = (_testGetSet->_viewport.bottom - _testGetSet->_viewport.top) / 2; // The X coordinate is unaffected by the viewport. - _testGetSet->_expectedCursorPos.X = sCol - 1; - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Top + (sRow - 1); + _testGetSet->_expectedCursorPos.x = sCol - 1; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.top + (sRow - 1); VERIFY_IS_TRUE(_pDispatch->CursorPosition(sRow, sCol)); _testGetSet->ValidateExpectedCursorPos(); @@ -618,8 +621,8 @@ public: _testGetSet->PrepData(CursorX::RIGHT, CursorY::BOTTOM); // The X coordinate is unaffected by the viewport. - _testGetSet->_expectedCursorPos.X = 0; - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Top; + _testGetSet->_expectedCursorPos.x = 0; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.top; VERIFY_IS_TRUE(_pDispatch->CursorPosition(1, 1)); _testGetSet->ValidateExpectedCursorPos(); @@ -627,11 +630,11 @@ public: Log::Comment(L"Test 3: Move beyond rectangle (down/right too far). Should be bounded back in."); _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); - sCol = (_testGetSet->_textBuffer->GetSize().Dimensions().X) * 2; - sRow = (_testGetSet->_viewport.Bottom - _testGetSet->_viewport.Top) * 2; + sCol = (_testGetSet->_textBuffer->GetSize().Dimensions().width) * 2; + sRow = (_testGetSet->_viewport.bottom - _testGetSet->_viewport.top) * 2; - _testGetSet->_expectedCursorPos.X = _testGetSet->_textBuffer->GetSize().Dimensions().X - 1; - _testGetSet->_expectedCursorPos.Y = _testGetSet->_viewport.Bottom - 1; + _testGetSet->_expectedCursorPos.x = _testGetSet->_textBuffer->GetSize().Dimensions().width - 1; + _testGetSet->_expectedCursorPos.y = _testGetSet->_viewport.bottom - 1; VERIFY_IS_TRUE(_pDispatch->CursorPosition(sRow, sCol)); _testGetSet->ValidateExpectedCursorPos(); @@ -663,16 +666,16 @@ public: { case AbsolutePosition::CursorHorizontal: Log::Comment(L"Testing cursor horizontal movement."); - sRangeEnd = _testGetSet->_textBuffer->GetSize().Dimensions().X; + sRangeEnd = _testGetSet->_textBuffer->GetSize().Dimensions().width; sRangeStart = 0; - psCursorExpected = &_testGetSet->_expectedCursorPos.X; + psCursorExpected = &_testGetSet->_expectedCursorPos.x; moveFunc = &AdaptDispatch::CursorHorizontalPositionAbsolute; break; case AbsolutePosition::VerticalLine: Log::Comment(L"Testing vertical line movement."); - sRangeEnd = _testGetSet->_viewport.Bottom; - sRangeStart = _testGetSet->_viewport.Top; - psCursorExpected = &_testGetSet->_expectedCursorPos.Y; + sRangeEnd = _testGetSet->_viewport.bottom; + sRangeStart = _testGetSet->_viewport.top; + psCursorExpected = &_testGetSet->_expectedCursorPos.y; moveFunc = &AdaptDispatch::VerticalLinePositionAbsolute; break; } @@ -772,7 +775,14 @@ public: Log::Comment(L"Verify successful API call modifies visibility state."); _testGetSet->PrepData(); _testGetSet->_textBuffer->GetCursor().SetIsVisible(fStart); - VERIFY_IS_TRUE(_pDispatch->CursorVisibility(fEnd)); + if (fEnd) + { + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::DECTCEM_TextCursorEnableMode)); + } + else + { + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::DECTCEM_TextCursorEnableMode)); + } VERIFY_ARE_EQUAL(fEnd, _testGetSet->_textBuffer->GetCursor().IsVisible()); } @@ -842,13 +852,13 @@ public: 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'"); @@ -882,8 +892,7 @@ public: 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: @@ -1123,6 +1132,66 @@ public: VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, cOptions })); } + TEST_METHOD(GraphicsSingleWithSubParamTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + 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..."); + _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::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); + 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; + 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; + } + _testGetSet->_textBuffer->SetCurrentAttributes(startingAttribute); + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ std::span{ rgOptions, cOptions }, subParams, subParamRanges })); + } + TEST_METHOD(GraphicsPushPopTests) { Log::Comment(L"Starting test..."); @@ -1130,6 +1199,8 @@ public: _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; @@ -1233,7 +1304,7 @@ public: _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; @@ -1242,14 +1313,14 @@ public: _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... @@ -1259,6 +1330,76 @@ public: _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) @@ -1358,7 +1499,7 @@ public: Log::Comment(L"Test 1: Verify failure when using bad status."); _testGetSet->PrepData(); - VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1)); + VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1, {})); } TEST_METHOD(DeviceStatus_OperatingStatusTests) @@ -1367,7 +1508,7 @@ public: Log::Comment(L"Test 1: Verify good operating condition."); _testGetSet->PrepData(); - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus, {})); _testGetSet->ValidateInputEvent(L"\x1b[0n"); } @@ -1384,17 +1525,17 @@ public: til::point coordCursorExpected{ _testGetSet->_textBuffer->GetCursor().GetPosition() }; // to get to VT, we have to adjust it to its position relative to the viewport top. - coordCursorExpected.Y -= _testGetSet->_viewport.Top; + coordCursorExpected.y -= _testGetSet->_viewport.top; // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.) - coordCursorExpected.X++; - coordCursorExpected.Y++; + coordCursorExpected.x++; + coordCursorExpected.y++; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); wchar_t pwszBuffer[50]; - swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%d;%dR", coordCursorExpected.Y, coordCursorExpected.X); + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%d;%dR", coordCursorExpected.y, coordCursorExpected.x); _testGetSet->ValidateInputEvent(pwszBuffer); } @@ -1409,22 +1550,22 @@ public: til::point coordCursorExpectedFirst{ _testGetSet->_textBuffer->GetCursor().GetPosition() }; // to get to VT, we have to adjust it to its position relative to the viewport top. - coordCursorExpectedFirst -= til::point{ 0, _testGetSet->_viewport.Top }; + coordCursorExpectedFirst -= til::point{ 0, _testGetSet->_viewport.top }; // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.) coordCursorExpectedFirst += til::point{ 1, 1 }; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); auto cursorPos = _testGetSet->_textBuffer->GetCursor().GetPosition(); - cursorPos.X++; - cursorPos.Y++; + cursorPos.x++; + cursorPos.y++; _testGetSet->_textBuffer->GetCursor().SetPosition(cursorPos); auto coordCursorExpectedSecond{ coordCursorExpectedFirst }; coordCursorExpectedSecond += til::point{ 1, 1 }; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {})); wchar_t pwszBuffer[50]; @@ -1444,22 +1585,89 @@ public: til::point coordCursorExpected{ _testGetSet->_textBuffer->GetCursor().GetPosition() }; // to get to VT, we have to adjust it to its position relative to the viewport top. - coordCursorExpected.Y -= _testGetSet->_viewport.Top; + coordCursorExpected.y -= _testGetSet->_viewport.top; // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.) - coordCursorExpected.X++; - coordCursorExpected.Y++; + coordCursorExpected.x++; + coordCursorExpected.y++; // Until we support paging (GH#13892) the reported page number should always be 1. const auto pageExpected = 1; - VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport)); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport, {})); wchar_t pwszBuffer[50]; - swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.Y, coordCursorExpected.X, pageExpected); + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); _testGetSet->ValidateInputEvent(pwszBuffer); } + TEST_METHOD(DeviceStatus_MacroSpaceReportTest) + { + Log::Comment(L"Starting test..."); + + // Space is measured in blocks of 16 bytes. + const auto availableSpace = MacroBuffer::MAX_SPACE / 16; + + Log::Comment(L"Test 1: Verify maximum space available"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + wchar_t pwszBuffer[50]; + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 2: Verify space decrease"); + _testGetSet->PrepData(); + // Define four 8-byte macros, i.e. 32 byes (2 macro blocks). + _stateMachine->ProcessString(L"\033P1;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P3;0;0!z12345678\033\\"); + _stateMachine->ProcessString(L"\033P4;0;0!z12345678\033\\"); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace - 2); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 3: Verify space reset"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->HardReset()); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace); + _testGetSet->ValidateInputEvent(pwszBuffer); + } + + TEST_METHOD(DeviceStatus_MemoryChecksumReportTest) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify initial checksum is 0"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 12)); + + _testGetSet->ValidateInputEvent(L"\033P12!~0000\033\\"); + + Log::Comment(L"Test 2: Verify checksum after macros defined"); + _testGetSet->PrepData(); + // Define a couple of text macros + _stateMachine->ProcessString(L"\033P1;0;0!zABCD\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zabcd\033\\"); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 34)); + + // Checksum is a 16-bit negated sum of the macro buffer characters. + const auto checksum = gsl::narrow_cast(-('A' + 'B' + 'C' + 'D' + 'a' + 'b' + 'c' + 'd')); + wchar_t pwszBuffer[50]; + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\033P34!~%04X\033\\", checksum); + _testGetSet->ValidateInputEvent(pwszBuffer); + + Log::Comment(L"Test 3: Verify checksum resets to 0"); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->HardReset()); + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 56)); + + _testGetSet->ValidateInputEvent(L"\033P56!~0000\033\\"); + } + TEST_METHOD(DeviceAttributesTests) { Log::Comment(L"Starting test..."); @@ -1468,7 +1676,7 @@ public: _testGetSet->PrepData(); VERIFY_IS_TRUE(_pDispatch->DeviceAttributes()); - auto pwszExpectedResponse = L"\x1b[?1;0c"; + auto pwszExpectedResponse = L"\x1b[?61;1;6;7;21;22;23;24;28;32;42c"; _testGetSet->ValidateInputEvent(pwszExpectedResponse); Log::Comment(L"Test 2: Verify failure when ReturnResponse doesn't work."); @@ -1558,11 +1766,27 @@ public: Log::Comment(L"Requesting DECSTBM margins (full screen)."); _testGetSet->PrepData(); // Set screen height to 25 - this will be the expected margin range. - _testGetSet->_viewport.Bottom = _testGetSet->_viewport.Top + 25; + _testGetSet->_viewport.bottom = _testGetSet->_viewport.top + 25; _pDispatch->SetTopBottomScrollingMargins(0, 0); requestSetting(L"r"); _testGetSet->ValidateInputEvent(L"\033P1$r1;25r\033\\"); + Log::Comment(L"Requesting DECSLRM margins (5 to 10)."); + _testGetSet->PrepData(); + // We need to enable DECLRMM for horizontal margins to work. + _pDispatch->SetMode(DispatchTypes::DECLRMM_LeftRightMarginMode); + _pDispatch->SetLeftRightScrollingMargins(5, 10); + requestSetting(L"s"); + _testGetSet->ValidateInputEvent(L"\033P1$r5;10s\033\\"); + + Log::Comment(L"Requesting DECSLRM margins (full width)."); + _testGetSet->PrepData(); + _pDispatch->SetLeftRightScrollingMargins(0, 0); + requestSetting(L"s"); + _testGetSet->ValidateInputEvent(L"\033P1$r1;100s\033\\"); + // Reset DECLRMM once we're done with horizontal margin testing. + _pDispatch->ResetMode(DispatchTypes::DECLRMM_LeftRightMarginMode); + Log::Comment(L"Requesting SGR attributes (default)."); _testGetSet->PrepData(); TextAttribute attribute = {}; @@ -1574,12 +1798,20 @@ public: _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 = {}; @@ -1602,7 +1834,7 @@ public: 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"); @@ -1631,18 +1863,20 @@ public: 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(); @@ -1659,12 +1893,419 @@ public: requestSetting(L"\"q"); _testGetSet->ValidateInputEvent(L"\033P1$r1\"q\033\\"); + // Initialize the color alias indices for the DECAC tests below. + _testGetSet->PrepData(); + auto& renderSettings = _testGetSet->_renderer._renderSettings; + renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, 3); + renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, 5); + renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, 4); + renderSettings.SetColorAliasIndex(ColorAlias::FrameBackground, 6); + + Log::Comment(L"Requesting DECAC colors (default)."); + requestSetting(L",|"); + _testGetSet->ValidateInputEvent(L"\033P1$r1;3;5,|\033\\"); + + Log::Comment(L"Requesting DECAC colors (normal text)."); + requestSetting(L"1,|"); + _testGetSet->ValidateInputEvent(L"\033P1$r1;3;5,|\033\\"); + + Log::Comment(L"Requesting DECAC colors (window frame)."); + requestSetting(L"2,|"); + _testGetSet->ValidateInputEvent(L"\033P1$r2;4;6,|\033\\"); + + Log::Comment(L"Requesting DECAC colors (invalid item)."); + requestSetting(L"3,|"); + _testGetSet->ValidateInputEvent(L"\033P0$r\033\\"); + Log::Comment(L"Requesting an unsupported setting."); _testGetSet->PrepData(); requestSetting(L"x"); _testGetSet->ValidateInputEvent(L"\033P0$r\033\\"); } + TEST_METHOD(RequestStandardModeTests) + { + // The mode numbers below correspond to the ANSIStandardMode values + // in the ModeParams enum in DispatchTypes.hpp. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{4, 20}") + END_TEST_METHOD_PROPERTIES() + + VTInt modeNumber; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"modeNumber", modeNumber)); + const auto mode = DispatchTypes::ANSIStandardMode(modeNumber); + + // DISABLE_ + Log::Comment(NoThrowString().Format(L"Setting standard mode %d", modeNumber)); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->SetMode(mode)); + VERIFY_IS_TRUE(_pDispatch->RequestMode(mode)); + + wchar_t expectedResponse[20]; + swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[%d;1$y", modeNumber); + _testGetSet->ValidateInputEvent(expectedResponse); + + Log::Comment(NoThrowString().Format(L"Resetting standard mode %d", modeNumber)); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->ResetMode(mode)); + VERIFY_IS_TRUE(_pDispatch->RequestMode(mode)); + + swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[%d;2$y", modeNumber); + _testGetSet->ValidateInputEvent(expectedResponse); + } + + TEST_METHOD(RequestPrivateModeTests) + { + // The mode numbers below correspond to the DECPrivateMode values + // in the ModeParams enum in DispatchTypes.hpp. We don't include + // AnsiMode (2), because once that's disabled we'd be in VT52 mode, + // and DECRQM would not then be applicable. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 117, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}") + END_TEST_METHOD_PROPERTIES() + + VTInt modeNumber; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"modeNumber", modeNumber)); + const auto mode = DispatchTypes::DECPrivateMode(modeNumber); + + if (mode == DispatchTypes::DECCOLM_SetNumberOfColumns) + { + Log::Comment(L"Make sure DECCOLM is allowed"); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::XTERM_EnableDECCOLMSupport)); + } + + Log::Comment(NoThrowString().Format(L"Setting private mode %d", modeNumber)); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->SetMode(mode)); + VERIFY_IS_TRUE(_pDispatch->RequestMode(mode)); + + wchar_t expectedResponse[20]; + swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[?%d;1$y", modeNumber); + _testGetSet->ValidateInputEvent(expectedResponse); + + Log::Comment(NoThrowString().Format(L"Resetting private mode %d", modeNumber)); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->ResetMode(mode)); + VERIFY_IS_TRUE(_pDispatch->RequestMode(mode)); + + swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[?%d;2$y", modeNumber); + _testGetSet->ValidateInputEvent(expectedResponse); + } + + TEST_METHOD(RequestChecksumReportTests) + { + const auto requestChecksumReport = [this](const auto length) { + wchar_t checksumQuery[30]; + swprintf_s(checksumQuery, ARRAYSIZE(checksumQuery), L"\033[99;1;1;1;1;%zu*y", length); + _stateMachine->ProcessString(checksumQuery); + }; + + const auto verifyChecksumReport = [this](const auto checksum) { + wchar_t expectedResponse[20]; + swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1bP99!~%s\033\\", checksum); + _testGetSet->ValidateInputEvent(expectedResponse); + }; + + const auto outputText = [&](const auto text) { + _testGetSet->PrepData(); + _pDispatch->PrintString(text); + requestChecksumReport(text.length()); + }; + + const auto outputTextWithAttributes = [&](const auto text, const auto& attrCallback) { + _testGetSet->PrepData(); + auto attr = TextAttribute{}; + attrCallback(attr); + _testGetSet->_textBuffer->SetCurrentAttributes(attr); + _pDispatch->PrintString(text); + requestChecksumReport(text.length()); + }; + + using namespace std::string_view_literals; + + Log::Comment(L"Test 1: ASCII characters"); + outputText(L"A"sv); + verifyChecksumReport(L"FF4F"); + outputText(L" "sv); + verifyChecksumReport(L"FF70"); + outputText(L"~"sv); + verifyChecksumReport(L"FF12"); + outputText(L"ABC"sv); + verifyChecksumReport(L"FDEA"); + + Log::Comment(L"Test 2: Latin-1 characters"); + outputText(L"Á"sv); + verifyChecksumReport(L"FECF"); + outputText(L"¡"sv); + verifyChecksumReport(L"FEEF"); + outputText(L"ÿ"sv); + verifyChecksumReport(L"FE91"); + outputText(L"ÁÂÃ"sv); + verifyChecksumReport(L"FC6A"); + + Log::Comment(L"Test 3: Rendition attributes"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetIntense(true); + }); + verifyChecksumReport(L"FECF"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + }); + verifyChecksumReport(L"FF3F"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetBlinking(true); + }); + verifyChecksumReport(L"FF0F"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetReverseVideo(true); + }); + verifyChecksumReport(L"FF2F"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetInvisible(true); + }); + verifyChecksumReport(L"FF47"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetIntense(true); + attr.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + attr.SetReverseVideo(true); + }); + verifyChecksumReport(L"FE9F"); + + Log::Comment(L"Test 4: Selective erase"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetProtected(true); + }); + verifyChecksumReport(L"FF4B"); + outputTextWithAttributes(L"B"sv, [](auto& attr) { + attr.SetProtected(true); + }); + verifyChecksumReport(L"FF4A"); + + Log::Comment(L"Test 5: Color attributes"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetIndexedForeground(TextColor::DARK_RED); + }); + verifyChecksumReport(L"FFAF"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetIndexedBackground(TextColor::DARK_GREEN); + }); + verifyChecksumReport(L"FF4D"); + outputTextWithAttributes(L"A"sv, [](auto& attr) { + attr.SetIndexedForeground(TextColor::DARK_YELLOW); + attr.SetIndexedBackground(TextColor::DARK_BLUE); + }); + verifyChecksumReport(L"FF8B"); + } + + TEST_METHOD(TabulationStopReportTests) + { + _testGetSet->PrepData(); + auto& textBuffer = *_testGetSet->_textBuffer; + auto& stateMachine = *_testGetSet->_stateMachine; + + Log::Comment(L"Default tabs stops in 80-column mode"); + textBuffer.ResizeTraditional({ 80, 600 }); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73\033\\"); + + Log::Comment(L"Default tabs stops in 132-column mode"); + textBuffer.ResizeTraditional({ 132, 600 }); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u9/17/25/33/41/49/57/65/73/81/89/97/105/113/121/129\033\\"); + + Log::Comment(L"Custom tab stops in 80 columns"); + textBuffer.ResizeTraditional({ 80, 600 }); + _testGetSet->_stateMachine->ProcessString(L"\033P2$t30/60/120/240\033\\"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u30/60\033\\"); + + Log::Comment(L"After expanding width to 132 columns"); + textBuffer.ResizeTraditional({ 132, 600 }); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u30/60/120\033\\"); + textBuffer.ResizeTraditional({ 80, 600 }); + + Log::Comment(L"Out of order tab stops"); + stateMachine.ProcessString(L"\033P2$t44/22/66\033\\"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u22/44/66\033\\"); + + Log::Comment(L"Empty tab stop are ignored"); + stateMachine.ProcessString(L"\033P2$t3//7\033\\"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u3/7\033\\"); + + Log::Comment(L"'0' tab stops are ignored"); + stateMachine.ProcessString(L"\033P2$t0/5/10\033\\"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u5/10\033\\"); + + Log::Comment(L"'1' tab stops are ignored"); + stateMachine.ProcessString(L"\033P2$t1/8/18\033\\"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u8/18\033\\"); + + Log::Comment(L"Clear tab stops"); + _pDispatch->TabClear(DispatchTypes::TabClearType::ClearAllColumns); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::TabulationStopReport); + _testGetSet->ValidateInputEvent(L"\033P2$u\033\\"); + } + + TEST_METHOD(CursorInformationReportTests) + { + _testGetSet->PrepData(); + auto& textBuffer = *_testGetSet->_textBuffer; + auto& stateMachine = *_testGetSet->_stateMachine; + auto& termOutput = _pDispatch->_termOutput; + const auto viewportTop = _testGetSet->_viewport.top; + auto attributes = TextAttribute{}; + + Log::Comment(L"Initial state"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;@;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Cursor position"); + textBuffer.GetCursor().SetPosition({ 3, viewportTop + 2 }); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;@;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Intense rendition"); + attributes.SetIntense(true); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;A;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Underlined rendition"); + attributes.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;C;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Blinking rendition"); + attributes.SetBlinking(true); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;G;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Reverse video rendition"); + attributes.SetReverseVideo(true); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;O;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Invisible rendition"); + attributes.SetInvisible(true); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;_;@;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Protected attribute"); + attributes.SetProtected(true); + textBuffer.SetCurrentAttributes(attributes); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u3;4;1;_;A;@;0;2;@;BBBB\033\\"); + + Log::Comment(L"Origin mode"); + _pDispatch->SetMode(DispatchTypes::DECOM_OriginMode); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;A;0;2;@;BBBB\033\\"); + + Log::Comment(L"Single shift 2"); + _pDispatch->SingleShift(2); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;C;0;2;@;BBBB\033\\"); + + Log::Comment(L"Single shift 3"); + _pDispatch->SingleShift(3); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;1;1;_;A;E;0;2;@;BBBB\033\\"); + + Log::Comment(L"Delayed EOL wrap"); + _pDispatch->CursorForward(999); + _pDispatch->Print(L'*'); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;0;2;@;BBBB\033\\"); + + Log::Comment(L"Locking shifts"); + _pDispatch->LockingShift(1); + _pDispatch->LockingShiftRight(3); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;@;BBBB\033\\"); + + Log::Comment(L"94 charset designations"); + _pDispatch->Designate94Charset(0, "%5"); + _pDispatch->Designate94Charset(1, "<"); + _pDispatch->Designate94Charset(2, "0"); + _pDispatch->Designate94Charset(3, "K"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;@;%5<0K\033\\"); + + Log::Comment(L"96 charset designation (G1)"); + _pDispatch->Designate96Charset(1, "H"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;B;%5H0K\033\\"); + + Log::Comment(L"96 charset designation (G2)"); + _pDispatch->Designate96Charset(2, "M"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;F;%5HMK\033\\"); + + Log::Comment(L"96 charset designation (G3)"); + _pDispatch->Designate96Charset(3, "B"); + _pDispatch->RequestPresentationStateReport(DispatchTypes::PresentationReportFormat::CursorInformationReport); + _testGetSet->ValidateInputEvent(L"\033P1$u1;100;1;_;A;I;1;3;N;%5HMB\033\\"); + + Log::Comment(L"Restore cursor position"); + stateMachine.ProcessString(L"\033P1$t3;4;1;@;@;@;0;2;@;BBBB\033\\"); + auto expectedPosition = til::point{ 3, viewportTop + 2 }; + 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); // 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.SetUnderlineStyle(UnderlineStyle::SinglyUnderlined); + attributes.SetReverseVideo(true); + attributes.SetProtected(true); + VERIFY_ARE_EQUAL(attributes, textBuffer.GetCurrentAttributes()); + + Log::Comment(L"Restore flags"); + stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;E;0;2;@;BBBB\033\\"); + VERIFY_IS_TRUE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin)); + VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(2)); + VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(3)); + VERIFY_IS_FALSE(textBuffer.GetCursor().IsDelayedEOLWrap()); + stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;J;0;2;@;BBBB\033\\"); + VERIFY_IS_FALSE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin)); + VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(2)); + VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(3)); + VERIFY_IS_TRUE(textBuffer.GetCursor().IsDelayedEOLWrap()); + + Log::Comment(L"Restore charset configuration"); + stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;@;3;1;H;ABCF\033\\"); + VERIFY_ARE_EQUAL(3u, termOutput.GetLeftSetNumber()); + VERIFY_ARE_EQUAL(1u, termOutput.GetRightSetNumber()); + VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(0)); + VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(1)); + VERIFY_ARE_EQUAL(94u, termOutput.GetCharsetSize(2)); + VERIFY_ARE_EQUAL(96u, termOutput.GetCharsetSize(3)); + VERIFY_ARE_EQUAL(VTID("A"), termOutput.GetCharsetId(0)); + VERIFY_ARE_EQUAL(VTID("B"), termOutput.GetCharsetId(1)); + VERIFY_ARE_EQUAL(VTID("C"), termOutput.GetCharsetId(2)); + VERIFY_ARE_EQUAL(VTID("F"), termOutput.GetCharsetId(3)); + } + TEST_METHOD(CursorKeysModeTest) { Log::Comment(L"Starting test..."); @@ -1673,12 +2314,12 @@ public: // success cases // set numeric mode = true Log::Comment(L"Test 1: application mode = false"); - VERIFY_IS_TRUE(_pDispatch->SetCursorKeysMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::DECCKM_CursorKeysMode)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::CursorKey)); // set numeric mode = false Log::Comment(L"Test 2: application mode = true"); - VERIFY_IS_TRUE(_pDispatch->SetCursorKeysMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::DECCKM_CursorKeysMode)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::CursorKey)); } @@ -1726,13 +2367,13 @@ public: // set blinking mode = true Log::Comment(L"Test 1: enable blinking = true"); _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(false); - VERIFY_IS_TRUE(_pDispatch->EnableCursorBlinking(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::ATT610_StartCursorBlink)); VERIFY_IS_TRUE(_testGetSet->_textBuffer->GetCursor().IsBlinkingAllowed()); // set blinking mode = false Log::Comment(L"Test 2: enable blinking = false"); _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); - VERIFY_IS_TRUE(_pDispatch->EnableCursorBlinking(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::ATT610_StartCursorBlink)); VERIFY_IS_FALSE(_testGetSet->_textBuffer->GetCursor().IsBlinkingAllowed()); } @@ -1742,124 +2383,99 @@ public: til::inclusive_rect srTestMargins; _testGetSet->_textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, _testGetSet->_renderer); - _testGetSet->_viewport.Right = 8; - _testGetSet->_viewport.Bottom = 8; - auto sScreenHeight = _testGetSet->_viewport.Bottom - _testGetSet->_viewport.Top; + _testGetSet->_viewport.right = 8; + _testGetSet->_viewport.bottom = 8; + auto sScreenHeight = _testGetSet->_viewport.bottom - _testGetSet->_viewport.top; Log::Comment(L"Test 1: Verify having both values is valid."); - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); + VERIFY_ARE_EQUAL(2, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(6, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 2: Verify having only top is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 0); - _testGetSet->_expectedScrollRegion.Bottom = _testGetSet->_viewport.Bottom - 1; // We expect the bottom to be the bottom of the viewport, exclusive. - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(7, 0)); + VERIFY_ARE_EQUAL(7, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(sScreenHeight, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 3: Verify having only bottom is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 7); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, 7)); + VERIFY_ARE_EQUAL(1, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(7, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 4: Verify having no values is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 0); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, 0)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 5: Verify having both values, but bad bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 3); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(7, 3)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 6: Verify setting margins to (0, height) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, sScreenHeight); - _testGetSet->_expectedScrollRegion.Top = 0; - _testGetSet->_expectedScrollRegion.Bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, sScreenHeight)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 7: Verify setting margins to (1, height) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, sScreenHeight); - _testGetSet->_expectedScrollRegion.Top = 0; - _testGetSet->_expectedScrollRegion.Bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, sScreenHeight)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 8: Verify setting margins to (1, 0) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, 0); - _testGetSet->_expectedScrollRegion.Top = 0; - _testGetSet->_expectedScrollRegion.Bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, 0)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 9: Verify having top and bottom margin the same has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 4, 4); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(4, 4)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 10: Verify having top margin out of bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, sScreenHeight + 1, sScreenHeight + 10); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(sScreenHeight + 1, sScreenHeight + 10)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 11: Verify having bottom margin out of bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, sScreenHeight + 1); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, sScreenHeight + 1)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); } TEST_METHOD(LineFeedTest) { Log::Comment(L"Starting test..."); - // All test cases need the LineFeed call to succeed. - _testGetSet->_lineFeedResult = TRUE; + _testGetSet->PrepData(); + auto& cursor = _testGetSet->_textBuffer->GetCursor(); Log::Comment(L"Test 1: Line feed without carriage return."); - _testGetSet->_expectedLineFeedWithReturn = false; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::WithoutReturn)); + VERIFY_ARE_EQUAL(til::point(10, 1), cursor.GetPosition()); Log::Comment(L"Test 2: Line feed with carriage return."); - _testGetSet->_expectedLineFeedWithReturn = true; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::WithReturn)); + VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition()); Log::Comment(L"Test 3: Line feed depends on mode, and mode reset."); - _testGetSet->_getLineFeedModeResult = false; - _testGetSet->_expectedLineFeedWithReturn = false; + _testGetSet->_systemMode.reset(ITerminalApi::Mode::LineFeed); + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode)); + VERIFY_ARE_EQUAL(til::point(10, 1), cursor.GetPosition()); Log::Comment(L"Test 4: Line feed depends on mode, and mode set."); - _testGetSet->_getLineFeedModeResult = true; - _testGetSet->_expectedLineFeedWithReturn = true; + _testGetSet->_systemMode.set(ITerminalApi::Mode::LineFeed); + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode)); + VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition()); } TEST_METHOD(SetConsoleTitleTest) @@ -1885,44 +2501,44 @@ public: Log::Comment(L"Test 1: Test Default Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, false); - VERIFY_IS_TRUE(_pDispatch->EnableVT200MouseMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::VT200_MOUSE_MODE)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking)); - VERIFY_IS_TRUE(_pDispatch->EnableVT200MouseMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::VT200_MOUSE_MODE)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking)); Log::Comment(L"Test 2: Test UTF-8 Extended Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, false); - VERIFY_IS_TRUE(_pDispatch->EnableUTF8ExtendedMouseMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::UTF8_EXTENDED_MODE)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::Utf8MouseEncoding)); - VERIFY_IS_TRUE(_pDispatch->EnableUTF8ExtendedMouseMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::UTF8_EXTENDED_MODE)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::Utf8MouseEncoding)); Log::Comment(L"Test 3: Test SGR Extended Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, false); - VERIFY_IS_TRUE(_pDispatch->EnableSGRExtendedMouseMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::SGR_EXTENDED_MODE)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::SgrMouseEncoding)); - VERIFY_IS_TRUE(_pDispatch->EnableSGRExtendedMouseMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::SGR_EXTENDED_MODE)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::SgrMouseEncoding)); Log::Comment(L"Test 4: Test Button-Event Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, false); - VERIFY_IS_TRUE(_pDispatch->EnableButtonEventMouseMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::BUTTON_EVENT_MOUSE_MODE)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::ButtonEventMouseTracking)); - VERIFY_IS_TRUE(_pDispatch->EnableButtonEventMouseMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::BUTTON_EVENT_MOUSE_MODE)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::ButtonEventMouseTracking)); Log::Comment(L"Test 5: Test Any-Event Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, false); - VERIFY_IS_TRUE(_pDispatch->EnableAnyEventMouseMode(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::ANY_EVENT_MOUSE_MODE)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::AnyEventMouseTracking)); - VERIFY_IS_TRUE(_pDispatch->EnableAnyEventMouseMode(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::ANY_EVENT_MOUSE_MODE)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::AnyEventMouseTracking)); Log::Comment(L"Test 6: Test Alt Scroll Mouse Mode"); _terminalInput.SetInputMode(TerminalInput::Mode::AlternateScroll, false); - VERIFY_IS_TRUE(_pDispatch->EnableAlternateScroll(true)); + VERIFY_IS_TRUE(_pDispatch->SetMode(DispatchTypes::ALTERNATE_SCROLL)); VERIFY_IS_TRUE(_terminalInput.GetInputMode(TerminalInput::Mode::AlternateScroll)); - VERIFY_IS_TRUE(_pDispatch->EnableAlternateScroll(false)); + VERIFY_IS_TRUE(_pDispatch->ResetMode(DispatchTypes::ALTERNATE_SCROLL)); VERIFY_IS_FALSE(_terminalInput.GetInputMode(TerminalInput::Mode::AlternateScroll)); } @@ -2020,6 +2636,95 @@ public: rgOptions[4] = 123; _testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 123)); VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 5 })); + + Log::Comment(L"Test 6: 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::ForegroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::RGBColorOrFaint; + rgOptions[2] = 283; + rgOptions[3] = 182; + rgOptions[4] = 123; + // expect no change + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 5 })); + + Log::Comment(L"Test 7: 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; + rgOptions[1] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index; + rgOptions[2] = 283; + // expect no change + _testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 3 })); + } + + TEST_METHOD(XtermExtendedSubParameterColorTest) + { + Log::Comment(L"Starting test..."); + + _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + VTParameter rgOptions[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; + _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; + _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; + _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; + _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; + _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; + _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 })); + + 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; + // 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. + _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 })); + + 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; + _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 })); } TEST_METHOD(SetColorTableValue) @@ -2059,9 +2764,9 @@ public: RETURN_BOOL_IF_FALSE(fontBuffer.FinalizeSixelData()); const auto cellSize = fontBuffer.GetCellSize(); - Log::Comment(NoThrowString().Format(L"Cell size: %dx%d", cellSize.cx, cellSize.cy)); - VERIFY_ARE_EQUAL(expectedCellSize.cx, cellSize.cx); - VERIFY_ARE_EQUAL(expectedCellSize.cy, cellSize.cy); + Log::Comment(NoThrowString().Format(L"Cell size: %dx%d", cellSize.width, cellSize.height)); + VERIFY_ARE_EQUAL(expectedCellSize.width, cellSize.width); + VERIFY_ARE_EQUAL(expectedCellSize.height, cellSize.height); return true; }; @@ -2292,8 +2997,191 @@ public: VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1)); } + TEST_METHOD(MacroDefinitions) + { + const auto getMacroText = [&](const auto id) { + return _pDispatch->_macroBuffer->_macros.at(id); + }; + + Log::Comment(L"Text encoding"); + _stateMachine->ProcessString(L"\033P1;0;0!zText Encoding\033\\"); + VERIFY_ARE_EQUAL(L"Text Encoding", getMacroText(1)); + + Log::Comment(L"Hex encoding (uppercase)"); + _stateMachine->ProcessString(L"\033P2;0;1!z486578204A4B4C4D4E4F\033\\"); + VERIFY_ARE_EQUAL(L"Hex JKLMNO", getMacroText(2)); + + Log::Comment(L"Hex encoding (lowercase)"); + _stateMachine->ProcessString(L"\033P3;0;1!z486578206a6b6c6d6e6f\033\\"); + VERIFY_ARE_EQUAL(L"Hex jklmno", getMacroText(3)); + + Log::Comment(L"Default encoding is text"); + _stateMachine->ProcessString(L"\033P4;0;!zDefault Encoding\033\\"); + VERIFY_ARE_EQUAL(L"Default Encoding", getMacroText(4)); + + Log::Comment(L"Default ID is 0"); + _stateMachine->ProcessString(L"\033P;0;0!zDefault ID\033\\"); + VERIFY_ARE_EQUAL(L"Default ID", getMacroText(0)); + + Log::Comment(L"Replacing a single macro"); + _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"Retained", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Replacing all macros"); + _stateMachine->ProcessString(L"\033P1;0;0!zErased\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;1;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Default replacement is a single macro"); + _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\"); + _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P2;;0!zNew\033\\"); + VERIFY_ARE_EQUAL(L"Retained", getMacroText(1)); + VERIFY_ARE_EQUAL(L"New", getMacroText(2)); + + Log::Comment(L"Repeating three times"); + _stateMachine->ProcessString(L"\033P5;0;1!z526570656174!3;206563686F;207468726565\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo echo echo three", getMacroText(5)); + + Log::Comment(L"Zero repeats once"); + _stateMachine->ProcessString(L"\033P6;0;1!z526570656174!0;206563686F;207A65726F\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo zero", getMacroText(6)); + + Log::Comment(L"Default repeats once"); + _stateMachine->ProcessString(L"\033P7;0;1!z526570656174!;206563686F;2064656661756C74\033\\"); + VERIFY_ARE_EQUAL(L"Repeat echo default", getMacroText(7)); + + Log::Comment(L"Unterminated repeat sequence"); + _stateMachine->ProcessString(L"\033P8;0;1!z556E7465726D696E61746564!3;206563686F\033\\"); + VERIFY_ARE_EQUAL(L"Unterminated echo echo echo", getMacroText(8)); + + Log::Comment(L"Unexpected semicolon cancels definition"); + _stateMachine->ProcessString(L"\033P9;0;0!zReplaced\033\\"); + _stateMachine->ProcessString(L"\033P9;0;1!z526570656174!3;206563;686F;207468726565\033\\"); + VERIFY_ARE_EQUAL(L"", getMacroText(9)); + + Log::Comment(L"Control characters in a text encoding"); + _stateMachine->ProcessString(L"\033P10;0;0!zA\aB\bC\tD\nE\vF\fG\rH\033\\"); + VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(10)); + + Log::Comment(L"Control characters in a hex encoding"); + _stateMachine->ProcessString(L"\033P11;0;1!z41\a42\b43\t44\n45\v46\f47\r48\033\\"); + VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(11)); + + Log::Comment(L"Control characters in a repeat"); + _stateMachine->ProcessString(L"\033P12;0;1!z!\a3\b;\t4\n1\v4\f2\r4\a3\b;\033\\"); + VERIFY_ARE_EQUAL(L"ABCABCABC", getMacroText(12)); + + Log::Comment(L"Encoded control characters"); + _stateMachine->ProcessString(L"\033P13;0;1!z410742084309440A450B460C470D481B49\033\\"); + VERIFY_ARE_EQUAL(L"A\aB\bC\tD\nE\vF\fG\rH\033I", getMacroText(13)); + + _pDispatch->_macroBuffer = nullptr; + } + + TEST_METHOD(MacroInvokes) + { + _pDispatch->_macroBuffer = std::make_shared(); + + const auto setMacroText = [&](const auto id, const auto value) { + _pDispatch->_macroBuffer->_macros.at(id) = value; + }; + + setMacroText(0, L"Macro 0"); + setMacroText(1, L"Macro 1"); + setMacroText(2, L"Macro 2"); + setMacroText(63, L"Macro 63"); + + const auto getBufferOutput = [&]() { + const auto& textBuffer = _testGetSet->GetTextBuffer(); + const auto cursorPos = textBuffer.GetCursor().GetPosition(); + return textBuffer.GetRowByOffset(cursorPos.y).GetText().substr(0, cursorPos.x); + }; + + Log::Comment(L"Simple macro invoke"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[2*z"); + VERIFY_ARE_EQUAL(L"Macro 2", getBufferOutput()); + + Log::Comment(L"Default macro invoke"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[*z"); + VERIFY_ARE_EQUAL(L"Macro 0", getBufferOutput()); + + Log::Comment(L"Maximum ID number"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[63*z"); + VERIFY_ARE_EQUAL(L"Macro 63", getBufferOutput()); + + Log::Comment(L"Out of range ID number"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[64*z"); + VERIFY_ARE_EQUAL(L"", getBufferOutput()); + + Log::Comment(L"Only one ID parameter allowed"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[2;0;1*z"); + VERIFY_ARE_EQUAL(L"Macro 2", getBufferOutput()); + + Log::Comment(L"DECDMAC ignored when inside a macro"); + setMacroText(10, L"[\033P1;0;0!zReplace Macro 1\033\\]"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[10*z"); + _stateMachine->ProcessString(L"\033[1*z"); + VERIFY_ARE_EQUAL(L"[]Macro 1", getBufferOutput()); + + Log::Comment(L"Maximum recursive depth is 16"); + setMacroText(0, L"<\033[1*z>"); + setMacroText(1, L"[\033[0*z]"); + _testGetSet->PrepData(); + _stateMachine->ProcessString(L"\033[0*z"); + VERIFY_ARE_EQUAL(L"<[<[<[<[<[<[<[<[]>]>]>]>]>]>]>]>", getBufferOutput()); + + _pDispatch->_macroBuffer = nullptr; + } + + TEST_METHOD(WindowManipulationTypeTests) + { + _testGetSet->PrepData(); + _pDispatch->WindowManipulation(DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters, NULL, NULL); + const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", _testGetSet->GetViewport().height(), _testGetSet->GetTextBuffer().GetSize().Width()); + _testGetSet->ValidateInputEvent(expectedResponse.c_str()); + } + + TEST_METHOD(MenuCompletionsTests) + { + _testGetSet->PrepData(); + + Log::Comment(L"Not enough parameters"); + VERIFY_IS_FALSE(_pDispatch->DoVsCodeAction(LR"(garbage)")); + + Log::Comment(L"Not enough parameters"); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions)")); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;)")); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;)")); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20)")); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;)")); + Log::Comment(L"No trailing semicolon"); + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;3)")); + + Log::Comment(L"Normal, good case"); + _testGetSet->_expectedMenuJson = LR"({ "foo": 1, "bar": 2 })"; + _testGetSet->_expectedReplaceLength = 2; + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;1;2;3;{ "foo": 1, "bar": 2 })")); + + Log::Comment(L"JSON has a semicolon in it"); + _testGetSet->_expectedMenuJson = LR"({ "foo": "what;ever", "bar": 2 })"; + _testGetSet->_expectedReplaceLength = 20; + VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;30;{ "foo": "what;ever", "bar": 2 })")); + } + private: - TerminalInput _terminalInput{ nullptr }; + TerminalInput _terminalInput; std::unique_ptr _testGetSet; AdaptDispatch* _pDispatch; // non-ownership pointer std::unique_ptr _stateMachine; diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp index b86d5d95a4..581f03ed92 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; @@ -27,18 +23,11 @@ namespace Microsoft }; using namespace Microsoft::Console::VirtualTerminal; -// For magic reasons, this has to live outside the class. Something wonderful about TAEF macros makes it -// invisible to the linker when inside the class. -static std::wstring s_expectedInput; - class Microsoft::Console::VirtualTerminal::InputTest { public: TEST_CLASS(InputTest); - static void s_TerminalInputTestCallback(_In_ std::deque>& inEvents); - static void s_TerminalInputTestNullCallback(_In_ std::deque>& inEvents); - TEST_METHOD(TerminalInputTests); TEST_METHOD(TestFocusEvents); TEST_METHOD(TerminalInputModifierKeyTests); @@ -79,91 +68,17 @@ public: } }; -void InputTest::s_TerminalInputTestCallback(_In_ std::deque>& inEvents) -{ - auto records = IInputEvent::ToInputRecords(inEvents); - - if (VERIFY_ARE_EQUAL(s_expectedInput.size(), records.size(), L"Verify expected and actual input array lengths matched.")) - { - Log::Comment(L"We are expecting always key events and always key down. All other properties should not be written by simulated keys."); - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - - Log::Comment(L"Verifying individual array members..."); - for (size_t i = 0; i < records.size(); i++) - { - irExpected.Event.KeyEvent.uChar.UnicodeChar = s_expectedInput[i]; - VERIFY_ARE_EQUAL(irExpected, records[i], NoThrowString().Format(L"%c, %c", s_expectedInput[i], records[i].Event.KeyEvent.uChar.UnicodeChar)); - } - } -} - -void InputTest::s_TerminalInputTestNullCallback(_In_ std::deque>& inEvents) -{ - auto records = IInputEvent::ToInputRecords(inEvents); - - if (records.size() == 1) - { - Log::Comment(L"We are expecting a null input event."); - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - irExpected.Event.KeyEvent.wVirtualKeyCode = LOBYTE(OneCoreSafeVkKeyScanW(0)); - irExpected.Event.KeyEvent.dwControlKeyState = LEFT_CTRL_PRESSED; - irExpected.Event.KeyEvent.wVirtualScanCode = 0; - irExpected.Event.KeyEvent.uChar.UnicodeChar = L'\x0'; - - VERIFY_ARE_EQUAL(irExpected, records[0]); - } - else if (records.size() == 2) - { - Log::Comment(L"We are expecting a null input event, preceded by an escape"); - - INPUT_RECORD irExpectedEscape = { 0 }; - irExpectedEscape.EventType = KEY_EVENT; - irExpectedEscape.Event.KeyEvent.bKeyDown = TRUE; - irExpectedEscape.Event.KeyEvent.wRepeatCount = 1; - irExpectedEscape.Event.KeyEvent.wVirtualKeyCode = 0; - irExpectedEscape.Event.KeyEvent.dwControlKeyState = 0; - irExpectedEscape.Event.KeyEvent.wVirtualScanCode = 0; - irExpectedEscape.Event.KeyEvent.uChar.UnicodeChar = L'\x1b'; - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - irExpected.Event.KeyEvent.wVirtualKeyCode = 0; - irExpected.Event.KeyEvent.dwControlKeyState = 0; - irExpected.Event.KeyEvent.wVirtualScanCode = 0; - irExpected.Event.KeyEvent.uChar.UnicodeChar = L'\x0'; - - VERIFY_ARE_EQUAL(irExpectedEscape, records[0]); - VERIFY_ARE_EQUAL(irExpected, records[1]); - } - else - { - VERIFY_FAIL(NoThrowString().Format(L"Expected either one or two inputs, got %zu", records.size())); - } -} - void InputTest::TerminalInputTests() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) { Log::Comment(NoThrowString().Format(L"Testing Key 0x%x", vkey)); - auto fExpectedKeyHandled = true; - INPUT_RECORD irTest = { 0 }; irTest.EventType = KEY_EVENT; irTest.Event.KeyEvent.wRepeatCount = 1; @@ -171,103 +86,100 @@ void InputTest::TerminalInputTests() irTest.Event.KeyEvent.bKeyDown = TRUE; irTest.Event.KeyEvent.uChar.UnicodeChar = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR)); - // Set up expected result + TerminalInput::OutputType expected; switch (vkey) { case VK_TAB: - s_expectedInput = L"\x09"; + expected = TerminalInput::MakeOutput(L"\x09"); break; case VK_BACK: - s_expectedInput = L"\x7f"; + expected = TerminalInput::MakeOutput(L"\x7f"); break; case VK_ESCAPE: - s_expectedInput = L"\x1b"; + expected = TerminalInput::MakeOutput(L"\x1b"); break; case VK_PAUSE: - s_expectedInput = L"\x1a"; + expected = TerminalInput::MakeOutput(L"\x1a"); break; case VK_UP: - s_expectedInput = L"\x1b[A"; + expected = TerminalInput::MakeOutput(L"\x1b[A"); break; case VK_DOWN: - s_expectedInput = L"\x1b[B"; + expected = TerminalInput::MakeOutput(L"\x1b[B"); break; case VK_RIGHT: - s_expectedInput = L"\x1b[C"; + expected = TerminalInput::MakeOutput(L"\x1b[C"); break; case VK_LEFT: - s_expectedInput = L"\x1b[D"; + expected = TerminalInput::MakeOutput(L"\x1b[D"); break; case VK_HOME: - s_expectedInput = L"\x1b[H"; + expected = TerminalInput::MakeOutput(L"\x1b[H"); break; case VK_INSERT: - s_expectedInput = L"\x1b[2~"; + expected = TerminalInput::MakeOutput(L"\x1b[2~"); break; case VK_DELETE: - s_expectedInput = L"\x1b[3~"; + expected = TerminalInput::MakeOutput(L"\x1b[3~"); break; case VK_END: - s_expectedInput = L"\x1b[F"; + expected = TerminalInput::MakeOutput(L"\x1b[F"); break; case VK_PRIOR: - s_expectedInput = L"\x1b[5~"; + expected = TerminalInput::MakeOutput(L"\x1b[5~"); break; case VK_NEXT: - s_expectedInput = L"\x1b[6~"; + expected = TerminalInput::MakeOutput(L"\x1b[6~"); break; case VK_F1: - s_expectedInput = L"\x1bOP"; + expected = TerminalInput::MakeOutput(L"\x1bOP"); break; case VK_F2: - s_expectedInput = L"\x1bOQ"; + expected = TerminalInput::MakeOutput(L"\x1bOQ"); break; case VK_F3: - s_expectedInput = L"\x1bOR"; + expected = TerminalInput::MakeOutput(L"\x1bOR"); break; case VK_F4: - s_expectedInput = L"\x1bOS"; + expected = TerminalInput::MakeOutput(L"\x1bOS"); break; case VK_F5: - s_expectedInput = L"\x1b[15~"; + expected = TerminalInput::MakeOutput(L"\x1b[15~"); break; case VK_F6: - s_expectedInput = L"\x1b[17~"; + expected = TerminalInput::MakeOutput(L"\x1b[17~"); break; case VK_F7: - s_expectedInput = L"\x1b[18~"; + expected = TerminalInput::MakeOutput(L"\x1b[18~"); break; case VK_F8: - s_expectedInput = L"\x1b[19~"; + expected = TerminalInput::MakeOutput(L"\x1b[19~"); break; case VK_F9: - s_expectedInput = L"\x1b[20~"; + expected = TerminalInput::MakeOutput(L"\x1b[20~"); break; case VK_F10: - s_expectedInput = L"\x1b[21~"; + expected = TerminalInput::MakeOutput(L"\x1b[21~"); break; case VK_F11: - s_expectedInput = L"\x1b[23~"; + expected = TerminalInput::MakeOutput(L"\x1b[23~"); break; case VK_F12: - s_expectedInput = L"\x1b[24~"; + expected = TerminalInput::MakeOutput(L"\x1b[24~"); break; case VK_CANCEL: - s_expectedInput = L"\x3"; + expected = TerminalInput::MakeOutput(L"\x3"); break; default: - fExpectedKeyHandled = false; + if (irTest.Event.KeyEvent.uChar.UnicodeChar != 0) + { + expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 }); + } break; } - if (!fExpectedKeyHandled && irTest.Event.KeyEvent.uChar.UnicodeChar != 0) - { - s_expectedInput.clear(); - s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); - fExpectedKeyHandled = true; - } - auto inputEvent = IInputEvent::Create(irTest); + // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(fExpectedKeyHandled, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(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."); @@ -281,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(false, pInput->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."); @@ -292,18 +203,15 @@ void InputTest::TerminalInputTests() Log::Comment(L"Testing MOUSE_EVENT"); irUnhandled.EventType = MOUSE_EVENT; - auto inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify MOUSE_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(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(false, pInput->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(false, pInput->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 } @@ -313,54 +221,15 @@ void InputTest::TestFocusEvents() // GH#12900, #13238 // Focus events that come in from the API should never be translated to VT sequences. // We're relying on the fact that the INPUT_RECORD version of the ctor is only called by the API - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; - INPUT_RECORD irTest = { 0 }; - irTest.EventType = FOCUS_EVENT; + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(false)); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(true)); - { - irTest.Event.FocusEvent.bSetFocus = false; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - irTest.Event.FocusEvent.bSetFocus = true; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - auto inputEvent = std::make_unique(false); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was NOT handled."); - } - { - auto inputEvent = std::make_unique(true); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was NOT handled."); - } + input.SetInputMode(TerminalInput::Mode::FocusEvent, true); - Log::Comment(L"Enable focus event handling"); - - pInput->SetInputMode(TerminalInput::Mode::FocusEvent, true); - - { - irTest.Event.FocusEvent.bSetFocus = false; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - irTest.Event.FocusEvent.bSetFocus = true; - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - s_expectedInput = L"\x1b[O"; - auto inputEvent = std::make_unique(false); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was handled."); - } - { - s_expectedInput = L"\x1b[I"; - auto inputEvent = std::make_unique(true); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was handled."); - } + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[O"), input.HandleFocus(false)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[I"), input.HandleFocus(true)); } void InputTest::TerminalInputModifierKeyTests() @@ -379,7 +248,7 @@ void InputTest::TerminalInputModifierKeyTests() unsigned int uiKeystate; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiKeystate)); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; const auto slashVkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); const auto nullVkey = LOBYTE(OneCoreSafeVkKeyScanW(0)); @@ -413,7 +282,7 @@ void InputTest::TerminalInputModifierKeyTests() } } - // Set up expected result + TerminalInput::OutputType expected; switch (vkey) { case VK_BACK: @@ -421,98 +290,98 @@ void InputTest::TerminalInputModifierKeyTests() case VK_OEM_2: // VK_OEM_2 is typically the '/?' key continue; - // s_expectedInput = L"\x7f"; + // expected = TerminalInput::MakeOutput(L"\x7f"); break; case VK_PAUSE: - s_expectedInput = L"\x1a"; + expected = TerminalInput::MakeOutput(L"\x1a"); break; case VK_UP: fModifySequence = true; - s_expectedInput = L"\x1b[1;mA"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mA"); break; case VK_DOWN: fModifySequence = true; - s_expectedInput = L"\x1b[1;mB"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mB"); break; case VK_RIGHT: fModifySequence = true; - s_expectedInput = L"\x1b[1;mC"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mC"); break; case VK_LEFT: fModifySequence = true; - s_expectedInput = L"\x1b[1;mD"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mD"); break; case VK_HOME: fModifySequence = true; - s_expectedInput = L"\x1b[1;mH"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mH"); break; case VK_INSERT: fModifySequence = true; - s_expectedInput = L"\x1b[2;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[2;m~"); break; case VK_DELETE: fModifySequence = true; - s_expectedInput = L"\x1b[3;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[3;m~"); break; case VK_END: fModifySequence = true; - s_expectedInput = L"\x1b[1;mF"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mF"); break; case VK_PRIOR: fModifySequence = true; - s_expectedInput = L"\x1b[5;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[5;m~"); break; case VK_NEXT: fModifySequence = true; - s_expectedInput = L"\x1b[6;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[6;m~"); break; case VK_F1: fModifySequence = true; - s_expectedInput = L"\x1b[1;mP"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mP"); break; case VK_F2: fModifySequence = true; - s_expectedInput = L"\x1b[1;mQ"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mQ"); break; case VK_F3: fModifySequence = true; - s_expectedInput = L"\x1b[1;mR"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mR"); break; case VK_F4: fModifySequence = true; - s_expectedInput = L"\x1b[1;mS"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mS"); break; case VK_F5: fModifySequence = true; - s_expectedInput = L"\x1b[15;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[15;m~"); break; case VK_F6: fModifySequence = true; - s_expectedInput = L"\x1b[17;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[17;m~"); break; case VK_F7: fModifySequence = true; - s_expectedInput = L"\x1b[18;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[18;m~"); break; case VK_F8: fModifySequence = true; - s_expectedInput = L"\x1b[19;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[19;m~"); break; case VK_F9: fModifySequence = true; - s_expectedInput = L"\x1b[20;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[20;m~"); break; case VK_F10: fModifySequence = true; - s_expectedInput = L"\x1b[21;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[21;m~"); break; case VK_F11: fModifySequence = true; - s_expectedInput = L"\x1b[23;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[23;m~"); break; case VK_F12: fModifySequence = true; - s_expectedInput = L"\x1b[24;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[24;m~"); break; case VK_TAB: if (AltPressed(uiKeystate)) @@ -522,11 +391,11 @@ void InputTest::TerminalInputModifierKeyTests() } else if (ShiftPressed(uiKeystate)) { - s_expectedInput = L"\x1b[Z"; + expected = TerminalInput::MakeOutput(L"\x1b[Z"); } else if (ControlPressed(uiKeystate)) { - s_expectedInput = L"\t"; + expected = TerminalInput::MakeOutput(L"\t"); } break; default: @@ -537,18 +406,16 @@ void InputTest::TerminalInputModifierKeyTests() // significant ones to be zeroed out (when using ASCII). if (AltPressed(uiKeystate) && ControlPressed(uiKeystate) && ch > 0x40 && ch <= 0x5A) { - s_expectedInput.clear(); - s_expectedInput.push_back(L'\x1b'); - s_expectedInput.push_back(ch & 0b11111); + const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast(ch & 0b11111) }; + expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); break; } // Alt+Key generates [0x1b, key] into the stream if (AltPressed(uiKeystate) && !ControlPressed(uiKeystate) && ch != 0) { - s_expectedInput.clear(); - s_expectedInput.push_back(L'\x1b'); - s_expectedInput.push_back(ch); + const wchar_t buffer[2]{ L'\x1b', ch }; + expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); break; } @@ -562,8 +429,7 @@ void InputTest::TerminalInputModifierKeyTests() if (ch != 0) { - s_expectedInput.clear(); - s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); + expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 }); break; } @@ -571,29 +437,27 @@ void InputTest::TerminalInputModifierKeyTests() break; } - if (fModifySequence && s_expectedInput.size() > 1) + if (fModifySequence) { auto fShift = !!(uiKeystate & SHIFT_PRESSED); auto fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); auto fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); - s_expectedInput[s_expectedInput.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); + auto& str = expected.value(); + str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); } - Log::Comment(NoThrowString().Format(L"Expected = \"%s\"", s_expectedInput.c_str())); - - auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(fExpectedKeyHandled, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been."); } } void InputTest::TerminalInputNullKeyTests() { - Log::Comment(L"Starting test..."); + using namespace std::string_view_literals; unsigned int uiKeystate = LEFT_CTRL_PRESSED; - const auto pInput = new TerminalInput(s_TerminalInputTestNullCallback); + TerminalInput input; Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); @@ -608,30 +472,26 @@ 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(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(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(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(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(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(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(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); } -void TestKey(TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0) +static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& input, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0) { Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); @@ -644,266 +504,183 @@ void TestKey(TerminalInput* const pInput, const unsigned int uiKeystate, const B irTest.Event.KeyEvent.uChar.UnicodeChar = wch; // Send key into object (will trigger callback and verification) - auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been."); } void InputTest::DifferentModifiersTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending a bunch of keystrokes that are a little weird."); unsigned int uiKeystate = 0; BYTE vkey = VK_BACK; - s_expectedInput = L"\x7f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_BACK; - s_expectedInput = L"\x8"; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_BACK; - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_DELETE; - s_expectedInput = L"\x1b[3;5~"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;5~"), input, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;5~"), input, uiKeystate, vkey); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_DELETE; - s_expectedInput = L"\x1b[3;3~"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;3~"), input, uiKeystate, vkey); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;3~"), input, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_TAB; - s_expectedInput = L"\t"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\t"), input, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\t"), input, uiKeystate, vkey); uiKeystate = SHIFT_PRESSED; vkey = VK_TAB; - s_expectedInput = L"\x1b[Z"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[Z"), input, uiKeystate, vkey); // C-/ -> C-_ -> 0x1f uiKeystate = LEFT_CTRL_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1f"; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/'); // M-/ -> ESC / uiKeystate = LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1b/"; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b/"), input, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b/"), input, uiKeystate, vkey, L'/'); // See https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 // C-? -> DEL -> 0x7f Log::Comment(NoThrowString().Format(L"Checking C-?")); // Use SHIFT_PRESSED to force us into differentiating between '/' and '?' vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?')); - s_expectedInput = L"\x7f"; - TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?'); - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?'); // C-M-/ -> 0x1b0x1f Log::Comment(NoThrowString().Format(L"Checking C-M-/")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1b\x1f"; - TestKey(pInput, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); - TestKey(pInput, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr - TestKey(pInput, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'/'); // C-M-? -> 0x1b0x7f Log::Comment(NoThrowString().Format(L"Checking C-M-?")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?')); - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'?'); } void InputTest::CtrlNumTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending the various Ctrl+Num keys."); unsigned int uiKeystate = LEFT_CTRL_PRESSED; BYTE vkey = static_cast('1'); - s_expectedInput = L"1"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"1"), input, uiKeystate, vkey); Log::Comment(NoThrowString().Format( L"Skipping Ctrl+2, since that's supposed to send NUL, and doesn't play " L"nicely with this test. Ctrl+2 is covered by other tests in this class.")); vkey = static_cast('3'); - s_expectedInput = L"\x1b"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b"), input, uiKeystate, vkey); vkey = static_cast('4'); - s_expectedInput = L"\x1c"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1c"), input, uiKeystate, vkey); vkey = static_cast('5'); - s_expectedInput = L"\x1d"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1d"), input, uiKeystate, vkey); vkey = static_cast('6'); - s_expectedInput = L"\x1e"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1e"), input, uiKeystate, vkey); vkey = static_cast('7'); - s_expectedInput = L"\x1f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey); vkey = static_cast('8'); - s_expectedInput = L"\x7f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, uiKeystate, vkey); vkey = static_cast('9'); - s_expectedInput = L"9"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"9"), input, uiKeystate, vkey); } void InputTest::BackarrowKeyModeTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; const BYTE vkey = VK_BACK; Log::Comment(L"Sending backspace key combos with DECBKM enabled."); - pInput->SetInputMode(TerminalInput::Mode::BackarrowKey, true); - - s_expectedInput = L"\x8"; - TestKey(pInput, 0, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + input.SetInputMode(TerminalInput::Mode::BackarrowKey, true); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, 0, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); Log::Comment(L"Sending backspace key combos with DECBKM disabled."); - pInput->SetInputMode(TerminalInput::Mode::BackarrowKey, false); - - s_expectedInput = L"\x7f"; - TestKey(pInput, 0, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + input.SetInputMode(TerminalInput::Mode::BackarrowKey, false); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, 0, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); } void InputTest::AutoRepeatModeTest() { - Log::Comment(L"Starting test..."); - - auto receivedChars = std::wstring{}; - const auto pInput = new TerminalInput([&](auto& inEvents) { - for (auto& record : IInputEvent::ToInputRecords(inEvents)) - { - receivedChars.push_back(record.Event.KeyEvent.uChar.UnicodeChar); - } - }); - - const auto repeatKey = [&](auto vKey, auto unicodeChar, int repeatCount) { - auto irTest = INPUT_RECORD{ 0 }; - irTest.EventType = KEY_EVENT; - irTest.Event.KeyEvent.wRepeatCount = 1; - irTest.Event.KeyEvent.wVirtualKeyCode = vKey; - irTest.Event.KeyEvent.uChar.UnicodeChar = unicodeChar; - irTest.Event.KeyEvent.bKeyDown = TRUE; - - // We're simulating a key being held down so that it repeats, by sending - // multiple KeyDown events followed by a single KeyUp event. - - for (auto i = 0; i < repeatCount; i++) - { - auto keyDownEvent = IInputEvent::Create(irTest); - VERIFY_IS_TRUE(pInput->HandleKey(keyDownEvent.get())); - } - - irTest.Event.KeyEvent.bKeyDown = FALSE; - auto keyUpEvent = IInputEvent::Create(irTest); - VERIFY_IS_FALSE(pInput->HandleKey(keyUpEvent.get())); - }; + 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."); - pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, false); - receivedChars.clear(); - repeatKey('A', L'a', 5); - repeatKey('B', L'b', 5); - repeatKey('C', L'c', 5); - VERIFY_ARE_EQUAL(L"abc", receivedChars); + input.SetInputMode(TerminalInput::Mode::AutoRepeat, false); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + 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."); - pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, true); - receivedChars.clear(); - repeatKey('A', L'a', 5); - repeatKey('B', L'b', 5); - repeatKey('C', L'c', 5); - VERIFY_ARE_EQUAL(L"aaaaabbbbbccccc", receivedChars); + input.SetInputMode(TerminalInput::Mode::AutoRepeat, true); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down)); + 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 b72a0d9714..ddcb81b7e9 100644 --- a/src/terminal/input/mouseInput.cpp +++ b/src/terminal/input/mouseInput.cpp @@ -2,10 +2,10 @@ // Licensed under the MIT license. #include "precomp.h" -#include #include "terminalInput.hpp" + +#include "../types/inc/IInputEvent.hpp" #include "../types/inc/utils.hpp" -#include "../../interactivity/inc/VtApiRedirection.hpp" using namespace Microsoft::Console::VirtualTerminal; @@ -260,7 +260,7 @@ static constexpr int _windowsButtonToSGREncoding(const unsigned int button, // - the translated coordinate. static constexpr til::point _winToVTCoord(const til::point coordWinCoordinate) noexcept { - return { coordWinCoordinate.X + 1, coordWinCoordinate.Y + 1 }; + return { coordWinCoordinate.x + 1, coordWinCoordinate.y + 1 }; } // Routine Description: @@ -297,12 +297,9 @@ bool TerminalInput::IsTrackingMouseInput() const noexcept // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // - state - the state of the mouse buttons at this moment // Return value: -// - true if the event was handled and we should stop event propagation to the default window handler. -bool TerminalInput::HandleMouse(const til::point position, - const unsigned int button, - const short modifierKeyState, - const short delta, - const MouseButtonState state) +// - Returns an empty optional if we didn't handle the mouse event and the caller can opt to handle it in some other way. +// - Returns a string if we successfully translated it into a VT input sequence. +TerminalInput::OutputType TerminalInput::HandleMouse(const til::point position, const unsigned int button, const short modifierKeyState, const short delta, const MouseButtonState state) { if (Utils::Sign(delta) != Utils::Sign(_mouseInputState.accumulatedDelta)) { @@ -322,7 +319,14 @@ bool TerminalInput::HandleMouse(const til::point position, // on the wheel, accumulate delta until we hit the amount required to dispatch one // "line" worth of scroll. // Mark the event as "handled" if we would have otherwise emitted a scroll event. - return IsTrackingMouseInput() || ShouldSendAlternateScroll(button, delta); + OutputType out; + if (IsTrackingMouseInput() || ShouldSendAlternateScroll(button, delta)) + { + // Emplacing an empty string marks the return value as "we handled it" + // but since it's empty it will not produce any actual output. + out.emplace(); + } + return out; } // We're ready to send this event through, but first we need to clear the accumulated; @@ -330,90 +334,65 @@ bool TerminalInput::HandleMouse(const til::point position, _mouseInputState.accumulatedDelta = 0; } - auto success = false; if (ShouldSendAlternateScroll(button, delta)) { - success = _SendAlternateScroll(delta); + return _makeAlternateScrollOutput(delta); } - else + + if (IsTrackingMouseInput()) { - success = IsTrackingMouseInput(); - if (success) + // isHover is only true for WM_MOUSEMOVE events + const auto isHover = _isHoverMsg(button); + const auto isButton = _isButtonMsg(button); + + const auto sameCoord = (position.x == _mouseInputState.lastPos.x) && + (position.y == _mouseInputState.lastPos.y) && + (_mouseInputState.lastButton == button); + + // If we have a WM_MOUSEMOVE, we need to know if any of the mouse + // buttons are actually pressed. If they are, + // _GetPressedButton will return the first pressed mouse button. + // If it returns WM_LBUTTONUP, then we can assume that the mouse + // moved without a button being pressed. + const auto realButton = isHover ? s_GetPressedButton(state) : button; + + // In default mode, only button presses/releases are sent + // In ButtonEvent mode, changing coord hovers WITH A BUTTON PRESSED + // (WM_LBUTTONUP is our sentinel that no button was pressed) are also sent. + // In AnyEvent, all coord change hovers are sent + const auto physicalButtonPressed = realButton != WM_LBUTTONUP; + + if (isButton || + (isHover && _inputMode.test(Mode::ButtonEventMouseTracking) && ((!sameCoord) && (physicalButtonPressed))) || + (isHover && _inputMode.test(Mode::AnyEventMouseTracking) && !sameCoord)) { - // isHover is only true for WM_MOUSEMOVE events - const auto isHover = _isHoverMsg(button); - const auto isButton = _isButtonMsg(button); - - const auto sameCoord = (position.X == _mouseInputState.lastPos.X) && - (position.Y == _mouseInputState.lastPos.Y) && - (_mouseInputState.lastButton == button); - - // If we have a WM_MOUSEMOVE, we need to know if any of the mouse - // buttons are actually pressed. If they are, - // _GetPressedButton will return the first pressed mouse button. - // If it returns WM_LBUTTONUP, then we can assume that the mouse - // moved without a button being pressed. - const auto realButton = isHover ? s_GetPressedButton(state) : button; - - // In default mode, only button presses/releases are sent - // In ButtonEvent mode, changing coord hovers WITH A BUTTON PRESSED - // (WM_LBUTTONUP is our sentinel that no button was pressed) are also sent. - // In AnyEvent, all coord change hovers are sent - const auto physicalButtonPressed = realButton != WM_LBUTTONUP; - - success = (isButton && IsTrackingMouseInput()) || - (isHover && _inputMode.test(Mode::ButtonEventMouseTracking) && ((!sameCoord) && (physicalButtonPressed))) || - (isHover && _inputMode.test(Mode::AnyEventMouseTracking) && !sameCoord); - - if (success) + if (_inputMode.any(Mode::ButtonEventMouseTracking, Mode::AnyEventMouseTracking)) { - std::wstring sequence; - if (_inputMode.test(Mode::Utf8MouseEncoding)) - { - sequence = _GenerateUtf8Sequence(position, - realButton, - isHover, - modifierKeyState, - delta); - } - else if (_inputMode.test(Mode::SgrMouseEncoding)) - { - // For SGR encoding, if no physical buttons were pressed, - // then we want to handle hovers with WM_MOUSEMOVE. - // However, if we're dragging (WM_MOUSEMOVE with a button pressed), - // then use that pressed button instead. - sequence = _GenerateSGRSequence(position, - physicalButtonPressed ? realButton : button, - _isButtonDown(realButton), // Use realButton here, to properly get the up/down state - isHover, - modifierKeyState, - delta); - } - else - { - sequence = _GenerateDefaultSequence(position, - realButton, - isHover, - modifierKeyState, - delta); - } - success = !sequence.empty(); + _mouseInputState.lastPos.x = position.x; + _mouseInputState.lastPos.y = position.y; + _mouseInputState.lastButton = button; + } - if (success) - { - _SendInputSequence(sequence); - success = true; - } - if (_inputMode.any(Mode::ButtonEventMouseTracking, Mode::AnyEventMouseTracking)) - { - _mouseInputState.lastPos.X = position.X; - _mouseInputState.lastPos.Y = position.Y; - _mouseInputState.lastButton = button; - } + if (_inputMode.test(Mode::Utf8MouseEncoding)) + { + return _GenerateUtf8Sequence(position, realButton, isHover, modifierKeyState, delta); + } + else if (_inputMode.test(Mode::SgrMouseEncoding)) + { + // For SGR encoding, if no physical buttons were pressed, + // then we want to handle hovers with WM_MOUSEMOVE. + // However, if we're dragging (WM_MOUSEMOVE with a button pressed), + // then use that pressed button instead. + return _GenerateSGRSequence(position, physicalButtonPressed ? realButton : button, _isButtonDown(realButton), isHover, modifierKeyState, delta); + } + else + { + return _GenerateDefaultSequence(position, realButton, isHover, modifierKeyState, delta); } } } - return success; + + return {}; } // Routine Description: @@ -425,25 +404,19 @@ bool TerminalInput::HandleMouse(const til::point position, // - isHover - true if the sequence is generated in response to a mouse hover // - modifierKeyState - the modifier keys pressed with this button // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) -// Return value: -// - The generated sequence. Will be empty if we couldn't generate. -std::wstring TerminalInput::_GenerateDefaultSequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateDefaultSequence(const til::point position, const unsigned int button, const bool isHover, const short modifierKeyState, const short delta) { // In the default, non-extended encoding scheme, coordinates above 94 shouldn't be supported, // because (95+32+1)=128, which is not an ASCII character. // There are more details in _GenerateUtf8Sequence, but basically, we can't put anything above x80 into the input // stream without bash.exe trying to convert it into utf8, and generating extra bytes in the process. - if (position.X <= s_MaxDefaultCoordinate && position.Y <= s_MaxDefaultCoordinate) + if (position.x <= s_MaxDefaultCoordinate && position.y <= s_MaxDefaultCoordinate) { const auto vtCoords = _winToVTCoord(position); - const auto encodedX = _encodeDefaultCoordinate(vtCoords.X); - const auto encodedY = _encodeDefaultCoordinate(vtCoords.Y); + const auto encodedX = _encodeDefaultCoordinate(vtCoords.x); + const auto encodedY = _encodeDefaultCoordinate(vtCoords.y); - std::wstring format{ L"\x1b[Mbxy" }; + StringType format{ L"\x1b[Mbxy" }; til::at(format, 3) = gsl::narrow_cast(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); til::at(format, 4) = gsl::narrow_cast(encodedX); til::at(format, 5) = gsl::narrow_cast(encodedY); @@ -464,11 +437,7 @@ std::wstring TerminalInput::_GenerateDefaultSequence(const til::point position, // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // Return value: // - The generated sequence. Will be empty if we couldn't generate. -std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateUtf8Sequence(const til::point position, const unsigned int button, const bool isHover, const short modifierKeyState, const short delta) { // So we have some complications here. // The windows input stream is typically encoded as UTF16. @@ -484,12 +453,13 @@ std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, // So bash would also need to change, but how could it tell the difference between them? no real good way. // I'm going to emit a utf16 encoded value for now. Besides, if a windows program really wants it, just use the SGR mode, which is unambiguous. // TODO: Followup once the UTF-8 input stack is ready, MSFT:8509613 - if (position.X <= (SHORT_MAX - 33) && position.Y <= (SHORT_MAX - 33)) + if (position.x <= (SHORT_MAX - 33) && position.y <= (SHORT_MAX - 33)) { const auto vtCoords = _winToVTCoord(position); - const auto encodedX = _encodeDefaultCoordinate(vtCoords.X); - const auto encodedY = _encodeDefaultCoordinate(vtCoords.Y); - std::wstring format{ L"\x1b[Mbxy" }; + const auto encodedX = _encodeDefaultCoordinate(vtCoords.x); + const auto encodedY = _encodeDefaultCoordinate(vtCoords.y); + + StringType format{ L"\x1b[Mbxy" }; // The short cast is safe because we know s_WindowsButtonToXEncoding never returns more than xff til::at(format, 3) = gsl::narrow_cast(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); til::at(format, 4) = gsl::narrow_cast(encodedX); @@ -512,23 +482,12 @@ std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // - ppwchSequence - On success, where to put the pointer to the generated sequence // - pcchLength - On success, where to put the length of the generated sequence -// Return value: -// - true if we were able to successfully generate a sequence. -// On success, caller is responsible for delete[]ing *ppwchSequence. -std::wstring TerminalInput::_GenerateSGRSequence(const til::point position, - const unsigned int button, - const bool isDown, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateSGRSequence(const til::point position, const unsigned int button, const bool isDown, const bool isHover, const short modifierKeyState, const short delta) { // Format for SGR events is: // "\x1b[<%d;%d;%d;%c", xButton, x+1, y+1, fButtonDown? 'M' : 'm' const auto xbutton = _windowsButtonToSGREncoding(button, isHover, modifierKeyState, delta); - - auto format = wil::str_printf(L"\x1b[<%d;%d;%d%c", xbutton, position.X + 1, position.Y + 1, isDown ? L'M' : L'm'); - - return format; + return fmt::format(FMT_COMPILE(L"\x1b[<{};{};{}{}"), xbutton, position.x + 1, position.y + 1, isDown ? L'M' : L'm'); } // Routine Description: @@ -552,17 +511,14 @@ bool TerminalInput::ShouldSendAlternateScroll(const unsigned int button, const s // - Sends a sequence to the input corresponding to cursor up / down depending on the sScrollDelta. // Parameters: // - delta: The scroll wheel delta of the input event -// Return value: -// True if the input sequence was sent successfully. -bool TerminalInput::_SendAlternateScroll(const short delta) const noexcept +TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const short delta) const { if (delta > 0) { - _SendInputSequence(_inputMode.test(Mode::CursorKey) ? ApplicationUpSequence : CursorUpSequence); + return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationUpSequence : CursorUpSequence); } else { - _SendInputSequence(_inputMode.test(Mode::CursorKey) ? ApplicationDownSequence : CursorDownSequence); + return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationDownSequence : CursorDownSequence); } - return true; } diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 390bc115a2..855ba74b28 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -2,26 +2,16 @@ // Licensed under the MIT license. #include "precomp.h" -#include #include "terminalInput.hpp" -#include "strsafe.h" +#include -#define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES -#include - -#include "../../interactivity/inc/VtApiRedirection.hpp" #include "../../inc/unicode.hpp" -#include "../../types/inc/Utf16Parser.hpp" +#include "../../interactivity/inc/VtApiRedirection.hpp" +#include "../types/inc/IInputEvent.hpp" using namespace Microsoft::Console::VirtualTerminal; -TerminalInput::TerminalInput(_In_ std::function>&)> pfn) : - _leadingSurrogate{} -{ - _pfnWriteEvents = pfn; -} - struct TermKeyMap { const WORD vkey; @@ -267,50 +257,57 @@ bool TerminalInput::GetInputMode(const Mode mode) const noexcept return _inputMode.test(mode); } +void TerminalInput::ResetInputModes() noexcept +{ + _inputMode = { Mode::Ansi, Mode::AutoRepeat }; + _mouseInputState.lastPos = { -1, -1 }; + _mouseInputState.lastButton = 0; +} + void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexcept { _forceDisableWin32InputMode = win32InputMode; } -static const gsl::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; } } } @@ -322,12 +319,12 @@ static const gsl::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, - gsl::span keyMapping) noexcept +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 @@ -341,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) @@ -355,146 +352,113 @@ static std::optional _searchKeyMapping(const KeyEvent& keyEven return std::nullopt; } -typedef std::function InputSender; - -// Routine Description: -// - Searches the s_modifierKeyMapping for a entry corresponding to this key event. -// Changes the second to last byte to correspond to the currently pressed modifier keys -// before sending to the input. -// Arguments: -// - keyEvent - Key event to translate -// - sender - Function to use to dispatch translated event -// Return Value: -// - True if there was a match to a key translation, and we successfully modified and sent it to the input -static bool _searchWithModifier(const KeyEvent& keyEvent, InputSender sender) +// Searches the s_modifierKeyMapping for a entry corresponding to this key event. +// Changes the second to last byte to correspond to the currently pressed modifier keys. +TerminalInput::OutputType TerminalInput::_searchWithModifier(const KEY_EVENT_RECORD& keyEvent) { - auto success = false; - - const auto match = _searchKeyMapping(keyEvent, - { s_modifierKeyMapping.data(), s_modifierKeyMapping.size() }); - if (match) + if (const auto match = _searchKeyMapping(keyEvent, s_modifierKeyMapping)) { const auto& v = match.value(); if (!v.sequence.empty()) { - std::wstring modified{ v.sequence }; // Make a copy so we can modify it. - const auto shift = keyEvent.IsShiftPressed(); - const auto alt = keyEvent.IsAltPressed(); - const auto ctrl = keyEvent.IsCtrlPressed(); - modified.at(modified.size() - 2) = L'1' + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0); - sender(modified); - success = true; + 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; } } + + // We didn't find the key in the map of modified keys that need editing, + // maybe it's in the other map of modified keys with sequences that + // don't need editing before sending. + else if (const auto match2 = _searchKeyMapping(keyEvent, s_simpleModifiedKeyMapping)) + { + // This mapping doesn't need to be changed at all. + return MakeOutput(match2->sequence); + } else { - // We didn't find the key in the map of modified keys that need editing, - // maybe it's in the other map of modified keys with sequences that - // don't need editing before sending. - const auto match2 = _searchKeyMapping(keyEvent, - { s_simpleModifiedKeyMapping.data(), s_simpleModifiedKeyMapping.size() }); - if (match2) + // One last check: + // * C-/ is supposed to be ^_ (the C0 character US) + // * C-? is supposed to be DEL + // * C-M-/ is supposed to be ^[^_ + // * C-M-? is supposed to be ^[^? + // + // But this whole scenario is tricky. '/' is not the same VKEY on + // all keyboards. On USASCII keyboards, '/' and '?' share the _same_ + // key. So we have to figure out the vkey at runtime, and we have to + // determine if the key that was pressed was '?' with some + // modifiers, or '/' with some modifiers. + // + // These translations are not in s_simpleModifiedKeyMapping, because + // the aforementioned fact that they aren't the same VKEY on all + // keyboards. + // + // See GH#3079 for details. + // Also see https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 + + // VkKeyScan will give us both the Vkey of the key needed for this + // character, and the modifiers the user might need to press to get + // this character. + const auto slashKeyScan = OneCoreSafeVkKeyScanW(L'/'); // On USASCII: 0x00bf + const auto questionMarkKeyScan = OneCoreSafeVkKeyScanW(L'?'); //On USASCII: 0x01bf + + const auto slashVkey = LOBYTE(slashKeyScan); + const auto questionMarkVkey = LOBYTE(questionMarkKeyScan); + + const auto ctrl = 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.wVirtualKeyCode; + const short keyScanFromEvent = vkey | + (shift ? 0x100 : 0) | + (ctrl ? 0x200 : 0) | + (alt ? 0x400 : 0); + + // Make sure the VKEY is an _exact_ match, and that the modifier + // bits also match. This handles the hypothetical case we get a + // keyscan back that's ctrl+alt+some_random_VK, and some_random_VK + // has bits that are a superset of the bits set for question mark. + const auto wasQuestionMark = vkey == questionMarkVkey && WI_AreAllFlagsSet(keyScanFromEvent, questionMarkKeyScan); + const auto wasSlash = vkey == slashVkey && WI_AreAllFlagsSet(keyScanFromEvent, slashKeyScan); + + // If the key pressed was exactly the ? key, then try to send the + // appropriate sequence for a modified '?'. Otherwise, check if this + // was a modified '/' keypress. These mappings don't need to be + // changed at all. + if ((ctrl && alt) && wasQuestionMark) { - // This mapping doesn't need to be changed at all. - sender(match2.value().sequence); - success = true; + return MakeOutput(CTRL_ALT_QUESTIONMARK_SEQUENCE); } - else + else if (ctrl && wasQuestionMark) { - // One last check: - // * C-/ is supposed to be ^_ (the C0 character US) - // * C-? is supposed to be DEL - // * C-M-/ is supposed to be ^[^_ - // * C-M-? is supposed to be ^[^? - // - // But this whole scenario is tricky. '/' is not the same VKEY on - // all keyboards. On USASCII keyboards, '/' and '?' share the _same_ - // key. So we have to figure out the vkey at runtime, and we have to - // determine if the key that was pressed was '?' with some - // modifiers, or '/' with some modifiers. - // - // These translations are not in s_simpleModifiedKeyMapping, because - // the aforementioned fact that they aren't the same VKEY on all - // keyboards. - // - // See GH#3079 for details. - // Also see https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 - - // VkKeyScan will give us both the Vkey of the key needed for this - // character, and the modifiers the user might need to press to get - // this character. - const auto slashKeyScan = OneCoreSafeVkKeyScanW(L'/'); // On USASCII: 0x00bf - const auto questionMarkKeyScan = OneCoreSafeVkKeyScanW(L'?'); //On USASCII: 0x01bf - - const auto slashVkey = LOBYTE(slashKeyScan); - const auto questionMarkVkey = LOBYTE(questionMarkKeyScan); - - const auto ctrl = keyEvent.IsCtrlPressed(); - const auto alt = keyEvent.IsAltPressed(); - const auto shift = keyEvent.IsShiftPressed(); - - // From the KeyEvent we're translating, synthesize the equivalent VkKeyScan result - const auto vkey = keyEvent.GetVirtualKeyCode(); - const short keyScanFromEvent = vkey | - (shift ? 0x100 : 0) | - (ctrl ? 0x200 : 0) | - (alt ? 0x400 : 0); - - // Make sure the VKEY is an _exact_ match, and that the modifier - // bits also match. This handles the hypothetical case we get a - // keyscan back that's ctrl+alt+some_random_VK, and some_random_VK - // has bits that are a superset of the bits set for question mark. - const auto wasQuestionMark = vkey == questionMarkVkey && WI_AreAllFlagsSet(keyScanFromEvent, questionMarkKeyScan); - const auto wasSlash = vkey == slashVkey && WI_AreAllFlagsSet(keyScanFromEvent, slashKeyScan); - - // If the key pressed was exactly the ? key, then try to send the - // appropriate sequence for a modified '?'. Otherwise, check if this - // was a modified '/' keypress. These mappings don't need to be - // changed at all. - if ((ctrl && alt) && wasQuestionMark) - { - sender(CTRL_ALT_QUESTIONMARK_SEQUENCE); - success = true; - } - else if (ctrl && wasQuestionMark) - { - sender(CTRL_QUESTIONMARK_SEQUENCE); - success = true; - } - else if ((ctrl && alt) && wasSlash) - { - sender(CTRL_ALT_SLASH_SEQUENCE); - success = true; - } - else if (ctrl && wasSlash) - { - sender(CTRL_SLASH_SEQUENCE); - success = true; - } + return MakeOutput(CTRL_QUESTIONMARK_SEQUENCE); + } + else if ((ctrl && alt) && wasSlash) + { + return MakeOutput(CTRL_ALT_SLASH_SEQUENCE); + } + else if (ctrl && wasSlash) + { + return MakeOutput(CTRL_SLASH_SEQUENCE); } } - return success; + return MakeUnhandled(); } -// Routine Description: -// - Searches the input array of mappings, and sends it to the input if a match was found. -// Arguments: -// - keyEvent - Key event to translate -// - keyMapping - Array of key mappings to search -// - sender - Function to use to dispatch translated event -// Return Value: -// - True if there was a match to a key translation, and we successfully sent it to the input -static bool _translateDefaultMapping(const KeyEvent& keyEvent, - const gsl::span keyMapping, - InputSender sender) +TerminalInput::OutputType TerminalInput::MakeUnhandled() noexcept { - const auto match = _searchKeyMapping(keyEvent, keyMapping); - if (match) - { - sender(match->sequence); - } - return match.has_value(); + return {}; +} + +TerminalInput::OutputType TerminalInput::MakeOutput(const std::wstring_view& str) +{ + return { StringType{ str } }; } // Routine Description: @@ -509,100 +473,84 @@ static bool _translateDefaultMapping(const KeyEvent& keyEvent, // Arguments: // - keyEvent - Key event to translate // Return Value: -// - True if the event was handled. -bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) +// - Returns an empty optional if we didn't handle the key event and the caller can opt to handle it in some other way. +// - Returns a string if we successfully translated it into a VT input sequence. +TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) { - if (!pInEvent) - { - return false; - } - - // GH#11682: If this was a focus event, we can handle this. Steal the - // focused state, and return true if we're actually in focus event mode. - if (pInEvent->EventType() == InputEventType::FocusEvent) - { - const auto& focusEvent = *static_cast(pInEvent); - - // BODGY - // GH#13238 - Filter out focus events that came from the API. - if (focusEvent.CameFromApi()) - { - return false; - } - - return HandleFocus(focusEvent.GetFocus()); - } - // On key presses, prepare to translate to VT compatible sequences - if (pInEvent->EventType() != InputEventType::KeyEvent) + if (event.EventType != KEY_EVENT) { - return false; + 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. // Only do this if win32-input-mode support isn't manually disabled. if (_inputMode.test(Mode::Win32) && !_forceDisableWin32InputMode) { - const auto seq = _GenerateWin32KeySequence(keyEvent); - _SendInputSequence(seq); - return true; + return _makeWin32Output(keyEvent); } // Check if this key matches the last recorded key code. - 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) { _lastVirtualKeyCode = std::nullopt; } - return false; + return MakeUnhandled(); } // If this is a repeat of the last recorded key press, and Auto Repeat Mode // is disabled, then we should suppress this event. if (matchingLastKeyPress && !_inputMode.test(Mode::AutoRepeat)) { - // Note that we must return true here to say we've handled the event, - // otherwise the key press can still end up being submitted. - return true; + // Note that we must return an empty string here to imply that we've handled + // the event, otherwise the key press can still end up being submitted. + return MakeOutput({}); } - _lastVirtualKeyCode = keyEvent.GetVirtualKeyCode(); + _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)) { - _SendEscapedInputSequence(seq); + return _makeEscapedOutput(seq); } else { - _SendInputSequence({ &seq, 1 }); + return MakeOutput({ &seq, 1 }); } - return true; + } + + // When the Line Feed mode is set, a VK_RETURN key should send both CR and LF. + // 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.wVirtualKeyCode == VK_RETURN && _inputMode.test(Mode::LineFeed)) + { + return MakeOutput(L"\r\n"); } // 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". @@ -612,16 +560,16 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // 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. @@ -631,8 +579,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) { // Pressing the control key causes all bits but the 5 least // significant ones to be zeroed out (when using ASCII). - _SendEscapedInputSequence(ctrlAltChar & 0b11111); - return true; + return _makeEscapedOutput(ctrlAltChar & 0b11111); } // Currently, when we're called with Alt+Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte. @@ -640,27 +587,24 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // -> Use the vkey to determine if Ctrl+@ is being pressed and produce ^[^@. if (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0))) { - _SendEscapedInputSequence(L'\0'); - return true; + return _makeEscapedOutput(L'\0'); } } - const auto senderFunc = [this](const std::wstring_view seq) noexcept { - _SendInputSequence(seq); - }; - // If a modifier key was pressed, then we need to try and send the modified sequence. - if (keyEvent.IsModifierPressed() && _searchWithModifier(keyEvent, senderFunc)) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, MOD_PRESSED)) { - return true; + if (auto out = _searchWithModifier(keyEvent)) + { + return out; + } } // This section is similar to the Alt modifier section above, // but handles cases without Ctrl modifiers. - if (keyEvent.IsAltPressed() && !keyEvent.IsCtrlPressed() && keyEvent.GetCharData() != 0) + if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && !WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED) && keyEvent.uChar.UnicodeChar != 0) { - _SendEscapedInputSequence(keyEvent.GetCharData()); - return true; + return _makeEscapedOutput(keyEvent.uChar.UnicodeChar); } // Pressing the control key causes all bits but the 5 least @@ -671,18 +615,17 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // -> 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 @). // -> Use the vkey to alternatively determine if Ctrl+@ is being pressed. if (ch == UNICODE_SPACE || (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0)))) { - _SendNullInputSequence(keyEvent.GetActiveModifierKeys()); - return true; + return _makeCharOutput(0); } // Not all keyboard layouts contain mappings for Ctrl-key combinations. @@ -691,14 +634,13 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) 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 // significant ones to be zeroed out (when using ASCII). mappedChar &= 0b11111; - _SendChar(mappedChar); - return true; + return _makeCharOutput(mappedChar); } } } @@ -707,131 +649,83 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // These mappings will kick in no matter which modifiers are pressed and as such // must be checked last, or otherwise we'd override more complex key combinations. const auto mapping = _getKeyMapping(keyEvent, _inputMode.test(Mode::Ansi), _inputMode.test(Mode::CursorKey), _inputMode.test(Mode::Keypad)); - if (_translateDefaultMapping(keyEvent, mapping, senderFunc)) + if (const auto match = _searchKeyMapping(keyEvent, mapping)) { - return true; + return MakeOutput(match->sequence); } // If all else fails we can finally try to send the character itself if there is any. - if (keyEvent.GetCharData() != 0) + if (keyEvent.uChar.UnicodeChar != 0) { - _SendChar(keyEvent.GetCharData()); - return true; + return _makeCharOutput(keyEvent.uChar.UnicodeChar); } - return false; + return MakeUnhandled(); } -bool TerminalInput::HandleFocus(const bool focused) noexcept +TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const { - const auto enabled{ _inputMode.test(Mode::FocusEvent) }; - if (enabled) + if (!_inputMode.test(Mode::FocusEvent)) { - _SendInputSequence(focused ? L"\x1b[I" : L"\x1b[O"); + return MakeUnhandled(); } - return enabled; + + return MakeOutput(focused ? L"\x1b[I" : L"\x1b[O"); } -// Routine Description: -// - Sends the given character to the shell. -// - Surrogate pairs are being aggregated by this function before being sent. -// Arguments: -// - ch: The UTF-16 character to send. -void TerminalInput::_SendChar(const wchar_t ch) +// Turns the given character into OutputType. +// If it encounters a surrogate pair, it'll buffer the leading character until a +// trailing one has been received and then flush both of them simultaneously. +// Surrogate pairs should always be handled as proper pairs after all. +TerminalInput::OutputType TerminalInput::_makeCharOutput(const wchar_t ch) { - if (Utf16Parser::IsLeadingSurrogate(ch)) + StringType str; + + if (til::is_leading_surrogate(ch)) { - if (_leadingSurrogate.has_value()) - { - // we already were storing a leading surrogate but we got another one. Go ahead and send the - // saved surrogate piece and save the new one - const auto formatted = wil::str_printf(L"%I32u", _leadingSurrogate.value()); - _SendInputSequence(formatted); - } - // save the leading portion of a surrogate pair so that they can be sent at the same time _leadingSurrogate.emplace(ch); } - else if (_leadingSurrogate.has_value()) + else if (_leadingSurrogate) { - std::array wstr{ { _leadingSurrogate.value(), ch } }; + const auto lead = *_leadingSurrogate; _leadingSurrogate.reset(); - _SendInputSequence({ wstr.data(), wstr.size() }); + + if (til::is_trailing_surrogate(ch)) + { + str.push_back(lead); + str.push_back(ch); + } } else { - _SendInputSequence({ &ch, 1 }); + str.push_back(ch); } + + return str; } -// Routine Description: -// - Sends the given char as a sequence representing Alt+wch, also the same as -// Meta+wch. -// Arguments: -// - wch - character to send to input paired with Esc -// Return Value: -// - None -void TerminalInput::_SendEscapedInputSequence(const wchar_t wch) const +// Sends the given char as a sequence representing Alt+wch, also the same as Meta+wch. +TerminalInput::OutputType TerminalInput::_makeEscapedOutput(const wchar_t wch) { - try - { - std::deque> inputEvents; - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, L'\x1b', 0)); - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } + StringType str; + str.push_back(L'\x1b'); + str.push_back(wch); + return str; } -void TerminalInput::_SendNullInputSequence(const DWORD controlKeyState) const +// 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 KEY_EVENT_RECORD& key) { - try - { - std::deque> inputEvents; - inputEvents.push_back(std::make_unique(true, - 1ui16, - LOBYTE(OneCoreSafeVkKeyScanW(0)), - 0ui16, - L'\x0', - controlKeyState)); - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } -} + // .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.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); -void TerminalInput::_SendInputSequence(const std::wstring_view sequence) const noexcept -{ - if (!sequence.empty()) - { - try - { - std::deque> inputEvents; - for (const auto& wch : sequence) - { - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); - } - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } - } -} - -// Method Description: -// - Synthesize a win32-input-mode sequence for the given keyevent. -// Arguments: -// - key: the KeyEvent to serialize. -// Return Value: -// - the formatted string representation of this key -std::wstring TerminalInput::_GenerateWin32KeySequence(const KeyEvent& key) -{ // Sequences are formatted as follows: // // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ @@ -843,11 +737,5 @@ std::wstring TerminalInput::_GenerateWin32KeySequence(const KeyEvent& key) // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. - return fmt::format(FMT_COMPILE(L"\x1b[{};{};{};{};{};{}_"), - key.GetVirtualKeyCode(), - key.GetVirtualScanCode(), - static_cast(key.GetCharData()), - key.IsKeyDown() ? 1 : 0, - key.GetActiveModifierKeys(), - key.GetRepeatCount()); + return fmt::format(FMT_COMPILE(L"\x1b[{};{};{};{};{};{}_"), vk, sc, uc, kd, cs, rc); } diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 7df69b41f5..1bb49cca7b 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -1,20 +1,6 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. -Module Name: -- terminalInput.hpp - -Abstract: -- This serves as an adapter between virtual key input from a user and the virtual terminal sequences that are - typically emitted by an xterm-compatible console. - -Author(s): -- Michael Niksa (MiNiksa) 30-Oct-2015 ---*/ - -#include -#include "../../types/inc/IInputEvent.hpp" #pragma once namespace Microsoft::Console::VirtualTerminal @@ -22,22 +8,25 @@ namespace Microsoft::Console::VirtualTerminal class TerminalInput final { public: - TerminalInput(_In_ std::function>&)> pfn); + using StringType = std::wstring; + using OutputType = std::optional; - TerminalInput() = delete; - TerminalInput(const TerminalInput& old) = default; - TerminalInput(TerminalInput&& moved) = default; + struct MouseButtonState + { + bool isLeftButtonDown; + bool isMiddleButtonDown; + bool isRightButtonDown; + }; - TerminalInput& operator=(const TerminalInput& old) = default; - TerminalInput& operator=(TerminalInput&& moved) = default; - - ~TerminalInput() = default; - - bool HandleKey(const IInputEvent* const pInEvent); - bool HandleFocus(const bool focused) noexcept; + [[nodiscard]] static OutputType MakeUnhandled() noexcept; + [[nodiscard]] static OutputType MakeOutput(const std::wstring_view& str); + [[nodiscard]] OutputType HandleKey(const INPUT_RECORD& pInEvent); + [[nodiscard]] OutputType HandleFocus(bool focused) const; + [[nodiscard]] OutputType HandleMouse(til::point position, unsigned int button, short modifierKeyState, short delta, MouseButtonState state); enum class Mode : size_t { + LineFeed, Ansi, AutoRepeat, Keypad, @@ -59,24 +48,12 @@ namespace Microsoft::Console::VirtualTerminal void SetInputMode(const Mode mode, const bool enabled) noexcept; bool GetInputMode(const Mode mode) const noexcept; + void ResetInputModes() noexcept; void ForceDisableWin32InputMode(const bool win32InputMode) noexcept; #pragma region MouseInput // These methods are defined in mouseInput.cpp - struct MouseButtonState - { - bool isLeftButtonDown; - bool isMiddleButtonDown; - bool isRightButtonDown; - }; - - bool HandleMouse(const til::point position, - const unsigned int button, - const short modifierKeyState, - const short delta, - const MouseButtonState state); - bool IsTrackingMouseInput() const noexcept; bool ShouldSendAlternateScroll(const unsigned int button, const short delta) const noexcept; #pragma endregion @@ -88,8 +65,6 @@ namespace Microsoft::Console::VirtualTerminal #pragma endregion private: - std::function>&)> _pfnWriteEvents; - // storage location for the leading surrogate of a utf-16 surrogate pair std::optional _leadingSurrogate; @@ -98,11 +73,10 @@ namespace Microsoft::Console::VirtualTerminal til::enumset _inputMode{ Mode::Ansi, Mode::AutoRepeat }; bool _forceDisableWin32InputMode{ false }; - void _SendChar(const wchar_t ch); - void _SendNullInputSequence(const DWORD dwControlKeyState) const; - void _SendInputSequence(const std::wstring_view sequence) const noexcept; - void _SendEscapedInputSequence(const wchar_t wch) const; - static std::wstring _GenerateWin32KeySequence(const KeyEvent& key); + [[nodiscard]] OutputType _makeCharOutput(wchar_t ch); + [[nodiscard]] static OutputType _makeEscapedOutput(wchar_t wch); + [[nodiscard]] static OutputType _makeWin32Output(const KEY_EVENT_RECORD& key); + [[nodiscard]] static OutputType _searchWithModifier(const KEY_EVENT_RECORD& keyEvent); #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp @@ -118,24 +92,11 @@ namespace Microsoft::Console::VirtualTerminal #pragma endregion #pragma region MouseInput - static std::wstring _GenerateDefaultSequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta); - static std::wstring _GenerateUtf8Sequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta); - static std::wstring _GenerateSGRSequence(const til::point position, - const unsigned int button, - const bool isDown, - const bool isHover, - const short modifierKeyState, - const short delta); + [[nodiscard]] static OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + [[nodiscard]] static OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + [[nodiscard]] static OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta); - bool _SendAlternateScroll(const short delta) const noexcept; + [[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const; static constexpr unsigned int s_GetPressedButton(const MouseButtonState state) noexcept; #pragma endregion diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index ab5492ed6d..95fb5a3e50 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(gsl::make_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(gsl::make_span(&rgInput, 1)); - return _pDispatch->WriteInput(inputEvents); + const auto rgInput = SynthesizeMouseEvent(uiPos, buttonState, controlKeyState, eventFlags); + return _pDispatch->WriteInput({ &rgInput, 1 }); } // Method Description: @@ -1095,7 +1056,7 @@ void InputStateMachineEngine::SetFlushToInputQueueCallback(std::function // - function - Receives the function type // Return Value: // - True iff we successfully pulled the function type from the parameters -bool InputStateMachineEngine::_GetWindowManipulationType(const gsl::span parameters, +bool InputStateMachineEngine::_GetWindowManipulationType(const std::span parameters, unsigned int& function) const noexcept { auto success = false; @@ -1127,7 +1088,7 @@ bool InputStateMachineEngine::_GetWindowManipulationType(const gsl::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 c4a814a1ee..74af20a9b3 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -15,7 +15,6 @@ Author(s): --*/ #pragma once -#include "telemetry.hpp" #include "IStateMachineEngine.hpp" #include #include "../../types/inc/IInputEvent.hpp" @@ -198,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 gsl::span parameters, + 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/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index c7fa93a2bb..e744c95ad0 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -44,10 +44,10 @@ bool OutputStateMachineEngine::ActionExecute(const wchar_t wch) { switch (wch) { - case AsciiChars::NUL: - // microsoft/terminal#1825 - VT applications expect to be able to write NUL - // and have _nothing_ happen. Filter the NULs here, so they don't fill the - // buffer with empty spaces. + case AsciiChars::ENQ: + // GH#11946: At some point we may want to add support for the VT + // answerback feature, which requires responding to an ENQ control + // with a user-defined reply, but until then we just ignore it. break; case AsciiChars::BEL: _dispatch->WarningBell(); @@ -79,9 +79,24 @@ bool OutputStateMachineEngine::ActionExecute(const wchar_t wch) case AsciiChars::SO: _dispatch->LockingShift(1); break; - default: + case AsciiChars::SUB: + // The SUB control is used to cancel a control sequence in the same + // way as CAN, but unlike CAN it also displays an error character, + // typically a reverse question mark. + _dispatch->Print(L'\u2E2E'); + break; + case AsciiChars::DEL: + // The DEL control can sometimes be translated into a printable glyph + // if a 96-character set is designated, so we need to pass it through + // to the Print method. If not translated, it will be filtered out + // there. _dispatch->Print(wch); break; + default: + // GH#1825, GH#10786: VT applications expect to be able to write other + // control characters and have _nothing_ happen. We filter out these + // characters here, so they don't fill the buffer. + break; } _ClearLastChar(); @@ -194,97 +209,80 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) // This is the 7-bit string terminator, which is essentially a no-op. success = true; break; + case EscActionCodes::DECBI_BackIndex: + success = _dispatch->BackIndex(); + break; case EscActionCodes::DECSC_CursorSave: success = _dispatch->CursorSaveState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSC); break; case EscActionCodes::DECRC_CursorRestore: success = _dispatch->CursorRestoreState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRC); + break; + case EscActionCodes::DECFI_ForwardIndex: + success = _dispatch->ForwardIndex(); break; case EscActionCodes::DECKPAM_KeypadApplicationMode: success = _dispatch->SetKeypadMode(true); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPAM); break; case EscActionCodes::DECKPNM_KeypadNumericMode: success = _dispatch->SetKeypadMode(false); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPNM); break; case EscActionCodes::NEL_NextLine: success = _dispatch->LineFeed(DispatchTypes::LineFeedType::WithReturn); - TermTelemetry::Instance().Log(TermTelemetry::Codes::NEL); break; case EscActionCodes::IND_Index: success = _dispatch->LineFeed(DispatchTypes::LineFeedType::WithoutReturn); - TermTelemetry::Instance().Log(TermTelemetry::Codes::IND); break; case EscActionCodes::RI_ReverseLineFeed: success = _dispatch->ReverseLineFeed(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::RI); break; case EscActionCodes::HTS_HorizontalTabSet: success = _dispatch->HorizontalTabSet(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::HTS); break; case EscActionCodes::DECID_IdentifyDevice: success = _dispatch->DeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA); break; case EscActionCodes::RIS_ResetToInitialState: success = _dispatch->HardReset(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::RIS); break; case EscActionCodes::SS2_SingleShift: success = _dispatch->SingleShift(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SS2); break; case EscActionCodes::SS3_SingleShift: success = _dispatch->SingleShift(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SS3); break; case EscActionCodes::LS2_LockingShift: success = _dispatch->LockingShift(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS2); break; case EscActionCodes::LS3_LockingShift: success = _dispatch->LockingShift(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3); break; case EscActionCodes::LS1R_LockingShift: success = _dispatch->LockingShiftRight(1); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS1R); break; case EscActionCodes::LS2R_LockingShift: success = _dispatch->LockingShiftRight(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS2R); break; case EscActionCodes::LS3R_LockingShift: success = _dispatch->LockingShiftRight(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3R); break; case EscActionCodes::DECAC1_AcceptC1Controls: success = _dispatch->AcceptC1Controls(true); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC1); break; case EscActionCodes::DECDHL_DoubleHeightLineTop: success = _dispatch->SetLineRendition(LineRendition::DoubleHeightTop); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); break; case EscActionCodes::DECDHL_DoubleHeightLineBottom: success = _dispatch->SetLineRendition(LineRendition::DoubleHeightBottom); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); break; case EscActionCodes::DECSWL_SingleWidthLine: success = _dispatch->SetLineRendition(LineRendition::SingleWidth); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSWL); break; case EscActionCodes::DECDWL_DoubleWidthLine: success = _dispatch->SetLineRendition(LineRendition::DoubleWidth); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDWL); break; case EscActionCodes::DECALN_ScreenAlignmentPattern: success = _dispatch->ScreenAlignmentPattern(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECALN); break; default: const auto commandChar = id[0]; @@ -293,35 +291,27 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) { case '%': success = _dispatch->DesignateCodingSystem(commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DOCS); break; case '(': success = _dispatch->Designate94Charset(0, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG0); break; case ')': success = _dispatch->Designate94Charset(1, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG1); break; case '*': success = _dispatch->Designate94Charset(2, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG2); break; case '+': success = _dispatch->Designate94Charset(3, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG3); break; case '-': success = _dispatch->Designate96Charset(1, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG1); break; case '.': success = _dispatch->Designate96Charset(2, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG2); break; case '/': success = _dispatch->Designate96Charset(3, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG3); break; default: // If no functions to call, overall dispatch was a failure. @@ -426,178 +416,156 @@ bool OutputStateMachineEngine::ActionVt52EscDispatch(const VTID id, const VTPara // - true iff we successfully dispatched the sequence. bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameters parameters) { + // Bail out if we receive subparameters, but we don't accept them in the sequence. + if (parameters.hasSubParams() && !_CanSeqAcceptSubParam(id, parameters)) [[unlikely]] + { + return false; + } + auto success = false; switch (id) { case CsiActionCodes::CUU_CursorUp: success = _dispatch->CursorUp(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUU); break; case CsiActionCodes::CUD_CursorDown: success = _dispatch->CursorDown(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUD); break; case CsiActionCodes::CUF_CursorForward: success = _dispatch->CursorForward(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUF); break; case CsiActionCodes::CUB_CursorBackward: success = _dispatch->CursorBackward(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUB); break; case CsiActionCodes::CNL_CursorNextLine: success = _dispatch->CursorNextLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CNL); break; case CsiActionCodes::CPL_CursorPrevLine: success = _dispatch->CursorPrevLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CPL); break; case CsiActionCodes::CHA_CursorHorizontalAbsolute: case CsiActionCodes::HPA_HorizontalPositionAbsolute: success = _dispatch->CursorHorizontalPositionAbsolute(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CHA); break; case CsiActionCodes::VPA_VerticalLinePositionAbsolute: success = _dispatch->VerticalLinePositionAbsolute(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::VPA); break; case CsiActionCodes::HPR_HorizontalPositionRelative: success = _dispatch->HorizontalPositionRelative(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::HPR); break; case CsiActionCodes::VPR_VerticalPositionRelative: success = _dispatch->VerticalPositionRelative(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::VPR); break; case CsiActionCodes::CUP_CursorPosition: case CsiActionCodes::HVP_HorizontalVerticalPosition: success = _dispatch->CursorPosition(parameters.at(0), parameters.at(1)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUP); break; - case CsiActionCodes::DECSTBM_SetScrollingRegion: + case CsiActionCodes::DECSTBM_SetTopBottomMargins: success = _dispatch->SetTopBottomScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM); + break; + case CsiActionCodes::DECSLRM_SetLeftRightMargins: + // Note that this can also be ANSISYSSC, depending on the state of DECLRMM. + success = _dispatch->SetLeftRightScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0)); break; case CsiActionCodes::ICH_InsertCharacter: success = _dispatch->InsertCharacter(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ICH); break; case CsiActionCodes::DCH_DeleteCharacter: success = _dispatch->DeleteCharacter(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DCH); break; case CsiActionCodes::ED_EraseDisplay: success = parameters.for_each([&](const auto eraseType) { return _dispatch->EraseInDisplay(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ED); break; case CsiActionCodes::DECSED_SelectiveEraseDisplay: success = parameters.for_each([&](const auto eraseType) { return _dispatch->SelectiveEraseInDisplay(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSED); break; case CsiActionCodes::EL_EraseLine: success = parameters.for_each([&](const auto eraseType) { return _dispatch->EraseInLine(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::EL); break; case CsiActionCodes::DECSEL_SelectiveEraseLine: success = parameters.for_each([&](const auto eraseType) { return _dispatch->SelectiveEraseInLine(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSEL); + break; + case CsiActionCodes::SM_SetMode: + success = parameters.for_each([&](const auto mode) { + return _dispatch->SetMode(DispatchTypes::ANSIStandardMode(mode)); + }); break; case CsiActionCodes::DECSET_PrivateModeSet: success = parameters.for_each([&](const auto mode) { return _dispatch->SetMode(DispatchTypes::DECPrivateMode(mode)); }); - //TODO: MSFT:6367459 Add specific logging for each of the DECSET/DECRST codes - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSET); + break; + case CsiActionCodes::RM_ResetMode: + success = parameters.for_each([&](const auto mode) { + return _dispatch->ResetMode(DispatchTypes::ANSIStandardMode(mode)); + }); break; case CsiActionCodes::DECRST_PrivateModeReset: success = parameters.for_each([&](const auto mode) { return _dispatch->ResetMode(DispatchTypes::DECPrivateMode(mode)); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRST); break; case CsiActionCodes::SGR_SetGraphicsRendition: success = _dispatch->SetGraphicsRendition(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SGR); break; case CsiActionCodes::DSR_DeviceStatusReport: - success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0))); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); + success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0)), parameters.at(1)); break; case CsiActionCodes::DSR_PrivateDeviceStatusReport: - success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0))); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); + success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0)), parameters.at(1)); break; case CsiActionCodes::DA_DeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->DeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA); break; case CsiActionCodes::DA2_SecondaryDeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->SecondaryDeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA2); break; case CsiActionCodes::DA3_TertiaryDeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->TertiaryDeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA3); break; case CsiActionCodes::DECREQTPARM_RequestTerminalParameters: success = _dispatch->RequestTerminalParameters(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECREQTPARM); break; case CsiActionCodes::SU_ScrollUp: success = _dispatch->ScrollUp(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SU); break; case CsiActionCodes::SD_ScrollDown: success = _dispatch->ScrollDown(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SD); - break; - case CsiActionCodes::ANSISYSSC_CursorSave: - success = parameters.empty() && _dispatch->CursorSaveState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSSC); break; case CsiActionCodes::ANSISYSRC_CursorRestore: - success = parameters.empty() && _dispatch->CursorRestoreState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSRC); + success = _dispatch->CursorRestoreState(); break; case CsiActionCodes::IL_InsertLine: success = _dispatch->InsertLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::IL); break; case CsiActionCodes::DL_DeleteLine: success = _dispatch->DeleteLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DL); break; case CsiActionCodes::CHT_CursorForwardTab: success = _dispatch->ForwardTab(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CHT); break; case CsiActionCodes::CBT_CursorBackTab: success = _dispatch->BackwardsTab(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CBT); break; case CsiActionCodes::TBC_TabClear: success = parameters.for_each([&](const auto clearType) { return _dispatch->TabClear(clearType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::TBC); break; case CsiActionCodes::ECH_EraseCharacters: success = _dispatch->EraseCharacters(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ECH); break; case CsiActionCodes::DTTERM_WindowManipulation: success = _dispatch->WindowManipulation(parameters.at(0), parameters.at(1), parameters.at(2)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DTTERM_WM); break; case CsiActionCodes::REP_RepeatCharacter: // Handled w/o the dispatch. This function is unique in that way @@ -612,65 +580,71 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete _dispatch->PrintString(wstr); } success = true; - TermTelemetry::Instance().Log(TermTelemetry::Codes::REP); break; case CsiActionCodes::DECSCUSR_SetCursorStyle: success = _dispatch->SetCursorStyle(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCUSR); break; case CsiActionCodes::DECSTR_SoftReset: success = _dispatch->SoftReset(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR); break; case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: success = _dispatch->SetCharacterProtectionAttribute(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCA); break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR); break; case CsiActionCodes::XT_PopSgr: case CsiActionCodes::XT_PopSgrAlias: success = _dispatch->PopGraphicsRendition(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR); + break; + case CsiActionCodes::DECRQM_RequestMode: + success = _dispatch->RequestMode(DispatchTypes::ANSIStandardMode(parameters.at(0))); + break; + case CsiActionCodes::DECRQM_PrivateRequestMode: + success = _dispatch->RequestMode(DispatchTypes::DECPrivateMode(parameters.at(0))); break; case CsiActionCodes::DECCARA_ChangeAttributesRectangularArea: success = _dispatch->ChangeAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCARA); break; case CsiActionCodes::DECRARA_ReverseAttributesRectangularArea: success = _dispatch->ReverseAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRARA); break; case CsiActionCodes::DECCRA_CopyRectangularArea: success = _dispatch->CopyRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.at(4), parameters.at(5), parameters.at(6), parameters.at(7)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCRA); + break; + case CsiActionCodes::DECRQPSR_RequestPresentationStateReport: + success = _dispatch->RequestPresentationStateReport(parameters.at(0)); break; case CsiActionCodes::DECFRA_FillRectangularArea: success = _dispatch->FillRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2), parameters.at(3).value_or(0), parameters.at(4).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFRA); break; case CsiActionCodes::DECERA_EraseRectangularArea: success = _dispatch->EraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECERA); break; case CsiActionCodes::DECSERA_SelectiveEraseRectangularArea: success = _dispatch->SelectiveEraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSERA); + break; + case CsiActionCodes::DECIC_InsertColumn: + success = _dispatch->InsertColumn(parameters.at(0)); + break; + case CsiActionCodes::DECDC_DeleteColumn: + success = _dispatch->DeleteColumn(parameters.at(0)); break; case CsiActionCodes::DECSACE_SelectAttributeChangeExtent: success = _dispatch->SelectAttributeChangeExtent(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE); + break; + case CsiActionCodes::DECRQCRA_RequestChecksumRectangularArea: + success = _dispatch->RequestChecksumRectangularArea(parameters.at(0).value_or(0), parameters.at(1).value_or(0), parameters.at(2), parameters.at(3), parameters.at(4).value_or(0), parameters.at(5).value_or(0)); + break; + case CsiActionCodes::DECINVM_InvokeMacro: + success = _dispatch->InvokeMacro(parameters.at(0).value_or(0)); break; case CsiActionCodes::DECAC_AssignColor: success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC); break; case CsiActionCodes::DECPS_PlaySound: success = _dispatch->PlaySounds(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECPS); break; default: // If no functions to call, overall dispatch was a failure. @@ -715,12 +689,18 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c parameters.at(6), parameters.at(7)); break; + case DcsActionCodes::DECDMAC_DefineMacro: + handler = _dispatch->DefineMacro(parameters.at(0).value_or(0), parameters.at(1), parameters.at(2)); + break; case DcsActionCodes::DECRSTS_RestoreTerminalState: handler = _dispatch->RestoreTerminalState(parameters.at(0)); break; case DcsActionCodes::DECRQSS_RequestSetting: handler = _dispatch->RequestSetting(); break; + case DcsActionCodes::DECRSPS_RestorePresentationState: + handler = _dispatch->RestorePresentationState(parameters.at(0)); + break; default: handler = nullptr; break; @@ -781,7 +761,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, std::wstring title; success = _GetOscTitle(string, title); success = success && _dispatch->SetWindowTitle(title); - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCWT); break; } case OscActionCodes::SetColor: @@ -795,7 +774,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, const auto rgb = til::at(colors, i); success = success && _dispatch->SetColorTableEntry(tableIndex, rgb); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCCT); break; } case OscActionCodes::SetForegroundColor: @@ -816,7 +794,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetDefaultForeground(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCFG); commandIndex++; colorIndex++; } @@ -828,7 +805,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetDefaultBackground(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCBG); commandIndex++; colorIndex++; } @@ -840,7 +816,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetCursorColor(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCSCC); commandIndex++; colorIndex++; } @@ -856,13 +831,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = _dispatch->SetClipboard(setClipboardContent); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCSCB); break; } case OscActionCodes::ResetCursorColor: { success = _dispatch->SetCursorColor(INVALID_COLOR); - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCRCC); break; } case OscActionCodes::Hyperlink: @@ -895,6 +868,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, success = _dispatch->DoFinalTermAction(string); break; } + case OscActionCodes::VsCodeAction: + { + success = _dispatch->DoVsCodeAction(string); + break; + } default: // If no functions to call, overall dispatch was a failure. success = false; @@ -989,7 +967,7 @@ bool OutputStateMachineEngine::_GetOscSetColorTable(const std::wstring_view stri } #pragma warning(push) -#pragma warning(disable : 26445) // Suppress lifetime check for a reference to gsl::span or std::string_view +#pragma warning(disable : 26445) // Suppress lifetime check for a reference to std::span or std::string_view // Routine Description: // - Given a hyperlink string, attempts to parse the URI encoded. An 'id' parameter @@ -1131,6 +1109,26 @@ bool OutputStateMachineEngine::_GetOscSetClipboard(const std::wstring_view strin return SUCCEEDED_LOG(Base64::Decode(substr, content)); } +// Routine Description: +// - Takes a sequence id ("final byte") and determines if it accepts sub parameters. +// Arguments: +// - id - The sequence id to check for. +// Return Value: +// - True, if it accepts sub parameters or else False. +bool OutputStateMachineEngine::_CanSeqAcceptSubParam(const VTID id, const VTParameters& parameters) noexcept +{ + switch (id) + { + case SGR_SetGraphicsRendition: + return true; + case DECCARA_ChangeAttributesRectangularArea: + case DECRARA_ReverseAttributesRectangularArea: + return !parameters.hasSubParamsFor(0) && !parameters.hasSubParamsFor(1) && !parameters.hasSubParamsFor(2) && !parameters.hasSubParamsFor(3); + default: + return false; + } +} + // Method Description: // - Clears our last stored character. The last stored character is the last // graphical character we printed, which is reset if any other action is diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index b125c7b714..1983fa9baa 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -13,7 +13,6 @@ Abstract: #include #include "../adapter/termDispatch.hpp" -#include "telemetry.hpp" #include "IStateMachineEngine.hpp" namespace Microsoft::Console::Render @@ -71,8 +70,10 @@ namespace Microsoft::Console::VirtualTerminal enum EscActionCodes : uint64_t { + DECBI_BackIndex = VTID("6"), DECSC_CursorSave = VTID("7"), DECRC_CursorRestore = VTID("8"), + DECFI_ForwardIndex = VTID("9"), DECKPAM_KeypadApplicationMode = VTID("="), DECKPNM_KeypadNumericMode = VTID(">"), IND_Index = VTID("D"), @@ -130,13 +131,15 @@ namespace Microsoft::Console::VirtualTerminal VPR_VerticalPositionRelative = VTID("e"), HVP_HorizontalVerticalPosition = VTID("f"), TBC_TabClear = VTID("g"), + SM_SetMode = VTID("h"), DECSET_PrivateModeSet = VTID("?h"), + RM_ResetMode = VTID("l"), DECRST_PrivateModeReset = VTID("?l"), SGR_SetGraphicsRendition = VTID("m"), DSR_DeviceStatusReport = VTID("n"), DSR_PrivateDeviceStatusReport = VTID("?n"), - DECSTBM_SetScrollingRegion = VTID("r"), - ANSISYSSC_CursorSave = VTID("s"), // NOTE: Overlaps with DECLRMM/DECSLRM. Fix when/if implemented. + DECSTBM_SetTopBottomMargins = VTID("r"), + DECSLRM_SetLeftRightMargins = VTID("s"), DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented. ANSISYSRC_CursorRestore = VTID("u"), DECREQTPARM_RequestTerminalParameters = VTID("x"), @@ -147,14 +150,21 @@ namespace Microsoft::Console::VirtualTerminal XT_PopSgrAlias = VTID("#q"), XT_PushSgr = VTID("#{"), XT_PopSgr = VTID("#}"), + DECRQM_RequestMode = VTID("$p"), + DECRQM_PrivateRequestMode = VTID("?$p"), DECCARA_ChangeAttributesRectangularArea = VTID("$r"), DECRARA_ReverseAttributesRectangularArea = VTID("$t"), DECCRA_CopyRectangularArea = VTID("$v"), + DECRQPSR_RequestPresentationStateReport = VTID("$w"), DECFRA_FillRectangularArea = VTID("$x"), DECERA_EraseRectangularArea = VTID("$z"), DECSERA_SelectiveEraseRectangularArea = VTID("${"), DECSCPP_SetColumnsPerPage = VTID("$|"), + DECIC_InsertColumn = VTID("'}"), + DECDC_DeleteColumn = VTID("'~"), DECSACE_SelectAttributeChangeExtent = VTID("*x"), + DECRQCRA_RequestChecksumRectangularArea = VTID("*y"), + DECINVM_InvokeMacro = VTID("*z"), DECAC_AssignColor = VTID(",|"), DECPS_PlaySound = VTID(",~") }; @@ -162,8 +172,10 @@ namespace Microsoft::Console::VirtualTerminal enum DcsActionCodes : uint64_t { DECDLD_DownloadDRCS = VTID("{"), + DECDMAC_DefineMacro = VTID("!z"), DECRSTS_RestoreTerminalState = VTID("$p"), - DECRQSS_RequestSetting = VTID("$q") + DECRQSS_RequestSetting = VTID("$q"), + DECRSPS_RestorePresentationState = VTID("$t"), }; enum Vt52ActionCodes : uint64_t @@ -202,6 +214,7 @@ namespace Microsoft::Console::VirtualTerminal ResetBackgroundColor = 111, // Not implemented ResetCursorColor = 112, FinalTermAction = 133, + VsCodeAction = 633, ITerm2Action = 1337, }; @@ -224,6 +237,8 @@ namespace Microsoft::Console::VirtualTerminal std::wstring& params, std::wstring& uri) const; + bool _CanSeqAcceptSubParam(const VTID id, const VTParameters& parameters) noexcept; + void _ClearLastChar() noexcept; }; } diff --git a/src/terminal/parser/ft_fuzzer/fuzzing_directed.h b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h index 72dc0641ca..2ab6e1e23a 100644 --- a/src/terminal/parser/ft_fuzzer/fuzzing_directed.h +++ b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h @@ -337,6 +337,9 @@ namespace fuzz FuzzTraits m_traits{ TRAIT_DEFAULT }; }; + template + class CFuzzArraySize; + // The CFuzzArray class is designed to allow fuzzing of element // arrays, potentially reallocating a fuzzed version of the array // that is either larger or smaller than the template buffer. Whether @@ -353,8 +356,7 @@ namespace fuzz class CFuzzArray : public CFuzzBase { public: - template - friend class CFuzzArraySize; + friend class CFuzzArraySize<_Type1, _Type2, _Args...>; // Creates a CFuzzArray instance that wraps a buffer specified by // rg, together with its size (note that this is the number of elements @@ -643,8 +645,7 @@ namespace fuzz class CFuzzArraySize { public: - template - friend class CFuzzArray; + friend class CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...>; CFuzzArraySize(__inout _Type2& cElems) : m_pcElems(&cElems), @@ -892,7 +893,7 @@ namespace fuzz __in ULONG cfte, __in _Type pt, __in _Args&&... args) : - CFuzzType(rgfte, cfte, pt, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, pt, std::forward<_Args>(args)...) { } @@ -933,7 +934,7 @@ namespace fuzz __in ULONG cfte, __in _Type* psz, __in _Args... args) : - CFuzzType(rgfte, cfte, psz, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, psz, std::forward<_Args>(args)...) { OnFuzzedValueFromMap(); } @@ -1061,7 +1062,7 @@ namespace fuzz __in ULONG cfte, __in _Type flags, __in _Args&&... args) : - CFuzzType(rgfte, cfte, flags, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, flags, std::forward<_Args>(args)...) { } diff --git a/src/terminal/parser/lib/parser.vcxproj.filters b/src/terminal/parser/lib/parser.vcxproj.filters index 409f4e4071..f62588b65d 100644 --- a/src/terminal/parser/lib/parser.vcxproj.filters +++ b/src/terminal/parser/lib/parser.vcxproj.filters @@ -18,9 +18,6 @@ Source Files - - Source Files - Source Files @@ -38,9 +35,6 @@ Header Files - - Header Files - Header Files diff --git a/src/terminal/parser/parser-common.vcxitems b/src/terminal/parser/parser-common.vcxitems index f60ddec6ee..a929e69446 100644 --- a/src/terminal/parser/parser-common.vcxitems +++ b/src/terminal/parser/parser-common.vcxitems @@ -6,7 +6,6 @@ - Create @@ -19,7 +18,6 @@ - diff --git a/src/terminal/parser/precomp.h b/src/terminal/parser/precomp.h index 38a4422bc2..03611a4794 100644 --- a/src/terminal/parser/precomp.h +++ b/src/terminal/parser/precomp.h @@ -22,5 +22,4 @@ Abstract: #define ENABLE_INTSAFE_SIGNED_FUNCTIONS #include -#include "telemetry.hpp" #include "tracing.hpp" diff --git a/src/terminal/parser/sources.inc b/src/terminal/parser/sources.inc index 5e05cc566f..e39b8a2aa3 100644 --- a/src/terminal/parser/sources.inc +++ b/src/terminal/parser/sources.inc @@ -33,7 +33,6 @@ SOURCES = \ ..\stateMachine.cpp \ ..\InputStateMachineEngine.cpp \ ..\OutputStateMachineEngine.cpp \ - ..\telemetry.cpp \ ..\tracing.cpp \ ..\base64.cpp \ diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp index 36848694f3..eb22ff9538 100644 --- a/src/terminal/parser/stateMachine.cpp +++ b/src/terminal/parser/stateMachine.cpp @@ -16,10 +16,13 @@ StateMachine::StateMachine(std::unique_ptr engine, const bo _state(VTStates::Ground), _trace(Microsoft::Console::VirtualTerminal::ParserTracing()), _parameters{}, - _parameterLimitReached(false), + _subParameters{}, + _subParameterRanges{}, + _parameterLimitOverflowed(false), + _subParameterLimitOverflowed(false), + _subParameterCounter(0), _oscString{}, - _cachedSequence{ std::nullopt }, - _processingIndividually(false) + _cachedSequence{ std::nullopt } { _ActionClear(); } @@ -159,7 +162,18 @@ static constexpr bool _isParameterDelimiter(const wchar_t wch) noexcept } // Routine Description: -// - Determines if a character is "control sequence" beginning indicator. +// - Determines if a character is a delimiter between a parameter and its sub-parameters in an escape sequence. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +static constexpr bool _isSubParameterDelimiter(const wchar_t wch) noexcept +{ + return wch == L':'; // 0x3A +} + +// Routine Description: +// - Determines if a character is a "control sequence" beginning indicator. // This immediately follows an escape and signifies a varying length control sequence. // Arguments: // - wch - Character to check. @@ -182,17 +196,6 @@ static constexpr bool _isCsiPrivateMarker(const wchar_t wch) noexcept return wch == L'<' || wch == L'=' || wch == L'>' || wch == L'?'; // 0x3C - 0x3F } -// Routine Description: -// - Determines if a character is invalid in a control sequence -// Arguments: -// - wch - Character to check. -// Return Value: -// - True if it is. False if it isn't. -static constexpr bool _isCsiInvalid(const wchar_t wch) noexcept -{ - return wch == L':'; // 0x3A -} - // Routine Description: // - Determines if a character is an invalid intermediate. // Arguments: @@ -202,7 +205,7 @@ static constexpr bool _isCsiInvalid(const wchar_t wch) noexcept static constexpr bool _isIntermediateInvalid(const wchar_t wch) noexcept { // 0x30 - 0x3F - return _isNumericParamValue(wch) || _isCsiInvalid(wch) || _isParameterDelimiter(wch) || _isCsiPrivateMarker(wch); + return _isNumericParamValue(wch) || _isSubParameterDelimiter(wch) || _isParameterDelimiter(wch) || _isCsiPrivateMarker(wch); } // Routine Description: @@ -213,8 +216,8 @@ static constexpr bool _isIntermediateInvalid(const wchar_t wch) noexcept // - True if it is. False if it isn't. static constexpr bool _isParameterInvalid(const wchar_t wch) noexcept { - // 0x3A, 0x3C - 0x3F - return _isCsiInvalid(wch) || _isCsiPrivateMarker(wch); + // 0x3C - 0x3F + return _isCsiPrivateMarker(wch); } // Routine Description: @@ -365,18 +368,6 @@ static constexpr bool _isApcIndicator(const wchar_t wch) noexcept return wch == L'_'; // 0x5F } -// Routine Description: -// - Determines if a character indicates an action that should be taken in the ground state - -// These are C0 characters and the C1 [single-character] CSI. -// Arguments: -// - wch - Character to check. -// Return Value: -// - True if it is. False if it isn't. -static constexpr bool _isActionableFromGround(const wchar_t wch) noexcept -{ - return (wch <= AsciiChars::US) || _isC1ControlCharacter(wch) || _isDelete(wch); -} - #pragma warning(pop) // Routine Description: @@ -447,7 +438,7 @@ void StateMachine::_ActionPrintString(const std::wstring_view string) void StateMachine::_ActionEscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"EscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionEscDispatch(_identifier.Finalize(wch)); })); } @@ -462,14 +453,14 @@ void StateMachine::_ActionEscDispatch(const wchar_t wch) void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"Vt52EscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionVt52EscDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); })); } // Routine Description: // - Triggers the CsiDispatch action to indicate that the listener should handle a control sequence. -// These sequences perform various API-type commands that can include many parameters. +// These sequences perform various API-type commands that can include many parameters and sub parameters. // Arguments: // - wch - Character to dispatch. // Return Value: @@ -477,8 +468,9 @@ void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) void StateMachine::_ActionCsiDispatch(const wchar_t wch) { _trace.TraceOnAction(L"CsiDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { - return _engine->ActionCsiDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); + _trace.DispatchSequenceTrace(_SafeExecute([=]() { + return _engine->ActionCsiDispatch(_identifier.Finalize(wch), + { _parameters, _subParameters, _subParameterRanges }); })); } @@ -508,12 +500,14 @@ void StateMachine::_ActionParam(const wchar_t wch) _trace.TraceOnAction(L"Param"); // Once we've reached the parameter limit, additional parameters are ignored. - if (!_parameterLimitReached) + if (!_parameterLimitOverflowed) { // If we have no parameters and we're about to add one, get the next value ready here. if (_parameters.empty()) { _parameters.push_back({}); + const auto rangeStart = gsl::narrow_cast(_subParameters.size()); + _subParameterRanges.push_back({ rangeStart, rangeStart }); } // On a delimiter, increase the number of params we've seen. @@ -526,12 +520,16 @@ void StateMachine::_ActionParam(const wchar_t wch) // indicate that further parameter characters should be ignored. if (_parameters.size() >= MAX_PARAMETER_COUNT) { - _parameterLimitReached = true; + _parameterLimitOverflowed = true; } else { // Otherwise move to next param. _parameters.push_back({}); + _subParameterCounter = 0; + _subParameterLimitOverflowed = false; + const auto rangeStart = gsl::narrow_cast(_subParameters.size()); + _subParameterRanges.push_back({ rangeStart, rangeStart }); } } else @@ -545,6 +543,62 @@ void StateMachine::_ActionParam(const wchar_t wch) } } +// Routine Description: +// - Triggers the SubParam action to indicate that the state machine should +// store this character as a part of a sub-parameter to a control sequence. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionSubParam(const wchar_t wch) +{ + _trace.TraceOnAction(L"SubParam"); + + // Once we've reached the sub parameter limit, sub parameters are ignored. + if (!_subParameterLimitOverflowed) + { + // If we have no parameters and we're about to add a sub parameter, add an empty parameter here. + if (_parameters.empty()) + { + _parameters.push_back({}); + const auto rangeStart = gsl::narrow_cast(_subParameters.size()); + _subParameterRanges.push_back({ rangeStart, rangeStart }); + } + + // On a delimiter, increase the number of sub params we've seen. + // "Empty" sub params should still count as a sub param - + // eg "\x1b[0:::m" should be three sub params + if (_isSubParameterDelimiter(wch)) + { + // If we receive a delimiter after we've already accumulated the + // maximum allowed sub parameters for the parameter, then we need to + // set a flag to indicate that further sub parameter characters + // should be ignored. + if (_subParameterCounter >= MAX_SUBPARAMETER_COUNT) + { + _subParameterLimitOverflowed = true; + } + else + { + // Otherwise move to next sub-param. + _subParameters.push_back({}); + // increment current range's end index. + _subParameterRanges.back().second++; + // increment counter + _subParameterCounter++; + } + } + else + { + // Accumulate the character given into the last (current) sub-parameter. + // If the value hasn't been initialized yet, it'll start as 0. + auto currentSubParameter = _subParameters.back().value_or(0); + _AccumulateTo(wch, currentSubParameter); + _subParameters.back() = currentSubParameter; + } + } +} + // Routine Description: // - Triggers the Clear action to indicate that the state machine should erase all internal state. // Arguments: @@ -559,7 +613,12 @@ void StateMachine::_ActionClear() _identifier.Clear(); _parameters.clear(); - _parameterLimitReached = false; + _parameterLimitOverflowed = false; + + _subParameters.clear(); + _subParameterRanges.clear(); + _subParameterCounter = 0; + _subParameterLimitOverflowed = false; _oscString.clear(); _oscParameter = 0; @@ -572,7 +631,7 @@ void StateMachine::_ActionClear() // Routine Description: // - Triggers the Ignore action to indicate that the state machine should eat this character and say nothing. // Arguments: -// - wch - Character to dispatch. +// - // Return Value: // - void StateMachine::_ActionIgnore() noexcept @@ -635,7 +694,7 @@ void StateMachine::_ActionOscPut(const wchar_t wch) void StateMachine::_ActionOscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"OscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionOscDispatch(wch, _oscParameter, _oscString); })); } @@ -650,7 +709,7 @@ void StateMachine::_ActionOscDispatch(const wchar_t wch) void StateMachine::_ActionSs3Dispatch(const wchar_t wch) { _trace.TraceOnAction(L"Ss3Dispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionSs3Dispatch(wch, { _parameters.data(), _parameters.size() }); })); } @@ -666,7 +725,7 @@ void StateMachine::_ActionDcsDispatch(const wchar_t wch) { _trace.TraceOnAction(L"DcsDispatch"); - const auto success = _SafeExecuteWithLog(wch, [=]() { + const auto success = _SafeExecute([=]() { _dcsStringHandler = _engine->ActionDcsDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); // If the returned handler is null, the sequence is not supported. return _dcsStringHandler != nullptr; @@ -762,6 +821,20 @@ void StateMachine::_EnterCsiParam() noexcept _trace.TraceStateChange(L"CsiParam"); } +// Routine Description: +// - Moves the state machine into the CsiSubParam state. +// This state is entered: +// 1. When valid sub parameter characters are detected in CsiParam state. +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterCsiSubParam() noexcept +{ + _state = VTStates::CsiSubParam; + _trace.TraceStateChange(L"CsiSubParam"); +} + // Routine Description: // - Moves the state machine into the CsiIgnore state. // This state is entered: @@ -1133,8 +1206,8 @@ void StateMachine::_EventEscapeIntermediate(const wchar_t wch) // 1. Execute C0 control characters // 2. Ignore Delete characters // 3. Collect Intermediate characters -// 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) -// 5. Store parameter data +// 4. Store parameter data +// 5. Store sub parameter data // 6. Collect Control Sequence Private markers // 7. Dispatch a control sequence with parameters for action // Arguments: @@ -1157,15 +1230,16 @@ void StateMachine::_EventCsiEntry(const wchar_t wch) _ActionCollect(wch); _EnterCsiIntermediate(); } - else if (_isCsiInvalid(wch)) - { - _EnterCsiIgnore(); - } else if (_isNumericParamValue(wch) || _isParameterDelimiter(wch)) { _ActionParam(wch); _EnterCsiParam(); } + else if (_isSubParameterDelimiter(wch)) + { + _ActionSubParam(wch); + _EnterCsiSubParam(); + } else if (_isCsiPrivateMarker(wch)) { _ActionCollect(wch); @@ -1175,6 +1249,7 @@ void StateMachine::_EventCsiEntry(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1213,6 +1288,7 @@ void StateMachine::_EventCsiIntermediate(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1261,7 +1337,8 @@ void StateMachine::_EventCsiIgnore(const wchar_t wch) // 3. Collect Intermediate characters // 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) // 5. Store parameter data -// 6. Dispatch a control sequence with parameters for action +// 6. Store sub parameter data +// 7. Dispatch a control sequence with parameters for action // Arguments: // - wch - Character that triggered the event // Return Value: @@ -1281,6 +1358,11 @@ void StateMachine::_EventCsiParam(const wchar_t wch) { _ActionParam(wch); } + else if (_isSubParameterDelimiter(wch)) + { + _ActionSubParam(wch); + _EnterCsiSubParam(); + } else if (_isIntermediate(wch)) { _ActionCollect(wch); @@ -1294,6 +1376,58 @@ void StateMachine::_EventCsiParam(const wchar_t wch) { _ActionCsiDispatch(wch); _EnterGround(); + _ExecuteCsiCompleteCallback(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiSubParam state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Store sub parameter data +// 4. Store parameter data +// 5. Collect Intermediate characters for parameter. +// 6. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 7. Dispatch a control sequence with parameters for action +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventCsiSubParam(const wchar_t wch) +{ + _trace.TraceOnEvent(L"CsiSubParam"); + if (_isC0Code(wch)) + { + _ActionExecute(wch); + } + else if (_isDelete(wch)) + { + _ActionIgnore(); + } + else if (_isNumericParamValue(wch) || _isSubParameterDelimiter(wch)) + { + _ActionSubParam(wch); + } + else if (_isParameterDelimiter(wch)) + { + _ActionParam(wch); + _EnterCsiParam(); + } + else if (_isIntermediate(wch)) + { + _ActionCollect(wch); + _EnterCsiIntermediate(); + } + else if (_isParameterInvalid(wch)) + { + _EnterCsiIgnore(); + } + else + { + _ActionCsiDispatch(wch); + _EnterGround(); + _ExecuteCsiCompleteCallback(); } } @@ -1413,7 +1547,7 @@ void StateMachine::_EventSs3Entry(const wchar_t wch) { _ActionIgnore(); } - else if (_isCsiInvalid(wch)) + else if (_isSubParameterDelimiter(wch)) { // It's safe for us to go into the CSI ignore here, because both SS3 and // CSI sequences ignore characters the same way. @@ -1458,7 +1592,7 @@ void StateMachine::_EventSs3Param(const wchar_t wch) { _ActionParam(wch); } - else if (_isParameterInvalid(wch)) + else if (_isParameterInvalid(wch) || _isSubParameterDelimiter(wch)) { _EnterCsiIgnore(); } @@ -1531,7 +1665,7 @@ void StateMachine::_EventDcsEntry(const wchar_t wch) { _ActionIgnore(); } - else if (_isCsiInvalid(wch)) + else if (_isSubParameterDelimiter(wch)) { _EnterDcsIgnore(); } @@ -1635,7 +1769,7 @@ void StateMachine::_EventDcsParam(const wchar_t wch) _ActionCollect(wch); _EnterDcsIntermediate(); } - else if (_isParameterInvalid(wch)) + else if (_isParameterInvalid(wch) || _isSubParameterDelimiter(wch)) { _EnterDcsIgnore(); } @@ -1747,6 +1881,8 @@ void StateMachine::ProcessCharacter(const wchar_t wch) return _EventCsiIgnore(wch); case VTStates::CsiParam: return _EventCsiParam(wch); + case VTStates::CsiSubParam: + return _EventCsiSubParam(wch); case VTStates::OscParam: return _EventOscParam(wch); case VTStates::OscString: @@ -1818,6 +1954,119 @@ bool StateMachine::FlushToTerminal() return success; } +// Disable vectorization-unfriendly warnings. +#pragma warning(push) +#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + +// Returns true for C0 characters and C1 [single-character] CSI. +constexpr bool isActionableFromGround(const wchar_t wch) noexcept +{ + // This is equivalent to: + // return (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f); + // It's written like this to get MSVC to emit optimal assembly for findActionableFromGround. + // It lacks the ability to turn boolean operators into binary operations and also happens + // to fail to optimize the printable-ASCII range check into a subtraction & comparison. + return (wch <= 0x1f) | (static_cast(wch - 0x7f) <= 0x20); +} + +[[msvc::forceinline]] static size_t findActionableFromGroundPlain(const wchar_t* beg, const wchar_t* end, const wchar_t* it) noexcept +{ +#pragma loop(no_vector) + for (; it < end && !isActionableFromGround(*it); ++it) + { + } + return it - beg; +} + +static size_t findActionableFromGround(const wchar_t* data, size_t count) noexcept +{ + // The following vectorized code replicates isActionableFromGround which is equivalent to: + // (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f) + // or rather its more machine friendly equivalent: + // (wch <= 0x1f) | ((wch - 0x7f) <= 0x20) +#if defined(TIL_SSE_INTRINSICS) + + auto it = data; + + for (const auto end = data + (count & ~size_t{ 7 }); it < end; it += 8) + { + const auto wch = _mm_loadu_si128(reinterpret_cast(it)); + const auto z = _mm_setzero_si128(); + + // Dealing with unsigned numbers in SSE2 is annoying because it has poor support for that. + // We'll use subtractions with saturation ("SubS") to work around that. A check like + // a < b can be implemented as "max(0, a - b) == 0" and "max(0, a - b)" is what "SubS" is. + + // Check for (wch < 0x20) + auto a = _mm_subs_epu16(wch, _mm_set1_epi16(0x1f)); + // Check for "((wch - 0x7f) <= 0x20)" by adding 0x10000-0x7f, which overflows to a + // negative number if "wch >= 0x7f" and then subtracting 0x9f-0x7f with saturation to an + // unsigned number (= can't go lower than 0), which results in all numbers up to 0x9f to be 0. + auto b = _mm_subs_epu16(_mm_add_epi16(wch, _mm_set1_epi16(static_cast(0xff81))), _mm_set1_epi16(0x20)); + a = _mm_cmpeq_epi16(a, z); + b = _mm_cmpeq_epi16(b, z); + + const auto c = _mm_or_si128(a, b); + const auto mask = _mm_movemask_epi8(c); + + if (mask) + { + unsigned long offset; + _BitScanForward(&offset, mask); + it += offset / 2; + return it - data; + } + } + + return findActionableFromGroundPlain(data, data + count, it); + +#elif defined(TIL_ARM_NEON_INTRINSICS) + + auto it = data; + uint64_t mask; + + for (const auto end = data + (count & ~size_t{ 7 }); it < end;) + { + const auto wch = vld1q_u16(it); + const auto a = vcleq_u16(wch, vdupq_n_u16(0x1f)); + const auto b = vcleq_u16(vsubq_u16(wch, vdupq_n_u16(0x7f)), vdupq_n_u16(0x20)); + const auto c = vorrq_u16(a, b); + + mask = vgetq_lane_u64(c, 0); + if (mask) + { + goto exitWithMask; + } + it += 4; + + mask = vgetq_lane_u64(c, 1); + if (mask) + { + goto exitWithMask; + } + it += 4; + } + + return findActionableFromGroundPlain(data, data + count, it); + +exitWithMask: + unsigned long offset; + _BitScanForward64(&offset, mask); + it += offset / 16; + return it - data; + +#else + + return findActionableFromGroundPlain(data, data + count, p); + +#endif +} + +#pragma warning(pop) + // Routine Description: // - Helper for entry to the state machine. Will take an array of characters // and print as many as it can without encountering a character indicating @@ -1829,79 +2078,54 @@ bool StateMachine::FlushToTerminal() // - void StateMachine::ProcessString(const std::wstring_view string) { - size_t start = 0; - auto current = start; - + size_t i = 0; _currentString = string; _runOffset = 0; _runSize = 0; - while (current < string.size()) + if (_state != VTStates::Ground) { - // The run will be everything from the start INCLUDING the current one - // in case we process the current character and it turns into a passthrough - // fallback that picks up this _run inside `FlushToTerminal` above. - _runOffset = start; - _runSize = current - start + 1; + // Jump straight to where we need to. +#pragma warning(suppress : 26438) // Avoid 'goto'(es .76). + goto processStringLoopVtStart; + } - if (_processingIndividually) + while (i < string.size()) + { { - // Note whether we're dealing with the last character in the buffer. - _processingLastCharacter = (current + 1 >= string.size()); + _runOffset = i; + // Pointer arithmetic is perfectly fine for our hot path. +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).) + _runSize = findActionableFromGround(string.data() + i, string.size() - i); + + if (_runSize) + { + _ActionPrintString(_CurrentRun()); + + i += _runSize; + _runOffset = i; + _runSize = 0; + } + } + + processStringLoopVtStart: + if (i >= string.size()) + { + break; + } + + do + { + _runSize++; + _processingLastCharacter = i + 1 >= string.size(); // If we're processing characters individually, send it to the state machine. - ProcessCharacter(til::at(string, current)); - ++current; - if (_state == VTStates::Ground) // Then check if we're back at ground. If we are, the next character (pwchCurr) - { // is the start of the next run of characters that might be printable. - _processingIndividually = false; - start = current; - } - } - else - { - if (_isActionableFromGround(til::at(string, current))) // If the current char is the start of an escape sequence, or should be executed in ground state... - { - if (_runSize > 0) - { - // Because the run above is composed INCLUDING current, we must - // trim it off here since we just determined it's actionable - // and only pass through everything before it. - _runSize -= 1; - _ActionPrintString(_CurrentRun()); // ... print all the chars leading up to it as part of the run... - } - - _processingIndividually = true; // begin processing future characters individually... - start = current; - continue; - } - else - { - ++current; // Otherwise, add this char to the current run to be printed. - } - } + ProcessCharacter(til::at(string, i)); + ++i; + } while (i < string.size() && _state != VTStates::Ground); } - // When we leave the loop, current has been advanced to the length of the string itself - // (or one past the array index to the final char) so this `substr` operation doesn't +1 - // to include the final character (unlike the one inside the top of the loop above.) - if (start < string.size()) - { - _runOffset = start; - _runSize = std::string::npos; - } - else - { - _runSize = 0; - } - - const auto run = _CurrentRun(); // If we're at the end of the string and have remaining un-printed characters, - if (!_processingIndividually && !run.empty()) - { - // print the rest of the characters in the string - _ActionPrintString(run); - } - else if (_processingIndividually) + if (_state != VTStates::Ground) { // One of the "weird things" in VT input is the case of something like // alt+[. In VT, that's encoded as `\x1b[`. However, that's @@ -1921,6 +2145,7 @@ void StateMachine::ProcessString(const std::wstring_view string) // last character of the string. For our previous `\x1b[` scenario, that // means we'll make sure to call `_ActionEscDispatch('[')`., which will // properly decode the string as alt+[. + const auto run = _CurrentRun(); if (_isEngineForInput) { @@ -1949,6 +2174,7 @@ void StateMachine::ProcessString(const std::wstring_view string) case VTStates::CsiIntermediate: case VTStates::CsiIgnore: case VTStates::CsiParam: + case VTStates::CsiSubParam: _ActionCsiDispatch(*wchIter); break; case VTStates::OscParam: @@ -1996,6 +2222,18 @@ bool StateMachine::IsProcessingLastCharacter() const noexcept return _processingLastCharacter; } +// Routine Description: +// - Registers a function that will be called once the current CSI action is +// complete and the state machine has returned to the ground state. +// Arguments: +// - callback - The function that will be called +// Return Value: +// - +void StateMachine::OnCsiComplete(const std::function callback) +{ + _onCsiCompleteCallback = callback; +} + // Routine Description: // - Wherever the state machine is, whatever it's going, go back to ground. // This is used by conhost to "jiggle the handle" - when VT support is @@ -2048,13 +2286,23 @@ catch (...) return false; } -template -bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) +void StateMachine::_ExecuteCsiCompleteCallback() { - const bool success = _SafeExecute(std::forward(lambda)); - if (!success) + if (_onCsiCompleteCallback) { - TermTelemetry::Instance().LogFailed(wch); + // We need to save the state of the string that we're currently + // processing in case the callback injects another string. + const auto savedCurrentString = _currentString; + const auto savedRunOffset = _runOffset; + const auto savedRunSize = _runSize; + // We also need to take ownership of the callback function before + // executing it so there's no risk of it being run more than once. + const auto callback = std::move(_onCsiCompleteCallback); + callback(); + // Once the callback has returned, we can restore the original state + // and continue where we left off. + _currentString = savedCurrentString; + _runOffset = savedRunOffset; + _runSize = savedRunSize; } - return success; } diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp index 41bce50f97..01976b21b2 100644 --- a/src/terminal/parser/stateMachine.hpp +++ b/src/terminal/parser/stateMachine.hpp @@ -15,7 +15,6 @@ Abstract: #pragma once #include "IStateMachineEngine.hpp" -#include "telemetry.hpp" #include "tracing.hpp" #include @@ -32,6 +31,12 @@ namespace Microsoft::Console::VirtualTerminal // that number. constexpr size_t MAX_PARAMETER_COUNT = 32; + // Sub parameter limit for each parameter. + constexpr size_t MAX_SUBPARAMETER_COUNT = 6; + // we limit ourself to 256 sub parameters because we use bytes to store + // the their indexes. + static_assert(MAX_PARAMETER_COUNT * MAX_SUBPARAMETER_COUNT <= 256); + class StateMachine final { #ifdef UNIT_TESTING @@ -61,6 +66,8 @@ namespace Microsoft::Console::VirtualTerminal void ProcessString(const std::wstring_view string); bool IsProcessingLastCharacter() const noexcept; + void OnCsiComplete(const std::function callback); + void ResetState() noexcept; bool FlushToTerminal(); @@ -84,6 +91,7 @@ namespace Microsoft::Console::VirtualTerminal void _ActionVt52EscDispatch(const wchar_t wch); void _ActionCollect(const wchar_t wch) noexcept; void _ActionParam(const wchar_t wch); + void _ActionSubParam(const wchar_t wch); void _ActionCsiDispatch(const wchar_t wch); void _ActionOscParam(const wchar_t wch) noexcept; void _ActionOscPut(const wchar_t wch); @@ -100,6 +108,7 @@ namespace Microsoft::Console::VirtualTerminal void _EnterEscapeIntermediate() noexcept; void _EnterCsiEntry(); void _EnterCsiParam() noexcept; + void _EnterCsiSubParam() noexcept; void _EnterCsiIgnore() noexcept; void _EnterCsiIntermediate() noexcept; void _EnterOscParam() noexcept; @@ -122,6 +131,7 @@ namespace Microsoft::Console::VirtualTerminal void _EventCsiIntermediate(const wchar_t wch); void _EventCsiIgnore(const wchar_t wch); void _EventCsiParam(const wchar_t wch); + void _EventCsiSubParam(const wchar_t wch); void _EventOscParam(const wchar_t wch) noexcept; void _EventOscString(const wchar_t wch); void _EventOscTermination(const wchar_t wch); @@ -139,8 +149,8 @@ namespace Microsoft::Console::VirtualTerminal template bool _SafeExecute(TLambda&& lambda); - template - bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda); + + void _ExecuteCsiCompleteCallback(); enum class VTStates { @@ -151,6 +161,7 @@ namespace Microsoft::Console::VirtualTerminal CsiIntermediate, CsiIgnore, CsiParam, + CsiSubParam, OscParam, OscString, OscTermination, @@ -189,7 +200,11 @@ namespace Microsoft::Console::VirtualTerminal VTIDBuilder _identifier; std::vector _parameters; - bool _parameterLimitReached; + bool _parameterLimitOverflowed; + std::vector _subParameters; + std::vector> _subParameterRanges; + bool _subParameterLimitOverflowed; + BYTE _subParameterCounter; std::wstring _oscString; VTInt _oscParameter; @@ -200,7 +215,8 @@ namespace Microsoft::Console::VirtualTerminal // This is tracked per state machine instance so that separate calls to Process* // can start and finish a sequence. - bool _processingIndividually; bool _processingLastCharacter; + + std::function _onCsiCompleteCallback; }; } diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp deleted file mode 100644 index 4812fb7754..0000000000 --- a/src/terminal/parser/telemetry.cpp +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include - -#include "telemetry.hpp" - -#pragma warning(push) -#pragma warning(disable : 26494) // _Tlgdata uninitialized from TraceLoggingWrite -#pragma warning(disable : 26477) // Use nullptr instead of NULL or 0 from TraceLoggingWrite -#pragma warning(disable : 26485) // _Tlgdata, no array to pointer decay from TraceLoggingWrite -#pragma warning(disable : 26446) // Prefer gsl::at over unchecked subscript from TraceLoggingLevel -#pragma warning(disable : 26482) // Only index to arrays with constant expressions from TraceLoggingLevel - -TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider, - "Microsoft.Windows.Console.VirtualTerminal.Parser", - // {c9ba2a84-d3ca-5e19-2bd6-776a0910cb9d} - (0xc9ba2a84, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d)); - -using namespace Microsoft::Console::VirtualTerminal; - -#pragma warning(push) -// Disable 4351 so we can initialize the arrays to 0 without a warning. -#pragma warning(disable : 4351) -TermTelemetry::TermTelemetry() noexcept : - _uiTimesUsedCurrent(0), - _uiTimesFailedCurrent(0), - _uiTimesFailedOutsideRangeCurrent(0), - _uiTimesUsed(), - _uiTimesFailed(), - _uiTimesFailedOutsideRange(0), - _activityId(), - _fShouldWriteFinalLog(false) -{ - TraceLoggingRegister(g_hConsoleVirtTermParserEventTraceProvider); - - // Create a random activityId just in case it doesn't get set later in SetActivityId(). - EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &_activityId); -} -#pragma warning(pop) - -TermTelemetry::~TermTelemetry() -{ - try - { - WriteFinalTraceLog(); - TraceLoggingUnregister(g_hConsoleVirtTermParserEventTraceProvider); - } - CATCH_LOG() -} - -// Routine Description: -// - Logs the usage of a particular VT100 code. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::Log(const Codes code) noexcept -{ - // Initially we wanted to pass over a string (ex. "CUU") and use a dictionary data type to hold the counts. - // However we would have to search through the dictionary every time we called this method, so we decided - // to use an array which has very quick access times. - // The downside is we have to create an enum type, and then convert them to strings when we finally - // send out the telemetry, but the upside is we should have very good performance. - _uiTimesUsed[code]++; - _uiTimesUsedCurrent++; -} - -// Routine Description: -// - Logs a particular VT100 escape code failed or was unsupported. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::LogFailed(const wchar_t wch) noexcept -{ - if (wch > CHAR_MAX) - { - _uiTimesFailedOutsideRange++; - _uiTimesFailedOutsideRangeCurrent++; - } - else - { - // Even though we pass over a wide character, we only care about the ASCII single byte character. - _uiTimesFailed[wch]++; - _uiTimesFailedCurrent++; - } -} - -// Routine Description: -// - Gets and resets the total count of codes used. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesUsedCurrent() noexcept -{ - const auto temp = _uiTimesUsedCurrent; - _uiTimesUsedCurrent = 0; - return temp; -} - -// Routine Description: -// - Gets and resets the total count of codes failed. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesFailedCurrent() noexcept -{ - const auto temp = _uiTimesFailedCurrent; - _uiTimesFailedCurrent = 0; - return temp; -} - -// Routine Description: -// - Gets and resets the total count of codes failed outside the valid range. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesFailedOutsideRangeCurrent() noexcept -{ - const auto temp = _uiTimesFailedOutsideRangeCurrent; - _uiTimesFailedOutsideRangeCurrent = 0; - return temp; -} - -// Routine Description: -// - Lets us know whether we should write the final log. Typically set true when the console has been -// interacted with, to help reduce the amount of telemetry we're sending. -// -// Arguments: -// - writeLog - true if we should write the log. -// Return Value: -// - -void TermTelemetry::SetShouldWriteFinalLog(const bool writeLog) noexcept -{ - _fShouldWriteFinalLog = writeLog; -} - -// Routine Description: -// - Sets the activity Id, so we can match our events with other providers (such as Microsoft.Windows.Console.Host). -// -// Arguments: -// - activityId - Pointer to Guid to set our activity Id to. -// Return Value: -// - -void TermTelemetry::SetActivityId(const GUID* activityId) noexcept -{ - _activityId = *activityId; -} - -// Routine Description: -// - Writes the final log of all the telemetry collected. The primary reason to send back a final log instead -// of individual events is to reduce the amount of telemetry being sent and potentially overloading our servers. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::WriteFinalTraceLog() const -{ - if (_fShouldWriteFinalLog) - { - // Determine if we've logged any VT100 sequences at all. - auto fLoggedSequence = (_uiTimesFailedOutsideRange > 0); - - if (!fLoggedSequence) - { - for (auto n = 0; n < ARRAYSIZE(_uiTimesUsed); n++) - { - if (_uiTimesUsed[n] > 0) - { - fLoggedSequence = true; - break; - } - } - } - - if (!fLoggedSequence) - { - for (auto n = 0; n < ARRAYSIZE(_uiTimesFailed); n++) - { - if (_uiTimesFailed[n] > 0) - { - fLoggedSequence = true; - break; - } - } - } - - // Only send telemetry if we've logged some VT100 sequences. This should help reduce the amount of unnecessary - // telemetry being sent. - if (fLoggedSequence) - { - // I could use the TraceLoggingUIntArray, but then we would have to know the order of the enums on the backend. - // So just log each enum count separately with its string representation which makes it more human readable. - // Set the related activity to NULL since we aren't using it. - TraceLoggingWriteActivity(g_hConsoleVirtTermParserEventTraceProvider, - "ControlCodesUsed", - &_activityId, - NULL, - TraceLoggingUInt32(_uiTimesUsed[CUU], "CUU"), - TraceLoggingUInt32(_uiTimesUsed[CUD], "CUD"), - TraceLoggingUInt32(_uiTimesUsed[CUF], "CUF"), - TraceLoggingUInt32(_uiTimesUsed[CUB], "CUB"), - TraceLoggingUInt32(_uiTimesUsed[CNL], "CNL"), - TraceLoggingUInt32(_uiTimesUsed[CPL], "CPL"), - TraceLoggingUInt32(_uiTimesUsed[CHA], "CHA"), - TraceLoggingUInt32(_uiTimesUsed[CUP], "CUP"), - TraceLoggingUInt32(_uiTimesUsed[ED], "ED"), - TraceLoggingUInt32(_uiTimesUsed[DECSED], "DECSED"), - TraceLoggingUInt32(_uiTimesUsed[EL], "EL"), - TraceLoggingUInt32(_uiTimesUsed[DECSEL], "DECSEL"), - TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"), - TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"), - TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"), - TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"), - TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"), - TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"), - TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"), - TraceLoggingUInt32(_uiTimesUsed[DSR], "DSR"), - TraceLoggingUInt32(_uiTimesUsed[DA], "DA"), - TraceLoggingUInt32(_uiTimesUsed[DA2], "DA2"), - TraceLoggingUInt32(_uiTimesUsed[DA3], "DA3"), - TraceLoggingUInt32(_uiTimesUsed[DECREQTPARM], "DECREQTPARM"), - TraceLoggingUInt32(_uiTimesUsed[VPA], "VPA"), - TraceLoggingUInt32(_uiTimesUsed[HPR], "HPR"), - TraceLoggingUInt32(_uiTimesUsed[VPR], "VPR"), - TraceLoggingUInt32(_uiTimesUsed[ICH], "ICH"), - TraceLoggingUInt32(_uiTimesUsed[DCH], "DCH"), - TraceLoggingUInt32(_uiTimesUsed[IL], "IL"), - TraceLoggingUInt32(_uiTimesUsed[DL], "DL"), - TraceLoggingUInt32(_uiTimesUsed[SU], "SU"), - TraceLoggingUInt32(_uiTimesUsed[SD], "SD"), - TraceLoggingUInt32(_uiTimesUsed[ANSISYSSC], "ANSISYSSC"), - TraceLoggingUInt32(_uiTimesUsed[ANSISYSRC], "ANSISYSRC"), - TraceLoggingUInt32(_uiTimesUsed[DECSTBM], "DECSTBM"), - TraceLoggingUInt32(_uiTimesUsed[NEL], "NEL"), - TraceLoggingUInt32(_uiTimesUsed[IND], "IND"), - TraceLoggingUInt32(_uiTimesUsed[RI], "RI"), - TraceLoggingUInt32(_uiTimesUsed[OSCWT], "OscWindowTitle"), - TraceLoggingUInt32(_uiTimesUsed[HTS], "HTS"), - TraceLoggingUInt32(_uiTimesUsed[CHT], "CHT"), - TraceLoggingUInt32(_uiTimesUsed[CBT], "CBT"), - TraceLoggingUInt32(_uiTimesUsed[TBC], "TBC"), - TraceLoggingUInt32(_uiTimesUsed[ECH], "ECH"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG0], "DesignateG0"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG1], "DesignateG1"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG2], "DesignateG2"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG3], "DesignateG3"), - TraceLoggingUInt32(_uiTimesUsed[LS2], "LS2"), - TraceLoggingUInt32(_uiTimesUsed[LS3], "LS3"), - TraceLoggingUInt32(_uiTimesUsed[LS1R], "LS1R"), - TraceLoggingUInt32(_uiTimesUsed[LS2R], "LS2R"), - TraceLoggingUInt32(_uiTimesUsed[LS3R], "LS3R"), - TraceLoggingUInt32(_uiTimesUsed[SS2], "SS2"), - TraceLoggingUInt32(_uiTimesUsed[SS3], "SS3"), - TraceLoggingUInt32(_uiTimesUsed[DOCS], "DOCS"), - TraceLoggingUInt32(_uiTimesUsed[HVP], "HVP"), - TraceLoggingUInt32(_uiTimesUsed[DECSTR], "DECSTR"), - TraceLoggingUInt32(_uiTimesUsed[RIS], "RIS"), - TraceLoggingUInt32(_uiTimesUsed[DECSCUSR], "DECSCUSR"), - TraceLoggingUInt32(_uiTimesUsed[DECSCA], "DECSCA"), - TraceLoggingUInt32(_uiTimesUsed[DTTERM_WM], "DTTERM_WM"), - TraceLoggingUInt32(_uiTimesUsed[OSCCT], "OscColorTable"), - TraceLoggingUInt32(_uiTimesUsed[OSCSCC], "OscSetCursorColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCRCC], "OscResetCursorColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCFG], "OscForegroundColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCBG], "OscBackgroundColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"), - TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), - TraceLoggingUInt32(_uiTimesUsed[DECAC1], "DECAC1"), - TraceLoggingUInt32(_uiTimesUsed[DECSWL], "DECSWL"), - TraceLoggingUInt32(_uiTimesUsed[DECDWL], "DECDWL"), - TraceLoggingUInt32(_uiTimesUsed[DECDHL], "DECDHL"), - TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"), - TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"), - TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"), - TraceLoggingUInt32(_uiTimesUsed[DECCARA], "DECCARA"), - TraceLoggingUInt32(_uiTimesUsed[DECRARA], "DECRARA"), - TraceLoggingUInt32(_uiTimesUsed[DECCRA], "DECCRA"), - TraceLoggingUInt32(_uiTimesUsed[DECFRA], "DECFRA"), - TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"), - TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"), - TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"), - TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"), - TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"), - TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"), - TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange")); - } - } -} - -#pragma warning(pop) diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp deleted file mode 100644 index 283a163402..0000000000 --- a/src/terminal/parser/telemetry.hpp +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/* -Module Name: -- telemetry.hpp - -Abstract: -- This module is used for recording all telemetry feedback from the console virtual terminal parser -*/ -#pragma once - -// Including TraceLogging essentials for the binary -#include -#include -#include -#include "climits" - -TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); - -namespace Microsoft::Console::VirtualTerminal -{ - class TermTelemetry sealed - { - public: - // Implement this as a singleton class. - static TermTelemetry& Instance() noexcept - { - static TermTelemetry s_Instance; - return s_Instance; - } - - // Names primarily from http://inwap.com/pdp10/ansicode.txt - enum Codes - { - CUU = 0, - CUD, - CUF, - CUB, - CNL, - CPL, - CHA, - CUP, - ED, - DECSED, - EL, - DECSEL, - SGR, - DECSC, - DECRC, - DECSET, - DECRST, - DECKPAM, - DECKPNM, - DSR, - DA, - DA2, - DA3, - DECREQTPARM, - VPA, - HPR, - VPR, - ICH, - DCH, - SU, - SD, - ANSISYSSC, - ANSISYSRC, - IL, - DL, - DECSTBM, - NEL, - IND, - RI, - OSCWT, - HTS, - CHT, - CBT, - TBC, - ECH, - DesignateG0, - DesignateG1, - DesignateG2, - DesignateG3, - LS2, - LS3, - LS1R, - LS2R, - LS3R, - SS2, - SS3, - DOCS, - HVP, - DECSTR, - RIS, - DECSCUSR, - DECSCA, - DTTERM_WM, - OSCCT, - OSCSCC, - OSCRCC, - REP, - OSCFG, - OSCBG, - DECAC1, - DECSWL, - DECDWL, - DECDHL, - DECALN, - OSCSCB, - XTPUSHSGR, - XTPOPSGR, - DECCARA, - DECRARA, - DECCRA, - DECFRA, - DECERA, - DECSERA, - DECSACE, - DECAC, - DECPS, - // Only use this last enum as a count of the number of codes. - NUMBER_OF_CODES - }; - void Log(const Codes code) noexcept; - void LogFailed(const wchar_t wch) noexcept; - void SetShouldWriteFinalLog(const bool writeLog) noexcept; - void SetActivityId(const GUID* activityId) noexcept; - unsigned int GetAndResetTimesUsedCurrent() noexcept; - unsigned int GetAndResetTimesFailedCurrent() noexcept; - unsigned int GetAndResetTimesFailedOutsideRangeCurrent() noexcept; - - private: - // Used to prevent multiple instances - TermTelemetry() noexcept; - ~TermTelemetry(); - TermTelemetry(const TermTelemetry&) = delete; - TermTelemetry(TermTelemetry&&) = delete; - TermTelemetry& operator=(const TermTelemetry&) = delete; - TermTelemetry& operator=(TermTelemetry&&) = delete; - - void WriteFinalTraceLog() const; - - unsigned int _uiTimesUsedCurrent; - unsigned int _uiTimesFailedCurrent; - unsigned int _uiTimesFailedOutsideRangeCurrent; - unsigned int _uiTimesUsed[NUMBER_OF_CODES]; - unsigned int _uiTimesFailed[CHAR_MAX + 1]; - unsigned int _uiTimesFailedOutsideRange; - GUID _activityId; - - bool _fShouldWriteFinalLog; - }; -} diff --git a/src/terminal/parser/tracing.cpp b/src/terminal/parser/tracing.cpp index 5ddc5f453e..9ab684ac95 100644 --- a/src/terminal/parser/tracing.cpp +++ b/src/terminal/parser/tracing.cpp @@ -10,6 +10,11 @@ using namespace Microsoft::Console::VirtualTerminal; #pragma warning(disable : 26447) // The function is declared 'noexcept' but calls function '_tlgWrapBinary()' which may throw exceptions #pragma warning(disable : 26477) // Use 'nullptr' rather than 0 or NULL +TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider, + "Microsoft.Windows.Console.VirtualTerminal.Parser", + // {c9ba2a84-d3ca-5e19-2bd6-776a0910cb9d} + (0xc9ba2a84, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d)); + void ParserTracing::TraceStateChange(_In_z_ const wchar_t* name) const noexcept { TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, diff --git a/src/terminal/parser/tracing.hpp b/src/terminal/parser/tracing.hpp index 27e3e3352d..ecc357c323 100644 --- a/src/terminal/parser/tracing.hpp +++ b/src/terminal/parser/tracing.hpp @@ -14,7 +14,11 @@ Abstract: #pragma once -#include "telemetry.hpp" +// Including TraceLogging essentials for the binary +#include +#include + +TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); namespace Microsoft::Console::VirtualTerminal { diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp index 80a1a92f92..2e21c77c74 100644 --- a/src/terminal/parser/ut_parser/InputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -75,11 +75,10 @@ public: { } - 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 @@ public: 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 @@ public: 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 @@ public: 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); @@ -960,35 +950,19 @@ void InputEngineTest::AltIntermediateTest() // We'll test this by creating both a TerminalInput and an // InputStateMachine, and piping the KeyEvents generated by the // InputStateMachine into the TerminalInput. - std::wstring expectedTranslation{}; - - // First create the callback TerminalInput will call - this will be - // triggered second, after both the state machine and the TerminalInput have - // translated the characters. - auto pfnTerminalInputCallback = [&](std::deque>& inEvents) { - // Get all the characters: - std::wstring wstr = L""; - for (auto& ev : inEvents) - { - if (ev->EventType() == InputEventType::KeyEvent) - { - auto& k = static_cast(*ev); - auto wch = k.GetCharData(); - wstr += wch; - } - } - - VERIFY_ARE_EQUAL(expectedTranslation, wstr); - }; - TerminalInput terminalInput{ pfnTerminalInputCallback }; + std::wstring translation; + TerminalInput terminalInput; // Create the callback that's fired when the state machine wants to write // input. We'll take the events and put them straight into the // TerminalInput. - auto pfnInputStateMachineCallback = [&](std::deque>& inEvents) { + auto pfnInputStateMachineCallback = [&](const std::span& inEvents) { for (auto& ev : inEvents) { - terminalInput.HandleKey(ev.get()); + if (const auto str = terminalInput.HandleKey(ev)) + { + translation.append(*str); + } } }; auto dispatch = std::make_unique(pfnInputStateMachineCallback, &testState); @@ -997,18 +971,20 @@ void InputEngineTest::AltIntermediateTest() VERIFY_IS_NOT_NULL(stateMachine); testState._stateMachine = stateMachine.get(); - // Write a Alt+/, Ctrl+e pair to the input engine, then take it's output and + // Write a Alt+/, Ctrl+e pair to the input engine, then take its output and // run it through the terminalInput translator. We should get ^[/^E back // out. std::wstring seq = L"\x1b/"; - expectedTranslation = seq; + translation.clear(); Log::Comment(NoThrowString().Format(L"Processing \"\\x1b/\"")); stateMachine->ProcessString(seq); + VERIFY_ARE_EQUAL(seq, translation); seq = L"\x05"; // 0x05 is ^E - expectedTranslation = seq; + translation.clear(); Log::Comment(NoThrowString().Format(L"Processing \"\\x05\"")); stateMachine->ProcessString(seq); + VERIFY_ARE_EQUAL(seq, translation); VerifyExpectedInputDrained(); } @@ -1094,7 +1070,7 @@ std::wstring InputEngineTest::GenerateSgrMouseSequence(const CsiMouseButtonCodes const wchar_t prefixChar = direction[0]; const wchar_t finalChar = direction[1]; - return wil::str_printf_failfast(L"\x1b[%c%d;%d;%d%c", prefixChar, static_cast(actionCode), position.X, position.Y, finalChar); + return wil::str_printf_failfast(L"\x1b[%c%d;%d;%d%c", prefixChar, static_cast(actionCode), position.x, position.y, finalChar); } void InputEngineTest::VerifySGRMouseData(const std::vector> testData) @@ -1488,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); } } @@ -1594,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/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp index 12246eb14d..9695c80936 100644 --- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -391,7 +391,113 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); } - TEST_METHOD(TestCsiIgnore) + TEST_METHOD(TestCsiSubParam) + { + auto dispatch = std::make_unique(); + auto engine = std::make_unique(std::move(dispatch)); + StateMachine mach(std::move(engine)); + + // "\e[:3;9:5::8J" + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'9'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L'5'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L'8'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + VERIFY_ARE_EQUAL(mach._parameters.size(), 2u); + VERIFY_IS_FALSE(mach._parameters.at(0).has_value()); + VERIFY_ARE_EQUAL(mach._parameters.at(1), 9); + + VERIFY_ARE_EQUAL(mach._subParameters.size(), 4u); + VERIFY_ARE_EQUAL(mach._subParameters.at(0), 3); + VERIFY_ARE_EQUAL(mach._subParameters.at(1), 5); + VERIFY_IS_FALSE(mach._subParameters.at(2).has_value()); + VERIFY_ARE_EQUAL(mach._subParameters.at(3), 8); + + VERIFY_ARE_EQUAL(mach._subParameterRanges.at(0).first, 0); + VERIFY_ARE_EQUAL(mach._subParameterRanges.at(0).second, 1); + VERIFY_ARE_EQUAL(mach._subParameterRanges.at(1).first, 1); + VERIFY_ARE_EQUAL(mach._subParameterRanges.at(1).second, 4); + + VERIFY_ARE_EQUAL(mach._subParameterRanges.size(), mach._parameters.size()); + VERIFY_IS_TRUE( + (mach._subParameterRanges.back().second == mach._subParameters.size() - 1) // lastIndex + || (mach._subParameterRanges.back().second == mach._subParameters.size()) // or lastIndex + 1 + ); + } + + TEST_METHOD(TestCsiMaxSubParamCount) + { + auto dispatch = std::make_unique(); + auto engine = std::make_unique(std::move(dispatch)); + StateMachine mach(std::move(engine)); + + Log::Comment(L"Output two parameters with 100 sub parameters each"); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + for (size_t nParam = 0; nParam < 2; nParam++) + { + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + for (size_t i = 0; i < 100; i++) + { + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + mach.ProcessCharacter(L'0' + i % 10); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + } + Log::Comment(L"Receiving 100 sub parameters should set the overflow flag"); + VERIFY_IS_TRUE(mach._subParameterLimitOverflowed); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + VERIFY_IS_FALSE(mach._subParameterLimitOverflowed); + } + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + Log::Comment(L"Only MAX_SUBPARAMETER_COUNT (6) sub parameters should be stored for each parameter"); + VERIFY_ARE_EQUAL(mach._subParameters.size(), 12u); + + // Verify that first 6 sub parameters are stored for each parameter. + // subParameters = { 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5 } + for (size_t i = 0; i < 12; i++) + { + VERIFY_IS_TRUE(mach._subParameters.at(i).has_value()); + VERIFY_ARE_EQUAL(mach._subParameters.at(i).value(), gsl::narrow_cast(i % 6)); + } + + auto firstRange = mach._subParameterRanges.at(0); + auto secondRange = mach._subParameterRanges.at(1); + VERIFY_ARE_EQUAL(firstRange.first, 0); + VERIFY_ARE_EQUAL(firstRange.second, 6); + VERIFY_ARE_EQUAL(secondRange.first, 6); + VERIFY_ARE_EQUAL(secondRange.second, 12); + } + + TEST_METHOD(TestLeadingZeroCsiSubParam) { auto dispatch = std::make_unique(); auto engine = std::make_unique(std::move(dispatch)); @@ -402,12 +508,30 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); mach.ProcessCharacter(L'['); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); - mach.ProcessCharacter(L':'); - VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); mach.ProcessCharacter(L'3'); - VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); - mach.ProcessCharacter(L'q'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + for (auto i = 0; i < 50; i++) // Any number of leading zeros should be supported + { + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + } + for (auto i = 0; i < 5; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'1' + i)); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiSubParam); + } + VERIFY_ARE_EQUAL(mach._subParameters.back(), 12345); + mach.ProcessCharacter(L'J'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestCsiIgnore) + { + auto dispatch = std::make_unique(); + auto engine = std::make_unique(std::move(dispatch)); + StateMachine mach(std::move(engine)); mach.ProcessCharacter(AsciiChars::ESC); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); @@ -417,7 +541,7 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); mach.ProcessCharacter(L';'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); - mach.ProcessCharacter(L':'); + mach.ProcessCharacter(L'='); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); mach.ProcessCharacter(L'8'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); @@ -1009,7 +1133,6 @@ public: _cursorPosition{ false }, _cursorSave{ false }, _cursorLoad{ false }, - _cursorVisible{ true }, _eraseDisplay{ false }, _eraseLine{ false }, _insertCharacter{ false }, @@ -1025,13 +1148,9 @@ public: _vt52DeviceAttributes{ false }, _requestTerminalParameters{ false }, _reportingPermission{ (DispatchTypes::ReportingPermission)-1 }, - _isAltBuffer{ false }, - _cursorKeysMode{ false }, - _cursorBlinking{ true }, - _isInAnsiMode{ true }, - _isScreenModeReversed{ false }, - _isOriginModeRelative{ false }, - _isAutoWrapEnabled{ true }, + _modeType{ (DispatchTypes::ModeParams)-1 }, + _modeTypes{}, + _modeEnabled{ false }, _warningBell{ false }, _carriageReturn{ false }, _lineFeed{ false }, @@ -1041,10 +1160,6 @@ public: _numTabs{ 0 }, _tabClear{ false }, _tabClearTypes{}, - _isDECCOLMAllowed{ false }, - _windowWidth{ 80 }, - _bracketedPasteMode{ false }, - _win32InputMode{ false }, _setDefaultForeground(false), _defaultForegroundColor{ RGB(0, 0, 0) }, _setDefaultBackground(false), @@ -1184,12 +1299,6 @@ public: return true; } - bool CursorVisibility(const bool fIsVisible) noexcept override - { - _cursorVisible = fIsVisible; - return true; - } - bool SetGraphicsRendition(const VTParameters options) noexcept override try { @@ -1203,7 +1312,7 @@ public: } CATCH_LOG_RETURN_FALSE() - bool DeviceStatusReport(const DispatchTypes::StatusType statusType) noexcept override + bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter /*id*/) noexcept override { _deviceStatusReport = true; _statusReportType = statusType; @@ -1249,53 +1358,11 @@ public: bool _ModeParamsHelper(_In_ DispatchTypes::ModeParams const param, const bool fEnable) { - auto fSuccess = false; - switch (param) - { - case DispatchTypes::ModeParams::DECCKM_CursorKeysMode: - // set - Enable Application Mode, reset - Numeric/normal mode - fSuccess = SetVirtualTerminalInputMode(fEnable); - break; - case DispatchTypes::ModeParams::DECANM_AnsiMode: - fSuccess = SetAnsiMode(fEnable); - break; - case DispatchTypes::ModeParams::DECCOLM_SetNumberOfColumns: - fSuccess = SetColumns(static_cast(fEnable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns)); - break; - case DispatchTypes::ModeParams::DECSCNM_ScreenMode: - fSuccess = SetScreenMode(fEnable); - break; - case DispatchTypes::ModeParams::DECOM_OriginMode: - // The cursor is also moved to the new home position when the origin mode is set or reset. - fSuccess = SetOriginMode(fEnable) && CursorPosition(1, 1); - break; - case DispatchTypes::ModeParams::DECAWM_AutoWrapMode: - fSuccess = SetAutoWrapMode(fEnable); - break; - case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - fSuccess = EnableCursorBlinking(fEnable); - break; - case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - fSuccess = CursorVisibility(fEnable); - break; - case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: - fSuccess = EnableDECCOLMSupport(fEnable); - break; - case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer: - fSuccess = fEnable ? UseAlternateScreenBuffer() : UseMainScreenBuffer(); - break; - case DispatchTypes::ModeParams::XTERM_BracketedPasteMode: - fSuccess = EnableXtermBracketedPasteMode(fEnable); - break; - case DispatchTypes::ModeParams::W32IM_Win32InputMode: - fSuccess = EnableWin32InputMode(fEnable); - break; - default: - // If no functions to call, overall dispatch was a failure. - fSuccess = false; - break; - } - return fSuccess; + _modeType = param; + _modeTypes.push_back(param); + _modeEnabled = fEnable; + + return true; } bool SetMode(const DispatchTypes::ModeParams param) noexcept override @@ -1308,57 +1375,9 @@ public: return _ModeParamsHelper(param, false); } - bool SetColumns(const VTInt uiColumns) noexcept override - { - _windowWidth = uiColumns; - return true; - } - - bool SetVirtualTerminalInputMode(const bool fApplicationMode) noexcept - { - _cursorKeysMode = fApplicationMode; - return true; - } - - bool EnableXtermBracketedPasteMode(const bool enable) noexcept - { - _bracketedPasteMode = enable; - return true; - } - - bool EnableWin32InputMode(const bool enable) noexcept - { - _win32InputMode = enable; - return true; - } - - bool EnableCursorBlinking(const bool bEnable) noexcept override - { - _cursorBlinking = bEnable; - return true; - } - bool SetAnsiMode(const bool ansiMode) noexcept override { - _isInAnsiMode = ansiMode; - return true; - } - - bool SetScreenMode(const bool reverseMode) noexcept override - { - _isScreenModeReversed = reverseMode; - return true; - } - - bool SetOriginMode(const bool fRelativeMode) noexcept override - { - _isOriginModeRelative = fRelativeMode; - return true; - } - - bool SetAutoWrapMode(const bool wrapAtEOL) noexcept override - { - _isAutoWrapEnabled = wrapAtEOL; + _ModeParamsHelper(DispatchTypes::DECANM_AnsiMode, ansiMode); return true; } @@ -1400,25 +1419,6 @@ public: _tabClearTypes.push_back(clearType); return true; } - - bool EnableDECCOLMSupport(const bool fEnabled) noexcept override - { - _isDECCOLMAllowed = fEnabled; - return true; - } - - bool UseAlternateScreenBuffer() noexcept override - { - _isAltBuffer = true; - return true; - } - - bool UseMainScreenBuffer() noexcept override - { - _isAltBuffer = false; - return true; - } - bool SetColorTableEntry(const size_t tableIndex, const COLORREF color) noexcept override { _setColorTableEntry = true; @@ -1494,7 +1494,6 @@ public: bool _cursorPosition; bool _cursorSave; bool _cursorLoad; - bool _cursorVisible; bool _eraseDisplay; bool _eraseLine; bool _insertCharacter; @@ -1510,13 +1509,9 @@ public: bool _vt52DeviceAttributes; bool _requestTerminalParameters; DispatchTypes::ReportingPermission _reportingPermission; - bool _isAltBuffer; - bool _cursorKeysMode; - bool _cursorBlinking; - bool _isInAnsiMode; - bool _isScreenModeReversed; - bool _isOriginModeRelative; - bool _isAutoWrapEnabled; + DispatchTypes::ModeParams _modeType; + std::vector _modeTypes; + bool _modeEnabled; bool _warningBell; bool _carriageReturn; bool _lineFeed; @@ -1526,10 +1521,6 @@ public: size_t _numTabs; bool _tabClear; std::vector _tabClearTypes; - bool _isDECCOLMAllowed; - size_t _windowWidth; - bool _bracketedPasteMode; - bool _win32InputMode; bool _setDefaultForeground; DWORD _defaultForegroundColor; bool _setDefaultBackground; @@ -1790,12 +1781,10 @@ class StateMachineExternalTest final pDispatch->ClearState(); - mach.ProcessCharacter(AsciiChars::ESC); - mach.ProcessCharacter(L'['); - mach.ProcessCharacter(L's'); - VERIFY_IS_TRUE(pDispatch->_cursorSave); - - pDispatch->ClearState(); + // Note that CSI s is dispatched as SetLeftRightScrollingMargins rather + // than CursorSaveState, so we don't test that here. The CursorSaveState + // will only be triggered by this sequence (in AdaptDispatch) when the + // Left-Right-Margin mode (DECLRMM) is disabled. mach.ProcessCharacter(AsciiChars::ESC); mach.ProcessCharacter(L'['); @@ -1805,24 +1794,6 @@ class StateMachineExternalTest final pDispatch->ClearState(); } - TEST_METHOD(TestCursorKeysMode) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?1h"); - VERIFY_IS_TRUE(pDispatch->_cursorKeysMode); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?1l"); - VERIFY_IS_FALSE(pDispatch->_cursorKeysMode); - - pDispatch->ClearState(); - } - TEST_METHOD(TestAnsiMode) { auto dispatch = std::make_unique(); @@ -1830,190 +1801,49 @@ class StateMachineExternalTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + pDispatch->_modeEnabled = true; mach.ProcessString(L"\x1b[?2l"); - VERIFY_IS_FALSE(pDispatch->_isInAnsiMode); + VERIFY_ARE_EQUAL(DispatchTypes::DECANM_AnsiMode, pDispatch->_modeType); + VERIFY_IS_FALSE(pDispatch->_modeEnabled); pDispatch->ClearState(); - pDispatch->_isInAnsiMode = false; mach.SetParserMode(StateMachine::Mode::Ansi, false); mach.ProcessString(L"\x1b<"); - VERIFY_IS_TRUE(pDispatch->_isInAnsiMode); + VERIFY_ARE_EQUAL(DispatchTypes::DECANM_AnsiMode, pDispatch->_modeType); + VERIFY_IS_TRUE(pDispatch->_modeEnabled); pDispatch->ClearState(); } - TEST_METHOD(TestSetNumberOfColumns) + TEST_METHOD(TestPrivateModes) { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 12, 25, 40, 1049}") + END_TEST_METHOD_PROPERTIES() + + VTInt modeNumber; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"modeNumber", modeNumber)); + + const auto modeType = DispatchTypes::DECPrivateMode(modeNumber); + const auto setModeSequence = wil::str_printf(L"\x1b[?%dh", modeNumber); + const auto resetModeSequence = wil::str_printf(L"\x1b[?%dl", modeNumber); + auto dispatch = std::make_unique(); auto pDispatch = dispatch.get(); auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); - mach.ProcessString(L"\x1b[?3h"); - VERIFY_ARE_EQUAL(pDispatch->_windowWidth, static_cast(DispatchTypes::s_sDECCOLMSetColumns)); + mach.ProcessString(setModeSequence); + VERIFY_ARE_EQUAL(modeType, pDispatch->_modeType); + VERIFY_IS_TRUE(pDispatch->_modeEnabled); pDispatch->ClearState(); + pDispatch->_modeEnabled = true; - mach.ProcessString(L"\x1b[?3l"); - VERIFY_ARE_EQUAL(pDispatch->_windowWidth, static_cast(DispatchTypes::s_sDECCOLMResetColumns)); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestScreenMode) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?5h"); - VERIFY_IS_TRUE(pDispatch->_isScreenModeReversed); - - pDispatch->ClearState(); - pDispatch->_isScreenModeReversed = true; - - mach.ProcessString(L"\x1b[?5l"); - VERIFY_IS_FALSE(pDispatch->_isScreenModeReversed); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestOriginMode) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?6h"); - VERIFY_IS_TRUE(pDispatch->_isOriginModeRelative); - VERIFY_IS_TRUE(pDispatch->_cursorPosition); - VERIFY_ARE_EQUAL(pDispatch->_line, 1u); - VERIFY_ARE_EQUAL(pDispatch->_column, 1u); - - pDispatch->ClearState(); - pDispatch->_isOriginModeRelative = true; - - mach.ProcessString(L"\x1b[?6l"); - VERIFY_IS_FALSE(pDispatch->_isOriginModeRelative); - VERIFY_IS_TRUE(pDispatch->_cursorPosition); - VERIFY_ARE_EQUAL(pDispatch->_line, 1u); - VERIFY_ARE_EQUAL(pDispatch->_column, 1u); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestAutoWrapMode) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?7l"); - VERIFY_IS_FALSE(pDispatch->_isAutoWrapEnabled); - - pDispatch->ClearState(); - pDispatch->_isAutoWrapEnabled = false; - - mach.ProcessString(L"\x1b[?7h"); - VERIFY_IS_TRUE(pDispatch->_isAutoWrapEnabled); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestCursorBlinking) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?12h"); - VERIFY_IS_TRUE(pDispatch->_cursorBlinking); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?12l"); - VERIFY_IS_FALSE(pDispatch->_cursorBlinking); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestCursorVisibility) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?25h"); - VERIFY_IS_TRUE(pDispatch->_cursorVisible); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?25l"); - VERIFY_IS_FALSE(pDispatch->_cursorVisible); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestAltBufferSwapping) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?1049h"); - VERIFY_IS_TRUE(pDispatch->_isAltBuffer); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?1049h"); - VERIFY_IS_TRUE(pDispatch->_isAltBuffer); - mach.ProcessString(L"\x1b[?1049h"); - VERIFY_IS_TRUE(pDispatch->_isAltBuffer); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?1049l"); - VERIFY_IS_FALSE(pDispatch->_isAltBuffer); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?1049h"); - VERIFY_IS_TRUE(pDispatch->_isAltBuffer); - mach.ProcessString(L"\x1b[?1049l"); - VERIFY_IS_FALSE(pDispatch->_isAltBuffer); - - pDispatch->ClearState(); - - mach.ProcessString(L"\x1b[?1049l"); - VERIFY_IS_FALSE(pDispatch->_isAltBuffer); - mach.ProcessString(L"\x1b[?1049l"); - VERIFY_IS_FALSE(pDispatch->_isAltBuffer); - - pDispatch->ClearState(); - } - - TEST_METHOD(TestEnableDECCOLMSupport) - { - auto dispatch = std::make_unique(); - auto pDispatch = dispatch.get(); - auto engine = std::make_unique(std::move(dispatch)); - StateMachine mach(std::move(engine)); - - mach.ProcessString(L"\x1b[?40h"); - VERIFY_IS_TRUE(pDispatch->_isDECCOLMAllowed); - - pDispatch->ClearState(); - pDispatch->_isDECCOLMAllowed = true; - - mach.ProcessString(L"\x1b[?40l"); - VERIFY_IS_FALSE(pDispatch->_isDECCOLMAllowed); + mach.ProcessString(resetModeSequence); + VERIFY_ARE_EQUAL(modeType, pDispatch->_modeType); + VERIFY_IS_FALSE(pDispatch->_modeEnabled); pDispatch->ClearState(); } @@ -2025,20 +1855,18 @@ class StateMachineExternalTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + const auto expectedModes = std::vector{ DispatchTypes::DECSCNM_ScreenMode, DispatchTypes::DECCKM_CursorKeysMode, DispatchTypes::DECOM_OriginMode }; + mach.ProcessString(L"\x1b[?5;1;6h"); - VERIFY_IS_TRUE(pDispatch->_isScreenModeReversed); - VERIFY_IS_TRUE(pDispatch->_cursorKeysMode); - VERIFY_IS_TRUE(pDispatch->_isOriginModeRelative); + VERIFY_IS_TRUE(pDispatch->_modeEnabled); + VERIFY_ARE_EQUAL(expectedModes, pDispatch->_modeTypes); pDispatch->ClearState(); - pDispatch->_isScreenModeReversed = true; - pDispatch->_cursorKeysMode = true; - pDispatch->_isOriginModeRelative = true; + pDispatch->_modeEnabled = true; mach.ProcessString(L"\x1b[?5;1;6l"); - VERIFY_IS_FALSE(pDispatch->_isScreenModeReversed); - VERIFY_IS_FALSE(pDispatch->_cursorKeysMode); - VERIFY_IS_FALSE(pDispatch->_isOriginModeRelative); + VERIFY_IS_FALSE(pDispatch->_modeEnabled); + VERIFY_ARE_EQUAL(expectedModes, pDispatch->_modeTypes); pDispatch->ClearState(); } @@ -2133,7 +1961,7 @@ class StateMachineExternalTest final pDispatch->ClearState(); } - void VerifyDispatchTypes(const gsl::span expectedOptions, + void VerifyDispatchTypes(const std::span expectedOptions, const StatefulDispatch& dispatch) { VERIFY_ARE_EQUAL(expectedOptions.size(), dispatch._options.size()); diff --git a/src/terminal/parser/ut_parser/StateMachineTest.cpp b/src/terminal/parser/ut_parser/StateMachineTest.cpp index 125036c16f..4893964c24 100644 --- a/src/terminal/parser/ut_parser/StateMachineTest.cpp +++ b/src/terminal/parser/ut_parser/StateMachineTest.cpp @@ -157,6 +157,8 @@ class Microsoft::Console::VirtualTerminal::StateMachineTest TEST_METHOD(PassThroughUnhandledSplitAcrossWrites); TEST_METHOD(DcsDataStringsReceivedByHandler); + + TEST_METHOD(VtParameterSubspanTest); }; void StateMachineTest::TwoStateMachinesDoNotInterfereWithEachOther() @@ -337,3 +339,38 @@ void StateMachineTest::DcsDataStringsReceivedByHandler() // Verify the control characters were executed (if expected). VERIFY_ARE_EQUAL(expectedExecuted, engine.executed); } + +void StateMachineTest::VtParameterSubspanTest() +{ + const auto parameterList = std::vector{ 12, 34, 56, 78 }; + const auto parameterSpan = VTParameters{ parameterList.data(), parameterList.size() }; + + { + Log::Comment(L"Subspan from 0 gives all the parameters"); + const auto subspan = parameterSpan.subspan(0); + VERIFY_ARE_EQUAL(4u, subspan.size()); + VERIFY_ARE_EQUAL(12, subspan.at(0)); + VERIFY_ARE_EQUAL(34, subspan.at(1)); + VERIFY_ARE_EQUAL(56, subspan.at(2)); + VERIFY_ARE_EQUAL(78, subspan.at(3)); + } + { + Log::Comment(L"Subspan from 2 gives the last 2 parameters"); + const auto subspan = parameterSpan.subspan(2); + VERIFY_ARE_EQUAL(2u, subspan.size()); + VERIFY_ARE_EQUAL(56, subspan.at(0)); + VERIFY_ARE_EQUAL(78, subspan.at(1)); + } + { + Log::Comment(L"Subspan at the end of the range gives 1 omitted value"); + const auto subspan = parameterSpan.subspan(4); + VERIFY_ARE_EQUAL(1u, subspan.size()); + VERIFY_IS_FALSE(subspan.at(0).has_value()); + } + { + Log::Comment(L"Subspan past the end of the range gives 1 omitted value"); + const auto subspan = parameterSpan.subspan(6); + VERIFY_ARE_EQUAL(1u, subspan.size()); + VERIFY_IS_FALSE(subspan.at(0).has_value()); + } +} diff --git a/src/til/ut_til/BitmapTests.cpp b/src/til/ut_til/BitmapTests.cpp index 39847cafdf..ee093f52a7 100644 --- a/src/til/ut_til/BitmapTests.cpp +++ b/src/til/ut_til/BitmapTests.cpp @@ -632,26 +632,19 @@ class BitmapTests _checkBits(expectedSet, bitmap); } - TEST_METHOD(SetResetExceptions) + TEST_METHOD(SetResetOutOfBounds) { til::bitmap map{ til::size{ 4, 4 } }; Log::Comment(L"1.) SetPoint out of bounds."); - { - auto fn = [&]() { - map.set(til::point{ 10, 10 }); - }; - - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - } + map.set(til::point{ 10, 10 }); Log::Comment(L"2.) SetRectangle out of bounds."); - { - auto fn = [&]() { - map.set(til::rect{ til::point{ 2, 2 }, til::size{ 10, 10 } }); - }; + map.set(til::rect{ til::point{ 2, 2 }, til::size{ 10, 10 } }); - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - } + const auto runs = map.runs(); + VERIFY_ARE_EQUAL(2u, runs.size()); + VERIFY_ARE_EQUAL(til::rect(2, 2, 4, 3), runs[0]); + VERIFY_ARE_EQUAL(til::rect(2, 3, 4, 4), runs[1]); } TEST_METHOD(Resize) diff --git a/src/til/ut_til/EnvTests.cpp b/src/til/ut_til/EnvTests.cpp new file mode 100644 index 0000000000..1a9fd53c60 --- /dev/null +++ b/src/til/ut_til/EnvTests.cpp @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "til/env.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +BOOL APIENTRY RegenerateUserEnvironment(void** pNewEnv, BOOL bSetCurrentEnv); + +class EnvTests +{ + TEST_CLASS(EnvTests); + + TEST_METHOD(Construct) + { + til::env environment; + } + + TEST_METHOD(Generate) + { + // Be sneaky and pull the method that @eryksun told us about to see if we've effectively recreated it. + wil::unique_hmodule shell32{ LoadLibraryW(L"shell32.dll") }; + auto func = GetProcAddressByFunctionDeclaration(shell32.get(), RegenerateUserEnvironment); + + // Use a WIL pointer to free it on scope exit. + wil::unique_environstrings_ptr newEnv; + VERIFY_WIN32_BOOL_SUCCEEDED(func((void**)&newEnv, FALSE)); + + // Parse the string into our environment table. + til::env expected{ newEnv.get() }; + + // Set up an empty table and tell it to generate the environment with a similar algorithm. + til::env actual; + actual.regenerate(); + + VERIFY_ARE_EQUAL(expected.as_map().size(), actual.as_map().size()); + for (const auto& [expectedKey, expectedValue] : expected.as_map()) + { + Log::Comment(String().Format(L"Environment Variable: '%s'", expectedKey.data())); + const auto actualValue = actual.as_map()[expectedKey]; + VERIFY_IS_TRUE(actual.as_map().find(expectedKey) != actual.as_map().end()); + VERIFY_ARE_EQUAL(expectedValue, actual.as_map()[expectedKey]); + } + } + + TEST_METHOD(ToString) + { + til::env environment; + environment.as_map().insert_or_assign(L"A", L"Apple"); + environment.as_map().insert_or_assign(L"B", L"Banana"); + environment.as_map().insert_or_assign(L"C", L"Cassowary"); + + wchar_t expectedArray[] = L"A=Apple\0B=Banana\0C=Cassowary\0"; + + const std::wstring expected{ expectedArray, ARRAYSIZE(expectedArray) }; + const auto& actual = environment.to_string(); + + VERIFY_ARE_EQUAL(expected.size(), actual.size()); + for (size_t i = 0; i < expected.size(); ++i) + { + VERIFY_ARE_EQUAL(expected[i], actual[i]); + } + } + + TEST_METHOD(TestExpandEnvironmentStrings) + { + { + til::env environment; + environment.as_map().insert_or_assign(L"ENV", L"Bar"); + + VERIFY_ARE_EQUAL(L"FooBarBaz", environment.expand_environment_strings(L"Foo%ENV%Baz")); + } + + { + til::env environment; + + VERIFY_ARE_EQUAL(L"Foo%ENV%Baz", environment.expand_environment_strings(L"Foo%ENV%Baz")); + } + + { + til::env environment; + + VERIFY_ARE_EQUAL(L"Foo%ENV", environment.expand_environment_strings(L"Foo%ENV")); + } + } +}; diff --git a/src/til/ut_til/FlatSetTests.cpp b/src/til/ut_til/FlatSetTests.cpp new file mode 100644 index 0000000000..43e4c5fb2b --- /dev/null +++ b/src/til/ut_til/FlatSetTests.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +struct Data +{ + static constexpr auto emptyMarker = std::numeric_limits::max(); + + constexpr operator bool() const noexcept + { + return value != emptyMarker; + } + + constexpr bool operator==(int key) const noexcept + { + return value == static_cast(key); + } + + constexpr Data& operator=(int key) noexcept + { + value = static_cast(key); + return *this; + } + + size_t value = emptyMarker; +}; + +template<> +struct ::std::hash +{ + constexpr size_t operator()(int key) const noexcept + { + return til::flat_set_hash_integer(static_cast(key)); + } + + constexpr size_t operator()(Data d) const noexcept + { + return til::flat_set_hash_integer(d.value); + } +}; + +class FlatSetTests +{ + TEST_CLASS(FlatSetTests); + + TEST_METHOD(Basic) + { + til::linear_flat_set set; + + // This simultaneously demonstrates how the class can't just do "heterogeneous lookups" + // like STL does, but also insert items with a different type. + const auto [entry1, inserted1] = set.insert(123); + VERIFY_IS_TRUE(inserted1); + + const auto [entry2, inserted2] = set.insert(123); + VERIFY_IS_FALSE(inserted2); + + VERIFY_ARE_EQUAL(&entry1, &entry2); + VERIFY_ARE_EQUAL(123u, entry2.value); + } +}; diff --git a/src/til/ut_til/GenerationalTests.cpp b/src/til/ut_til/GenerationalTests.cpp new file mode 100644 index 0000000000..ce8e4dc1cd --- /dev/null +++ b/src/til/ut_til/GenerationalTests.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +struct Data +{ + int value = 0; +}; + +class GenerationalTests +{ + TEST_CLASS(GenerationalTests); + + TEST_METHOD(Basic) + { + til::generational src; + til::generational dst; + + // Reads via -> and *, just like std::optional, etc. + VERIFY_ARE_EQUAL(0, src->value); + VERIFY_ARE_EQUAL(0, (*src).value); + + // Writes via .write()-> + src.write()->value = 123; + // ...which makes them not compare as equal. + VERIFY_ARE_NOT_EQUAL(dst, src); + + // Synchronize the two objects by copying them + dst = src; + // ...which results in both being considered equal again + VERIFY_ARE_EQUAL(dst, src); + // ...and all values are copied over. + VERIFY_ARE_EQUAL(123, dst->value); + } +}; diff --git a/src/til/ut_til/RunLengthEncodingTests.cpp b/src/til/ut_til/RunLengthEncodingTests.cpp index 340b48f16d..ce79cd754c 100644 --- a/src/til/ut_til/RunLengthEncodingTests.cpp +++ b/src/til/ut_til/RunLengthEncodingTests.cpp @@ -190,7 +190,7 @@ class RunLengthEncodingTests // We're testing replace() elsewhere, but this is special: // This ensures that even if we're default constructed we can add data. - rle.replace(0, 0, { 1, 5 }); + rle.replace(0, 0, rle_type{ 1, 5 }); VERIFY_ARE_EQUAL(5u, rle.size()); VERIFY_IS_FALSE(rle.empty()); } diff --git a/src/til/ut_til/SizeTests.cpp b/src/til/ut_til/SizeTests.cpp index 357ab30078..a75b48922a 100644 --- a/src/til/ut_til/SizeTests.cpp +++ b/src/til/ut_til/SizeTests.cpp @@ -306,7 +306,7 @@ class SizeTests constexpr auto scale = 1e12f; auto fn = [&]() { - sz.scale(til::math::ceiling, scale); + std::ignore = sz.scale(til::math::ceiling, scale); }; VERIFY_THROWS(fn(), gsl::narrowing_error); @@ -359,7 +359,7 @@ class SizeTests const til::size divisor{ 3, 2 }; auto fn = [&]() { - sz.divide_ceil(divisor); + std::ignore = sz.divide_ceil(divisor); }; VERIFY_THROWS(fn(), std::invalid_argument); diff --git a/src/til/ut_til/SmallVectorTests.cpp b/src/til/ut_til/SmallVectorTests.cpp index 9eaab3c0c6..f1a2ed88e5 100644 --- a/src/til/ut_til/SmallVectorTests.cpp +++ b/src/til/ut_til/SmallVectorTests.cpp @@ -368,4 +368,24 @@ class SmallVectorTests til::small_vector expected{ { -1, -1, 0, 1, 2, 3, 3, 3, 4, 5, 5 } }; VERIFY_ARE_EQUAL(expected, actual); } + + TEST_METHOD(CopyOntoItself) + { + til::small_vector actual(3); + actual.operator=(actual); + + const til::small_vector expected(3); + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(MoveOntoItself) + { + til::small_vector actual; + actual.resize(3); + actual.operator=(std::move(actual)); + + til::small_vector expected; + expected.resize(3); + VERIFY_ARE_EQUAL(expected, actual); + } }; diff --git a/src/til/ut_til/UnicodeTests.cpp b/src/til/ut_til/UnicodeTests.cpp new file mode 100644 index 0000000000..24d62beeaf --- /dev/null +++ b/src/til/ut_til/UnicodeTests.cpp @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +#define REPLACEMENT L"\xFFFD" +#define LEADING L"\xD801" +#define TRAILING L"\xDC01" +#define PAIR L"\xD801\xDC01" + +class UnicodeTests +{ + TEST_CLASS(UnicodeTests); + + TEST_METHOD(utf16_next) + { + struct Test + { + std::wstring_view input; + std::wstring_view expected; + }; + + static constexpr std::array tests{ + Test{ L"", REPLACEMENT }, + Test{ L"a", L"a" }, + Test{ L"abc", L"a" }, + Test{ L"a" PAIR, L"a" }, + Test{ L"a" LEADING, L"a" }, + Test{ L"a" TRAILING, L"a" }, + Test{ PAIR L"a", PAIR }, + Test{ LEADING L"a", REPLACEMENT }, + Test{ TRAILING L"a", REPLACEMENT }, + }; + + for (const auto& t : tests) + { + const auto actual = til::utf16_next(t.input); + VERIFY_ARE_EQUAL(t.expected, actual); + } + } + + TEST_METHOD(utf16_iterator) + { + struct Test + { + std::wstring_view input; + til::some expected; + }; + + static constexpr std::array tests{ + Test{ L"", {} }, + Test{ L"a", { L"a" } }, + Test{ L"abc", { L"a", L"b", L"c" } }, + Test{ PAIR L"a" PAIR L"b" PAIR, { PAIR, L"a", PAIR, L"b", PAIR } }, + Test{ LEADING L"a" LEADING L"b" LEADING, { REPLACEMENT, L"a", REPLACEMENT, L"b", REPLACEMENT } }, + Test{ TRAILING L"a" TRAILING L"b" TRAILING, { REPLACEMENT, L"a", REPLACEMENT, L"b", REPLACEMENT } }, + Test{ L"a" TRAILING LEADING L"b", { L"a", REPLACEMENT, REPLACEMENT, L"b" } }, + }; + + for (const auto& t : tests) + { + auto it = t.expected.begin(); + const auto end = t.expected.end(); + + for (const auto& v : til::utf16_iterator{ t.input }) + { + VERIFY_ARE_NOT_EQUAL(end, it); + VERIFY_ARE_EQUAL(*it, v); + ++it; + } + + VERIFY_ARE_EQUAL(end, it); + } + } +}; diff --git a/src/til/ut_til/sources b/src/til/ut_til/sources index 85ca3ebd73..ffe976d7ca 100644 --- a/src/til/ut_til/sources +++ b/src/til/ut_til/sources @@ -19,6 +19,7 @@ SOURCES = \ CoalesceTests.cpp \ ColorTests.cpp \ EnumSetTests.cpp \ + EnvTests.cpp \ HashTests.cpp \ MathTests.cpp \ mutex.cpp \ @@ -33,6 +34,7 @@ SOURCES = \ StaticMapTests.cpp \ string.cpp \ u8u16convertTests.cpp \ + UnicodeTests.cpp \ DefaultResource.rc \ # These tests are disabled because of a missing symbol. diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 5861bb04d0..04694e68a1 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -19,6 +19,9 @@ + + + @@ -35,15 +38,19 @@ + + + + @@ -63,7 +70,9 @@ + + @@ -71,6 +80,12 @@ ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories) + + + + {6bae5851-50d5-4934-8d5e-30361a8a40f3} + + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index b06dbef5d6..c25b011398 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -26,6 +26,10 @@ + + + + @@ -50,6 +54,9 @@ inc + + inc + inc @@ -110,6 +117,18 @@ inc + + inc + + + inc + + + inc + + + inc + diff --git a/src/tools/ConsoleMonitor/ConsoleMonitor.exe.manifest b/src/tools/ConsoleMonitor/ConsoleMonitor.exe.manifest new file mode 100644 index 0000000000..fa8586b473 --- /dev/null +++ b/src/tools/ConsoleMonitor/ConsoleMonitor.exe.manifest @@ -0,0 +1,21 @@ + + + + + true + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj b/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj new file mode 100644 index 0000000000..6114ce6a4f --- /dev/null +++ b/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj @@ -0,0 +1,32 @@ + + + + {328729e9-6723-416e-9c98-951f1473bbe1} + Win32Proj + ConsoleMonitor + ConsoleMonitor + ConsoleMonitor + Application + + + + + + + Create + + + + + + + + + + + pch.h + + + + + \ No newline at end of file diff --git a/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj.filters b/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj.filters new file mode 100644 index 0000000000..41d833de1b --- /dev/null +++ b/src/tools/ConsoleMonitor/ConsoleMonitor.vcxproj.filters @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/tools/ConsoleMonitor/main.cpp b/src/tools/ConsoleMonitor/main.cpp new file mode 100644 index 0000000000..1fadd1ca86 --- /dev/null +++ b/src/tools/ConsoleMonitor/main.cpp @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// This application allows you to monitor the text buffer contents of ConPTY. +// All you need to do is run this application in Windows Terminal and it will pop up a window. + +#include "pch.h" + +#include + +#pragma warning(disable : 4100) // '...': unreferenced formal parameter + +// WS_OVERLAPPEDWINDOW without WS_THICKFRAME, which disables resize by the user. +static constexpr DWORD windowStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; + +static CONSOLE_SCREEN_BUFFER_INFOEX g_info{ .cbSize = sizeof(g_info) }; +static std::vector g_buffer; +static COORD g_bufferSize; +static COORD g_cellCount; + +static wil::unique_hfont g_font; +static SIZE g_cellSize; +static LONG g_dpi; + +static std::vector g_text; +static std::vector g_textAdvance; + +static bool equalCoord(const COORD& a, const COORD& b) noexcept +{ + return memcmp(&a, &b, sizeof(COORD)) == 0; +} + +static void updateFont(HWND hwnd, LONG dpi) +{ + const LOGFONTW lf{ + .lfHeight = -MulDiv(10, dpi, 72), + .lfWeight = FW_REGULAR, + .lfCharSet = DEFAULT_CHARSET, + .lfQuality = PROOF_QUALITY, + .lfPitchAndFamily = FIXED_PITCH | FF_MODERN, + .lfFaceName = L"Consolas", + }; + g_font = wil::unique_hfont{ CreateFontIndirectW(&lf) }; + g_dpi = dpi; + + const auto dc = wil::GetDC(hwnd); + const auto restoreFont = wil::SelectObject(dc.get(), g_font.get()); + GetTextExtentPoint32W(dc.get(), L"0", 1, &g_cellSize); +} + +static void updateWindowSize(HWND hwnd) +{ + RECT windowArea{ 0, 0, g_cellSize.cx * g_cellCount.X, g_cellSize.cy * g_cellCount.Y }; + AdjustWindowRectExForDpi(&windowArea, windowStyle, FALSE, 0, g_dpi); + SetWindowPos(hwnd, nullptr, 0, 0, windowArea.right - windowArea.left, windowArea.bottom - windowArea.top, SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOREDRAW); +} + +static void updateConsoleState(HWND hwnd) +{ + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + if (!GetConsoleScreenBufferInfoEx(out, &g_info)) + { + PostQuitMessage(0); + return; + } + + // Add some extra columns/rows just in case the window is being resized + // in-between GetConsoleScreenBufferInfoEx and ReadConsoleOutputW. + const COORD bufferSize{ g_info.dwSize.X + 10, g_info.dwSize.Y + 10 }; + if (!equalCoord(g_bufferSize, bufferSize)) + { + g_bufferSize = bufferSize; + g_buffer.resize(static_cast(g_bufferSize.X) * g_bufferSize.Y); + } + + SMALL_RECT readArea{ .Right = bufferSize.X, .Bottom = bufferSize.Y }; + if (!ReadConsoleOutputW(out, g_buffer.data(), bufferSize, {}, &readArea)) + { + PostQuitMessage(0); + return; + } + + const COORD cellCount{ readArea.Right + 1, readArea.Bottom + 1 }; + if (!equalCoord(g_cellCount, cellCount)) + { + g_cellCount = cellCount; + updateWindowSize(hwnd); + } +} + +static void paintConsole(HWND hwnd) +{ + const auto dc = wil::BeginPaint(hwnd); + const auto restoreFont = wil::SelectObject(dc.get(), g_font.get()); + + if (g_buffer.empty()) + { + return; + } + + for (SHORT y = 0; y < g_cellCount.Y; y++) + { + const auto ciOffset = g_bufferSize.X * y; + + for (SHORT x = 0, x2; x < g_cellCount.X; x = x2) + { + const auto& ci = g_buffer[ciOffset + x]; + const auto fg = g_info.ColorTable[(ci.Attributes >> 0) & 15]; + const auto bg = g_info.ColorTable[(ci.Attributes >> 4) & 15]; + + g_text.clear(); + g_textAdvance.clear(); + g_text.push_back(ci.Char.UnicodeChar); + g_textAdvance.push_back(g_cellSize.cx); + + // Accumulate characters and advance widths until either the foreground or background + // color changes. It also handles joining wide glyphs in a somewhat poor manner. + for (x2 = x + 1; x2 < g_cellCount.X; x2++) + { + const auto& ci2 = g_buffer[ciOffset + x2]; + const auto fg2 = g_info.ColorTable[(ci2.Attributes >> 0) & 15]; + const auto bg2 = g_info.ColorTable[(ci2.Attributes >> 4) & 15]; + + if (fg2 != fg || bg2 != bg) + { + break; + } + + if (ci2.Attributes & COMMON_LVB_TRAILING_BYTE) + { + g_textAdvance.back() *= 2; + } + else + { + g_text.push_back(ci2.Char.UnicodeChar); + g_textAdvance.push_back(g_cellSize.cx); + } + } + + RECT r; + r.left = g_cellSize.cx * x; + r.top = g_cellSize.cy * y; + r.right = g_cellSize.cx * x2; + r.bottom = r.top + g_cellSize.cy; + + SetTextColor(dc.get(), fg); + SetBkColor(dc.get(), bg); + ExtTextOutW(dc.get(), r.left, r.top, ETO_CLIPPED, &r, g_text.data(), static_cast(g_text.size()), g_textAdvance.data()); + } + } + + RECT cursorRect; + cursorRect.left = g_info.dwCursorPosition.X * g_cellSize.cx; + cursorRect.top = g_info.dwCursorPosition.Y * g_cellSize.cy; + cursorRect.right = cursorRect.left + g_dpi / 96; + cursorRect.bottom = cursorRect.top + g_cellSize.cy; + FillRect(dc.get(), &cursorRect, static_cast(GetStockObject(WHITE_BRUSH))); +} + +LRESULT WndProc(HWND hwnd, UINT message, size_t wParam, LPARAM lParam) +{ + switch (message) + { + case WM_DPICHANGED: + updateFont(hwnd, LOWORD(wParam)); + updateWindowSize(hwnd); + return 0; + case WM_PAINT: + paintConsole(hwnd); + return 0; + case WM_TIMER: + updateConsoleState(hwnd); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + return DefWindowProcW(hwnd, message, wParam, lParam); + } +} + +static void winMainImpl(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) +{ + if (!AttachConsole(ATTACH_PARENT_PROCESS)) + { + MessageBoxW(nullptr, L"This application needs to be spawned from within a console session.", L"Failure", MB_ICONWARNING | MB_OK); + return; + } + + const WNDCLASSEXW wc{ + .cbSize = sizeof(wc), + .style = CS_OWNDC, + .lpfnWndProc = WndProc, + .hInstance = hInstance, + .hCursor = LoadCursor(nullptr, IDC_ARROW), + .lpszClassName = L"ConsoleMonitor", + }; + THROW_LAST_ERROR_IF(!RegisterClassExW(&wc)); + + const wil::unique_hwnd hwnd{ + THROW_LAST_ERROR_IF_NULL(CreateWindowExW( + 0, + wc.lpszClassName, + L"ConsoleMonitor", + windowStyle, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + nullptr, + nullptr, + wc.hInstance, + nullptr)) + }; + + updateFont(hwnd.get(), static_cast(GetDpiForWindow(hwnd.get()))); + updateConsoleState(hwnd.get()); + + ShowWindow(hwnd.get(), SW_SHOWNORMAL); + UpdateWindow(hwnd.get()); + + SetTimer(hwnd.get(), 0, 30, nullptr); + + MSG msg; + while (GetMessageW(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +} + +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) +{ + try + { + winMainImpl(hInstance, hPrevInstance, lpCmdLine, nShowCmd); + } + catch (const wil::ResultException& e) + { + wchar_t message[2048]; + wil::GetFailureLogString(&message[0], 2048, e.GetFailureInfo()); + MessageBoxW(nullptr, &message[0], L"Exception", MB_ICONERROR | MB_OK); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + + return 0; +} diff --git a/src/tools/ConsoleMonitor/pch.cpp b/src/tools/ConsoleMonitor/pch.cpp new file mode 100644 index 0000000000..9e6b2e073e --- /dev/null +++ b/src/tools/ConsoleMonitor/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/tools/ConsoleMonitor/pch.h b/src/tools/ConsoleMonitor/pch.h new file mode 100644 index 0000000000..9864398990 --- /dev/null +++ b/src/tools/ConsoleMonitor/pch.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include + +#include + +#include diff --git a/src/tools/RenderingTests/RenderingTests.vcxproj b/src/tools/RenderingTests/RenderingTests.vcxproj new file mode 100644 index 0000000000..5de14c0f43 --- /dev/null +++ b/src/tools/RenderingTests/RenderingTests.vcxproj @@ -0,0 +1,27 @@ + + + + 16.0 + Win32Proj + {37c995e0-2349-4154-8e77-4a52c0c7f46d} + RenderingTests + RenderingTests + 10.0 + + + + + + NotUsing + _CONSOLE;%(PreprocessorDefinitions) + + + Console + + + + + + + + diff --git a/src/tools/RenderingTests/RenderingTests.vcxproj.filters b/src/tools/RenderingTests/RenderingTests.vcxproj.filters new file mode 100644 index 0000000000..0f14913f3c --- /dev/null +++ b/src/tools/RenderingTests/RenderingTests.vcxproj.filters @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp new file mode 100644 index 0000000000..a82e38bb81 --- /dev/null +++ b/src/tools/RenderingTests/main.cpp @@ -0,0 +1,294 @@ +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include + +// Another variant of "defer" for C++. +namespace +{ + namespace detail + { + + template + class scope_guard + { + public: + scope_guard(F f) noexcept : + func(std::move(f)) + { + } + + ~scope_guard() + { + func(); + } + + scope_guard(const scope_guard&) = delete; + scope_guard(scope_guard&& rhs) = delete; + scope_guard& operator=(const scope_guard&) = delete; + scope_guard& operator=(scope_guard&&) = delete; + + private: + F func; + }; + + enum class scope_guard_helper + { + }; + + template + scope_guard operator+(scope_guard_helper /*unused*/, F&& fn) + { + return scope_guard(std::forward(fn)); + } + + } // namespace detail + +// The extra indirection is necessary to prevent __LINE__ to be treated literally. +#define _DEFER_CONCAT_IMPL(a, b) a##b +#define _DEFER_CONCAT(a, b) _DEFER_CONCAT_IMPL(a, b) +#define defer const auto _DEFER_CONCAT(_defer_, __LINE__) = ::detail::scope_guard_helper() + [&]() +} + +static void printUTF16(const wchar_t* str) +{ + WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), str, static_cast(wcslen(str)), nullptr, nullptr); +} + +// wprintf() in the uCRT prints every single wchar_t individually and thus breaks surrogate +// pairs apart which Windows Terminal treats as invalid input and replaces it with U+FFFD. +static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const format, ...) +{ + std::array buffer; + + va_list args; + va_start(args, format); + const auto length = _vsnwprintf_s(buffer.data(), buffer.size(), _TRUNCATE, format, args); + va_end(args); + + assert(length >= 0); + WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), buffer.data(), length, nullptr, nullptr); +} + +static void wait() +{ + printUTF16(L"\x1B[9999;1HPress any key to continue..."); + _getch(); +} + +static void clear() +{ + printUTF16( + L"\x1B[H" // move cursor to 0,0 + L"\x1B[2J" // clear screen + ); +} + +int main() +{ + const auto outputHandle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD consoleMode = ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT; + GetConsoleMode(outputHandle, &consoleMode); + SetConsoleMode(outputHandle, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + defer + { + SetConsoleMode(outputHandle, consoleMode); + }; + + printUTF16( + L"\x1b[?1049h" // enable alternative screen buffer + ); + defer + { + printUTF16( + L"\x1b[?1049l" // disable alternative screen buffer + ); + }; + + { + struct ConsoleAttributeTest + { + const wchar_t* text = nullptr; + WORD attribute = 0; + }; + static constexpr ConsoleAttributeTest consoleAttributeTests[]{ + { L"Console attributes:", 0 }, +#define MAKE_TEST_FOR_ATTRIBUTE(attr) { L## #attr, attr } + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_HORIZONTAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_LVERTICAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_RVERTICAL), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_REVERSE_VIDEO), + MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_UNDERSCORE), +#undef MAKE_TEST_FOR_ATTRIBUTE + { L"all gridlines", COMMON_LVB_GRID_HORIZONTAL | COMMON_LVB_GRID_LVERTICAL | COMMON_LVB_GRID_RVERTICAL | COMMON_LVB_UNDERSCORE }, + { L"all attributes", COMMON_LVB_GRID_HORIZONTAL | COMMON_LVB_GRID_LVERTICAL | COMMON_LVB_GRID_RVERTICAL | COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE }, + }; + + SHORT row = 2; + for (const auto& t : consoleAttributeTests) + { + const auto length = static_cast(wcslen(t.text)); + printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text); + + WORD attributes[32]; + std::fill_n(&attributes[0], length, static_cast(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute)); + + DWORD numberOfAttrsWritten; + WriteConsoleOutputAttribute(outputHandle, attributes, length, { 4, row }, &numberOfAttrsWritten); + + row += 2; + } + + struct VTAttributeTest + { + const wchar_t* text = nullptr; + int sgr = 0; + }; + static constexpr VTAttributeTest vtAttributeTests[]{ + { L"ANSI escape SGR:", 0 }, + { L"bold", 1 }, + { L"faint", 2 }, + { L"italic", 3 }, + { L"underline", 4 }, + { L"reverse", 7 }, + { L"strikethrough", 9 }, + { L"double underline", 21 }, + { L"overlined", 53 }, + }; + + row = 3; + for (const auto& t : vtAttributeTests) + { + printfUTF16(L"\x1B[%d;45H\x1b[%dm%s\x1b[m", row, t.sgr, t.text); + row += 2; + } + + printfUTF16(L"\x1B[%d;45H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); + + wait(); + clear(); + } + + { + printUTF16( + L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" + L"\x1B[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" + L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" + L"\x1B[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m" + L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" + L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); + + static constexpr WORD attributes[]{ + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_LVERTICAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_LVERTICAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_RVERTICAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_RVERTICAL, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_UNDERSCORE, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_UNDERSCORE, + }; + + DWORD numberOfAttrsWritten; + DWORD offset = 0; + + for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 }) + { + COORD coord; + coord.X = r > 14 ? 2 : 4; + coord.X += offset ? 2 : 0; + coord.Y = static_cast(r); + WriteConsoleOutputAttribute(outputHandle, &attributes[offset], 4, coord, &numberOfAttrsWritten); + offset = (offset + 4) & 7; + } + + wait(); + clear(); + } + + { + defer + { + // Setting an empty DRCS gets us back to the regular font. + printUTF16(L"\x1bP1;1;2{ @\x1b\\"); + }; + + constexpr auto width = 14; + const auto glyph = + "W W " + "W W " + "W W W " + "W W W " + "W W W " + "W W W TTTTTTT" + " W W T " + " T " + " T " + " T " + " T " + " T "; + + // Convert the above visual glyph to sixels + wchar_t rows[2][width]; + for (int r = 0; r < 2; ++r) + { + const auto glyphData = &glyph[r * width * 6]; + + for (int x = 0; x < width; ++x) + { + unsigned int accumulator = 0; + for (int y = 5; y >= 0; --y) + { + const auto isSet = glyphData[y * width + x] != ' '; + accumulator <<= 1; + accumulator |= static_cast(isSet); + } + + rows[r][x] = static_cast(L'?' + accumulator); + } + } + + // DECDLD - Dynamically Redefinable Character Sets + printfUTF16( + // * Pfn | font number | 1 | + // * Pcn | starting character | 3 | = ASCII 0x23 "#" + // * Pe | erase control | 2 | erase all + // Pcmw | character matrix width | %d | `width` pixels + // Pw | font width | 0 | 80 columns + // Pt | text or full cell | 0 | text + // Pcmh | character matrix height | 0 | 12 pixels + // Pcss | character set size | 0 | 94 + // * Dscs | character set name | " @" | unregistered soft set + L"\x1bP1;3;2;%d{ @%.15s/%.15s\x1b\\", + width, + rows[0], + rows[1]); + +#define DRCS_SEQUENCE L"\x1b( @#\x1b(A" + printUTF16( + L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" + L"\x1B[5;5HRegular: " DRCS_SEQUENCE L"" + L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" + L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" + L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" + // We map soft fonts into the private use area starting at U+EF20. This test ensures + // that we correctly map actual fallback glyphs mixed into the DRCS glyphs. + L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); +#undef DRCS_SEQUENCE + + wait(); + } + + return 0; +} diff --git a/src/tools/TerminalStress/Program.cs b/src/tools/TerminalStress/Program.cs new file mode 100644 index 0000000000..220d64d2fb --- /dev/null +++ b/src/tools/TerminalStress/Program.cs @@ -0,0 +1,83 @@ +namespace TerminalStress +{ + using System; + using System.Text; + + class Program + { + static void Main(string[] args) + { + + Random r = new Random(); + +#pragma warning disable SYSLIB0001 + Console.OutputEncoding = args.Length > 0 ? Encoding.UTF7 : Encoding.UTF8; + string s = string.Empty; + + var colors = new ConsoleColor[] + { + ConsoleColor.Black, + ConsoleColor.Red, + ConsoleColor.Green, + ConsoleColor.Blue, + ConsoleColor.Cyan, + ConsoleColor.Magenta, + ConsoleColor.Yellow, + ConsoleColor.White, + ConsoleColor.DarkYellow, + ConsoleColor.DarkCyan, + ConsoleColor.DarkBlue, + ConsoleColor.DarkGray, + ConsoleColor.DarkGreen, + ConsoleColor.DarkMagenta, + ConsoleColor.DarkRed + }; + + while (true) + { + try + { + Console.SetCursorPosition(r.Next(Console.WindowWidth), r.Next(Console.WindowHeight)); + } + catch + { + Console.Write("☠️"); + } + + try + { + var color = colors[r.Next(colors.Length)]; + Console.ForegroundColor = color; + + char c = (char)r.Next(0, 0xFFFF); + Console.Write(c); + s += c; + + if (r.Next(1_000) == 1) + { + Console.Clear(); + Console.WriteLine(s); + } + + if (s.Length > 1_000) + { + s = string.Empty; + } + + if (r.Next(1_000_000) < 100) + { + for (int i = 0; i < 100; i++) + { + Console.Write("☠️"); + } + } + } + catch + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("👀🤬💀👀👀💀"); + } + } + } + } +} diff --git a/src/tools/TerminalStress/TerminalStress.csproj b/src/tools/TerminalStress/TerminalStress.csproj new file mode 100644 index 0000000000..208c8da8e4 --- /dev/null +++ b/src/tools/TerminalStress/TerminalStress.csproj @@ -0,0 +1,12 @@ + + + + Exe + net6.0 + 6.0.9 + + + + + + diff --git a/src/tools/benchcat/benchcat.vcxproj b/src/tools/benchcat/benchcat.vcxproj new file mode 100644 index 0000000000..67a51675a1 --- /dev/null +++ b/src/tools/benchcat/benchcat.vcxproj @@ -0,0 +1,45 @@ + + + + 16.0 + Win32Proj + {2C836962-9543-4CE5-B834-D28E1F124B66} + benchcat + benchcat + 10.0 + bc + + + + + NotUsing + + + Console + + + + false + false + + + + false + false + false + NODEFAULTLIB;%(PreprocessorDefinitions) + false + + + main + true + + + + + + + + + + diff --git a/src/tools/benchcat/crt.cpp b/src/tools/benchcat/crt.cpp new file mode 100644 index 0000000000..f3dc3f6016 --- /dev/null +++ b/src/tools/benchcat/crt.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#ifdef NODEFAULTLIB + +#include + +#pragma function(memcpy) +void* memcpy(void* dst, const void* src, size_t size) +{ + __movsb(static_cast(dst), static_cast(src), size); + return dst; +} + +#pragma function(memset) +void* memset(void* dst, int val, size_t size) +{ + __stosb(static_cast(dst), static_cast(val), size); + return dst; +} + +#endif diff --git a/src/tools/benchcat/main.cpp b/src/tools/benchcat/main.cpp new file mode 100644 index 0000000000..ff2e63434b --- /dev/null +++ b/src/tools/benchcat/main.cpp @@ -0,0 +1,692 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include + +#include + +#include "crt.cpp" + +// This warning is broken on if/else chains with init statements. +#pragma warning(disable : 4456) // declaration of '...' hides previous local declaration + +namespace pcg_engines +{ + /* + * PCG Random Number Generation for C++ + * + * Copyright 2014-2017 Melissa O'Neill , + * and the PCG Project contributors. + * + * SPDX-License-Identifier: (Apache-2.0 OR MIT) + * + * Licensed under the Apache License, Version 2.0 (provided in + * LICENSE-APACHE.txt and at http://www.apache.org/licenses/LICENSE-2.0) + * or under the MIT license (provided in LICENSE-MIT.txt and at + * http://opensource.org/licenses/MIT), at your option. This file may not + * be copied, modified, or distributed except according to those terms. + * + * Distributed on an "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See your chosen license for details. + * + * For additional information about the PCG random number generation scheme, + * visit http://www.pcg-random.org/. + */ + + class oneseq_dxsm_64_32 + { + using xtype = uint32_t; + using itype = uint64_t; + + itype state_; + + static constexpr uint64_t multiplier() noexcept + { + return 6364136223846793005ULL; + } + + static constexpr uint64_t increment() noexcept + { + return 1442695040888963407ULL; + } + + static constexpr itype bump(itype state) noexcept + { + return state * multiplier() + increment(); + } + + constexpr itype base_generate0() noexcept + { + itype old_state = state_; + state_ = bump(state_); + return old_state; + } + + public: + explicit constexpr oneseq_dxsm_64_32(itype state = 0xcafef00dd15ea5e5ULL) noexcept : + state_(bump(state + increment())) + { + } + + constexpr xtype operator()() noexcept + { + constexpr auto xtypebits = uint8_t(sizeof(xtype) * 8); + constexpr auto itypebits = uint8_t(sizeof(itype) * 8); + static_assert(xtypebits <= itypebits / 2, "Output type must be half the size of the state type."); + + auto internal = base_generate0(); + auto hi = xtype(internal >> (itypebits - xtypebits)); + auto lo = xtype(internal); + + lo |= 1; + hi ^= hi >> (xtypebits / 2); + hi *= xtype(multiplier()); + hi ^= hi >> (3 * (xtypebits / 4)); + hi *= lo; + return hi; + } + + constexpr xtype operator()(xtype upper_bound) noexcept + { + uint32_t threshold = (UINT64_MAX + uint32_t(1) - upper_bound) % upper_bound; + for (;;) + { + auto r = operator()(); + if (r >= threshold) + return r % upper_bound; + } + } + }; +} // namespace pcg_engines + +template +constexpr T min(T a, T b) +{ + return a < b ? a : b; +} + +template +constexpr T max(T a, T b) +{ + return a > b ? a : b; +} + +template +constexpr T clamp(T val, T min, T max) +{ + return val < min ? min : (val > max ? max : val); +} + +static uint32_t parse_number(const wchar_t* str, const wchar_t** end) noexcept +{ + static constexpr uint32_t max = 0x0fffffff; + uint32_t accumulator = 0; + + for (;; ++str) + { + if (*str == '\0' || *str < '0' || *str > '9') + { + break; + } + + accumulator = accumulator * 10 + *str - '0'; + if (accumulator >= max) + { + accumulator = max; + break; + } + } + + if (end) + { + *end = str; + } + + return accumulator; +} + +static uint32_t parse_number_with_suffix(const wchar_t* str) noexcept +{ + const wchar_t* str_end = nullptr; + auto value = parse_number(str, &str_end); + + if (str_end[0]) + { + uint32_t mul = 1000; + + if (str_end[1]) + { + mul = 1024; + + if (str_end[1] != L'i') + { + value = 0; + } + } + + switch (*str_end) + { + case L'g': + case L'G': + value *= mul; + [[fallthrough]]; + case L'm': + case L'M': + value *= mul; + [[fallthrough]]; + case L'k': + case L'K': + value *= mul; + break; + default: + value = 0; + break; + } + } + + return value; +} + +static bool has_suffix(const wchar_t* str, const wchar_t* suffix) +{ + return wcscmp(str, suffix) == 0; +} + +static const wchar_t* split_prefix(const wchar_t* str, const wchar_t* prefix) noexcept +{ + for (; *prefix; ++prefix, ++str) + { + if (*str != *prefix) + { + return nullptr; + } + } + return str; +} + +static char* buffer_append_long(char* dst, const void* src, size_t size) +{ + memcpy(dst, src, size); + return dst + size; +} + +static char* buffer_append(char* dst, const char* src, size_t size) +{ + for (size_t i = 0; i < size; ++i, ++src, ++dst) + { + *dst = *src; + } + return dst; +} + +static char* buffer_append_string(char* dst, const char* src) +{ + return buffer_append(dst, src, strlen(src)); +} + +char* buffer_append_number(char* dst, uint8_t val) +{ + if (val >= 10) + { + if (val >= 100) + { + const uint8_t d = val / 100; + *dst++ = '0' + d; + val -= d * 100; + } + + const uint8_t d = val / 10; + *dst++ = '0' + d; + val -= d * 10; + } + + *dst++ = '0' + val; + return dst; +} + +struct FormatResult +{ + LONGLONG integral; + LONGLONG fractional; + const char* suffix; +}; + +#define FORMAT_RESULT_FMT "%lld.%03lld%s" +#define FORMAT_RESULT_ARGS(r) r.integral, r.fractional, r.suffix + +static FormatResult format_size(LONGLONG value) +{ + FormatResult result; + if (value >= 1'000'000'000) + { + result.integral = value / 1'000'000'000; + result.fractional = ((value + 500'000) / 1'000'000) % 1000; + result.suffix = "G"; + } + else if (value >= 1'000'000) + { + result.integral = value / 1'000'000; + result.fractional = ((value + 500) / 1'000) % 1000; + result.suffix = "M"; + } + else if (value >= 1'000) + { + result.integral = value / 1'000; + result.fractional = value % 1'000; + result.suffix = "k"; + } + else + { + result.integral = value; + result.fractional = 0; + result.suffix = ""; + } + return result; +} + +static FormatResult format_duration(LONGLONG microseconds) +{ + FormatResult result; + if (microseconds >= 1'000'000) + { + result.integral = microseconds / 1'000'000; + result.fractional = ((microseconds + 500) / 1'000) % 1000; + result.suffix = ""; + } + else + { + result.integral = microseconds / 1'000; + result.fractional = microseconds % 1'000; + result.suffix = "m"; + } + return result; +} + +static int format(char* buffer, int size, const char* format, ...) noexcept +{ + va_list vl; + va_start(vl, format); + const auto length = wvnsprintfA(buffer, size, format, vl); + va_end(vl); + return length; +} + +enum class VtMode +{ + Off, + On, + Italic, + Color +}; + +static HANDLE g_stdout; +static HANDLE g_stderr; +static UINT g_console_cp_old; +static DWORD g_console_mode_old; +static size_t g_large_page_minimum; + +[[noreturn]] static void clean_exit(UINT code) +{ + if (g_console_cp_old) + { + SetConsoleCP(g_console_cp_old); + } + if (g_console_mode_old) + { + SetConsoleMode(g_stdout, g_console_mode_old); + } + ExitProcess(code); +} + +[[noreturn]] static void eprintf(const char* format, ...) noexcept +{ + char buffer[1024]; + + va_list vl; + va_start(vl, format); + const auto length = wvnsprintfA(buffer, sizeof(buffer), format, vl); + va_end(vl); + + if (length > 0) + { + WriteFile(g_stderr, buffer, length, nullptr, nullptr); + } + + clean_exit(1); +} + +[[noreturn]] static void print_last_error(const char* what) noexcept +{ + eprintf("\r\nfailed to %s with 0x%08lx\r\n", what, GetLastError()); +} + +static void acquire_lock_memory_privilege() noexcept +{ + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) + { + return; + } + + TOKEN_PRIVILEGES privileges{}; + privileges.PrivilegeCount = 1; + privileges.Privileges[0].Luid = { 4, 0 }; // SE_LOCK_MEMORY_PRIVILEGE is a well known LUID and always {4, 0} + privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // AdjustTokenPrivileges can return true and still set the last error to ERROR_NOT_ALL_ASSIGNED. This API is nuts... + const bool success = AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr); + if (success && GetLastError() == S_OK) + { + g_large_page_minimum = GetLargePageMinimum(); + } + + CloseHandle(token); +} + +static char* allocate(size_t size) +{ + if (g_large_page_minimum) + { + const auto large_size = (size + g_large_page_minimum - 1) & ~(g_large_page_minimum - 1); + if (const auto address = static_cast(VirtualAlloc(nullptr, large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE))) + { + return address; + } + } + + if (const auto address = static_cast(VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))) + { + return address; + } + + print_last_error("allocate memory"); +} + +static BOOL WINAPI consoleCtrlHandler(DWORD) +{ + CancelIoEx(g_stdout, nullptr); + return TRUE; +} + +int __stdcall main() noexcept +{ + g_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + g_stderr = GetStdHandle(STD_OUTPUT_HANDLE); + g_console_cp_old = GetConsoleOutputCP(); + + SetConsoleCtrlHandler(consoleCtrlHandler, TRUE); + SetConsoleOutputCP(CP_UTF8); + + const wchar_t* path = nullptr; + uint32_t chunk_size = 128 * 1024; + uint32_t repeat = 1; + VtMode vt = VtMode::Off; + uint64_t seed = 0; + bool has_seed = false; + + { + int argc; + const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc); + + for (int i = 1; i < argc; ++i) + { + if (const auto suffix = split_prefix(argv[i], L"-c")) + { + // 1GiB is the maximum buffer size WriteFile seems to accept. + chunk_size = min(parse_number_with_suffix(suffix), 1024 * 1024 * 1024); + } + else if (const auto suffix = split_prefix(argv[i], L"-r")) + { + repeat = parse_number_with_suffix(suffix); + } + else if (const auto suffix = split_prefix(argv[i], L"-v")) + { + vt = VtMode::On; + if (has_suffix(suffix, L"i")) + { + vt = VtMode::Italic; + } + else if (has_suffix(suffix, L"c")) + { + vt = VtMode::Color; + } + else if (*suffix) + { + break; + } + } + else if (has_suffix(argv[i], L"-s")) + { + seed = parse_number_with_suffix(suffix); + has_seed = true; + } + else + { + if (argc - i == 1) + { + path = argv[i]; + } + break; + } + } + } + + if (!path || !chunk_size || !repeat) + { + eprintf( + "bc [options] \r\n" + " -v enable VT\r\n" + " -vi print as italic\r\n" + " -vc print colorized\r\n" + " -c{d}{u} chunk size, defaults to 128Ki\r\n" + " -r{d}{u} repeats, defaults to 1\r\n" + " -s{d} RNG seed\r\n" + "{d} are base-10 digits\r\n" + "{u} are suffix units k, Ki, M, Mi, G, Gi\r\n"); + } + + if (!has_seed && vt == VtMode::Color) + { + const auto cryptbase = LoadLibraryExW(L"cryptbase.dll", nullptr, 0); + if (!cryptbase) + { + print_last_error("get handle to cryptbase.dll"); + } + + const auto RtlGenRandom = reinterpret_cast(GetProcAddress(cryptbase, "SystemFunction036")); + if (!RtlGenRandom) + { + print_last_error("get handle to RtlGenRandom"); + } + + RtlGenRandom(&seed, sizeof(seed)); + } + + pcg_engines::oneseq_dxsm_64_32 rng{ seed }; + + const auto stdout = GetStdHandle(STD_OUTPUT_HANDLE); + const auto file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + print_last_error("open file"); + } + + size_t file_size = 0; + { +#ifdef _WIN64 + LARGE_INTEGER i; + if (!GetFileSizeEx(file, &i)) + { + print_last_error("open file"); + } + + file_size = static_cast(i.QuadPart); + +#else + file_size = GetFileSize(file, nullptr); + if (file_size == INVALID_FILE_SIZE) + { + print_last_error("open file"); + } +#endif + } + + acquire_lock_memory_privilege(); + + const auto file_data = allocate(file_size); + auto stdout_size = file_size; + auto stdout_data = file_data; + + { + auto read_data = file_data; + DWORD read = 0; + + for (auto remaining = file_size; remaining > 0; remaining -= read, read_data += read) + { + read = static_cast(min(0xffffffff, remaining)); + if (!ReadFile(file, read_data, read, &read, nullptr)) + { + print_last_error("read"); + } + } + } + + switch (vt) + { + case VtMode::Italic: + { + stdout_data = allocate(file_size + 16); + auto p = stdout_data; + p = buffer_append_string(p, "\x1b[3m"); + p = buffer_append_long(p, file_data, file_size); + p = buffer_append_string(p, "\x1b[0m"); + stdout_size = static_cast(p - stdout_data); + break; + } + case VtMode::Color: + { + if (const auto icu = LoadLibraryExW(L"icuuc.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)) + { + stdout_data = allocate(file_size * 20 + 8); + auto p = stdout_data; + + const auto p_utext_openUTF8 = reinterpret_cast(GetProcAddress(icu, "utext_openUTF8")); + const auto p_ubrk_open = reinterpret_cast(GetProcAddress(icu, "ubrk_open")); + const auto p_ubrk_setUText = reinterpret_cast(GetProcAddress(icu, "ubrk_setUText")); + const auto p_ubrk_next = reinterpret_cast(GetProcAddress(icu, "ubrk_next")); + + auto error = U_ZERO_ERROR; + UText text = UTEXT_INITIALIZER; + p_utext_openUTF8(&text, file_data, file_size, &error); + + const auto it = p_ubrk_open(UBRK_CHARACTER, "", nullptr, 0, &error); + p_ubrk_setUText(it, &text, &error); + + for (int32_t ubrk0 = 0, ubrk1; (ubrk1 = p_ubrk_next(it)) != UBRK_DONE; ubrk0 = ubrk1) + { + p = buffer_append_string(p, "\x1b[38;2"); + for (int i = 0; i < 3; i++) + { + *p++ = ';'; + p = buffer_append_number(p, static_cast(rng())); + } + p = buffer_append_string(p, "m"); + p = buffer_append(p, file_data + ubrk0, ubrk1 - ubrk0); + } + + p = buffer_append_string(p, "\x1b[39;49m"); + stdout_size = static_cast(p - stdout_data); + } + break; + } + default: + break; + } + + { + DWORD mode = 0; + if (!GetConsoleMode(g_stdout, &mode)) + { + print_last_error("get console mode"); + } + + g_console_mode_old = mode; + + mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT; + mode &= ~(ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + if (vt != VtMode::Off) + { + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + + if (!SetConsoleMode(g_stdout, mode)) + { + print_last_error("set console mode"); + } + } + + LARGE_INTEGER frequency, beg, end; + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&beg); + + for (size_t iteration = 0; iteration < repeat; ++iteration) + { + auto write_data = stdout_data; + DWORD written = 0; + + for (auto remaining = stdout_size; remaining != 0; remaining -= written, write_data += written) + { + written = static_cast(min(remaining, chunk_size)); + if (!WriteFile(stdout, write_data, written, &written, nullptr)) + { + print_last_error("write"); + } + } + } + + QueryPerformanceCounter(&end); + + const auto elapsed_ticks = end.QuadPart - beg.QuadPart; + const auto elapsed_us = (elapsed_ticks * 1'000'000) / frequency.QuadPart; + LONGLONG total_size = static_cast(stdout_size) * repeat; + const auto bytes_per_second = (total_size * frequency.QuadPart) / elapsed_ticks; + + const auto written = format_size(total_size); + const auto duration = format_duration(elapsed_us); + const auto throughput = format_size(bytes_per_second); + + char status[128]; + const auto status_length = format( + &status[0], + 1024, + FORMAT_RESULT_FMT "B, " FORMAT_RESULT_FMT "s, " FORMAT_RESULT_FMT "B/s", + FORMAT_RESULT_ARGS(written), + FORMAT_RESULT_ARGS(duration), + FORMAT_RESULT_ARGS(throughput)); + + if (status_length <= 0) + { + clean_exit(1); + } + + char buffer[256]; + char* buffer_end = &buffer[0]; + + buffer_end = buffer_append_string(buffer_end, "\r\n"); + for (int i = 0; i < status_length; ++i) + { + *buffer_end++ = '-'; + } + buffer_end = buffer_append_string(buffer_end, "\r\n"); + buffer_end = buffer_append_long(buffer_end, &status[0], static_cast(status_length)); + buffer_end = buffer_append_string(buffer_end, "\r\n"); + + WriteFile(g_stderr, &buffer[0], static_cast(buffer_end - &buffer[0]), nullptr, nullptr); + clean_exit(0); +} diff --git a/src/tools/lnkd/main.cpp b/src/tools/lnkd/main.cpp index 250e594a5f..e5d3875f9a 100644 --- a/src/tools/lnkd/main.cpp +++ b/src/tools/lnkd/main.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include +#include "precomp.h" void PrintUsage() { diff --git a/src/tools/pixels/main.cpp b/src/tools/pixels/main.cpp index 9c8e5f6aef..b2851f7b77 100644 --- a/src/tools/pixels/main.cpp +++ b/src/tools/pixels/main.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include +#include "precomp.h" #include diff --git a/src/tools/test/main.cpp b/src/tools/test/main.cpp index a617f007fa..ffc9b9a0cd 100644 --- a/src/tools/test/main.cpp +++ b/src/tools/test/main.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include +#include "precomp.h" #include #include diff --git a/src/tsf/TfConvArea.cpp b/src/tsf/TfConvArea.cpp index b9ab24b4f6..940bb7af09 100644 --- a/src/tsf/TfConvArea.cpp +++ b/src/tsf/TfConvArea.cpp @@ -45,8 +45,8 @@ Notes: const auto encodedAttributes = _DisplayAttributesToEncodedAttributes(DisplayAttributes, CompCursorPos); - gsl::span attributes(encodedAttributes.data(), encodedAttributes.size()); - gsl::span colorArray(colors.data(), colors.size()); + std::span attributes(encodedAttributes.data(), encodedAttributes.size()); + std::span colorArray(colors.data(), colors.size()); return ImeComposeData(CompStr, attributes, colorArray); } diff --git a/src/types/CodepointWidthDetector.cpp b/src/types/CodepointWidthDetector.cpp index c48b90fb9c..6848aa5b61 100644 --- a/src/types/CodepointWidthDetector.cpp +++ b/src/types/CodepointWidthDetector.cpp @@ -9,9 +9,9 @@ namespace // used to store range data in CodepointWidthDetector's internal map struct UnicodeRange final { - unsigned int lowerBound; - unsigned int upperBound; - CodepointWidth width; + char32_t lowerBound; + char32_t upperBound : 31; + char32_t isAmbiguous : 1; }; static bool operator<(const UnicodeRange& range, const unsigned int searchTerm) noexcept @@ -19,382 +19,348 @@ namespace return range.upperBound < searchTerm; } - // Generated by Generate-CodepointWidthsFromUCD.ps1 -Pack:True -Full:False -NoOverrides:False - // on 9/14/2022 7:12:26 PM (UTC) from Unicode 15.0.0. - // 321281 (0x4E701) codepoints covered. + // Generated by Generate-CodepointWidthsFromUCD.ps1 -Pack:True -Full: -NoOverrides:False + // on 2022-11-15 19:54:23Z from Unicode 15.0.0. + // 321149 (0x4E67D) codepoints covered. // 240 (0xF0) codepoints overridden. // Override path: .\src\types\unicode_width_overrides.xml - static constexpr std::array s_wideAndAmbiguousTable{ - UnicodeRange{ 0xa1, 0xa1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xa4, 0xa4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xa7, 0xa8, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xaa, 0xaa, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xad, 0xae, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xb0, 0xb4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xb6, 0xba, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xbc, 0xbf, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xc6, 0xc6, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xd0, 0xd0, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xd7, 0xd8, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xde, 0xe1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xe6, 0xe6, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xe8, 0xea, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xec, 0xed, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xf0, 0xf0, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xf2, 0xf3, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xf7, 0xfa, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xfc, 0xfc, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xfe, 0xfe, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x101, 0x101, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x111, 0x111, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x113, 0x113, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x11b, 0x11b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x126, 0x127, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x12b, 0x12b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x131, 0x133, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x138, 0x138, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x13f, 0x142, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x144, 0x144, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x148, 0x14b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x14d, 0x14d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x152, 0x153, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x166, 0x167, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x16b, 0x16b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1ce, 0x1ce, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1d0, 0x1d0, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1d2, 0x1d2, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1d4, 0x1d4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1d6, 0x1d6, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1d8, 0x1d8, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1da, 0x1da, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1dc, 0x1dc, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x251, 0x251, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x261, 0x261, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2c4, 0x2c4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2c7, 0x2c7, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2c9, 0x2cb, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2cd, 0x2cd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2d0, 0x2d0, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2d8, 0x2db, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2dd, 0x2dd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2df, 0x2df, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x300, 0x36f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x391, 0x3a1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x3a3, 0x3a9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x3b1, 0x3c1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x3c3, 0x3c9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x401, 0x401, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x410, 0x44f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x451, 0x451, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1100, 0x115f, CodepointWidth::Wide }, - UnicodeRange{ 0x2010, 0x2010, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2013, 0x2016, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2018, 0x2019, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x201c, 0x201d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2020, 0x2022, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2024, 0x2027, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2030, 0x2030, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2032, 0x2033, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2035, 0x2035, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x203b, 0x203b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x203e, 0x203e, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2074, 0x2074, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x207f, 0x207f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2081, 0x2084, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x20ac, 0x20ac, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2103, 0x2103, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2105, 0x2105, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2109, 0x2109, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2113, 0x2113, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2116, 0x2116, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2121, 0x2122, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2126, 0x2126, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x212b, 0x212b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2153, 0x2154, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x215b, 0x215e, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2160, 0x216b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2170, 0x2179, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2189, 0x2189, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2190, 0x2199, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x21b8, 0x21b9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x21d2, 0x21d2, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x21d4, 0x21d4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x21e7, 0x21e7, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2200, 0x2200, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2202, 0x2203, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2207, 0x2208, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x220b, 0x220b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x220f, 0x220f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2211, 0x2211, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2215, 0x2215, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x221a, 0x221a, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x221d, 0x2220, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2223, 0x2223, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2225, 0x2225, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2227, 0x222c, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x222e, 0x222e, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2234, 0x2237, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x223c, 0x223d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2248, 0x2248, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x224c, 0x224c, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2252, 0x2252, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2260, 0x2261, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2264, 0x2267, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x226a, 0x226b, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x226e, 0x226f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2282, 0x2283, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2286, 0x2287, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2295, 0x2295, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2299, 0x2299, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x22a5, 0x22a5, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x22bf, 0x22bf, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2312, 0x2312, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x231a, 0x231b, CodepointWidth::Wide }, - UnicodeRange{ 0x2329, 0x232a, CodepointWidth::Wide }, - UnicodeRange{ 0x23e9, 0x23ec, CodepointWidth::Wide }, - UnicodeRange{ 0x23f0, 0x23f0, CodepointWidth::Wide }, - UnicodeRange{ 0x23f3, 0x23f3, CodepointWidth::Wide }, - UnicodeRange{ 0x2460, 0x24e9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x24eb, 0x24ff, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2500, 0x259f, CodepointWidth::Narrow }, // box-drawing and block elements require 1-cell alignment - UnicodeRange{ 0x25a0, 0x25a1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25a3, 0x25a9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25b2, 0x25b3, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25b6, 0x25b7, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25bc, 0x25bd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25c0, 0x25c1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25c6, 0x25c8, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25cb, 0x25cb, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25ce, 0x25d1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25e2, 0x25e5, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25ef, 0x25ef, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x25fd, 0x25fe, CodepointWidth::Wide }, - UnicodeRange{ 0x2605, 0x2606, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2609, 0x2609, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x260e, 0x260f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2614, 0x2615, CodepointWidth::Wide }, - UnicodeRange{ 0x261c, 0x261c, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x261e, 0x261e, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2640, 0x2640, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2642, 0x2642, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2648, 0x2653, CodepointWidth::Wide }, - UnicodeRange{ 0x2660, 0x2661, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2663, 0x2665, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2667, 0x266a, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x266c, 0x266d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x266f, 0x266f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x267f, 0x267f, CodepointWidth::Wide }, - UnicodeRange{ 0x2693, 0x2693, CodepointWidth::Wide }, - UnicodeRange{ 0x269e, 0x269f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26a1, 0x26a1, CodepointWidth::Wide }, - UnicodeRange{ 0x26aa, 0x26ab, CodepointWidth::Wide }, - UnicodeRange{ 0x26bd, 0x26be, CodepointWidth::Wide }, - UnicodeRange{ 0x26bf, 0x26bf, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26c4, 0x26c5, CodepointWidth::Wide }, - UnicodeRange{ 0x26c6, 0x26cd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26ce, 0x26ce, CodepointWidth::Wide }, - UnicodeRange{ 0x26cf, 0x26d3, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26d4, 0x26d4, CodepointWidth::Wide }, - UnicodeRange{ 0x26d5, 0x26e1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26e3, 0x26e3, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26e8, 0x26e9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26ea, 0x26ea, CodepointWidth::Wide }, - UnicodeRange{ 0x26eb, 0x26f1, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26f2, 0x26f3, CodepointWidth::Wide }, - UnicodeRange{ 0x26f4, 0x26f4, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26f5, 0x26f5, CodepointWidth::Wide }, - UnicodeRange{ 0x26f6, 0x26f9, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26fa, 0x26fa, CodepointWidth::Wide }, - UnicodeRange{ 0x26fb, 0x26fc, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x26fd, 0x26fd, CodepointWidth::Wide }, - UnicodeRange{ 0x26fe, 0x26ff, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2705, 0x2705, CodepointWidth::Wide }, - UnicodeRange{ 0x270a, 0x270b, CodepointWidth::Wide }, - UnicodeRange{ 0x2728, 0x2728, CodepointWidth::Wide }, - UnicodeRange{ 0x273d, 0x273d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x274c, 0x274c, CodepointWidth::Wide }, - UnicodeRange{ 0x274e, 0x274e, CodepointWidth::Wide }, - UnicodeRange{ 0x2753, 0x2755, CodepointWidth::Wide }, - UnicodeRange{ 0x2757, 0x2757, CodepointWidth::Wide }, - UnicodeRange{ 0x2776, 0x277f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2795, 0x2797, CodepointWidth::Wide }, - UnicodeRange{ 0x27b0, 0x27b0, CodepointWidth::Wide }, - UnicodeRange{ 0x27bf, 0x27bf, CodepointWidth::Wide }, - UnicodeRange{ 0x2b1b, 0x2b1c, CodepointWidth::Wide }, - UnicodeRange{ 0x2b50, 0x2b50, CodepointWidth::Wide }, - UnicodeRange{ 0x2b55, 0x2b55, CodepointWidth::Wide }, - UnicodeRange{ 0x2b56, 0x2b59, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x2e80, 0x2e99, CodepointWidth::Wide }, - UnicodeRange{ 0x2e9b, 0x2ef3, CodepointWidth::Wide }, - UnicodeRange{ 0x2f00, 0x2fd5, CodepointWidth::Wide }, - UnicodeRange{ 0x2ff0, 0x2ffb, CodepointWidth::Wide }, - UnicodeRange{ 0x3000, 0x303e, CodepointWidth::Wide }, - UnicodeRange{ 0x3041, 0x3096, CodepointWidth::Wide }, - UnicodeRange{ 0x3099, 0x30ff, CodepointWidth::Wide }, - UnicodeRange{ 0x3105, 0x312f, CodepointWidth::Wide }, - UnicodeRange{ 0x3131, 0x318e, CodepointWidth::Wide }, - UnicodeRange{ 0x3190, 0x31e3, CodepointWidth::Wide }, - UnicodeRange{ 0x31f0, 0x321e, CodepointWidth::Wide }, - UnicodeRange{ 0x3220, 0x3247, CodepointWidth::Wide }, - UnicodeRange{ 0x3248, 0x324f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x3250, 0x4dbf, CodepointWidth::Wide }, - UnicodeRange{ 0x4dc0, 0x4dff, CodepointWidth::Narrow }, // hexagrams are historically narrow - UnicodeRange{ 0x4e00, 0xa48c, CodepointWidth::Wide }, - UnicodeRange{ 0xa490, 0xa4c6, CodepointWidth::Wide }, - UnicodeRange{ 0xa960, 0xa97c, CodepointWidth::Wide }, - UnicodeRange{ 0xac00, 0xd7a3, CodepointWidth::Wide }, - UnicodeRange{ 0xe000, 0xf8ff, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xf900, 0xfaff, CodepointWidth::Wide }, - UnicodeRange{ 0xfe00, 0xfe0f, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xfe10, 0xfe19, CodepointWidth::Wide }, - UnicodeRange{ 0xfe20, 0xfe2f, CodepointWidth::Narrow }, // narrow combining ligatures (split into left/right halves, which take 2 columns together) - UnicodeRange{ 0xfe30, 0xfe52, CodepointWidth::Wide }, - UnicodeRange{ 0xfe54, 0xfe66, CodepointWidth::Wide }, - UnicodeRange{ 0xfe68, 0xfe6b, CodepointWidth::Wide }, - UnicodeRange{ 0xff01, 0xff60, CodepointWidth::Wide }, - UnicodeRange{ 0xffe0, 0xffe6, CodepointWidth::Wide }, - UnicodeRange{ 0xfffd, 0xfffd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x16fe0, 0x16fe4, CodepointWidth::Wide }, - UnicodeRange{ 0x16ff0, 0x16ff1, CodepointWidth::Wide }, - UnicodeRange{ 0x17000, 0x187f7, CodepointWidth::Wide }, - UnicodeRange{ 0x18800, 0x18cd5, CodepointWidth::Wide }, - UnicodeRange{ 0x18d00, 0x18d08, CodepointWidth::Wide }, - UnicodeRange{ 0x1aff0, 0x1aff3, CodepointWidth::Wide }, - UnicodeRange{ 0x1aff5, 0x1affb, CodepointWidth::Wide }, - UnicodeRange{ 0x1affd, 0x1affe, CodepointWidth::Wide }, - UnicodeRange{ 0x1b000, 0x1b122, CodepointWidth::Wide }, - UnicodeRange{ 0x1b132, 0x1b132, CodepointWidth::Wide }, - UnicodeRange{ 0x1b150, 0x1b152, CodepointWidth::Wide }, - UnicodeRange{ 0x1b155, 0x1b155, CodepointWidth::Wide }, - UnicodeRange{ 0x1b164, 0x1b167, CodepointWidth::Wide }, - UnicodeRange{ 0x1b170, 0x1b2fb, CodepointWidth::Wide }, - UnicodeRange{ 0x1f004, 0x1f004, CodepointWidth::Wide }, - UnicodeRange{ 0x1f0cf, 0x1f0cf, CodepointWidth::Wide }, - UnicodeRange{ 0x1f100, 0x1f10a, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f110, 0x1f12d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f130, 0x1f169, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f170, 0x1f18d, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f18e, 0x1f18e, CodepointWidth::Wide }, - UnicodeRange{ 0x1f18f, 0x1f190, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f191, 0x1f19a, CodepointWidth::Wide }, - UnicodeRange{ 0x1f19b, 0x1f1ac, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x1f1e6, 0x1f202, CodepointWidth::Wide }, - UnicodeRange{ 0x1f210, 0x1f23b, CodepointWidth::Wide }, - UnicodeRange{ 0x1f240, 0x1f248, CodepointWidth::Wide }, - UnicodeRange{ 0x1f250, 0x1f251, CodepointWidth::Wide }, - UnicodeRange{ 0x1f260, 0x1f265, CodepointWidth::Wide }, - UnicodeRange{ 0x1f300, 0x1f320, CodepointWidth::Wide }, - UnicodeRange{ 0x1f32d, 0x1f335, CodepointWidth::Wide }, - UnicodeRange{ 0x1f337, 0x1f37c, CodepointWidth::Wide }, - UnicodeRange{ 0x1f37e, 0x1f393, CodepointWidth::Wide }, - UnicodeRange{ 0x1f3a0, 0x1f3ca, CodepointWidth::Wide }, - UnicodeRange{ 0x1f3cf, 0x1f3d3, CodepointWidth::Wide }, - UnicodeRange{ 0x1f3e0, 0x1f3f0, CodepointWidth::Wide }, - UnicodeRange{ 0x1f3f4, 0x1f3f4, CodepointWidth::Wide }, - UnicodeRange{ 0x1f3f8, 0x1f43e, CodepointWidth::Wide }, - UnicodeRange{ 0x1f440, 0x1f440, CodepointWidth::Wide }, - UnicodeRange{ 0x1f442, 0x1f4fc, CodepointWidth::Wide }, - UnicodeRange{ 0x1f4ff, 0x1f53d, CodepointWidth::Wide }, - UnicodeRange{ 0x1f54b, 0x1f54e, CodepointWidth::Wide }, - UnicodeRange{ 0x1f550, 0x1f567, CodepointWidth::Wide }, - UnicodeRange{ 0x1f57a, 0x1f57a, CodepointWidth::Wide }, - UnicodeRange{ 0x1f595, 0x1f596, CodepointWidth::Wide }, - UnicodeRange{ 0x1f5a4, 0x1f5a4, CodepointWidth::Wide }, - UnicodeRange{ 0x1f5fb, 0x1f64f, CodepointWidth::Wide }, - UnicodeRange{ 0x1f680, 0x1f6c5, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6cc, 0x1f6cc, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6d0, 0x1f6d2, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6d5, 0x1f6d7, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6dc, 0x1f6df, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6eb, 0x1f6ec, CodepointWidth::Wide }, - UnicodeRange{ 0x1f6f4, 0x1f6fc, CodepointWidth::Wide }, - UnicodeRange{ 0x1f7e0, 0x1f7eb, CodepointWidth::Wide }, - UnicodeRange{ 0x1f7f0, 0x1f7f0, CodepointWidth::Wide }, - UnicodeRange{ 0x1f90c, 0x1f93a, CodepointWidth::Wide }, - UnicodeRange{ 0x1f93c, 0x1f945, CodepointWidth::Wide }, - UnicodeRange{ 0x1f947, 0x1f9ff, CodepointWidth::Wide }, - UnicodeRange{ 0x1fa70, 0x1fa7c, CodepointWidth::Wide }, - UnicodeRange{ 0x1fa80, 0x1fa88, CodepointWidth::Wide }, - UnicodeRange{ 0x1fa90, 0x1fabd, CodepointWidth::Wide }, - UnicodeRange{ 0x1fabf, 0x1fac5, CodepointWidth::Wide }, - UnicodeRange{ 0x1face, 0x1fadb, CodepointWidth::Wide }, - UnicodeRange{ 0x1fae0, 0x1fae8, CodepointWidth::Wide }, - UnicodeRange{ 0x1faf0, 0x1faf8, CodepointWidth::Wide }, - UnicodeRange{ 0x20000, 0x2fffd, CodepointWidth::Wide }, - UnicodeRange{ 0x30000, 0x3fffd, CodepointWidth::Wide }, - UnicodeRange{ 0xe0100, 0xe01ef, CodepointWidth::Ambiguous }, - UnicodeRange{ 0xf0000, 0xffffd, CodepointWidth::Ambiguous }, - UnicodeRange{ 0x100000, 0x10fffd, CodepointWidth::Ambiguous }, + static constexpr std::array s_wideAndAmbiguousTable{ + UnicodeRange{ 0xa1, 0xa1, 1 }, + UnicodeRange{ 0xa4, 0xa4, 1 }, + UnicodeRange{ 0xa7, 0xa8, 1 }, + UnicodeRange{ 0xaa, 0xaa, 1 }, + UnicodeRange{ 0xad, 0xae, 1 }, + UnicodeRange{ 0xb0, 0xb4, 1 }, + UnicodeRange{ 0xb6, 0xba, 1 }, + UnicodeRange{ 0xbc, 0xbf, 1 }, + UnicodeRange{ 0xc6, 0xc6, 1 }, + UnicodeRange{ 0xd0, 0xd0, 1 }, + UnicodeRange{ 0xd7, 0xd8, 1 }, + UnicodeRange{ 0xde, 0xe1, 1 }, + UnicodeRange{ 0xe6, 0xe6, 1 }, + UnicodeRange{ 0xe8, 0xea, 1 }, + UnicodeRange{ 0xec, 0xed, 1 }, + UnicodeRange{ 0xf0, 0xf0, 1 }, + UnicodeRange{ 0xf2, 0xf3, 1 }, + UnicodeRange{ 0xf7, 0xfa, 1 }, + UnicodeRange{ 0xfc, 0xfc, 1 }, + UnicodeRange{ 0xfe, 0xfe, 1 }, + UnicodeRange{ 0x101, 0x101, 1 }, + UnicodeRange{ 0x111, 0x111, 1 }, + UnicodeRange{ 0x113, 0x113, 1 }, + UnicodeRange{ 0x11b, 0x11b, 1 }, + UnicodeRange{ 0x126, 0x127, 1 }, + UnicodeRange{ 0x12b, 0x12b, 1 }, + UnicodeRange{ 0x131, 0x133, 1 }, + UnicodeRange{ 0x138, 0x138, 1 }, + UnicodeRange{ 0x13f, 0x142, 1 }, + UnicodeRange{ 0x144, 0x144, 1 }, + UnicodeRange{ 0x148, 0x14b, 1 }, + UnicodeRange{ 0x14d, 0x14d, 1 }, + UnicodeRange{ 0x152, 0x153, 1 }, + UnicodeRange{ 0x166, 0x167, 1 }, + UnicodeRange{ 0x16b, 0x16b, 1 }, + UnicodeRange{ 0x1ce, 0x1ce, 1 }, + UnicodeRange{ 0x1d0, 0x1d0, 1 }, + UnicodeRange{ 0x1d2, 0x1d2, 1 }, + UnicodeRange{ 0x1d4, 0x1d4, 1 }, + UnicodeRange{ 0x1d6, 0x1d6, 1 }, + UnicodeRange{ 0x1d8, 0x1d8, 1 }, + UnicodeRange{ 0x1da, 0x1da, 1 }, + UnicodeRange{ 0x1dc, 0x1dc, 1 }, + UnicodeRange{ 0x251, 0x251, 1 }, + UnicodeRange{ 0x261, 0x261, 1 }, + UnicodeRange{ 0x2c4, 0x2c4, 1 }, + UnicodeRange{ 0x2c7, 0x2c7, 1 }, + UnicodeRange{ 0x2c9, 0x2cb, 1 }, + UnicodeRange{ 0x2cd, 0x2cd, 1 }, + UnicodeRange{ 0x2d0, 0x2d0, 1 }, + UnicodeRange{ 0x2d8, 0x2db, 1 }, + UnicodeRange{ 0x2dd, 0x2dd, 1 }, + UnicodeRange{ 0x2df, 0x2df, 1 }, + UnicodeRange{ 0x300, 0x36f, 1 }, + UnicodeRange{ 0x391, 0x3a1, 1 }, + UnicodeRange{ 0x3a3, 0x3a9, 1 }, + UnicodeRange{ 0x3b1, 0x3c1, 1 }, + UnicodeRange{ 0x3c3, 0x3c9, 1 }, + UnicodeRange{ 0x401, 0x401, 1 }, + UnicodeRange{ 0x410, 0x44f, 1 }, + UnicodeRange{ 0x451, 0x451, 1 }, + UnicodeRange{ 0x1100, 0x115f, 0 }, + UnicodeRange{ 0x2010, 0x2010, 1 }, + UnicodeRange{ 0x2013, 0x2016, 1 }, + UnicodeRange{ 0x2018, 0x2019, 1 }, + UnicodeRange{ 0x201c, 0x201d, 1 }, + UnicodeRange{ 0x2020, 0x2022, 1 }, + UnicodeRange{ 0x2024, 0x2027, 1 }, + UnicodeRange{ 0x2030, 0x2030, 1 }, + UnicodeRange{ 0x2032, 0x2033, 1 }, + UnicodeRange{ 0x2035, 0x2035, 1 }, + UnicodeRange{ 0x203b, 0x203b, 1 }, + UnicodeRange{ 0x203e, 0x203e, 1 }, + UnicodeRange{ 0x2074, 0x2074, 1 }, + UnicodeRange{ 0x207f, 0x207f, 1 }, + UnicodeRange{ 0x2081, 0x2084, 1 }, + UnicodeRange{ 0x20ac, 0x20ac, 1 }, + UnicodeRange{ 0x2103, 0x2103, 1 }, + UnicodeRange{ 0x2105, 0x2105, 1 }, + UnicodeRange{ 0x2109, 0x2109, 1 }, + UnicodeRange{ 0x2113, 0x2113, 1 }, + UnicodeRange{ 0x2116, 0x2116, 1 }, + UnicodeRange{ 0x2121, 0x2122, 1 }, + UnicodeRange{ 0x2126, 0x2126, 1 }, + UnicodeRange{ 0x212b, 0x212b, 1 }, + UnicodeRange{ 0x2153, 0x2154, 1 }, + UnicodeRange{ 0x215b, 0x215e, 1 }, + UnicodeRange{ 0x2160, 0x216b, 1 }, + UnicodeRange{ 0x2170, 0x2179, 1 }, + UnicodeRange{ 0x2189, 0x2189, 1 }, + UnicodeRange{ 0x2190, 0x2199, 1 }, + UnicodeRange{ 0x21b8, 0x21b9, 1 }, + UnicodeRange{ 0x21d2, 0x21d2, 1 }, + UnicodeRange{ 0x21d4, 0x21d4, 1 }, + UnicodeRange{ 0x21e7, 0x21e7, 1 }, + UnicodeRange{ 0x2200, 0x2200, 1 }, + UnicodeRange{ 0x2202, 0x2203, 1 }, + UnicodeRange{ 0x2207, 0x2208, 1 }, + UnicodeRange{ 0x220b, 0x220b, 1 }, + UnicodeRange{ 0x220f, 0x220f, 1 }, + UnicodeRange{ 0x2211, 0x2211, 1 }, + UnicodeRange{ 0x2215, 0x2215, 1 }, + UnicodeRange{ 0x221a, 0x221a, 1 }, + UnicodeRange{ 0x221d, 0x2220, 1 }, + UnicodeRange{ 0x2223, 0x2223, 1 }, + UnicodeRange{ 0x2225, 0x2225, 1 }, + UnicodeRange{ 0x2227, 0x222c, 1 }, + UnicodeRange{ 0x222e, 0x222e, 1 }, + UnicodeRange{ 0x2234, 0x2237, 1 }, + UnicodeRange{ 0x223c, 0x223d, 1 }, + UnicodeRange{ 0x2248, 0x2248, 1 }, + UnicodeRange{ 0x224c, 0x224c, 1 }, + UnicodeRange{ 0x2252, 0x2252, 1 }, + UnicodeRange{ 0x2260, 0x2261, 1 }, + UnicodeRange{ 0x2264, 0x2267, 1 }, + UnicodeRange{ 0x226a, 0x226b, 1 }, + UnicodeRange{ 0x226e, 0x226f, 1 }, + UnicodeRange{ 0x2282, 0x2283, 1 }, + UnicodeRange{ 0x2286, 0x2287, 1 }, + UnicodeRange{ 0x2295, 0x2295, 1 }, + UnicodeRange{ 0x2299, 0x2299, 1 }, + UnicodeRange{ 0x22a5, 0x22a5, 1 }, + UnicodeRange{ 0x22bf, 0x22bf, 1 }, + UnicodeRange{ 0x2312, 0x2312, 1 }, + UnicodeRange{ 0x231a, 0x231b, 0 }, + UnicodeRange{ 0x2329, 0x232a, 0 }, + UnicodeRange{ 0x23e9, 0x23ec, 0 }, + UnicodeRange{ 0x23f0, 0x23f0, 0 }, + UnicodeRange{ 0x23f3, 0x23f3, 0 }, + UnicodeRange{ 0x2460, 0x24e9, 1 }, + UnicodeRange{ 0x24eb, 0x24ff, 1 }, + UnicodeRange{ 0x25a0, 0x25a1, 1 }, + UnicodeRange{ 0x25a3, 0x25a9, 1 }, + UnicodeRange{ 0x25b2, 0x25b3, 1 }, + UnicodeRange{ 0x25b6, 0x25b7, 1 }, + UnicodeRange{ 0x25bc, 0x25bd, 1 }, + UnicodeRange{ 0x25c0, 0x25c1, 1 }, + UnicodeRange{ 0x25c6, 0x25c8, 1 }, + UnicodeRange{ 0x25cb, 0x25cb, 1 }, + UnicodeRange{ 0x25ce, 0x25d1, 1 }, + UnicodeRange{ 0x25e2, 0x25e5, 1 }, + UnicodeRange{ 0x25ef, 0x25ef, 1 }, + UnicodeRange{ 0x25fd, 0x25fe, 0 }, + UnicodeRange{ 0x2605, 0x2606, 1 }, + UnicodeRange{ 0x2609, 0x2609, 1 }, + UnicodeRange{ 0x260e, 0x260f, 1 }, + UnicodeRange{ 0x2614, 0x2615, 0 }, + UnicodeRange{ 0x261c, 0x261c, 1 }, + UnicodeRange{ 0x261e, 0x261e, 1 }, + UnicodeRange{ 0x2640, 0x2640, 1 }, + UnicodeRange{ 0x2642, 0x2642, 1 }, + UnicodeRange{ 0x2648, 0x2653, 0 }, + UnicodeRange{ 0x2660, 0x2661, 1 }, + UnicodeRange{ 0x2663, 0x2665, 1 }, + UnicodeRange{ 0x2667, 0x266a, 1 }, + UnicodeRange{ 0x266c, 0x266d, 1 }, + UnicodeRange{ 0x266f, 0x266f, 1 }, + UnicodeRange{ 0x267f, 0x267f, 0 }, + UnicodeRange{ 0x2693, 0x2693, 0 }, + UnicodeRange{ 0x269e, 0x269f, 1 }, + UnicodeRange{ 0x26a1, 0x26a1, 0 }, + UnicodeRange{ 0x26aa, 0x26ab, 0 }, + UnicodeRange{ 0x26bd, 0x26be, 0 }, + UnicodeRange{ 0x26bf, 0x26bf, 1 }, + UnicodeRange{ 0x26c4, 0x26c5, 0 }, + UnicodeRange{ 0x26c6, 0x26cd, 1 }, + UnicodeRange{ 0x26ce, 0x26ce, 0 }, + UnicodeRange{ 0x26cf, 0x26d3, 1 }, + UnicodeRange{ 0x26d4, 0x26d4, 0 }, + UnicodeRange{ 0x26d5, 0x26e1, 1 }, + UnicodeRange{ 0x26e3, 0x26e3, 1 }, + UnicodeRange{ 0x26e8, 0x26e9, 1 }, + UnicodeRange{ 0x26ea, 0x26ea, 0 }, + UnicodeRange{ 0x26eb, 0x26f1, 1 }, + UnicodeRange{ 0x26f2, 0x26f3, 0 }, + UnicodeRange{ 0x26f4, 0x26f4, 1 }, + UnicodeRange{ 0x26f5, 0x26f5, 0 }, + UnicodeRange{ 0x26f6, 0x26f9, 1 }, + UnicodeRange{ 0x26fa, 0x26fa, 0 }, + UnicodeRange{ 0x26fb, 0x26fc, 1 }, + UnicodeRange{ 0x26fd, 0x26fd, 0 }, + UnicodeRange{ 0x26fe, 0x26ff, 1 }, + UnicodeRange{ 0x2705, 0x2705, 0 }, + UnicodeRange{ 0x270a, 0x270b, 0 }, + UnicodeRange{ 0x2728, 0x2728, 0 }, + UnicodeRange{ 0x273d, 0x273d, 1 }, + UnicodeRange{ 0x274c, 0x274c, 0 }, + UnicodeRange{ 0x274e, 0x274e, 0 }, + UnicodeRange{ 0x2753, 0x2755, 0 }, + UnicodeRange{ 0x2757, 0x2757, 0 }, + UnicodeRange{ 0x2776, 0x277f, 1 }, + UnicodeRange{ 0x2795, 0x2797, 0 }, + UnicodeRange{ 0x27b0, 0x27b0, 0 }, + UnicodeRange{ 0x27bf, 0x27bf, 0 }, + UnicodeRange{ 0x2b1b, 0x2b1c, 0 }, + UnicodeRange{ 0x2b50, 0x2b50, 0 }, + UnicodeRange{ 0x2b55, 0x2b55, 0 }, + UnicodeRange{ 0x2b56, 0x2b59, 1 }, + UnicodeRange{ 0x2e80, 0x2e99, 0 }, + UnicodeRange{ 0x2e9b, 0x2ef3, 0 }, + UnicodeRange{ 0x2f00, 0x2fd5, 0 }, + UnicodeRange{ 0x2ff0, 0x2ffb, 0 }, + UnicodeRange{ 0x3000, 0x303e, 0 }, + UnicodeRange{ 0x3041, 0x3096, 0 }, + UnicodeRange{ 0x3099, 0x30ff, 0 }, + UnicodeRange{ 0x3105, 0x312f, 0 }, + UnicodeRange{ 0x3131, 0x318e, 0 }, + UnicodeRange{ 0x3190, 0x31e3, 0 }, + UnicodeRange{ 0x31f0, 0x321e, 0 }, + UnicodeRange{ 0x3220, 0x3247, 0 }, + UnicodeRange{ 0x3248, 0x324f, 1 }, + UnicodeRange{ 0x3250, 0x4dbf, 0 }, + UnicodeRange{ 0x4e00, 0xa48c, 0 }, + UnicodeRange{ 0xa490, 0xa4c6, 0 }, + UnicodeRange{ 0xa960, 0xa97c, 0 }, + UnicodeRange{ 0xac00, 0xd7a3, 0 }, + UnicodeRange{ 0xe000, 0xf8ff, 1 }, + UnicodeRange{ 0xf900, 0xfaff, 0 }, + UnicodeRange{ 0xfe00, 0xfe0f, 1 }, + UnicodeRange{ 0xfe10, 0xfe19, 0 }, + UnicodeRange{ 0xfe30, 0xfe52, 0 }, + UnicodeRange{ 0xfe54, 0xfe66, 0 }, + UnicodeRange{ 0xfe68, 0xfe6b, 0 }, + UnicodeRange{ 0xff01, 0xff60, 0 }, + UnicodeRange{ 0xffe0, 0xffe6, 0 }, + UnicodeRange{ 0xfffd, 0xfffd, 1 }, + UnicodeRange{ 0x16fe0, 0x16fe4, 0 }, + UnicodeRange{ 0x16ff0, 0x16ff1, 0 }, + UnicodeRange{ 0x17000, 0x187f7, 0 }, + UnicodeRange{ 0x18800, 0x18cd5, 0 }, + UnicodeRange{ 0x18d00, 0x18d08, 0 }, + UnicodeRange{ 0x1aff0, 0x1aff3, 0 }, + UnicodeRange{ 0x1aff5, 0x1affb, 0 }, + UnicodeRange{ 0x1affd, 0x1affe, 0 }, + UnicodeRange{ 0x1b000, 0x1b122, 0 }, + UnicodeRange{ 0x1b132, 0x1b132, 0 }, + UnicodeRange{ 0x1b150, 0x1b152, 0 }, + UnicodeRange{ 0x1b155, 0x1b155, 0 }, + UnicodeRange{ 0x1b164, 0x1b167, 0 }, + UnicodeRange{ 0x1b170, 0x1b2fb, 0 }, + UnicodeRange{ 0x1f004, 0x1f004, 0 }, + UnicodeRange{ 0x1f0cf, 0x1f0cf, 0 }, + UnicodeRange{ 0x1f100, 0x1f10a, 1 }, + UnicodeRange{ 0x1f110, 0x1f12d, 1 }, + UnicodeRange{ 0x1f130, 0x1f169, 1 }, + UnicodeRange{ 0x1f170, 0x1f18d, 1 }, + UnicodeRange{ 0x1f18e, 0x1f18e, 0 }, + UnicodeRange{ 0x1f18f, 0x1f190, 1 }, + UnicodeRange{ 0x1f191, 0x1f19a, 0 }, + UnicodeRange{ 0x1f19b, 0x1f1ac, 1 }, + UnicodeRange{ 0x1f1e6, 0x1f202, 0 }, + UnicodeRange{ 0x1f210, 0x1f23b, 0 }, + UnicodeRange{ 0x1f240, 0x1f248, 0 }, + UnicodeRange{ 0x1f250, 0x1f251, 0 }, + UnicodeRange{ 0x1f260, 0x1f265, 0 }, + UnicodeRange{ 0x1f300, 0x1f320, 0 }, + UnicodeRange{ 0x1f32d, 0x1f335, 0 }, + UnicodeRange{ 0x1f337, 0x1f37c, 0 }, + UnicodeRange{ 0x1f37e, 0x1f393, 0 }, + UnicodeRange{ 0x1f3a0, 0x1f3ca, 0 }, + UnicodeRange{ 0x1f3cf, 0x1f3d3, 0 }, + UnicodeRange{ 0x1f3e0, 0x1f3f0, 0 }, + UnicodeRange{ 0x1f3f4, 0x1f3f4, 0 }, + UnicodeRange{ 0x1f3f8, 0x1f43e, 0 }, + UnicodeRange{ 0x1f440, 0x1f440, 0 }, + UnicodeRange{ 0x1f442, 0x1f4fc, 0 }, + UnicodeRange{ 0x1f4ff, 0x1f53d, 0 }, + UnicodeRange{ 0x1f54b, 0x1f54e, 0 }, + UnicodeRange{ 0x1f550, 0x1f567, 0 }, + UnicodeRange{ 0x1f57a, 0x1f57a, 0 }, + UnicodeRange{ 0x1f595, 0x1f596, 0 }, + UnicodeRange{ 0x1f5a4, 0x1f5a4, 0 }, + UnicodeRange{ 0x1f5fb, 0x1f64f, 0 }, + UnicodeRange{ 0x1f680, 0x1f6c5, 0 }, + UnicodeRange{ 0x1f6cc, 0x1f6cc, 0 }, + UnicodeRange{ 0x1f6d0, 0x1f6d2, 0 }, + UnicodeRange{ 0x1f6d5, 0x1f6d7, 0 }, + UnicodeRange{ 0x1f6dc, 0x1f6df, 0 }, + UnicodeRange{ 0x1f6eb, 0x1f6ec, 0 }, + UnicodeRange{ 0x1f6f4, 0x1f6fc, 0 }, + UnicodeRange{ 0x1f7e0, 0x1f7eb, 0 }, + UnicodeRange{ 0x1f7f0, 0x1f7f0, 0 }, + UnicodeRange{ 0x1f90c, 0x1f93a, 0 }, + UnicodeRange{ 0x1f93c, 0x1f945, 0 }, + UnicodeRange{ 0x1f947, 0x1f9ff, 0 }, + UnicodeRange{ 0x1fa70, 0x1fa7c, 0 }, + UnicodeRange{ 0x1fa80, 0x1fa88, 0 }, + UnicodeRange{ 0x1fa90, 0x1fabd, 0 }, + UnicodeRange{ 0x1fabf, 0x1fac5, 0 }, + UnicodeRange{ 0x1face, 0x1fadb, 0 }, + UnicodeRange{ 0x1fae0, 0x1fae8, 0 }, + UnicodeRange{ 0x1faf0, 0x1faf8, 0 }, + UnicodeRange{ 0x20000, 0x2fffd, 0 }, + UnicodeRange{ 0x30000, 0x3fffd, 0 }, + UnicodeRange{ 0xe0100, 0xe01ef, 1 }, + UnicodeRange{ 0xf0000, 0xffffd, 1 }, + UnicodeRange{ 0x100000, 0x10fffd, 1 }, }; } -// Routine Description: -// - Constructs an instance of the CodepointWidthDetector class -CodepointWidthDetector::CodepointWidthDetector() noexcept : - _fallbackCache{}, - _pfnFallbackMethod{} -{ -} - // Routine Description: // - returns the width type of codepoint as fast as we can by using quick lookup table and fallback cache. // Arguments: // - glyph - the utf16 encoded codepoint to search for // Return Value: // - the width type of the codepoint -CodepointWidth CodepointWidthDetector::GetWidth(const std::wstring_view glyph) const +CodepointWidth CodepointWidthDetector::GetWidth(const std::wstring_view& glyph) noexcept { - THROW_HR_IF(E_INVALIDARG, glyph.empty()); - if (glyph.size() == 1) - { - // We first attempt to look at our custom quick lookup table of char width preferences. - const auto width = GetQuickCharWidth(glyph.front()); + char32_t codepoint = 0; - // If it's invalid, the quick width had no opinion, so go to the lookup table. - if (width == CodepointWidth::Invalid) - { - return _lookupGlyphWidthWithCache(glyph); - } - // If it's ambiguous, the quick width wanted us to ask the font directly, try that if we can. - // If not, go to the lookup table. - else if (width == CodepointWidth::Ambiguous) - { - if (_pfnFallbackMethod) - { - return _checkFallbackViaCache(glyph) ? CodepointWidth::Wide : CodepointWidth::Ambiguous; - } - else - { - return _lookupGlyphWidthWithCache(glyph); - } - } - // Otherwise, return Width as it is. - else - { - return width; - } - } - else + switch (glyph.size()) { - return _lookupGlyphWidthWithCache(glyph); + case 1: + codepoint = til::at(glyph, 0); + break; + case 2: + codepoint = (til::at(glyph, 0) & 0x3FF) << 10; + codepoint |= til::at(glyph, 1) & 0x3FF; + codepoint += 0x10000; + break; + default: + codepoint = 0; + break; } -} -// Routine Description: -// - checks if wch is wide. will attempt to fallback as much possible until an answer is determined -// Arguments: -// - wch - the wchar to check width of -// Return Value: -// - true if wch is wide -bool CodepointWidthDetector::IsWide(const wchar_t wch) const noexcept -{ - try + if (codepoint < 0x80) { - return IsWide({ &wch, 1 }); + return CodepointWidth::Narrow; } - CATCH_LOG(); - return true; + // The return value of _lookupGlyphWidth coincides with the enum value of CodepointWidth + // on purpose to allow for this easy conversion to happen. Optimally, we should probably + // remove CodepointWidth altogether to allow for zero-width joiners and other characters. + static_assert(WI_EnumValue(CodepointWidth::Narrow) == 1); + static_assert(WI_EnumValue(CodepointWidth::Wide) == 2); + return static_cast(_lookupGlyphWidth(codepoint, glyph)); } // Routine Description: @@ -403,123 +369,54 @@ bool CodepointWidthDetector::IsWide(const wchar_t wch) const noexcept // - glyph - the utf16 encoded codepoint to check width of // Return Value: // - true if codepoint is wide -bool CodepointWidthDetector::IsWide(const std::wstring_view glyph) const +bool CodepointWidthDetector::IsWide(const std::wstring_view& glyph) noexcept { return GetWidth(glyph) == CodepointWidth::Wide; } -// Routine Description: -// - returns the width type of codepoint by searching the map generated from the unicode spec -// Arguments: -// - glyph - the utf16 encoded codepoint to search for -// Return Value: -// - the width type of the codepoint -CodepointWidth CodepointWidthDetector::_lookupGlyphWidth(const std::wstring_view glyph) const +// GetWidth's slow-path for non-ASCII characters. Returns the number of columns the codepoint takes up in the terminal. +uint8_t CodepointWidthDetector::_lookupGlyphWidth(const char32_t codepoint, const std::wstring_view& glyph) noexcept { - if (glyph.empty()) - { - return CodepointWidth::Invalid; - } - - const auto codepoint = _extractCodepoint(glyph); +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function 'lower_bound<...>()' which may throw exceptions (f.6). const auto it = std::lower_bound(s_wideAndAmbiguousTable.begin(), s_wideAndAmbiguousTable.end(), codepoint); + uint8_t width = 1; - // For characters that are not _in_ the table, lower_bound will return the nearest item that is. - // We must check its bounds to make sure that our hit was a true hit. if (it != s_wideAndAmbiguousTable.end() && codepoint >= it->lowerBound && codepoint <= it->upperBound) { - return it->width; - } - - return CodepointWidth::Narrow; -} - -// Routine Description: -// - returns the width type of codepoint using fallback methods. -// Arguments: -// - glyph - the utf16 encoded codepoint to check width of -// Return Value: -// - the width type of the codepoint -CodepointWidth CodepointWidthDetector::_lookupGlyphWidthWithCache(const std::wstring_view glyph) const noexcept -{ - try - { - // Use our generated table to try to lookup the width based on the Unicode standard. - const auto width = _lookupGlyphWidth(glyph); - - // If it's ambiguous, then ask the font if we can. - if (width == CodepointWidth::Ambiguous) + width = 2; + if (it->isAmbiguous) { - if (_pfnFallbackMethod) - { - return _checkFallbackViaCache(glyph) ? CodepointWidth::Wide : CodepointWidth::Ambiguous; - } - else - { - return CodepointWidth::Ambiguous; - } - } - // If it's not ambiguous, it should say wide or narrow. - else - { - return width; + width = _checkFallbackViaCache(codepoint, glyph); } } - CATCH_LOG(); - // If we got this far, we couldn't figure it out. - // It's better to be too wide than too narrow. - return CodepointWidth::Wide; + return width; } -// Routine Description: -// - Checks the fallback function but caches the results until the font changes -// because the lookup function is usually very expensive and will return the same results -// for the same inputs. -// Arguments: -// - glyph - the utf16 encoded codepoint to check width of -// - true if codepoint is wide or false if it is narrow -bool CodepointWidthDetector::_checkFallbackViaCache(const std::wstring_view glyph) const +// Call the function specified via SetFallbackMethod() to turn CodepointWidth::Ambiguous into Narrow/Wide. +// Caches the results in _fallbackCache. This is _lookupGlyphWidth's even-slower-path. +uint8_t CodepointWidthDetector::_checkFallbackViaCache(const char32_t codepoint, const std::wstring_view& glyph) noexcept +try { - const std::wstring findMe{ glyph }; - - // TODO: Cache needs to be emptied when font changes. - const auto it = _fallbackCache.find(findMe); - if (it == _fallbackCache.end()) + // Ambiguous glyphs are considered narrow by default. See microsoft/terminal#2066 for more info. + if (!_pfnFallbackMethod) { - auto result = _pfnFallbackMethod(glyph); - _fallbackCache.insert_or_assign(findMe, result); - return result; + return 1; } - else + + if (const auto it = _fallbackCache.find(codepoint); it != _fallbackCache.end()) { return it->second; } -} -// Routine Description: -// - extract unicode codepoint from utf16 encoding -// Arguments: -// - glyph - the utf16 encoded codepoint convert -// Return Value: -// - the codepoint being stored -unsigned int CodepointWidthDetector::_extractCodepoint(const std::wstring_view glyph) noexcept + const uint8_t width = _pfnFallbackMethod(glyph) ? 2 : 1; + _fallbackCache.insert_or_assign(codepoint, width); + return width; +} +catch (...) { - if (glyph.size() == 1) - { - return static_cast(glyph.front()); - } - else - { - const unsigned int mask = 0x3FF; - // leading bits, shifted over to make space for trailing bits - auto codepoint = (glyph.at(0) & mask) << 10; - // trailing bits - codepoint |= (glyph.at(1) & mask); - // 0x10000 is subtracted from the codepoint to encode a surrogate pair, add it back - codepoint += 0x10000; - return codepoint; - } + LOG_CAUGHT_EXCEPTION(); + return 1; } // Method Description: @@ -532,9 +429,9 @@ unsigned int CodepointWidthDetector::_extractCodepoint(const std::wstring_view g // - pfnFallback - the function to use as the fallback method. // Return Value: // - -void CodepointWidthDetector::SetFallbackMethod(std::function pfnFallback) +void CodepointWidthDetector::SetFallbackMethod(std::function pfnFallback) noexcept { - _pfnFallbackMethod = pfnFallback; + _pfnFallbackMethod = std::move(pfnFallback); } // Method Description: @@ -545,7 +442,8 @@ void CodepointWidthDetector::SetFallbackMethod(std::function // Return Value: // - -void CodepointWidthDetector::NotifyFontChanged() const noexcept +void CodepointWidthDetector::NotifyFontChanged() noexcept { +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function 'clear()' which may throw exceptions (f.6). _fallbackCache.clear(); } diff --git a/src/types/ColorFix.cpp b/src/types/ColorFix.cpp index 723d7059f6..eda47d396a 100644 --- a/src/types/ColorFix.cpp +++ b/src/types/ColorFix.cpp @@ -1,231 +1,212 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// A lot of code was taken from -// https://github.com/Maximus5/ConEmu/blob/master/src/ConEmu/ColorFix.cpp -// and then adjusted to fit our style guidelines - #include "precomp.h" - #include "inc/ColorFix.hpp" +// This table contains a direct mapping from 8-bit sRGB to linear RGB. +// It was generated using the following code: +// #include +// +// int main() { +// char buffer[32]; +// +// for (int i = 0; i < 256; ++i) { +// auto v = i / 255.0f; +// v = v <= 0.04045f ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f); +// +// auto p = std::to_chars(&buffer[0], &buffer[32], v, std::chars_format::hex, -1).ptr; +// strcpy_s(p, &buffer[32] - p, "f,"); +// +// if (i % 16 == 0) printf("\n "); +// printf("0x%-15s", &buffer[0]); +// } +// +// return 0; +// } +// +// clang-format off +static constexpr float srgbToRgbLUT[256]{ + 0x0.000000p+0f, 0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f, 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f, 0x1.3e4568p-9f, 0x1.660e16p-9f, 0x1.8dd6c2p-9f, 0x1.b6a31ap-9f, 0x1.e1e31ap-9f, 0x1.07c38cp-8f, 0x1.1fcc2cp-8f, 0x1.390ffap-8f, + 0x1.53936ep-8f, 0x1.6f5adep-8f, 0x1.8c6a92p-8f, 0x1.aac6c2p-8f, 0x1.ca7382p-8f, 0x1.eb74e0p-8f, 0x1.06e76ap-7f, 0x1.18c2a4p-7f, 0x1.2b4e06p-7f, 0x1.3e8b7cp-7f, 0x1.527cd6p-7f, 0x1.6723eep-7f, 0x1.7c8292p-7f, 0x1.929a86p-7f, 0x1.a96d8ep-7f, 0x1.c0fd62p-7f, + 0x1.d94bbep-7f, 0x1.f25a44p-7f, 0x1.061550p-6f, 0x1.135f3ep-6f, 0x1.210bb6p-6f, 0x1.2f1b8ap-6f, 0x1.3d8f84p-6f, 0x1.4c6866p-6f, 0x1.5ba6fap-6f, 0x1.6b4c02p-6f, 0x1.7b5840p-6f, 0x1.8bcc72p-6f, 0x1.9ca956p-6f, 0x1.adefaap-6f, 0x1.bfa020p-6f, 0x1.d1bb72p-6f, + 0x1.e44258p-6f, 0x1.f73582p-6f, 0x1.054ad2p-5f, 0x1.0f31b8p-5f, 0x1.194fccp-5f, 0x1.23a55ep-5f, 0x1.2e32c6p-5f, 0x1.38f85cp-5f, 0x1.43f678p-5f, 0x1.4f2d64p-5f, 0x1.5a9d76p-5f, 0x1.664700p-5f, 0x1.722a56p-5f, 0x1.7e47c6p-5f, 0x1.8a9fa2p-5f, 0x1.973238p-5f, + 0x1.a3ffdep-5f, 0x1.b108d4p-5f, 0x1.be4d6ep-5f, 0x1.cbcdfcp-5f, 0x1.d98ac8p-5f, 0x1.e7841ep-5f, 0x1.f5ba52p-5f, 0x1.0216cep-4f, 0x1.096f2ap-4f, 0x1.10e660p-4f, 0x1.187c94p-4f, 0x1.2031ecp-4f, 0x1.28068ap-4f, 0x1.2ffa94p-4f, 0x1.380e2cp-4f, 0x1.404176p-4f, + 0x1.489496p-4f, 0x1.5107aep-4f, 0x1.599ae0p-4f, 0x1.624e50p-4f, 0x1.6b2224p-4f, 0x1.741676p-4f, 0x1.7d2b6ap-4f, 0x1.866124p-4f, 0x1.8fb7c4p-4f, 0x1.992f6cp-4f, 0x1.a2c83ep-4f, 0x1.ac8258p-4f, 0x1.b65ddep-4f, 0x1.c05aeep-4f, 0x1.ca79aap-4f, 0x1.d4ba32p-4f, + 0x1.df1ca4p-4f, 0x1.e9a122p-4f, 0x1.f447d0p-4f, 0x1.ff10c2p-4f, 0x1.04fe0ep-3f, 0x1.0a8500p-3f, 0x1.101d46p-3f, 0x1.15c6f0p-3f, 0x1.1b820ap-3f, 0x1.214ea8p-3f, 0x1.272cd6p-3f, 0x1.2d1ca4p-3f, 0x1.331e20p-3f, 0x1.39315ap-3f, 0x1.3f5660p-3f, 0x1.458d46p-3f, + 0x1.4bd612p-3f, 0x1.5230d8p-3f, 0x1.589da0p-3f, 0x1.5f1c82p-3f, 0x1.65ad88p-3f, 0x1.6c50c2p-3f, 0x1.73063cp-3f, 0x1.79ce06p-3f, 0x1.80a82cp-3f, 0x1.8794bep-3f, 0x1.8e93c8p-3f, 0x1.95a55cp-3f, 0x1.9cc984p-3f, 0x1.a4004ep-3f, 0x1.ab49ccp-3f, 0x1.b2a606p-3f, + 0x1.ba1516p-3f, 0x1.c196f8p-3f, 0x1.c92bc0p-3f, 0x1.d0d37ep-3f, 0x1.d88e40p-3f, 0x1.e05c18p-3f, 0x1.e83d08p-3f, 0x1.f03120p-3f, 0x1.f83870p-3f, 0x1.002984p-2f, 0x1.044078p-2f, 0x1.08611ap-2f, 0x1.0c8b74p-2f, 0x1.10bf8ap-2f, 0x1.14fd64p-2f, 0x1.194506p-2f, + 0x1.1d967ap-2f, 0x1.21f1c2p-2f, 0x1.2656e8p-2f, 0x1.2ac5f0p-2f, 0x1.2f3ee2p-2f, 0x1.33c1c4p-2f, 0x1.384e9cp-2f, 0x1.3ce56ep-2f, 0x1.418644p-2f, 0x1.463124p-2f, 0x1.4ae610p-2f, 0x1.4fa512p-2f, 0x1.546e30p-2f, 0x1.59416ep-2f, 0x1.5e1ed2p-2f, 0x1.630666p-2f, + 0x1.67f830p-2f, 0x1.6cf430p-2f, 0x1.71fa6ep-2f, 0x1.770af2p-2f, 0x1.7c25c2p-2f, 0x1.814ae2p-2f, 0x1.867a5ap-2f, 0x1.8bb42ep-2f, 0x1.90f866p-2f, 0x1.964706p-2f, 0x1.9ba016p-2f, 0x1.a1039ap-2f, 0x1.a67198p-2f, 0x1.abea16p-2f, 0x1.b16d1ap-2f, 0x1.b6faa8p-2f, + 0x1.bc92cap-2f, 0x1.c23582p-2f, 0x1.c7e2d6p-2f, 0x1.cd9acep-2f, 0x1.d35d6cp-2f, 0x1.d92abap-2f, 0x1.df02bap-2f, 0x1.e4e574p-2f, 0x1.ead2ecp-2f, 0x1.f0cb28p-2f, 0x1.f6ce2ep-2f, 0x1.fcdc04p-2f, 0x1.017a5ap-1f, 0x1.048c1cp-1f, 0x1.07a34ep-1f, 0x1.0abff2p-1f, + 0x1.0de20cp-1f, 0x1.11099ep-1f, 0x1.1436acp-1f, 0x1.176936p-1f, 0x1.1aa140p-1f, 0x1.1ddecep-1f, 0x1.2121e2p-1f, 0x1.246a7cp-1f, 0x1.27b8a2p-1f, 0x1.2b0c56p-1f, 0x1.2e659ap-1f, 0x1.31c472p-1f, 0x1.3528dep-1f, 0x1.3892e2p-1f, 0x1.3c0282p-1f, 0x1.3f77bep-1f, + 0x1.42f29cp-1f, 0x1.46731cp-1f, 0x1.49f940p-1f, 0x1.4d850cp-1f, 0x1.511682p-1f, 0x1.54ada6p-1f, 0x1.584a7ap-1f, 0x1.5bed00p-1f, 0x1.5f953cp-1f, 0x1.63432ep-1f, 0x1.66f6d8p-1f, 0x1.6ab040p-1f, 0x1.6e6f66p-1f, 0x1.72344cp-1f, 0x1.75fef8p-1f, 0x1.79cf6ap-1f, + 0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b20p-1f, 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f, 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a54430p-1f, 0x1.a95aaep-1f, 0x1.ad7714p-1f, 0x1.b19960p-1f, 0x1.b5c19ap-1f, 0x1.b9efc0p-1f, + 0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f, 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f, 0x1.e09b70p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f, 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71c0p-1f, 0x1.000000p+0f, +}; +// clang-format on + TIL_FAST_MATH_BEGIN -static constexpr float gMinThreshold = 12.0f; -static constexpr float gExpThreshold = 20.0f; -static constexpr float gLStep = 5.0f; - -static constexpr float rad006 = 0.104719755119659774f; -static constexpr float rad025 = 0.436332312998582394f; -static constexpr float rad030 = 0.523598775598298873f; -static constexpr float rad060 = 1.047197551196597746f; -static constexpr float rad063 = 1.099557428756427633f; -static constexpr float rad180 = 3.141592653589793238f; -static constexpr float rad275 = 4.799655442984406336f; -static constexpr float rad360 = 6.283185307179586476f; - -ColorFix::ColorFix(COLORREF color) noexcept +constexpr float saturate(float f) noexcept { - rgb = color; - _ToLab(); + return f < 0 ? 0 : (f > 1 ? 1 : f); } -// Method Description: -// - Helper function to calculate HPrime -float ColorFix::_GetHPrimeFn(float x, float y) noexcept +// FP32 is the very epitome of defined behavior. +#pragma warning(suppress : 26497) // You can attempt to make 'cbrtf_est' constexpr unless it contains any undefined behavior (f.4).) +__forceinline float cbrtf_est(float a) noexcept { - if (x == 0 && y == 0) + // We can't use std::bit_cast here, even though it exists exactly for this purpose. + // MSVC is dumb and introduces function calls to std::bit_cast + // even at the highest optimization and inlining levels. What. + union FP32 { - return 0; + uint32_t u; + float f; + }; + + // http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root: + // float_as_uint32_t / 3 + 709921077 + // It's similar to the well known "fast inverse square root" trick. Lots of numbers around 709921077 perform + // at least equally well to 709921077, and it is unknown how and why 709921077 was chosen specifically. + FP32 fp{ .f = a }; // evil floating point bit level hacking + fp.u = fp.u / 3 + 709921077; // what the fuck? + const auto x = fp.f; + + // One round of Newton's method. It follows the Wikipedia article at + // https://en.wikipedia.org/wiki/Cube_root#Numerical_methods + // For `a`s in the range between 0 and 1, this results in a maximum error of + // less than 6.7e-4f, which is not good, but good enough for us, because + // we're not an image editor. The benefit is that it's really fast. + return (1.0f / 3.0f) * (a / (x * x) + (x + x)); // 1st iteration +} + +// This namespace contains functions and structures as defined by: https://bottosson.github.io/posts/oklab/ +// It has been released into the public domain and is also available under an MIT license. +// The only change I made is to replace cbrtf() with cbrtf_est() to cut the CPU cost by a third. +namespace oklab +{ + struct Lab + { + float l; + float a; + float b; + }; + + struct RGB + { + float r; + float g; + float b; + }; + + __forceinline Lab linear_srgb_to_oklab(const RGB& c) noexcept + { + const auto l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; + const auto m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b; + const auto s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b; + + const auto l_ = cbrtf_est(l); + const auto m_ = cbrtf_est(m); + const auto s_ = cbrtf_est(s); + + return { + 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_, + 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_, + 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_, + }; } - const auto hueAngle = atan2(x, y); - return hueAngle >= 0 ? hueAngle : hueAngle + rad360; -} - -// Method Description: -// - Given 2 colors, computes the DeltaE value between them -// Arguments: -// - x1: the first color -// - x2: the second color -// Return Value: -// - The DeltaE value between x1 and x2 -float ColorFix::_GetDeltaE(ColorFix x1, ColorFix x2) noexcept -{ - constexpr float kSubL = 1; - constexpr float kSubC = 1; - constexpr float kSubH = 1; - - // Delta L Prime - const auto deltaLPrime = x2.L - x1.L; - - // L Bar - const auto lBar = (x1.L + x2.L) / 2; - - // C1 & C2 - const auto c1 = sqrtf(powf(x1.A, 2) + powf(x1.B, 2)); - const auto c2 = sqrtf(powf(x2.A, 2) + powf(x2.B, 2)); - - // C Bar - const auto cBar = (c1 + c2) / 2; - - // A Prime 1 - const auto aPrime1 = x1.A + (x1.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // A Prime 2 - const auto aPrime2 = x2.A + (x2.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7)))); - - // C Prime 1 - const auto cPrime1 = sqrtf(powf(aPrime1, 2) + powf(x1.B, 2)); - - // C Prime 2 - const auto cPrime2 = sqrtf(powf(aPrime2, 2) + powf(x2.B, 2)); - - // C Bar Prime - const auto cBarPrime = (cPrime1 + cPrime2) / 2; - - // Delta C Prime - const auto deltaCPrime = cPrime2 - cPrime1; - - // S sub L - const auto sSubL = 1 + ((0.015f * powf(lBar - 50, 2)) / sqrtf(20 + powf(lBar - 50, 2))); - - // S sub C - const auto sSubC = 1 + 0.045f * cBarPrime; - - // h Prime 1 - const auto hPrime1 = _GetHPrimeFn(x1.B, aPrime1); - - // h Prime 2 - const auto hPrime2 = _GetHPrimeFn(x2.B, aPrime2); - - // Delta H Prime - const auto deltaHPrime = 0 == c1 || 0 == c2 ? 0 : 2 * sqrtf(cPrime1 * cPrime2) * sin(abs(hPrime1 - hPrime2) <= rad180 ? hPrime2 - hPrime1 : (hPrime2 <= hPrime1 ? hPrime2 - hPrime1 + rad360 : hPrime2 - hPrime1 - rad360) / 2); - - // H Bar Prime - const auto hBarPrime = (abs(hPrime1 - hPrime2) > rad180) ? (hPrime1 + hPrime2 + rad360) / 2 : (hPrime1 + hPrime2) / 2; - - // T - const auto t = 1 - 0.17f * cosf(hBarPrime - rad030) + 0.24f * cosf(2 * hBarPrime) + 0.32f * cosf(3 * hBarPrime + rad006) - 0.20f * cosf(4 * hBarPrime - rad063); - - // S sub H - const auto sSubH = 1 + 0.015f * cBarPrime * t; - - // R sub T - const auto rSubT = -2 * sqrtf(powf(cBarPrime, 7) / (powf(cBarPrime, 7) + powf(25.0f, 7))) * sin(rad060 * exp(-powf((hBarPrime - rad275) / rad025, 2))); - - // Put it all together! - const auto lightness = deltaLPrime / (kSubL * sSubL); - const auto chroma = deltaCPrime / (kSubC * sSubC); - const auto hue = deltaHPrime / (kSubH * sSubH); - - return sqrtf(powf(lightness, 2) + powf(chroma, 2) + powf(hue, 2) + rSubT * chroma * hue); -} - -// Method Description: -// - Populates our L, A, B values, based on our r, g, b values -// - Converts a color in rgb format to a color in lab format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToLab() noexcept -{ - auto var_R = r / 255.0f; - auto var_G = g / 255.0f; - auto var_B = b / 255.0f; - - var_R = var_R > 0.04045f ? powf(((var_R + 0.055f) / 1.055f), 2.4f) : var_R / 12.92f; - var_G = var_G > 0.04045f ? powf(((var_G + 0.055f) / 1.055f), 2.4f) : var_G / 12.92f; - var_B = var_B > 0.04045f ? powf(((var_B + 0.055f) / 1.055f), 2.4f) : var_B / 12.92f; - - var_R = var_R * 100.0f; - var_G = var_G * 100.0f; - var_B = var_B * 100.0f; - - //Observer. = 2 degrees, Illuminant = D65 - const auto X = var_R * 0.4124f + var_G * 0.3576f + var_B * 0.1805f; - const auto Y = var_R * 0.2126f + var_G * 0.7152f + var_B * 0.0722f; - const auto Z = var_R * 0.0193f + var_G * 0.1192f + var_B * 0.9505f; - - auto var_X = X / 95.047f; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - auto var_Y = Y / 100.000f; //ref_Y = 100.000 - auto var_Z = Z / 108.883f; //ref_Z = 108.883 - - var_X = var_X > 0.008856f ? powf(var_X, (1.0f / 3.0f)) : (7.787f * var_X) + (16.0f / 116.0f); - var_Y = var_Y > 0.008856f ? powf(var_Y, (1.0f / 3.0f)) : (7.787f * var_Y) + (16.0f / 116.0f); - var_Z = var_Z > 0.008856f ? powf(var_Z, (1.0f / 3.0f)) : (7.787f * var_Z) + (16.0f / 116.0f); - - L = (116.0f * var_Y) - 16.0f; - A = 500.0f * (var_X - var_Y); - B = 200.0f * (var_Y - var_Z); -} - -// Method Description: -// - Populates our r, g, b values, based on our L, A, B values -// - Converts a color in lab format to a color in rgb format -// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1 -void ColorFix::_ToRGB() -{ - auto var_Y = (L + 16.0f) / 116.0f; - auto var_X = A / 500.0f + var_Y; - auto var_Z = var_Y - B / 200.0f; - - var_Y = (powf(var_Y, 3) > 0.008856f) ? powf(var_Y, 3) : (var_Y - 16.0f / 116.0f) / 7.787f; - var_X = (powf(var_X, 3) > 0.008856f) ? powf(var_X, 3) : (var_X - 16.0f / 116.0f) / 7.787f; - var_Z = (powf(var_Z, 3) > 0.008856f) ? powf(var_Z, 3) : (var_Z - 16.0f / 116.0f) / 7.787f; - - const auto X = 95.047f * var_X; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65) - const auto Y = 100.000f * var_Y; //ref_Y = 100.000 - const auto Z = 108.883f * var_Z; //ref_Z = 108.883 - - var_X = X / 100.0f; //X from 0 to 95.047 (Observer = 2 degrees, Illuminant = D65) - var_Y = Y / 100.0f; //Y from 0 to 100.000 - var_Z = Z / 100.0f; //Z from 0 to 108.883 - - auto var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f; - auto var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f; - auto var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f; - - var_R = var_R > 0.0031308f ? 1.055f * powf(var_R, (1 / 2.4f)) - 0.055f : var_R = 12.92f * var_R; - var_G = var_G > 0.0031308f ? 1.055f * powf(var_G, (1 / 2.4f)) - 0.055f : var_G = 12.92f * var_G; - var_B = var_B > 0.0031308f ? 1.055f * powf(var_B, (1 / 2.4f)) - 0.055f : var_B = 12.92f * var_B; - - r = (BYTE)std::clamp(var_R * 255.0f, 0.0f, 255.0f); - g = (BYTE)std::clamp(var_G * 255.0f, 0.0f, 255.0f); - b = (BYTE)std::clamp(var_B * 255.0f, 0.0f, 255.0f); -} - -// Method Description: -// - Given foreground and background colors, change the foreground color to -// make it more perceivable if necessary -// - Arguments: -// - fg: the foreground color -// - bg: the background color -// - Return Value: -// - The foreground color after performing any necessary changes to make it more perceivable -COLORREF ColorFix::GetPerceivableColor(COLORREF fg, COLORREF bg) -{ - const ColorFix backLab(bg); - ColorFix frontLab(fg); - const auto de1 = _GetDeltaE(frontLab, backLab); - if (de1 < gMinThreshold) + __forceinline RGB oklab_to_linear_srgb(const Lab& c) noexcept { - for (auto i = 0; i <= 1; i++) - { - const auto step = (i == 0) ? gLStep : -gLStep; - frontLab.L += step; + const auto l_ = c.l + 0.3963377774f * c.a + 0.2158037573f * c.b; + const auto m_ = c.l - 0.1055613458f * c.a - 0.0638541728f * c.b; + const auto s_ = c.l - 0.0894841775f * c.a - 1.2914855480f * c.b; - while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0))) - { - const auto de2 = _GetDeltaE(frontLab, backLab); - if (de2 >= gExpThreshold) - { - frontLab._ToRGB(); - return frontLab.rgb; - } - frontLab.L += step; - } - } + const auto l = l_ * l_ * l_; + const auto m = m_ * m_ * m_; + const auto s = s_ * s_ * s_; + + return { + +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, + -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, + -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, + }; } - return frontLab.rgb; +} + +#pragma warning(push) +#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2). + +__forceinline oklab::RGB colorrefToLinear(COLORREF c) noexcept +{ + const auto r = srgbToRgbLUT[(c >> 0) & 0xff]; + const auto g = srgbToRgbLUT[(c >> 8) & 0xff]; + const auto b = srgbToRgbLUT[(c >> 16) & 0xff]; + return { r, g, b }; +} + +#pragma warning(pop) + +__forceinline COLORREF linearToColorref(const oklab::RGB& c) noexcept +{ + auto r = saturate(c.r); + auto g = saturate(c.g); + auto b = saturate(c.b); + r = (r > 0.0031308f ? (1.055f * powf(r, 1.0f / 2.4f) - 0.055f) : 12.92f * r) * 255.0f; + g = (g > 0.0031308f ? (1.055f * powf(g, 1.0f / 2.4f) - 0.055f) : 12.92f * g) * 255.0f; + b = (b > 0.0031308f ? (1.055f * powf(b, 1.0f / 2.4f) - 0.055f) : 12.92f * b) * 255.0f; + return lrintf(r) | (lrintf(g) << 8) | (lrintf(b) << 16); +} + +// This function changes `color` so that it is visually different +// enough from `reference` that it's (much more easily) readable. +// See /doc/color_nudging.html +COLORREF ColorFix::GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept +{ + const auto referenceOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(reference)); + auto colorOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(color)); + + // To determine whether the two colors are too close to each other we use the ΔEOK metric + // based on the Oklab color space. It's defined as the simple euclidean distance between. + auto dl = referenceOklab.l - colorOklab.l; + auto da = referenceOklab.a - colorOklab.a; + auto db = referenceOklab.b - colorOklab.b; + dl *= dl; + da *= da; + db *= db; + + const auto distance = dl + da + db; + if (distance >= minSquaredDistance) + { + return color; + } + + // Thanks to ΔEOK being the euclidean distance we can immediately compute the + // minimum .l distance that's required for `distance` to be >= `minSquaredDistance`. + auto deltaL = sqrtf(minSquaredDistance - da - db); + + // Try to retain the brightness relationship between `reference` and `color`. + // If `color` is darker than `reference`, we should first try to make it even darker. + if (colorOklab.l < referenceOklab.l) + { + deltaL = -deltaL; + } + + // This primitive way of adjusting the lightness will result in gamut clipping. That's really not good + // (it reduces the contrast significantly for some colors), but on the other hand, doing proper gamut + // mapping is annoying and expensive. I was unable to find an easy and cheap (!) algorithm that would + // change the chroma so that we're (mostly) back inside the gamut, but found none. I'm sure it's + // possible to come up with something, but I left that as a future improvement. + colorOklab.l = referenceOklab.l + deltaL; + if (colorOklab.l < 0 || colorOklab.l > 1) + { + colorOklab.l = referenceOklab.l - deltaL; + } + + return linearToColorref(oklab::oklab_to_linear_srgb(colorOklab)) | (color & 0xff000000); } TIL_FAST_MATH_END diff --git a/src/types/Environment.cpp b/src/types/Environment.cpp deleted file mode 100644 index 9e6a111858..0000000000 --- a/src/types/Environment.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "inc/Environment.hpp" - -using namespace ::Microsoft::Console::Utils; - -// We cannot use spand or not_null because we're dealing with \0\0-terminated buffers of unknown length -#pragma warning(disable : 26481 26429) - -// Function Description: -// - Updates an EnvironmentVariableMapW with the current process's unicode -// environment variables ignoring ones already set in the provided map. -// Arguments: -// - map: The map to populate with the current processes's environment variables. -// Return Value: -// - S_OK if we succeeded, or an appropriate HRESULT for failing -HRESULT Microsoft::Console::Utils::UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept -try -{ - LPWCH currentEnvVars{}; - auto freeCurrentEnv = wil::scope_exit([&] { - if (currentEnvVars) - { - FreeEnvironmentStringsW(currentEnvVars); - currentEnvVars = nullptr; - } - }); - - currentEnvVars = ::GetEnvironmentStringsW(); - RETURN_HR_IF_NULL(E_OUTOFMEMORY, currentEnvVars); - - // Each entry is NULL-terminated; block is guaranteed to be double-NULL terminated at a minimum. - for (const wchar_t* lastCh{ currentEnvVars }; *lastCh != '\0'; ++lastCh) - { - // Copy current entry into temporary map. - const auto cchEntry{ ::wcslen(lastCh) }; - const std::wstring_view entry{ lastCh, cchEntry }; - - // Every entry is of the form "name=value\0". - const auto pos = entry.find_first_of(L"=", 0, 1); - RETURN_HR_IF(E_UNEXPECTED, pos == std::wstring::npos); - - std::wstring name{ entry.substr(0, pos) }; // portion before '=' - std::wstring value{ entry.substr(pos + 1) }; // portion after '=' - - // Don't replace entries that already exist. - map.try_emplace(std::move(name), std::move(value)); - lastCh += cchEntry; - } - - return S_OK; -} -CATCH_RETURN(); - -// Function Description: -// - Creates a new environment block using the provided vector as appropriate -// (resizing if needed) based on the provided environment variable map -// matching the format of GetEnvironmentStringsW. -// Arguments: -// - map: The map to populate the new environment block vector with. -// - newEnvVars: The vector that will be used to create the new environment block. -// Return Value: -// - S_OK if we succeeded, or an appropriate HRESULT for failing -HRESULT Microsoft::Console::Utils::EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map, std::vector& newEnvVars) noexcept -try -{ - // Clear environment block before use. - constexpr auto cbChar{ sizeof(decltype(newEnvVars.begin())::value_type) }; - - if (!newEnvVars.empty()) - { - ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); - } - - // Resize environment block to fit map. - size_t cchEnv{ 2 }; // For the block's double NULL-terminator. - for (const auto& [name, value] : map) - { - // Final form of "name=value\0". - cchEnv += name.size() + 1 + value.size() + 1; - } - newEnvVars.resize(cchEnv); - - // Ensure new block is wiped if we exit due to failure. - auto zeroNewEnv = wil::scope_exit([&]() noexcept { - ::SecureZeroMemory(newEnvVars.data(), newEnvVars.size() * cbChar); - }); - - // Transform each map entry and copy it into the new environment block. - auto pEnvVars{ newEnvVars.data() }; - auto cbRemaining{ cchEnv * cbChar }; - for (const auto& [name, value] : map) - { - // Final form of "name=value\0" for every entry. - { - const auto cchSrc{ name.size() }; - const auto cbSrc{ cchSrc * cbChar }; - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, name.c_str(), cbSrc) != 0); - pEnvVars += cchSrc; - cbRemaining -= cbSrc; - } - - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"=", cbChar) != 0); - ++pEnvVars; - cbRemaining -= cbChar; - - { - const auto cchSrc{ value.size() }; - const auto cbSrc{ cchSrc * cbChar }; - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, value.c_str(), cbSrc) != 0); - pEnvVars += cchSrc; - cbRemaining -= cbSrc; - } - - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0", cbChar) != 0); - ++pEnvVars; - cbRemaining -= cbChar; - } - - // Environment block only has to be NULL-terminated, but double NULL-terminate anyway. - RETURN_HR_IF(E_OUTOFMEMORY, memcpy_s(pEnvVars, cbRemaining, L"\0\0", cbChar * 2) != 0); - cbRemaining -= cbChar * 2; - - RETURN_HR_IF(E_UNEXPECTED, cbRemaining != 0); - - zeroNewEnv.release(); // success; don't wipe new environment block on exit - - return S_OK; -} -CATCH_RETURN(); diff --git a/src/types/FocusEvent.cpp b/src/types/FocusEvent.cpp deleted file mode 100644 index 09a93b2f4a..0000000000 --- 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/GlyphWidth.cpp b/src/types/GlyphWidth.cpp index f90c79f948..2f7d0578a4 100644 --- a/src/types/GlyphWidth.cpp +++ b/src/types/GlyphWidth.cpp @@ -12,7 +12,7 @@ static CodepointWidthDetector widthDetector; // Function Description: // - determines if the glyph represented by the string of characters should be // wide or not. See CodepointWidthDetector::IsWide -bool IsGlyphFullWidth(const std::wstring_view glyph) +bool IsGlyphFullWidth(const std::wstring_view& glyph) noexcept { return widthDetector.IsWide(glyph); } @@ -22,7 +22,7 @@ bool IsGlyphFullWidth(const std::wstring_view glyph) // wide or not. See CodepointWidthDetector::IsWide bool IsGlyphFullWidth(const wchar_t wch) noexcept { - return widthDetector.IsWide(wch); + return wch < 0x80 ? false : IsGlyphFullWidth({ &wch, 1 }); } // Function Description: @@ -35,9 +35,9 @@ bool IsGlyphFullWidth(const wchar_t wch) noexcept // - pfnFallback - the function to use as the fallback method. // Return Value: // - -void SetGlyphWidthFallback(std::function pfnFallback) +void SetGlyphWidthFallback(std::function pfnFallback) noexcept { - widthDetector.SetFallbackMethod(pfnFallback); + widthDetector.SetFallbackMethod(std::move(pfnFallback)); } // Function Description: diff --git a/src/types/IBaseData.h b/src/types/IBaseData.h deleted file mode 100644 index b0fd28fea5..0000000000 --- a/src/types/IBaseData.h +++ /dev/null @@ -1,50 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- IUiaData.hpp - -Abstract: -- This serves as the interface defining all information needed for the UI Automation Tree and the Renderer - -Author(s): -- Carlos Zamora (CaZamor) Aug-2019 ---*/ - -#pragma once - -#include "inc/viewport.hpp" -#include "../renderer/inc/FontInfo.hpp" - -class TextBuffer; - -namespace Microsoft::Console::Types -{ - class IBaseData - { - public: - virtual ~IBaseData() = 0; - - protected: - IBaseData() = default; - IBaseData(const IBaseData&) = default; - IBaseData(IBaseData&&) = default; - IBaseData& operator=(const IBaseData&) = default; - IBaseData& operator=(IBaseData&&) = default; - - public: - virtual Microsoft::Console::Types::Viewport GetViewport() noexcept = 0; - virtual til::point GetTextBufferEndPosition() const noexcept = 0; - virtual const TextBuffer& GetTextBuffer() const noexcept = 0; - virtual const FontInfo& GetFontInfo() const noexcept = 0; - - virtual std::vector GetSelectionRects() noexcept = 0; - - virtual void LockConsole() noexcept = 0; - virtual void UnlockConsole() noexcept = 0; - }; - - // See docs/virtual-dtors.md for an explanation of why this is weird. - inline IBaseData::~IBaseData() = default; -} diff --git a/src/types/IInputEvent.cpp b/src/types/IInputEvent.cpp deleted file mode 100644 index c01ff7067b..0000000000 --- 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(gsl::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 6e4e9ff331..0000000000 --- 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.X << L", " << - L"Y: " << pEvent->_size.Y << 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/IUiaData.h b/src/types/IUiaData.h deleted file mode 100644 index f4c132f34c..0000000000 --- a/src/types/IUiaData.h +++ /dev/null @@ -1,49 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- IUiaData.hpp - -Abstract: -- This serves as the interface defining all information needed for the UI Automation Tree - -Author(s): -- Carlos Zamora (CaZamor) Aug-2019 ---*/ - -#pragma once - -#include "IBaseData.h" - -struct ITextRangeProvider; - -namespace Microsoft::Console::Types -{ - class IUiaData : public IBaseData - { - public: - ~IUiaData() = 0; - - protected: - IUiaData() = default; - IUiaData(const IUiaData&) = default; - IUiaData(IUiaData&&) = default; - IUiaData& operator=(const IUiaData&) = default; - IUiaData& operator=(IUiaData&&) = default; - - public: - virtual std::pair GetAttributeColors(const TextAttribute& attr) const noexcept = 0; - virtual const bool IsSelectionActive() const = 0; - virtual const bool IsBlockSelection() const = 0; - virtual void ClearSelection() = 0; - 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; - }; - - // See docs/virtual-dtors.md for an explanation of why this is weird. - inline IUiaData::~IUiaData() = default; -} diff --git a/src/types/KeyEvent.cpp b/src/types/KeyEvent.cpp deleted file mode 100644 index 791c099e9c..0000000000 --- a/src/types/KeyEvent.cpp +++ /dev/null @@ -1,181 +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 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 445a215f4b..0000000000 --- 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 603b52c799..0000000000 --- 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 4179cf4011..0000000000 --- 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/ScreenInfoUiaProviderBase.cpp b/src/types/ScreenInfoUiaProviderBase.cpp index 6c9a8f42e7..27dfb8c3b7 100644 --- a/src/types/ScreenInfoUiaProviderBase.cpp +++ b/src/types/ScreenInfoUiaProviderBase.cpp @@ -9,7 +9,7 @@ using namespace Microsoft::Console::Types; // A helper function to create a SafeArray Version of an int array of a specified length -SAFEARRAY* BuildIntSafeArray(gsl::span data) +SAFEARRAY* BuildIntSafeArray(std::span data) { auto psa = SafeArrayCreateVector(VT_I4, 0, gsl::narrow(data.size())); if (psa != nullptr) @@ -31,7 +31,7 @@ SAFEARRAY* BuildIntSafeArray(gsl::span data) } #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT ScreenInfoUiaProviderBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ std::wstring_view wordDelimiters) noexcept +HRESULT ScreenInfoUiaProviderBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ std::wstring_view wordDelimiters) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, pData); @@ -191,7 +191,7 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetRuntimeId(_Outptr_result_maybenull_ // AppendRuntimeId is a magic Number that tells UIAutomation to Append its own Runtime ID(From the HWND) const std::array rId{ UiaAppendRuntimeId, -1 }; - const gsl::span span{ rId.data(), rId.size() }; + const std::span span{ rId.data(), rId.size() }; // BuildIntSafeArray is a custom function to hide the SafeArray creation *ppRuntimeId = BuildIntSafeArray(span); RETURN_IF_NULL_ALLOC(*ppRuntimeId); diff --git a/src/types/ScreenInfoUiaProviderBase.h b/src/types/ScreenInfoUiaProviderBase.h index 0ca1394cdd..2634d468fa 100644 --- a/src/types/ScreenInfoUiaProviderBase.h +++ b/src/types/ScreenInfoUiaProviderBase.h @@ -22,8 +22,8 @@ Author(s): #pragma once #include "../buffer/out/textBuffer.hpp" +#include "../renderer/inc/IRenderData.hpp" #include "UiaTextRangeBase.hpp" -#include "IUiaData.h" #include "IUiaTraceable.h" #include @@ -39,7 +39,7 @@ namespace Microsoft::Console::Types public IUiaTraceable { public: - virtual HRESULT RuntimeClassInitialize(_In_ IUiaData* pData, _In_ std::wstring_view wordDelimiters = UiaTextRangeBase::DefaultWordDelimiter) noexcept; + virtual HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ std::wstring_view wordDelimiters = UiaTextRangeBase::DefaultWordDelimiter) noexcept; ScreenInfoUiaProviderBase(const ScreenInfoUiaProviderBase&) = delete; ScreenInfoUiaProviderBase(ScreenInfoUiaProviderBase&&) = delete; @@ -59,13 +59,12 @@ namespace Microsoft::Console::Types IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) noexcept override; // IRawElementProviderFragment methods - virtual IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, - _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) = 0; + IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) override = 0; IFACEMETHODIMP GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId) override; - virtual IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect) = 0; + IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect) override = 0; IFACEMETHODIMP GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots) noexcept override; IFACEMETHODIMP SetFocus() override; - virtual IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) = 0; + IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) override = 0; // ITextProvider IFACEMETHODIMP GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) override; @@ -104,8 +103,8 @@ namespace Microsoft::Console::Types const std::wstring_view wordDelimiters, _COM_Outptr_result_maybenull_ UiaTextRangeBase** ppUtr) = 0; - // weak reference to IUiaData - IUiaData* _pData{ nullptr }; + // weak reference to IRenderData + Render::IRenderData* _pData{ nullptr }; std::wstring _wordDelimiters{}; diff --git a/src/types/TermControlUiaProvider.cpp b/src/types/TermControlUiaProvider.cpp index c0434567a4..b41b819139 100644 --- a/src/types/TermControlUiaProvider.cpp +++ b/src/types/TermControlUiaProvider.cpp @@ -9,11 +9,11 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::WRL; #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT TermControlUiaProvider::RuntimeClassInitialize(_In_ ::Microsoft::Console::Types::IUiaData* const uiaData, +HRESULT TermControlUiaProvider::RuntimeClassInitialize(_In_ Console::Render::IRenderData* const renderData, _In_ ::Microsoft::Console::Types::IControlAccessibilityInfo* controlInfo) noexcept { - RETURN_HR_IF_NULL(E_INVALIDARG, uiaData); - RETURN_IF_FAILED(ScreenInfoUiaProviderBase::RuntimeClassInitialize(uiaData)); + RETURN_HR_IF_NULL(E_INVALIDARG, renderData); + RETURN_IF_FAILED(ScreenInfoUiaProviderBase::RuntimeClassInitialize(renderData)); _controlInfo = controlInfo; return S_OK; diff --git a/src/types/TermControlUiaProvider.hpp b/src/types/TermControlUiaProvider.hpp index 823db52bf4..6cc8a7774c 100644 --- a/src/types/TermControlUiaProvider.hpp +++ b/src/types/TermControlUiaProvider.hpp @@ -29,7 +29,7 @@ namespace Microsoft::Terminal { public: TermControlUiaProvider() = default; - HRESULT RuntimeClassInitialize(_In_ ::Microsoft::Console::Types::IUiaData* const uiaData, + HRESULT RuntimeClassInitialize(_In_ Console::Render::IRenderData* const renderData, _In_ ::Microsoft::Console::Types::IControlAccessibilityInfo* controlInfo) noexcept; // IRawElementProviderSimple methods diff --git a/src/types/TermControlUiaTextRange.cpp b/src/types/TermControlUiaTextRange.cpp index d200cad6bd..6f3c82e818 100644 --- a/src/types/TermControlUiaTextRange.cpp +++ b/src/types/TermControlUiaTextRange.cpp @@ -10,12 +10,12 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::WRL; // degenerate range constructor. -HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters) noexcept +HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters) noexcept { return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, wordDelimiters); } -HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const Cursor& cursor, const std::wstring_view wordDelimiters) noexcept @@ -23,7 +23,7 @@ HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, return UiaTextRangeBase::RuntimeClassInitialize(pData, pProvider, cursor, wordDelimiters); } -HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const til::point start, const til::point end, @@ -35,7 +35,7 @@ HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, // returns a degenerate text range of the start of the row closest to the y value of point #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT TermControlUiaTextRange::RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const UiaPoint point, const std::wstring_view wordDelimiters) diff --git a/src/types/TermControlUiaTextRange.hpp b/src/types/TermControlUiaTextRange.hpp index ce2cf7ecc0..70ba9cac97 100644 --- a/src/types/TermControlUiaTextRange.hpp +++ b/src/types/TermControlUiaTextRange.hpp @@ -16,6 +16,7 @@ Author(s): #pragma once +#include "../renderer/inc/IRenderData.hpp" #include "../types/UiaTextRangeBase.hpp" namespace Microsoft::Terminal @@ -26,18 +27,18 @@ namespace Microsoft::Terminal TermControlUiaTextRange() = default; // degenerate range - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // degenerate range at cursor position - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const Cursor& cursor, const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // specific endpoint range - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const til::point start, const til::point end, @@ -45,7 +46,7 @@ namespace Microsoft::Terminal const std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept override; // range from a UiaPoint - HRESULT RuntimeClassInitialize(_In_ Microsoft::Console::Types::IUiaData* pData, + HRESULT RuntimeClassInitialize(_In_ Console::Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, const UiaPoint point, const std::wstring_view wordDelimiters = DefaultWordDelimiter); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index fb2a2d6b15..af5ebb431e 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; @@ -17,7 +16,7 @@ static constexpr long _RemoveAlpha(COLORREF color) noexcept // degenerate range constructor. #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ std::wstring_view wordDelimiters) noexcept +HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ std::wstring_view wordDelimiters) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, pProvider); @@ -36,7 +35,7 @@ try CATCH_RETURN(); #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const Cursor& cursor, _In_ std::wstring_view wordDelimiters) noexcept @@ -59,7 +58,7 @@ try CATCH_RETURN(); #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize -HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, +HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const til::point start, _In_ const til::point end, @@ -69,18 +68,9 @@ try { RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); - // start is before/at end, so this is valid - if (_pData->GetTextBuffer().GetSize().CompareInBounds(start, end, true) <= 0) - { - _start = start; - _end = end; - } - else - { - // start is after end, so we need to flip our concept of start/end - _start = end; - _end = start; - } + // start must be before or equal to end + _start = std::min(start, end); + _end = std::max(start, end); // This should be the only way to set if we are a blockRange // This is used for blockSelection @@ -102,11 +92,11 @@ void UiaTextRangeBase::Initialize(_In_ const UiaPoint point) til::CoordType row = 0; if (clientPoint.y <= windowRect.top) { - row = viewport.Top; + row = viewport.top; } else if (clientPoint.y >= windowRect.bottom) { - row = viewport.Bottom; + row = viewport.bottom; } else { @@ -114,7 +104,7 @@ void UiaTextRangeBase::Initialize(_In_ const UiaPoint point) _TranslatePointFromScreen(&clientPoint); const auto currentFontSize = _getScreenFontSize(); - row = clientPoint.y / currentFontSize.Y + viewport.Top; + row = clientPoint.y / currentFontSize.height + viewport.top; } _start = { 0, row }; _end = _start; @@ -165,21 +155,13 @@ bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const til: { case TextPatternRangeEndpoint_End: _end = val; - // if end is before start... - if (bufferSize.CompareInBounds(_end, _start, true) < 0) - { - // make this range degenerate at end - _start = _end; - } + // if start is past end, make this a degenerate range + _start = std::min(_start, _end); break; case TextPatternRangeEndpoint_Start: _start = val; - // if start is after end... - if (bufferSize.CompareInBounds(_start, _end, true) > 0) - { - // make this range degenerate at start - _end = _start; - } + // if end is before start, make this a degenerate range + _end = std::max(_start, _end); break; default: break; @@ -293,7 +275,7 @@ void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) // If we're past document end, // set us to ONE BEFORE the document end. // This allows us to expand properly. - if (bufferSize.CompareInBounds(_start, documentEnd, true) >= 0) + if (_start >= documentEnd) { _start = documentEnd; bufferSize.DecrementInBounds(_start, true); @@ -313,8 +295,8 @@ void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) else if (unit <= TextUnit_Line) { // expand to line - _start.X = 0; - if (_start.Y == documentEnd.y) + _start.x = 0; + if (_start.y == documentEnd.y) { // we're on the last line _end = documentEnd; @@ -322,8 +304,8 @@ void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) } else { - _end.X = 0; - _end.Y = _start.Y + 1; + _end.x = 0; + _end.y = _start.y + 1; } } else @@ -428,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; } @@ -467,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); @@ -558,10 +540,10 @@ try auto viewportRange{ bufferSize }; if (_blockRange) { - const auto originX{ std::min(_start.X, inclusiveEnd.X) }; - const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; - const auto width{ std::abs(inclusiveEnd.X - _start.X + 1) }; - const auto height{ std::abs(inclusiveEnd.Y - _start.Y + 1) }; + const auto originX{ std::min(_start.x, inclusiveEnd.x) }; + const auto originY{ std::min(_start.y, inclusiveEnd.y) }; + const auto width{ std::abs(inclusiveEnd.x - _start.x + 1) }; + const auto height{ std::abs(inclusiveEnd.y - _start.y + 1) }; viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); } auto iter{ buffer.GetCellDataAt(searchStart, viewportRange) }; @@ -625,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) + // MovePastPoint() moves *past* the given point. + // -> We need to turn [_beg,_end) into (_beg,_end). + exclusiveBegin.x--; + + _searcher.ResetIfStale(*_pData, queryText, searchBackward, ignoreCase); + _searcher.MovePastPoint(searchBackward ? _end : exclusiveBegin); + + til::point hitBeg{ til::CoordTypeMax, til::CoordTypeMax }; + til::point hitEnd{ til::CoordTypeMin, til::CoordTypeMin }; + + if (const auto hit = _searcher.GetCurrent()) { - searchDirection = Search::Direction::Backward; - - // we need to convert the end to inclusive - // because Search operates with an inclusive til::point - searchAnchor = _end; - bufferSize.DecrementInBounds(searchAnchor, true); - } - - Search searcher{ *_pData, queryText, searchDirection, sensitivity, searchAnchor }; - - if (searcher.FindNext()) - { - 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 && bufferSize.CompareInBounds(end, _end, true) < 0) || - (searchDirection == Search::Direction::Backward && bufferSize.CompareInBounds(start, _start) > 0)) - { - RETURN_IF_FAILED(Clone(ppRetVal)); - auto& range = static_cast(**ppRetVal); - range._start = start; - range._end = end; - - UiaTracing::TextRange::FindText(*this, queryText, searchBackward, ignoreCase, range); - } + _pData->GetTextBuffer().GetSize().IncrementInBounds(hitEnd, true); } + + 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(); @@ -721,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. @@ -817,10 +795,10 @@ try auto viewportRange{ bufferSize }; if (_blockRange) { - const auto originX{ std::min(_start.X, inclusiveEnd.X) }; - const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; - const auto width{ std::abs(inclusiveEnd.X - _start.X + 1) }; - const auto height{ std::abs(inclusiveEnd.Y - _start.Y + 1) }; + const auto originX{ std::min(_start.x, inclusiveEnd.x) }; + const auto originY{ std::min(_start.y, inclusiveEnd.y) }; + const auto width{ std::abs(inclusiveEnd.x - _start.x + 1) }; + const auto height{ std::abs(inclusiveEnd.y - _start.y + 1) }; viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); } auto iter{ buffer.GetCellDataAt(_start, viewportRange) }; @@ -870,26 +848,16 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ const auto viewportOrigin = viewport.Origin(); const auto viewportEnd = viewport.EndExclusive(); - // startAnchor: the earliest til::point we will get a bounding rect for - auto startAnchor = GetEndpoint(TextPatternRangeEndpoint_Start); - if (bufferSize.CompareInBounds(startAnchor, viewportOrigin, true) < 0) - { - // earliest we can be is the origin - startAnchor = viewportOrigin; - } + // startAnchor: the earliest til::point we will get a bounding rect for; at least the viewport origin + const auto startAnchor = std::max(GetEndpoint(TextPatternRangeEndpoint_Start), viewportOrigin); - // endAnchor: the latest til::point we will get a bounding rect for - auto endAnchor = GetEndpoint(TextPatternRangeEndpoint_End); - if (bufferSize.CompareInBounds(endAnchor, viewportEnd, true) > 0) - { - // latest we can be is the viewport end - endAnchor = viewportEnd; - } + // endAnchor: the latest til::point we will get a bounding rect for; at most the viewport end + auto endAnchor = std::min(GetEndpoint(TextPatternRangeEndpoint_End), viewportEnd); // _end is exclusive, let's be inclusive so we don't have to think about it anymore for bounding rects bufferSize.DecrementInBounds(endAnchor, true); - if (IsDegenerate() || bufferSize.CompareInBounds(_start, viewportEnd, true) > 0 || bufferSize.CompareInBounds(_end, viewportOrigin, true) < 0) + if (IsDegenerate() || _start > viewportEnd || _end < viewportOrigin) { // An empty array is returned for a degenerate (empty) text range. // reference: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getboundingrectangles @@ -907,7 +875,7 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ { // Convert the buffer coordinates to an equivalent range of // screen cells, taking line rendition into account. - const auto lineRendition = buffer.GetLineRendition(rect.Top); + const auto lineRendition = buffer.GetLineRendition(rect.top); til::rect r{ BufferToScreenLine(rect, lineRendition) }; r -= viewportOrigin; _getBoundingRect(r, coords); @@ -1052,14 +1020,8 @@ try constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start; const auto bufferSize{ _pData->GetTextBuffer().GetSize() }; const auto documentEnd = _getDocumentEnd(); - if (bufferSize.CompareInBounds(_start, documentEnd, true) > 0) - { - _start = documentEnd; - } - if (bufferSize.CompareInBounds(_end, documentEnd, true) > 0) - { - _end = documentEnd; - } + _start = std::min(_start, documentEnd); + _end = std::min(_end, documentEnd); const auto wasDegenerate = IsDegenerate(); if (count != 0) @@ -1126,14 +1088,8 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoin } CATCH_LOG(); - if (bufferSize.CompareInBounds(_start, documentEnd, true) > 0) - { - _start = documentEnd; - } - if (bufferSize.CompareInBounds(_end, documentEnd, true) > 0) - { - _end = documentEnd; - } + _start = std::min(_start, documentEnd); + _end = std::min(_end, documentEnd); try { @@ -1246,8 +1202,8 @@ try const auto oldViewport = _pData->GetViewport().ToInclusive(); const auto viewportHeight = _getViewportHeight(oldViewport); // range rows - const auto startScreenInfoRow = _start.Y; - const auto endScreenInfoRow = _end.Y; + const auto startScreenInfoRow = _start.y; + const auto endScreenInfoRow = _end.y; // screen buffer rows constexpr til::CoordType topRow = 0; const auto bottomRow = _pData->GetTextBuffer().TotalRowCount() - 1; @@ -1262,15 +1218,15 @@ try if (startScreenInfoRow + viewportHeight <= bottomRow) { // we can align to the top - newViewport.Top = startScreenInfoRow; - newViewport.Bottom = startScreenInfoRow + viewportHeight - 1; + newViewport.top = startScreenInfoRow; + newViewport.bottom = startScreenInfoRow + viewportHeight - 1; } else { // we can't align to the top so we'll just move the viewport // to the bottom of the screen buffer - newViewport.Bottom = bottomRow; - newViewport.Top = bottomRow - viewportHeight + 1; + newViewport.bottom = bottomRow; + newViewport.top = bottomRow - viewportHeight + 1; } } else @@ -1284,20 +1240,20 @@ try // so we need to clamp to the bottom row to stay in bounds // we can align to bottom - newViewport.Bottom = std::min(endScreenInfoRow, bottomRow); - newViewport.Top = newViewport.Bottom - viewportHeight + 1; + newViewport.bottom = std::min(endScreenInfoRow, bottomRow); + newViewport.top = newViewport.bottom - viewportHeight + 1; } else { // we can't align to bottom so we'll move the viewport to // the top of the screen buffer - newViewport.Top = topRow; - newViewport.Bottom = topRow + viewportHeight - 1; + newViewport.top = topRow; + newViewport.bottom = topRow + viewportHeight - 1; } } - assert(newViewport.Top >= topRow); - assert(newViewport.Bottom <= bottomRow); + assert(newViewport.top >= topRow); + assert(newViewport.bottom <= bottomRow); assert(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport)); Unlock.reset(); @@ -1331,8 +1287,8 @@ til::size UiaTextRangeBase::_getScreenFontSize() const noexcept auto coordRet = _pData->GetFontInfo().GetSize(); // For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations. - coordRet.X = std::max(coordRet.X, 1); - coordRet.Y = std::max(coordRet.Y, 1); + coordRet.width = std::max(coordRet.width, 1); + coordRet.height = std::max(coordRet.height, 1); return coordRet; } @@ -1345,10 +1301,10 @@ til::size UiaTextRangeBase::_getScreenFontSize() const noexcept // - The viewport height til::CoordType UiaTextRangeBase::_getViewportHeight(const til::inclusive_rect& viewport) const noexcept { - assert(viewport.Bottom >= viewport.Top); + assert(viewport.bottom >= viewport.top); // + 1 because til::inclusive_rect is inclusive on both sides so subtracting top // and bottom gets rid of 1 more then it should. - return viewport.Bottom - viewport.Top + 1; + return viewport.bottom - viewport.top + 1; } // Routine Description: @@ -1364,8 +1320,8 @@ Viewport UiaTextRangeBase::_getOptimizedBufferSize() const noexcept // we need to add 1 to the X/Y of textBufferEnd // because we want the returned viewport to include this til::point const auto textBufferEnd = _pData->GetTextBufferEndPosition(); - const auto width = textBufferEnd.X + 1; - const auto height = textBufferEnd.Y + 1; + const auto width = textBufferEnd.x + 1; + const auto height = textBufferEnd.y + 1; return Viewport::FromDimensions({ 0, 0 }, width, height); } @@ -1379,9 +1335,9 @@ til::point UiaTextRangeBase::_getDocumentEnd() const { const auto optimizedBufferSize{ _getOptimizedBufferSize() }; const auto& buffer{ _pData->GetTextBuffer() }; - const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(optimizedBufferSize) }; + const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(&optimizedBufferSize) }; const auto cursorPos{ buffer.GetCursor().GetPosition() }; - return { optimizedBufferSize.Left(), std::max(lastCharPos.Y, cursorPos.Y) + 1 }; + return { optimizedBufferSize.Left(), std::max(lastCharPos.y, cursorPos.y) + 1 }; } // Routine Description: @@ -1403,11 +1359,11 @@ void UiaTextRangeBase::_getBoundingRect(const til::rect& textRect, _Inout_ std:: // we want to clamp to a long (output type), not a short (input type) // so we need to explicitly say - topLeft.x = textRect.left * currentFontSize.X; - topLeft.y = textRect.top * currentFontSize.Y; + topLeft.x = textRect.left * currentFontSize.width; + topLeft.y = textRect.top * currentFontSize.height; - bottomRight.x = textRect.right * currentFontSize.X; - bottomRight.y = textRect.bottom * currentFontSize.Y; + bottomRight.x = textRect.right * currentFontSize.width; + bottomRight.y = textRect.bottom * currentFontSize.height; // convert the coords to be relative to the screen instead of // the client window @@ -1523,7 +1479,7 @@ void UiaTextRangeBase::_moveEndpointByUnitWord(_In_ const int moveCount, { case MovementDirection::Forward: { - if (bufferSize.CompareInBounds(nextPos, documentEnd, true) >= 0) + if (nextPos >= documentEnd) { success = false; } @@ -1642,14 +1598,14 @@ void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, { case MovementDirection::Forward: { - if (nextPos.Y >= documentEnd.Y) + if (nextPos.y >= documentEnd.y) { // Corner Case: we're past the limit // Clamp us to the limit resultPos = documentEnd; success = false; } - else if (preventBoundary && nextPos.Y == documentEnd.Y - 1) + else if (preventBoundary && nextPos.y == documentEnd.y - 1) { // Corner Case: we're just before the limit // and we're not allowed onto the exclusive end. @@ -1658,7 +1614,7 @@ void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, } else { - nextPos.X = bufferSize.RightInclusive(); + nextPos.x = bufferSize.RightInclusive(); success = bufferSize.IncrementInBounds(nextPos, allowBottomExclusive); if (success) { @@ -1672,7 +1628,7 @@ void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, { if (preventBoundary) { - if (nextPos.Y == bufferSize.Top()) + if (nextPos.y == bufferSize.Top()) { // can't move past top success = false; @@ -1683,7 +1639,7 @@ void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, // GH#10924: as a non-degenerate range, we are supposed to act // like we already encompass the line. // Move to the left boundary so we try to wrap around - nextPos.X = bufferSize.Left(); + nextPos.x = bufferSize.Left(); } } @@ -1692,7 +1648,7 @@ void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, if (success) { - nextPos.X = bufferSize.Left(); + nextPos.x = bufferSize.Left(); resultPos = nextPos; (*pAmountMoved)--; } @@ -1744,7 +1700,7 @@ void UiaTextRangeBase::_moveEndpointByUnitDocument(_In_ const int moveCount, } CATCH_LOG(); - if (preventBoundary || bufferSize.CompareInBounds(target, documentEnd, true) >= 0) + if (preventBoundary || target >= documentEnd) { return; } diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index 16f395170f..eec30a3dcb 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 "IUiaData.h" -#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; @@ -51,18 +46,18 @@ namespace Microsoft::Console::Types static constexpr std::wstring_view DefaultWordDelimiter{ &UNICODE_SPACE, 1 }; // degenerate range - virtual HRESULT RuntimeClassInitialize(_In_ IUiaData* pData, + virtual HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept; // degenerate range at cursor position - virtual HRESULT RuntimeClassInitialize(_In_ IUiaData* pData, + virtual HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const Cursor& cursor, _In_ std::wstring_view wordDelimiters = DefaultWordDelimiter) noexcept; // specific endpoint range - virtual HRESULT RuntimeClassInitialize(_In_ IUiaData* pData, + virtual HRESULT RuntimeClassInitialize(_In_ Render::IRenderData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const til::point start, _In_ const til::point end, @@ -82,7 +77,7 @@ namespace Microsoft::Console::Types bool IsDegenerate() const noexcept; // ITextRangeProvider methods - virtual IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) = 0; + IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) override = 0; IFACEMETHODIMP Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) noexcept override; IFACEMETHODIMP CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint, _In_ ITextRangeProvider* pTargetRange, @@ -121,11 +116,12 @@ namespace Microsoft::Console::Types protected: UiaTextRangeBase() = default; - IUiaData* _pData{ nullptr }; + Render::IRenderData* _pData{ nullptr }; 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 8c8a1c0d31..dae05a073b 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); @@ -78,8 +78,8 @@ try std::wstringstream stream; stream << " _id: " << utr.GetId(); - stream << " _start: { " << start.X << ", " << start.Y << " }"; - stream << " _end: { " << end.X << ", " << end.Y << " }"; + stream << " _start: { " << start.x << ", " << start.y << " }"; + stream << " _end: { " << end.x << ", " << end.y << " }"; stream << " _degenerate: " << utr.IsDegenerate(); stream << " _wordDelimiters: " << utr._wordDelimiters; stream << " content: " << utr._getTextValue(); @@ -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 f9752caf48..58c95231a1 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; diff --git a/src/types/Utf16Parser.cpp b/src/types/Utf16Parser.cpp deleted file mode 100644 index 777388c640..0000000000 --- a/src/types/Utf16Parser.cpp +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "inc/Utf16Parser.hpp" -#include "unicode.hpp" - -// Routine Description: -// - Finds the next single collection for the codepoint out of the given UTF-16 string information. -// - In simpler terms, it will group UTF-16 surrogate pairs into a single unit or give you a valid single-item UTF-16 character. -// - Does not validate UTF-16 input beyond proper leading/trailing character sequences. -// Arguments: -// - wstr - The UTF-16 string to parse. -// Return Value: -// - A view into the string given of just the next codepoint unit. -std::wstring_view Utf16Parser::ParseNext(std::wstring_view wstr) noexcept -{ - for (size_t pos = 0; pos < wstr.size(); ++pos) - { - const auto wch = wstr.at(pos); - - // If it's a lead and followed directly by a trail, then return the pair. - // If it's not followed directly by the trail, go around again and seek forward. - if (IsLeadingSurrogate(wch)) - { - // Try to find the next item... if it isn't there, we'll go around again. - const auto posNext = pos + 1; - if (posNext < wstr.size()) - { - // If we found it and it's trailing, return the pair. - const auto wchNext = wstr.at(posNext); - if (IsTrailingSurrogate(wchNext)) - { - return wstr.substr(pos, 2); - } - } - // If we missed either if in any way, we'll fall through and go around again searching for more. - } - // If it's just a trail at this point, go around again and seek forward. - else if (IsTrailingSurrogate(wch)) - { - continue; - } - // If it's neither lead nor trail, then it's < U+10000 and it can be returned as a single wchar_t point. - else - { - return wstr.substr(pos, 1); - } - } - - // If we get all the way through and there's nothing valid, then this is just a replacement character as it was broken/garbage. - return std::wstring_view{ &UNICODE_REPLACEMENT, 1 }; -} - -// Routine Description: -// - formats a utf16 encoded wstring and splits the codepoints into individual collections. -// - will drop badly formatted leading/trailing char sequences. -// - does not validate utf16 input beyond proper leading/trailing char sequences. -// Arguments: -// - wstr - the string to parse -// Return Value: -// - a vector of utf16 codepoints. glyphs that require surrogate pairs will be grouped -// together in a vector and codepoints that use only one wchar will be in a vector by themselves. -std::vector> Utf16Parser::Parse(std::wstring_view wstr) -{ - std::vector> result; - std::vector sequence; - for (const auto wch : wstr) - { - if (IsLeadingSurrogate(wch)) - { - sequence.clear(); - sequence.push_back(wch); - } - else if (IsTrailingSurrogate(wch)) - { - if (!sequence.empty()) - { - sequence.push_back(wch); - result.push_back(sequence); - sequence.clear(); - } - } - else - { - result.push_back({ wch }); - } - } - return result; -} diff --git a/src/types/WindowBufferSizeEvent.cpp b/src/types/WindowBufferSizeEvent.cpp deleted file mode 100644 index 456d452cae..0000000000 --- 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/colorTable.cpp b/src/types/colorTable.cpp index 15a51f0d45..f21b858621 100644 --- a/src/types/colorTable.cpp +++ b/src/types/colorTable.cpp @@ -267,7 +267,7 @@ static constexpr std::array standard256ColorTable{ til::color{ 0xEE, 0xEE, 0xEE }, }; -static constexpr til::presorted_static_map xorgAppVariantColorTable{ +static constinit til::presorted_static_map xorgAppVariantColorTable{ std::pair{ "antiquewhite"sv, std::array{ til::color{ 250, 235, 215 }, til::color{ 255, 239, 219 }, til::color{ 238, 223, 204 }, til::color{ 205, 192, 176 }, til::color{ 139, 131, 120 } } }, std::pair{ "aquamarine"sv, std::array{ til::color{ 127, 255, 212 }, til::color{ 127, 255, 212 }, til::color{ 118, 238, 198 }, til::color{ 102, 205, 170 }, til::color{ 69, 139, 116 } } }, std::pair{ "azure"sv, std::array{ til::color{ 240, 255, 255 }, til::color{ 240, 255, 255 }, til::color{ 224, 238, 238 }, til::color{ 193, 205, 205 }, til::color{ 131, 139, 139 } } }, @@ -348,7 +348,7 @@ static constexpr til::presorted_static_map xorgAppVariantColorTable{ std::pair{ "yellow"sv, std::array{ til::color{ 255, 255, 0 }, til::color{ 255, 255, 0 }, til::color{ 238, 238, 0 }, til::color{ 205, 205, 0 }, til::color{ 139, 139, 0 } } }, }; -static constexpr til::presorted_static_map xorgAppColorTable{ +static constinit til::presorted_static_map xorgAppColorTable{ std::pair{ "aliceblue"sv, til::color{ 240, 248, 255 } }, std::pair{ "aqua"sv, til::color{ 0, 255, 255 } }, std::pair{ "beige"sv, til::color{ 245, 245, 220 } }, @@ -435,9 +435,9 @@ static constexpr til::presorted_static_map xorgAppColorTable{ std::pair{ "yellowgreen"sv, til::color{ 154, 205, 50 } } }; -gsl::span Utils::CampbellColorTable() +std::span Utils::CampbellColorTable() noexcept { - return gsl::make_span(standard256ColorTable).first(16); + return std::span{ standard256ColorTable }.first(16); } // Function Description: @@ -446,7 +446,7 @@ gsl::span Utils::CampbellColorTable() // - table: a color table to be filled // Return Value: // - -void Utils::InitializeColorTable(const gsl::span table) +void Utils::InitializeColorTable(const std::span table) { const auto tableSize = std::min(table.size(), standard256ColorTable.size()); std::copy_n(standard256ColorTable.begin(), tableSize, table.begin()); diff --git a/src/types/convert.cpp b/src/types/convert.cpp index 4427e93dae..abc03d5d3d 100644 --- a/src/types/convert.cpp +++ b/src/types/convert.cpp @@ -130,40 +130,6 @@ return cchTarget; } -// Routine Description: -// - naively determines the width of a UCS2 encoded wchar -// Arguments: -// - wch - the wchar_t to measure -// Return Value: -// - CodepointWidth indicating width of wch -// Notes: -// 04-08-92 ShunK Created. -// Jul-27-1992 KazuM Added Screen Information and Code Page Information. -// Jan-29-1992 V-Hirots Substruct Screen Information. -// Oct-06-1996 KazuM Not use RtlUnicodeToMultiByteSize and WideCharToMultiByte -// Because 950 (Chinese Traditional) only defined 13500 chars, -// and unicode defined almost 18000 chars. -// So there are almost 4000 chars can not be mapped to big5 code. -// Apr-30-2015 MiNiksa Corrected unknown character code assumption. Max Width in Text Metric -// is not reliable for calculating half/full width. Must use current -// display font data (cached) instead. -// May-23-2017 migrie Forced Box-Drawing Characters (x2500-x257F) to narrow. -// Jan-16-2018 migrie Separated core lookup from asking the renderer the width -// May-01-2019 MiNiksa Forced lookup-via-renderer for retroactively recategorized emoji -// that used to be narrow but now might be wide. (approx x2194-x2b55, not inclusive) -// Also forced block characters segment (x2580-x259F) to narrow -// Oct-25-2020 DuHowett Replaced the entire table with a set of overrides that get built into -// CodepointWidthDetector (unicode_width_overrides.xml) -CodepointWidth GetQuickCharWidth(const wchar_t wch) noexcept -{ - if (0x20 <= wch && wch <= 0x7e) - { - /* ASCII */ - return CodepointWidth::Narrow; - } - return CodepointWidth::Invalid; -} - wchar_t Utf16ToUcs2(const std::wstring_view charData) { THROW_HR_IF(E_INVALIDARG, charData.empty()); diff --git a/src/types/inc/CodepointWidthDetector.hpp b/src/types/inc/CodepointWidthDetector.hpp index 810b4bd7bd..79d0f3e858 100644 --- a/src/types/inc/CodepointWidthDetector.hpp +++ b/src/types/inc/CodepointWidthDetector.hpp @@ -14,38 +14,24 @@ Author: #pragma once #include "convert.hpp" -#include - -static_assert(sizeof(unsigned int) == sizeof(wchar_t) * 2, - "UnicodeRange expects to be able to store a unicode codepoint in an unsigned int"); // use to measure the width of a codepoint class CodepointWidthDetector final { public: - CodepointWidthDetector() noexcept; - CodepointWidthDetector(const CodepointWidthDetector&) = delete; - CodepointWidthDetector(CodepointWidthDetector&&) = delete; - ~CodepointWidthDetector() = default; - CodepointWidthDetector& operator=(const CodepointWidthDetector&) = delete; - CodepointWidthDetector& operator=(CodepointWidthDetector&&) = delete; - - CodepointWidth GetWidth(const std::wstring_view glyph) const; - bool IsWide(const std::wstring_view glyph) const; - bool IsWide(const wchar_t wch) const noexcept; - void SetFallbackMethod(std::function pfnFallback); - void NotifyFontChanged() const noexcept; + CodepointWidth GetWidth(const std::wstring_view& glyph) noexcept; + bool IsWide(const std::wstring_view& glyph) noexcept; + void SetFallbackMethod(std::function pfnFallback) noexcept; + void NotifyFontChanged() noexcept; #ifdef UNIT_TESTING friend class CodepointWidthDetectorTests; #endif private: - CodepointWidth _lookupGlyphWidth(const std::wstring_view glyph) const; - CodepointWidth _lookupGlyphWidthWithCache(const std::wstring_view glyph) const noexcept; - bool _checkFallbackViaCache(const std::wstring_view glyph) const; - static unsigned int _extractCodepoint(const std::wstring_view glyph) noexcept; + uint8_t _lookupGlyphWidth(char32_t codepoint, const std::wstring_view& glyph) noexcept; + uint8_t _checkFallbackViaCache(char32_t codepoint, const std::wstring_view& glyph) noexcept; - mutable std::unordered_map _fallbackCache; - std::function _pfnFallbackMethod; + std::unordered_map _fallbackCache; + std::function _pfnFallbackMethod; }; diff --git a/src/types/inc/ColorFix.hpp b/src/types/inc/ColorFix.hpp index a233d1268a..02bdc0631b 100644 --- a/src/types/inc/ColorFix.hpp +++ b/src/types/inc/ColorFix.hpp @@ -1,53 +1,13 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- ColorFix - -Abstract: -- Implementation of perceptual color nudging, which allows the Terminal - to slightly shift the foreground color to make it more perceivable on - the current background (for cases where the foreground is very close - to being imperceivable on the background). - -Author(s): -- Pankaj Bhojwani - Sep 2021 - ---*/ - +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. +// +// Implementation of perceptual color nudging, which allows the Terminal +// to slightly shift the foreground color to make it more perceivable on +// the current background (for cases where the foreground is very close +// to being imperceivable on the background). #pragma once -struct ColorFix +namespace ColorFix { -public: - ColorFix(COLORREF color) noexcept; - - static COLORREF GetPerceivableColor(COLORREF fg, COLORREF bg); - -#pragma warning(push) - // CL will complain about the both nameless and anonymous struct. -#pragma warning(disable : 4201) - // RGB - union - { - struct - { - BYTE r, g, b, dummy; - }; - COLORREF rgb; - }; - - // Lab - struct - { - float L, A, B; - }; -#pragma warning(pop) - -private: - static float _GetHPrimeFn(float x, float y) noexcept; - static float _GetDeltaE(ColorFix x1, ColorFix x2) noexcept; - void _ToLab() noexcept; - void _ToRGB(); -}; + COLORREF GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept; +} diff --git a/src/types/inc/Environment.hpp b/src/types/inc/Environment.hpp deleted file mode 100644 index 0994a17776..0000000000 --- a/src/types/inc/Environment.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -namespace Microsoft::Console::Utils -{ - // - // A case-insensitive wide-character map is used to store environment variables - // due to documented requirements: - // - // "All strings in the environment block must be sorted alphabetically by name. - // The sort is case-insensitive, Unicode order, without regard to locale. - // Because the equal sign is a separator, it must not be used in the name of - // an environment variable." - // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/changing-environment-variables - // - struct WStringCaseInsensitiveCompare - { - [[nodiscard]] bool operator()(const std::wstring& lhs, const std::wstring& rhs) const noexcept - { - return (::_wcsicmp(lhs.c_str(), rhs.c_str()) < 0); - } - }; - - using EnvironmentVariableMapW = std::map; - - [[nodiscard]] HRESULT UpdateEnvironmentMapW(EnvironmentVariableMapW& map) noexcept; - - [[nodiscard]] HRESULT EnvironmentMapToEnvironmentStringsW(EnvironmentVariableMapW& map, - std::vector& newEnvVars) noexcept; - -}; diff --git a/src/types/inc/GlyphWidth.hpp b/src/types/inc/GlyphWidth.hpp index 7888915beb..11982bf5a0 100644 --- a/src/types/inc/GlyphWidth.hpp +++ b/src/types/inc/GlyphWidth.hpp @@ -8,11 +8,14 @@ Abstract: - Helpers for determining the width of a particular string of chars. */ +#pragma once #include #include -bool IsGlyphFullWidth(const std::wstring_view glyph); +#include "convert.hpp" + +bool IsGlyphFullWidth(const std::wstring_view& glyph) noexcept; bool IsGlyphFullWidth(const wchar_t wch) noexcept; -void SetGlyphWidthFallback(std::function pfnFallback); +void SetGlyphWidthFallback(std::function pfnFallback) noexcept; void NotifyGlyphWidthFontChanged() noexcept; diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp index 21867f4c1e..4641c17332 100644 --- a/src/types/inc/IInputEvent.hpp +++ b/src/types/inc/IInputEvent.hpp @@ -1,559 +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(gsl::span records); - static std::deque> Create(const std::deque& records); - - static std::vector ToInputRecords(const std::deque>& events); - - virtual ~IInputEvent() = 0; - IInputEvent() = default; - IInputEvent(const IInputEvent&) = default; - IInputEvent(IInputEvent&&) = default; - IInputEvent& operator=(const IInputEvent&) & = default; - IInputEvent& operator=(IInputEvent&&) & = default; - - virtual INPUT_RECORD ToInputRecord() const noexcept = 0; - - virtual InputEventType EventType() const noexcept = 0; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const IInputEvent* const pEvent); -#endif -}; - -inline IInputEvent::~IInputEvent() = default; - -#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; -}; +using InputEventQueue = til::small_vector; -enum class ModifierKeyState +// 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) { - RightAlt, - LeftAlt, - RightCtrl, - LeftCtrl, - Shift, - NumLock, - ScrollLock, - CapsLock, - EnhancedKey, - NlsDbcsChar, - NlsAlphanumeric, - NlsKatakana, - NlsHiragana, - NlsRoman, - NlsImeConversion, - AltNumpad, - NlsImeDisable, - ENUM_COUNT // must be the last element in the enum class -}; - -std::unordered_set FromVkKeyScan(const short vkKeyScanFlags); -std::unordered_set FromConsoleControlKeyFlags(const DWORD flags); -DWORD ToConsoleControlKeyFlag(const ModifierKeyState modifierKey) noexcept; - -class KeyEvent : public IInputEvent -{ -public: - 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 + return INPUT_RECORD{ + .EventType = KEY_EVENT, + .Event = { + .KeyEvent = { + .bKeyDown = bKeyDown, + .wRepeatCount = wRepeatCount, + .wVirtualKeyCode = wVirtualKeyCode, + .wVirtualScanCode = wVirtualScanCode, + .uChar = { .UnicodeChar = UnicodeChar }, + .dwControlKeyState = dwControlKeyState, + }, + }, }; - - 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 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); - }; - -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 } - { - } + 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, + }, + }, + }; +} - 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 - -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) } - { - } + return INPUT_RECORD{ + .EventType = WINDOW_BUFFER_SIZE_EVENT, + .Event = { + .WindowBufferSizeEvent = { + .dwSize = { + ::base::saturated_cast(dwSize.width), + ::base::saturated_cast(dwSize.height), + }, + }, + }, + }; +} - 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 - -class MenuEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeMenuEvent(uint32_t dwCommandId) { -public: - constexpr MenuEvent(const MENU_EVENT_RECORD& record) : - _commandId{ record.dwCommandId } - { - } + return INPUT_RECORD{ + .EventType = MENU_EVENT, + .Event = { + .MenuEvent = { + .dwCommandId = dwCommandId, + }, + }, + }; +} - constexpr MenuEvent(const UINT commandId) : - _commandId{ commandId } - { - } - - ~MenuEvent(); - MenuEvent(const MenuEvent&) = default; - MenuEvent(MenuEvent&&) = default; - MenuEvent& operator=(const MenuEvent&) & = default; - MenuEvent& operator=(MenuEvent&&) & = default; - - INPUT_RECORD ToInputRecord() const noexcept override; - InputEventType EventType() const noexcept override; - - constexpr UINT GetCommandId() const noexcept - { - return _commandId; - } - - void SetCommandId(const UINT commandId) noexcept; - -private: - UINT _commandId; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); -#endif -}; - -#ifdef UNIT_TESTING -std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); -#endif - -class FocusEvent : public IInputEvent +constexpr INPUT_RECORD SynthesizeFocusEvent(bool bSetFocus) { -public: - constexpr FocusEvent(const FOCUS_EVENT_RECORD& record) : - _focus{ !!record.bSetFocus }, - _cameFromApi{ true } - { - } - - constexpr FocusEvent(const bool focus) : - _focus{ focus }, - _cameFromApi{ false } - { - } - - ~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; - - // BODGY - see FocusEvent.cpp for details. - constexpr bool CameFromApi() const noexcept - { - return _cameFromApi; - } - -private: - bool _focus; - bool _cameFromApi; - -#ifdef UNIT_TESTING - friend std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); -#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/inc/Utf16Parser.hpp b/src/types/inc/Utf16Parser.hpp deleted file mode 100644 index 5ce8eba4e5..0000000000 --- a/src/types/inc/Utf16Parser.hpp +++ /dev/null @@ -1,46 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation - -Module Name: -- Utf16Parser.hpp - -Abstract: -- Parser for grouping together utf16 codepoints from a string of utf16 encoded text - -Author(s): -- Austin Diviness (AustDi) 25-Apr-2018 - ---*/ - -#pragma once - -#include - -class Utf16Parser final -{ -public: - static std::vector> Parse(std::wstring_view wstr); - static std::wstring_view ParseNext(std::wstring_view wstr) noexcept; - - // Routine Description: - // - checks if wchar is a utf16 leading surrogate - // Arguments: - // - wch - the wchar to check - // Return Value: - // - true if wch is a leading surrogate, false otherwise - static constexpr bool IsLeadingSurrogate(const wchar_t wch) noexcept - { - return wch >= 0xD800 && wch <= 0xDBFF; - } - - // Routine Description: - // - checks if wchar is a utf16 trailing surrogate - // Arguments: - // - wch - the wchar to check - // Return Value: - // - true if wch is a trailing surrogate, false otherwise - static constexpr bool IsTrailingSurrogate(const wchar_t wch) noexcept - { - return wch >= 0xDC00 && wch <= 0xDFFF; - } -}; diff --git a/src/types/inc/colorTable.hpp b/src/types/inc/colorTable.hpp index f68284aaa0..d81445fc18 100644 --- a/src/types/inc/colorTable.hpp +++ b/src/types/inc/colorTable.hpp @@ -12,8 +12,8 @@ Abstract: namespace Microsoft::Console::Utils { - void InitializeColorTable(const gsl::span table); - gsl::span CampbellColorTable(); + void InitializeColorTable(const std::span table); + std::span CampbellColorTable() noexcept; std::optional ColorFromXOrgAppColorName(const std::wstring_view wstr) noexcept; } diff --git a/src/types/inc/convert.hpp b/src/types/inc/convert.hpp index 6223a07dec..d4cd299c91 100644 --- a/src/types/inc/convert.hpp +++ b/src/types/inc/convert.hpp @@ -19,10 +19,9 @@ Author: enum class CodepointWidth : BYTE { + Ambiguous = 0, // could be narrow or wide depending on the current codepage and font Narrow, Wide, - Ambiguous, // could be narrow or wide depending on the current codepage and font - Invalid // not a valid unicode codepoint }; [[nodiscard]] std::wstring ConvertToW(const UINT codepage, @@ -34,6 +33,4 @@ enum class CodepointWidth : BYTE [[nodiscard]] size_t GetALengthFromW(const UINT codepage, const std::wstring_view source); -CodepointWidth GetQuickCharWidth(const wchar_t wch) noexcept; - wchar_t Utf16ToUcs2(const std::wstring_view charData); diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index 0efae69598..381e07b243 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -94,9 +94,10 @@ namespace Microsoft::Console::Utils return value; } - GUID CreateV5Uuid(const GUID& namespaceGuid, const gsl::span name); + GUID CreateV5Uuid(const GUID& namespaceGuid, const std::span name); - bool IsElevated(); + bool CanUwpDragDrop(); + bool IsRunningElevated(); // This function is only ever used by the ConPTY connection in // TerminalConnection. However, that library does not have a good system of @@ -111,4 +112,9 @@ namespace Microsoft::Console::Utils // testing easier. std::wstring_view TrimPaste(std::wstring_view textView) noexcept; + // Same deal, but in TerminalPage::_evaluatePathForCwd + std::wstring EvaluateStartingDirectory(std::wstring_view cwd, std::wstring_view startingDirectory); + + bool IsWindows11() noexcept; + } diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj index 6240434281..4da14e915b 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -15,14 +15,7 @@ - - - - - - - @@ -30,30 +23,24 @@ - - Create - - - - @@ -72,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 c547708c04..efb65b2bc4 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 @@ -57,9 +36,6 @@ Source Files - - Source Files - Source Files @@ -72,9 +48,6 @@ Source Files - - Source Files - Source Files @@ -110,9 +83,6 @@ Header Files - - Header Files - Header Files @@ -122,27 +92,6 @@ Header Files - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - Header Files @@ -152,12 +101,6 @@ Header Files - - Header Files - - - Header Files - Header Files @@ -167,9 +110,6 @@ Header Files - - Header Files - Header Files diff --git a/src/types/sgrStack.cpp b/src/types/sgrStack.cpp index a342dc59a5..77e1c62c59 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) { - result.SetUnderlined(savedAttribute.IsUnderlined()); + // 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) + { + 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)) { diff --git a/src/types/sources.inc b/src/types/sources.inc index d8863d731a..6527fbed57 100644 --- a/src/types/sources.inc +++ b/src/types/sources.inc @@ -30,18 +30,11 @@ 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 \ - ..\Utf16Parser.cpp \ ..\utils.cpp \ ..\ThemeUtils.cpp \ ..\ScreenInfoUiaProviderBase.cpp \ diff --git a/src/types/ut_types/UtilsTests.cpp b/src/types/ut_types/UtilsTests.cpp index f6246df87d..f83b1b4cad 100644 --- a/src/types/ut_types/UtilsTests.cpp +++ b/src/types/ut_types/UtilsTests.cpp @@ -33,6 +33,8 @@ class UtilsTests TEST_METHOD(TestTrimTrailingWhitespace); TEST_METHOD(TestDontTrimTrailingWhitespace); + TEST_METHOD(TestEvaluateStartingDirectory); + void _VerifyXTermColorResult(const std::wstring_view wstr, DWORD colorValue); void _VerifyXTermColorInvalid(const std::wstring_view wstr); }; @@ -546,3 +548,65 @@ void UtilsTests::TestDontTrimTrailingWhitespace() // * trim when there's a tab followed by only whitespace // * not trim then there's a tab in the middle, and the string ends in whitespace } + +void UtilsTests::TestEvaluateStartingDirectory() +{ + // Continue on failures + const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; + + auto test = [](auto& expected, auto& cwd, auto& startingDir) { + VERIFY_ARE_EQUAL(expected, EvaluateStartingDirectory(cwd, startingDir)); + }; + + // A NOTE: EvaluateStartingDirectory makes no attempt to cannonicalize the + // path. So if you do any sort of relative paths, it'll literally just + // append. + + { + std::wstring cwd = L"C:\\Windows\\System32"; + + // Literally blank + test(L"C:\\Windows\\System32\\", cwd, L""); + + // Absolute Windows path + test(L"C:\\Windows", cwd, L"C:\\Windows"); + test(L"C:/Users/migrie", cwd, L"C:/Users/migrie"); + + // Relative Windows path + test(L"C:\\Windows\\System32\\.", cwd, L"."); // ? + test(L"C:\\Windows\\System32\\.\\System32", cwd, L".\\System32"); // ? + test(L"C:\\Windows\\System32\\./dev", cwd, L"./dev"); + + // WSL '~' path + test(L"~", cwd, L"~"); + test(L"~/dev", cwd, L"~/dev"); + + // WSL or Windows / path - this will ultimately be evaluated by the connection + test(L"/", cwd, L"/"); + test(L"/dev", cwd, L"/dev"); + } + + { + std::wstring cwd = L"C:/Users/migrie"; + + // Literally blank + test(L"C:/Users/migrie\\", cwd, L""); + + // Absolute Windows path + test(L"C:\\Windows", cwd, L"C:\\Windows"); + test(L"C:/Users/migrie", cwd, L"C:/Users/migrie"); + + // Relative Windows path + test(L"C:/Users/migrie\\.", cwd, L"."); // ? + test(L"C:/Users/migrie\\.\\System32", cwd, L".\\System32"); // ? + test(L"C:/Users/migrie\\./dev", cwd, L"./dev"); + + // WSL '~' path + test(L"~", cwd, L"~"); + test(L"~/dev", cwd, L"~/dev"); + + // WSL or Windows / path - this will ultimately be evaluated by the connection + test(L"/", cwd, L"/"); + test(L"/dev", cwd, L"/dev"); + } +} diff --git a/src/types/ut_types/UuidTests.cpp b/src/types/ut_types/UuidTests.cpp index af35c4ebd2..e0abbc6a6f 100644 --- a/src/types/ut_types/UuidTests.cpp +++ b/src/types/ut_types/UuidTests.cpp @@ -25,7 +25,7 @@ class UuidTests const GUID uuidExpected{ 0x8b9d4336, 0x0c82, 0x54c4, { 0xb3, 0x15, 0xf1, 0xd2, 0xd2, 0x7e, 0xc6, 0xda } }; std::string name{ "testing" }; - auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); + auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, std::as_bytes(std::span{ name })); VERIFY_ARE_EQUAL(uuidExpected, uuidActual); } @@ -36,7 +36,7 @@ class UuidTests // This'll come out in little endian; the reference GUID was generated as such. std::wstring name{ L"testing" }; - auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); + auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, std::as_bytes(std::span{ name })); VERIFY_ARE_EQUAL(uuidExpected, uuidActual); } diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 8d4edf1e75..91fd21e263 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -623,7 +623,7 @@ bool Utils::IsValidHandle(const HANDLE handle) noexcept // - name: Bytes comprising the name (in a namespace-specific format) // Return Value: // - a new stable v5 UUID -GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span name) +GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const std::span name) { // v5 uuid generation happens over values in network byte order, so let's enforce that auto correctEndianNamespaceGuid{ EndianSwap(namespaceGuid) }; @@ -635,7 +635,7 @@ GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span(&correctEndianNamespaceGuid), sizeof(GUID), 0)); // BCryptHashData is ill-specified in that it leaves off "const" qualification for pbInput - THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast(const_cast(name.data())), gsl::narrow(name.size()), 0)); + THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast(const_cast(name.data())), gsl::narrow(name.size()), 0)); std::array buffer; THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hash.get(), buffer.data(), gsl::narrow(buffer.size()), 0)); @@ -653,9 +653,18 @@ GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span they _can_ drag drop } + // If they are running admin, they cannot drag drop. + return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + // This failed? That's very peculiar indeed. Let's err on the side + // of "drag drop is broken", just in case. + return true; + } + }(); + + return !isDragDropBroken; +} + +// See CanUwpDragDrop, GH#13928 for why this is different. +bool Utils::IsRunningElevated() +{ + static auto isElevated = []() { + try + { return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); } catch (...) @@ -698,7 +729,7 @@ std::tuple Utils::MangleStartingDirectoryForWSL(std: const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with " const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 }; const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) }; - const auto executableFilename{ executablePath.filename().wstring() }; + const auto executableFilename{ executablePath.filename() }; if (executableFilename == L"wsl" || executableFilename == L"wsl.exe") { // We've got a WSL -- let's just make sure it's the right one. @@ -753,7 +784,7 @@ std::tuple Utils::MangleStartingDirectoryForWSL(std: } return { - fmt::format(LR"("{}" --cd "{}" {})", executablePath.wstring(), mangledDirectory, arguments), + fmt::format(LR"("{}" --cd "{}" {})", executablePath.native(), mangledDirectory, arguments), std::wstring{} }; } @@ -797,3 +828,46 @@ std::wstring_view Utils::TrimPaste(std::wstring_view textView) noexcept return textView.substr(0, lastNonSpace + 1); } + +std::wstring Utils::EvaluateStartingDirectory( + std::wstring_view currentDirectory, + std::wstring_view startingDirectory) +{ + std::wstring resultPath{ startingDirectory }; + + // We only want to resolve the new WD against the CWD if it doesn't look + // like a Linux path (see GH#592) + + // Append only if it DOESN'T look like a linux-y path. A linux-y path starts + // with `~` or `/`. + const bool looksLikeLinux = + resultPath.size() >= 1 && + (til::at(resultPath, 0) == L'~' || til::at(resultPath, 0) == L'/'); + + if (!looksLikeLinux) + { + std::filesystem::path cwd{ currentDirectory }; + cwd /= startingDirectory; + resultPath = cwd.wstring(); + } + return resultPath; +} + +bool Utils::IsWindows11() noexcept +{ + static const bool isWindows11 = []() noexcept { + OSVERSIONINFOEXW osver{}; + osver.dwOSVersionInfoSize = sizeof(osver); + osver.dwBuildNumber = 22000; + + DWORDLONG dwlConditionMask = 0; + VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); + + if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) + { + return true; + } + return false; + }(); + return isWindows11; +} diff --git a/src/types/viewport.cpp b/src/types/viewport.cpp index 15dc97a0a3..2459e19ab6 100644 --- a/src/types/viewport.cpp +++ b/src/types/viewport.cpp @@ -39,10 +39,10 @@ Viewport Viewport::FromDimensions(const til::point origin, const til::CoordType height) noexcept { return Viewport(til::inclusive_rect{ - origin.X, - origin.Y, - origin.X + width - 1, - origin.Y + height - 1, + origin.x, + origin.y, + origin.x + width - 1, + origin.y + height - 1, }); } @@ -58,10 +58,10 @@ Viewport Viewport::FromDimensions(const til::point origin, const til::size dimensions) noexcept { return Viewport(til::inclusive_rect{ - origin.X, - origin.Y, - origin.X + dimensions.X - 1, - origin.Y + dimensions.Y - 1, + origin.x, + origin.y, + origin.x + dimensions.width - 1, + origin.y + dimensions.height - 1, }); } @@ -85,37 +85,37 @@ Viewport Viewport::FromDimensions(const til::size dimensions) noexcept // - a 1x1 Viewport at the given coordinate Viewport Viewport::FromCoord(const til::point origin) noexcept { - return FromInclusive(til::inclusive_rect{ origin.X, origin.Y, origin.X, origin.Y }); + return FromInclusive(til::inclusive_rect{ origin.x, origin.y, origin.x, origin.y }); } til::CoordType Viewport::Left() const noexcept { - return _sr.Left; + return _sr.left; } til::CoordType Viewport::RightInclusive() const noexcept { - return _sr.Right; + return _sr.right; } til::CoordType Viewport::RightExclusive() const noexcept { - return _sr.Right + 1; + return _sr.right + 1; } til::CoordType Viewport::Top() const noexcept { - return _sr.Top; + return _sr.top; } til::CoordType Viewport::BottomInclusive() const noexcept { - return _sr.Bottom; + return _sr.bottom; } til::CoordType Viewport::BottomExclusive() const noexcept { - return _sr.Bottom + 1; + return _sr.bottom + 1; } til::CoordType Viewport::Height() const noexcept @@ -215,8 +215,8 @@ bool Viewport::IsInBounds(const til::point pos, bool allowEndExclusive) const no return true; } - return pos.X >= Left() && pos.X < RightExclusive() && - pos.Y >= Top() && pos.Y < BottomExclusive(); + return pos.x >= Left() && pos.x < RightExclusive() && + pos.y >= Top() && pos.y < BottomExclusive(); } // Method Description: @@ -229,8 +229,8 @@ void Viewport::Clamp(til::point& pos) const { THROW_HR_IF(E_NOT_VALID_STATE, !IsValid()); // we can't clamp to an invalid viewport. - pos.X = std::clamp(pos.X, Left(), RightInclusive()); - pos.Y = std::clamp(pos.Y, Top(), BottomInclusive()); + pos.x = std::clamp(pos.x, Left(), RightInclusive()); + pos.y = std::clamp(pos.y, Top(), BottomInclusive()); } // Method Description: @@ -243,10 +243,10 @@ Viewport Viewport::Clamp(const Viewport& other) const noexcept { auto clampMe = other.ToInclusive(); - clampMe.Left = std::clamp(clampMe.Left, Left(), RightInclusive()); - clampMe.Right = std::clamp(clampMe.Right, Left(), RightInclusive()); - clampMe.Top = std::clamp(clampMe.Top, Top(), BottomInclusive()); - clampMe.Bottom = std::clamp(clampMe.Bottom, Top(), BottomInclusive()); + clampMe.left = std::clamp(clampMe.left, Left(), RightInclusive()); + clampMe.right = std::clamp(clampMe.right, Left(), RightInclusive()); + clampMe.top = std::clamp(clampMe.top, Top(), BottomInclusive()); + clampMe.bottom = std::clamp(clampMe.bottom, Top(), BottomInclusive()); return Viewport::FromInclusive(clampMe); } @@ -375,11 +375,11 @@ int Viewport::CompareInBounds(const til::point first, const til::point second, b // First set the distance vertically // If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160. // For the same row, it'll be 0 rows * 80 character width = 0 difference. - auto retVal = (first.Y - second.Y) * Width(); + auto retVal = (first.y - second.y) * Width(); // Now adjust for horizontal differences // If first is in position 15 and second is in position 30, first is -15 left in relation to 30. - retVal += (first.X - second.X); + retVal += (first.x - second.x); // Further notes: // If we already moved behind one row, this will help correct for when first is right of second. @@ -437,71 +437,71 @@ bool Viewport::WalkInBoundsCircular(til::point& pos, const WalkDir dir, bool all if (dir.x == XWalk::LeftToRight) { - if (allowEndExclusive && pos.X == Left() && pos.Y == BottomExclusive()) + if (allowEndExclusive && pos.x == Left() && pos.y == BottomExclusive()) { - pos.Y = Top(); + pos.y = Top(); return false; } - else if (pos.X == RightInclusive()) + else if (pos.x == RightInclusive()) { - pos.X = Left(); + pos.x = Left(); if (dir.y == YWalk::TopToBottom) { - pos.Y++; - if (allowEndExclusive && pos.Y == BottomExclusive()) + pos.y++; + if (allowEndExclusive && pos.y == BottomExclusive()) { return true; } - else if (pos.Y > BottomInclusive()) + else if (pos.y > BottomInclusive()) { - pos.Y = Top(); + pos.y = Top(); return false; } } else { - pos.Y--; - if (pos.Y < Top()) + pos.y--; + if (pos.y < Top()) { - pos.Y = BottomInclusive(); + pos.y = BottomInclusive(); return false; } } } else { - pos.X++; + pos.x++; } } else { - if (pos.X == Left()) + if (pos.x == Left()) { - pos.X = RightInclusive(); + pos.x = RightInclusive(); if (dir.y == YWalk::TopToBottom) { - pos.Y++; - if (pos.Y > BottomInclusive()) + pos.y++; + if (pos.y > BottomInclusive()) { - pos.Y = Top(); + pos.y = Top(); return false; } } else { - pos.Y--; - if (pos.Y < Top()) + pos.y--; + if (pos.y < Top()) { - pos.Y = BottomInclusive(); + pos.y = BottomInclusive(); return false; } } } else { - pos.X--; + pos.x--; } } @@ -521,8 +521,8 @@ bool Viewport::WalkInBoundsCircular(til::point& pos, const WalkDir dir, bool all til::point Viewport::GetWalkOrigin(const WalkDir dir) const noexcept { til::point origin; - origin.X = dir.x == XWalk::LeftToRight ? Left() : RightInclusive(); - origin.Y = dir.y == YWalk::TopToBottom ? Top() : BottomInclusive(); + origin.x = dir.x == XWalk::LeftToRight ? Left() : RightInclusive(); + origin.y = dir.y == YWalk::TopToBottom ? Top() : BottomInclusive(); return origin; } @@ -682,8 +682,8 @@ Viewport::WalkDir Viewport::DetermineWalkDirection(const Viewport& source, const const auto sourceOrigin = source.Origin(); const auto targetOrigin = target.Origin(); - return Viewport::WalkDir{ targetOrigin.X < sourceOrigin.X ? Viewport::XWalk::LeftToRight : Viewport::XWalk::RightToLeft, - targetOrigin.Y < sourceOrigin.Y ? Viewport::YWalk::TopToBottom : Viewport::YWalk::BottomToTop }; + return Viewport::WalkDir{ targetOrigin.x < sourceOrigin.x ? Viewport::XWalk::LeftToRight : Viewport::XWalk::RightToLeft, + targetOrigin.y < sourceOrigin.y ? Viewport::YWalk::TopToBottom : Viewport::YWalk::BottomToTop }; } // Method Description: @@ -695,17 +695,17 @@ Viewport::WalkDir Viewport::DetermineWalkDirection(const Viewport& source, const // - true iff the clipped rectangle is valid (with a width and height both >0) bool Viewport::TrimToViewport(_Inout_ til::rect* psr) const noexcept { - psr->Left = std::max(psr->Left, Left()); - psr->Right = std::min(psr->Right, RightExclusive()); - psr->Top = std::max(psr->Top, Top()); - psr->Bottom = std::min(psr->Bottom, BottomExclusive()); + psr->left = std::max(psr->left, Left()); + psr->right = std::min(psr->right, RightExclusive()); + psr->top = std::max(psr->top, Top()); + psr->bottom = std::min(psr->bottom, BottomExclusive()); - return psr->Left < psr->Right && psr->Top < psr->Bottom; + return psr->left < psr->right && psr->top < psr->bottom; } // Method Description: // - Translates the input til::rect out of our coordinate space, whose origin is -// at (this.Left, this.Right) +// at (this.left, this.right) // Arguments: // - psr: a pointer to a til::rect the translate into our coordinate space. // Return Value: @@ -714,15 +714,15 @@ void Viewport::ConvertToOrigin(_Inout_ til::rect* psr) const noexcept { const auto dx = Left(); const auto dy = Top(); - psr->Left -= dx; - psr->Right -= dx; - psr->Top -= dy; - psr->Bottom -= dy; + psr->left -= dx; + psr->right -= dx; + psr->top -= dy; + psr->bottom -= dy; } // Method Description: // - Translates the input til::inclusive_rect out of our coordinate space, whose origin is -// at (this.Left, this.Right) +// at (this.left, this.right) // Arguments: // - psr: a pointer to a til::inclusive_rect the translate into our coordinate space. // Return Value: @@ -731,28 +731,28 @@ void Viewport::ConvertToOrigin(_Inout_ til::inclusive_rect* const psr) const noe { const auto dx = Left(); const auto dy = Top(); - psr->Left -= dx; - psr->Right -= dx; - psr->Top -= dy; - psr->Bottom -= dy; + psr->left -= dx; + psr->right -= dx; + psr->top -= dy; + psr->bottom -= dy; } // Method Description: // - Translates the input coordinate out of our coordinate space, whose origin is -// at (this.Left, this.Right) +// at (this.left, this.right) // Arguments: // - pcoord: a pointer to a coordinate the translate into our coordinate space. // Return Value: // - void Viewport::ConvertToOrigin(_Inout_ til::point* const pcoord) const noexcept { - pcoord->X -= Left(); - pcoord->Y -= Top(); + pcoord->x -= Left(); + pcoord->y -= Top(); } // Method Description: // - Translates the input til::inclusive_rect to our coordinate space, whose origin is -// at (this.Left, this.Right) +// at (this.left, this.right) // Arguments: // - psr: a pointer to a til::inclusive_rect the translate into our coordinate space. // Return Value: @@ -761,23 +761,23 @@ void Viewport::ConvertFromOrigin(_Inout_ til::inclusive_rect* const psr) const n { const auto dx = Left(); const auto dy = Top(); - psr->Left += dx; - psr->Right += dx; - psr->Top += dy; - psr->Bottom += dy; + psr->left += dx; + psr->right += dx; + psr->top += dy; + psr->bottom += dy; } // Method Description: // - Translates the input coordinate to our coordinate space, whose origin is -// at (this.Left, this.Right) +// at (this.left, this.right) // Arguments: // - pcoord: a pointer to a coordinate the translate into our coordinate space. // Return Value: // - void Viewport::ConvertFromOrigin(_Inout_ til::point* const pcoord) const noexcept { - pcoord->X += Left(); - pcoord->Y += Top(); + pcoord->x += Left(); + pcoord->y += Top(); } // Method Description: @@ -827,7 +827,7 @@ Viewport Viewport::ToOrigin() const noexcept // Arguments: // - other: the viewport to convert to this coordinate space // Return Value: -// - the input viewport in a the coordinate space with origin at (this.Top, this.Left) +// - the input viewport in a the coordinate space with origin at (this.top, this.left) [[nodiscard]] Viewport Viewport::ConvertToOrigin(const Viewport& other) const noexcept { auto returnVal = other; @@ -863,10 +863,10 @@ Viewport Viewport::ToOrigin() const noexcept // - NOTE: Throws on safe math failure. [[nodiscard]] Viewport Viewport::Offset(const Viewport& original, const til::point delta) noexcept { - const auto newLeft = original._sr.Left + delta.X; - const auto newTop = original._sr.Top + delta.Y; - const auto newRight = original._sr.Right + delta.X; - const auto newBottom = original._sr.Bottom + delta.Y; + const auto newLeft = original._sr.left + delta.x; + const auto newTop = original._sr.top + delta.y; + const auto newRight = original._sr.right + delta.x; + const auto newBottom = original._sr.bottom + delta.y; return Viewport({ newLeft, newTop, newRight, newBottom }); } diff --git a/src/winconpty/dll/winconpty.def b/src/winconpty/dll/winconpty.def index c5b808e3a9..ba5d52515f 100644 --- a/src/winconpty/dll/winconpty.def +++ b/src/winconpty/dll/winconpty.def @@ -4,10 +4,11 @@ EXPORTS ConptyCreatePseudoConsoleAsUser ConptyResizePseudoConsole ConptyClosePseudoConsole - ConptyClosePseudoConsoleNoWait + ConptyClosePseudoConsoleTimeout ConptyClearPseudoConsole ConptyShowHidePseudoConsole ConptyReparentPseudoConsole + ConptyReleasePseudoConsole ConptyPackPseudoConsole ; Compatibility aliases for P/Invoke; only required for compatibility diff --git a/src/winconpty/ft_pty/ConPtyTests.cpp b/src/winconpty/ft_pty/ConPtyTests.cpp index 4d41e6135e..699e2c665d 100644 --- a/src/winconpty/ft_pty/ConPtyTests.cpp +++ b/src/winconpty/ft_pty/ConPtyTests.cpp @@ -2,23 +2,105 @@ // Licensed under the MIT license. #include "precomp.h" + +#include + #include "../winconpty.h" using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; +using unique_hpcon = wil::unique_any; + +struct InOut +{ + wil::unique_handle in; + wil::unique_handle out; +}; + +struct Pipes +{ + InOut our; + InOut conpty; +}; + +static Pipes createPipes() +{ + Pipes p; + SECURITY_ATTRIBUTES sa{ + .nLength = sizeof(sa), + .bInheritHandle = TRUE, + }; + + VERIFY_IS_TRUE(CreatePipe(p.conpty.in.addressof(), p.our.in.addressof(), &sa, 0)); + VERIFY_IS_TRUE(CreatePipe(p.our.out.addressof(), p.conpty.out.addressof(), &sa, 0)); + VERIFY_IS_TRUE(SetHandleInformation(p.our.in.get(), HANDLE_FLAG_INHERIT, 0)); + VERIFY_IS_TRUE(SetHandleInformation(p.our.out.get(), HANDLE_FLAG_INHERIT, 0)); + + return p; +} + +struct PTY +{ + unique_hpcon hpcon; + InOut pipes; +}; + +static PTY createPseudoConsole() +{ + auto pipes = createPipes(); + unique_hpcon hpcon; + VERIFY_SUCCEEDED(ConptyCreatePseudoConsole({ 80, 30 }, pipes.conpty.in.get(), pipes.conpty.out.get(), 0, hpcon.addressof())); + return { + .hpcon = std::move(hpcon), + .pipes = std::move(pipes.our), + }; +} + +static std::string readOutputToEOF(const InOut& io) +{ + std::string accumulator; + char buffer[1024]; + + for (;;) + { + DWORD read; + const auto ok = ReadFile(io.out.get(), &buffer[0], sizeof(buffer), &read, nullptr); + + if (!ok) + { + const auto lastError = GetLastError(); + if (lastError == ERROR_BROKEN_PIPE) + { + break; + } + else + { + VERIFY_WIN32_BOOL_SUCCEEDED(false); + } + } + + accumulator.append(&buffer[0], read); + } + + return accumulator; +} + class ConPtyTests { - TEST_CLASS(ConPtyTests); + BEGIN_TEST_CLASS(ConPtyTests) + TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:15") // 15s timeout + END_TEST_CLASS() + const COORD defaultSize = { 80, 30 }; TEST_METHOD(CreateConPtyNoPipes); TEST_METHOD(CreateConPtyBadSize); TEST_METHOD(GoodCreate); TEST_METHOD(GoodCreateMultiple); TEST_METHOD(SurvivesOnBreakOutput); - TEST_METHOD(DiesOnBreakBoth); TEST_METHOD(DiesOnClose); + TEST_METHOD(ReleasePseudoConsole); }; static HRESULT _CreatePseudoConsole(const COORD size, @@ -30,7 +112,7 @@ static HRESULT _CreatePseudoConsole(const COORD size, return _CreatePseudoConsole(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, pPty); } -static HRESULT AttachPseudoConsole(HPCON hPC, std::wstring& command, PROCESS_INFORMATION* ppi) +static HRESULT AttachPseudoConsole(HPCON hPC, std::wstring command, PROCESS_INFORMATION* ppi) { SIZE_T size = 0; InitializeProcThreadAttributeList(nullptr, 1, 0, &size); @@ -83,10 +165,10 @@ void ConPtyTests::CreateConPtyNoPipes() VERIFY_FAILED(_CreatePseudoConsole(defaultSize, nullptr, nullptr, 0, &pcon)); VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, nullptr, goodOut, 0, &pcon)); - _ClosePseudoConsoleMembers(&pcon, TRUE); + _ClosePseudoConsoleMembers(&pcon, INFINITE); VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, goodIn, nullptr, 0, &pcon)); - _ClosePseudoConsoleMembers(&pcon, TRUE); + _ClosePseudoConsoleMembers(&pcon, INFINITE); } void ConPtyTests::CreateConPtyBadSize() @@ -130,7 +212,7 @@ void ConPtyTests::GoodCreate() &pcon)); auto closePty = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pcon, TRUE); + _ClosePseudoConsoleMembers(&pcon, INFINITE); }); } @@ -159,7 +241,7 @@ void ConPtyTests::GoodCreateMultiple() 0, &pcon1)); auto closePty1 = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pcon1, TRUE); + _ClosePseudoConsoleMembers(&pcon1, INFINITE); }); VERIFY_SUCCEEDED( @@ -169,7 +251,7 @@ void ConPtyTests::GoodCreateMultiple() 0, &pcon2)); auto closePty2 = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pcon2, TRUE); + _ClosePseudoConsoleMembers(&pcon2, INFINITE); }); } @@ -196,7 +278,7 @@ void ConPtyTests::SurvivesOnBreakOutput() 0, &pty)); auto closePty1 = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pty, TRUE); + _ClosePseudoConsoleMembers(&pty, INFINITE); }); DWORD dwExit; @@ -218,63 +300,6 @@ void ConPtyTests::SurvivesOnBreakOutput() VERIFY_ARE_EQUAL(dwExit, (DWORD)STILL_ACTIVE); } -void ConPtyTests::DiesOnBreakBoth() -{ - PseudoConsole pty = { 0 }; - wil::unique_handle outPipeOurSide; - wil::unique_handle inPipeOurSide; - wil::unique_handle outPipePseudoConsoleSide; - wil::unique_handle inPipePseudoConsoleSide; - SECURITY_ATTRIBUTES sa; - sa.nLength = sizeof(sa); - sa.bInheritHandle = TRUE; - sa.lpSecurityDescriptor = nullptr; - VERIFY_IS_TRUE(CreatePipe(inPipePseudoConsoleSide.addressof(), inPipeOurSide.addressof(), &sa, 0)); - VERIFY_IS_TRUE(CreatePipe(outPipeOurSide.addressof(), outPipePseudoConsoleSide.addressof(), &sa, 0)); - VERIFY_IS_TRUE(SetHandleInformation(inPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0)); - VERIFY_IS_TRUE(SetHandleInformation(outPipeOurSide.get(), HANDLE_FLAG_INHERIT, 0)); - - VERIFY_SUCCEEDED( - _CreatePseudoConsole(defaultSize, - inPipePseudoConsoleSide.get(), - outPipePseudoConsoleSide.get(), - 0, - &pty)); - auto closePty1 = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pty, TRUE); - }); - - DWORD dwExit; - VERIFY_IS_TRUE(GetExitCodeProcess(pty.hConPtyProcess, &dwExit)); - VERIFY_ARE_EQUAL(dwExit, (DWORD)STILL_ACTIVE); - - wil::unique_process_information piClient; - std::wstring realCommand = L"cmd.exe"; - VERIFY_SUCCEEDED(AttachPseudoConsole(&pty, realCommand, piClient.addressof())); - - VERIFY_IS_TRUE(GetExitCodeProcess(piClient.hProcess, &dwExit)); - VERIFY_ARE_EQUAL(dwExit, (DWORD)STILL_ACTIVE); - - // Close one of the pipes... - VERIFY_IS_TRUE(CloseHandle(outPipeOurSide.get())); - - // ... Wait for a couple seconds, make sure the child is still alive. - VERIFY_ARE_EQUAL(WaitForSingleObject(pty.hConPtyProcess, 2000), (DWORD)WAIT_TIMEOUT); - VERIFY_IS_TRUE(GetExitCodeProcess(pty.hConPtyProcess, &dwExit)); - VERIFY_ARE_EQUAL(dwExit, (DWORD)STILL_ACTIVE); - - // Tricky - write some input to the pcon. We need to do this so conhost can - // realize that the output pipe has broken. - VERIFY_SUCCEEDED(WriteFile(inPipeOurSide.get(), L"a", sizeof(wchar_t), nullptr, nullptr)); - - // Close the other pipe, and make sure conhost dies - VERIFY_IS_TRUE(CloseHandle(inPipeOurSide.get())); - - VERIFY_ARE_EQUAL(WaitForSingleObject(pty.hConPtyProcess, 10000), (DWORD)WAIT_OBJECT_0); - VERIFY_IS_TRUE(GetExitCodeProcess(pty.hConPtyProcess, &dwExit)); - VERIFY_ARE_NOT_EQUAL(dwExit, (DWORD)STILL_ACTIVE); -} - void ConPtyTests::DiesOnClose() { BEGIN_TEST_METHOD_PROPERTIES() @@ -312,7 +337,7 @@ void ConPtyTests::DiesOnClose() 0, &pty)); auto closePty1 = wil::scope_exit([&] { - _ClosePseudoConsoleMembers(&pty, TRUE); + _ClosePseudoConsoleMembers(&pty, INFINITE); }); DWORD dwExit; @@ -336,8 +361,22 @@ void ConPtyTests::DiesOnClose() Log::Comment(NoThrowString().Format(L"Sleep a bit to let the process attach")); Sleep(100); - _ClosePseudoConsoleMembers(&pty, TRUE); + _ClosePseudoConsoleMembers(&pty, INFINITE); GetExitCodeProcess(hConPtyProcess.get(), &dwExit); VERIFY_ARE_NOT_EQUAL(dwExit, (DWORD)STILL_ACTIVE); } + +// Issues with ConptyReleasePseudoConsole functionality might present itself as sporadic/flaky test failures, +// which should not ever happen (otherwise something is broken). This is because "start /b" runs concurrently +// with the initially spawned "cmd.exe" exiting and so this test involves sort of a race condition. +void ConPtyTests::ReleasePseudoConsole() +{ + const auto pty = createPseudoConsole(); + wil::unique_process_information pi; + VERIFY_SUCCEEDED(AttachPseudoConsole(pty.hpcon.get(), L"cmd.exe /c start /b cmd.exe /c echo foobar", pi.addressof())); + VERIFY_SUCCEEDED(ConptyReleasePseudoConsole(pty.hpcon.get())); + + const auto output = readOutputToEOF(pty.pipes); + VERIFY_ARE_NOT_EQUAL(std::string::npos, output.find("foobar")); +} diff --git a/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj b/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj index 9aa2c80894..eacf4d3ae9 100644 --- a/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj +++ b/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj @@ -34,6 +34,10 @@ + + + CONPTY_IMPEXP=;%(PreprocessorDefinitions) + $(OutDir)\conptylib.lib;%(AdditionalDependencies) diff --git a/src/winconpty/precomp.h b/src/winconpty/precomp.h index 6bf892eacb..c78377cc55 100644 --- a/src/winconpty/precomp.h +++ b/src/winconpty/precomp.h @@ -24,16 +24,8 @@ Abstract: #include #undef WIN32_NO_STATUS -// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs -typedef _Return_type_success_(return >= 0) LONG NTSTATUS; -/*lint -save -e624 */ // Don't complain about different typedefs. -typedef NTSTATUS* PNTSTATUS; -/*lint -restore */ // Resume checking for different typedefs. -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#include -// End From ntdef.h - -#define INLINE_NTSTATUS_FROM_WIN32 1 // Must use inline NTSTATUS or it will call the wrapped function twice. #pragma warning(push) #pragma warning(disable : 4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. #include diff --git a/src/winconpty/winconpty.cpp b/src/winconpty/winconpty.cpp index 7a4f9b5639..323e4f788f 100644 --- a/src/winconpty/winconpty.cpp +++ b/src/winconpty/winconpty.cpp @@ -81,7 +81,7 @@ static wchar_t* _ConsoleHostPath() // We tried the architecture infix version and failed, fall back to conhost. return _InboxConsoleHostPath(); } - auto modulePathAsString{ modulePath.wstring() }; + const auto& modulePathAsString = modulePath.native(); return wil::make_process_heap_string_nothrow(modulePathAsString.data(), modulePathAsString.size()); #endif // __INSIDE_WINDOWS }(); @@ -112,6 +112,14 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, wil::unique_handle serverHandle; RETURN_IF_NTSTATUS_FAILED(CreateServerHandle(serverHandle.addressof(), TRUE)); + // The hPtyReference we create here is used when the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute is processed. + // This ensures that conhost's client processes inherit the correct (= our) console handle. + wil::unique_handle referenceHandle; + RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(referenceHandle.addressof(), + serverHandle.get(), + L"\\Reference", + FALSE)); + wil::unique_handle signalPipeConhostSide; wil::unique_handle signalPipeOurSide; @@ -125,19 +133,17 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files - auto pwszFormat = L"\"%s\" --headless %s%s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; + auto pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; // This is plenty of space to hold the formatted string wchar_t cmd[MAX_PATH]{}; const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; - const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; const BOOL bPassthroughMode = (dwFlags & PSEUDOCONSOLE_PASSTHROUGH_MODE) == PSEUDOCONSOLE_PASSTHROUGH_MODE; swprintf_s(cmd, MAX_PATH, pwszFormat, _ConsoleHostPath(), bInheritCursor ? L"--inheritcursor " : L"", - bWin32InputMode ? L"--win32input " : L"", bResizeQuirk ? L"--resizeQuirk " : L"", bPassthroughMode ? L"--passthrough " : L"", size.X, @@ -226,16 +232,9 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, } } - // Move the process handle out of the PROCESS_INFORMATION into out Pseudoconsole - pPty->hConPtyProcess = pi.hProcess; - pi.hProcess = nullptr; - - RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(&pPty->hPtyReference, - serverHandle.get(), - L"\\Reference", - FALSE)); - pPty->hSignal = signalPipeOurSide.release(); + pPty->hPtyReference = referenceHandle.release(); + pPty->hConPtyProcess = std::exchange(pi.hProcess, nullptr); return S_OK; } @@ -326,15 +325,20 @@ HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const { return E_INVALIDARG; } + // sneaky way to pack a short and a uint64_t in a relatively literal way. #pragma pack(push, 1) struct _signal { const unsigned short id; const uint64_t hwnd; - } data{ PTY_SIGNAL_REPARENT_WINDOW, (uint64_t)(newParent) }; + }; #pragma pack(pop) + const _signal data{ + PTY_SIGNAL_REPARENT_WINDOW, + (uint64_t)(newParent), + }; const auto fSuccess = WriteFile(pPty->hSignal, &data, sizeof(data), nullptr, nullptr); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); @@ -350,7 +354,7 @@ HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const // - wait: If true, waits for conhost/OpenConsole to exit first. // Return Value: // - -void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait) +void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds) { if (pPty != nullptr) { @@ -361,27 +365,27 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait) CloseHandle(pPty->hSignal); pPty->hSignal = nullptr; } + // The reference handle ensures that conhost keeps running unless ClosePseudoConsole is called. + // We have to call it before calling WaitForSingleObject however in order to not deadlock, + // Due to conhost waiting for all clients to disconnect, while we wait for conhost to exit. + if (_HandleIsValid(pPty->hPtyReference)) + { + CloseHandle(pPty->hPtyReference); + pPty->hPtyReference = nullptr; + } // Then, wait on the conhost process before killing it. // We do this to make sure the conhost finishes flushing any output it // has yet to send before we hard kill it. if (_HandleIsValid(pPty->hConPtyProcess)) { - if (wait) + if (dwMilliseconds) { - WaitForSingleObject(pPty->hConPtyProcess, INFINITE); + WaitForSingleObject(pPty->hConPtyProcess, dwMilliseconds); } CloseHandle(pPty->hConPtyProcess); pPty->hConPtyProcess = nullptr; } - // Then take care of the reference handle. - // TODO GH#1810: Closing the reference handle late leaves conhost thinking - // that we have an outstanding connected client. - if (_HandleIsValid(pPty->hPtyReference)) - { - CloseHandle(pPty->hPtyReference); - pPty->hPtyReference = nullptr; - } } } @@ -394,11 +398,11 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait) // - wait: If true, waits for conhost/OpenConsole to exit first. // Return Value: // - -static void _ClosePseudoConsole(_In_ PseudoConsole* pPty, BOOL wait) noexcept +static void _ClosePseudoConsole(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds) noexcept { if (pPty != nullptr) { - _ClosePseudoConsoleMembers(pPty, wait); + _ClosePseudoConsoleMembers(pPty, dwMilliseconds); HeapFree(GetProcessHeap(), 0, pPty); } } @@ -439,12 +443,12 @@ extern "C" HRESULT WINAPI ConptyCreatePseudoConsole(_In_ COORD size, return ConptyCreatePseudoConsoleAsUser(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, phPC); } -extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, - _In_ COORD size, - _In_ HANDLE hInput, - _In_ HANDLE hOutput, - _In_ DWORD dwFlags, - _Out_ HPCON* phPC) +extern "C" HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, + _In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC) { if (phPC == nullptr) { @@ -459,7 +463,7 @@ extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, auto pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole)); RETURN_IF_NULL_ALLOC(pPty); auto cleanupPty = wil::scope_exit([&]() noexcept { - _ClosePseudoConsole(pPty, TRUE); + _ClosePseudoConsole(pPty, 0); }); wil::unique_handle duplicatedInput; @@ -523,13 +527,28 @@ extern "C" HRESULT WINAPI ConptyShowHidePseudoConsole(_In_ HPCON hPC, bool show) // - Used to support GH#2988 extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newParent) { - const PseudoConsole* const pPty = (PseudoConsole*)hPC; - auto hr = pPty == nullptr ? E_INVALIDARG : S_OK; - if (SUCCEEDED(hr)) + return _ReparentPseudoConsole((PseudoConsole*)hPC, newParent); +} + +// The \Reference handle ensures that conhost keeps running by keeping the ConDrv server pipe open. +// After you've finished setting up your PTY via PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, this method may be called +// to release that handle, allowing conhost to shut down automatically once the last client has disconnected. +// You'll know when this happens, because a ReadFile() on the output pipe will return ERROR_BROKEN_PIPE. +extern "C" HRESULT WINAPI ConptyReleasePseudoConsole(_In_ HPCON hPC) +{ + const auto pPty = (PseudoConsole*)hPC; + if (pPty == nullptr) { - hr = _ReparentPseudoConsole(pPty, newParent); + return E_INVALIDARG; } - return hr; + + if (_HandleIsValid(pPty->hPtyReference)) + { + CloseHandle(pPty->hPtyReference); + pPty->hPtyReference = nullptr; + } + + return S_OK; } // Function Description: @@ -541,7 +560,7 @@ extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newPa // Waits for conhost/OpenConsole to exit first. extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC) { - _ClosePseudoConsole((PseudoConsole*)hPC, TRUE); + _ClosePseudoConsole((PseudoConsole*)hPC, INFINITE); } // Function Description: @@ -551,9 +570,9 @@ extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC) // This can fail if the conhost hosting the pseudoconsole failed to be // terminated, or if the pseudoconsole was already terminated. // Doesn't wait for conhost/OpenConsole to exit. -extern "C" VOID WINAPI ConptyClosePseudoConsoleNoWait(_In_ HPCON hPC) +extern "C" VOID WINAPI ConptyClosePseudoConsoleTimeout(_In_ HPCON hPC, _In_ DWORD dwMilliseconds) { - _ClosePseudoConsole((PseudoConsole*)hPC, FALSE); + _ClosePseudoConsole((PseudoConsole*)hPC, dwMilliseconds); } // NOTE: This one is not defined in the Windows headers but is diff --git a/src/winconpty/winconpty.h b/src/winconpty/winconpty.h index c52c60e02a..744ec09916 100644 --- a/src/winconpty/winconpty.h +++ b/src/winconpty/winconpty.h @@ -7,10 +7,36 @@ extern "C" { #endif +// This structure is part of an ABI shared with the rest of the operating system. typedef struct _PseudoConsole { + // hSignal is a anonymous pipe used for out of band communication with conhost. + // It's used to send the various PTY_SIGNAL_* messages. HANDLE hSignal; + // The "server handle" in conhost represents the console IPC "pipe" over which all console + // messages, all client connect and disconnect events, API calls, text output, etc. flow. + // The full type of this handle is \Device\ConDrv\Server and is implemented in + // /minkernel/console/condrv/server.c. If you inspect conhost's handles it'll show up + // as a handle of name \Device\ConDrv, because that's the namespace of these handles. + // + // hPtyReference is derived from that handle (= a child), is named \Reference and is implemented + // in /minkernel/console/condrv/reference.c. While conhost is the sole owner and user of the + // "server handle", the "reference handle" is what console processes actually inherit in order + // to communicate with the console server (= conhost). When the reference count of the + // \Reference handle drops to 0, it'll release its reference to the server handle. + // The server handle in turn is implemented in such a way that the IPC pipe is broken + // once the reference count drops to 1, because then conhost must be the last one using it. + // + // In other words: As long as hPtyReference exists it'll keep the server handle alive + // and thus keep conhost alive. Closing this handle will make conhost exit as soon as all + // currently connected clients have disconnected and closed the reference handle as well. + // + // This benefit of this system is that it naturally works with handle inheritance in + // CreateProcess, which ensures that the reference handle is safely duplicated and + // transmitted from a parent process to a new child process, even if the parent + // process exits before the OS has even finished spawning the child process. HANDLE hPtyReference; + // hConPtyProcess is a process handle to the conhost instance that we've spawned for ConPTY. HANDLE hConPtyProcess; } PseudoConsole; @@ -23,11 +49,18 @@ typedef struct _PseudoConsole #define PTY_SIGNAL_RESIZE_WINDOW (8u) // CreatePseudoConsole Flags -// The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo -// #define PSEUDOCONSOLE_INHERIT_CURSOR (0x1) +#ifndef PSEUDOCONSOLE_INHERIT_CURSOR +#define PSEUDOCONSOLE_INHERIT_CURSOR (0x1) +#endif +#ifndef PSEUDOCONSOLE_RESIZE_QUIRK #define PSEUDOCONSOLE_RESIZE_QUIRK (0x2) +#endif +#ifndef PSEUDOCONSOLE_WIN32_INPUT_MODE #define PSEUDOCONSOLE_WIN32_INPUT_MODE (0x4) +#endif +#ifndef PSEUDOCONSOLE_PASSTHROUGH_MODE #define PSEUDOCONSOLE_PASSTHROUGH_MODE (0x8) +#endif // Implementations of the various PseudoConsole functions. HRESULT _CreatePseudoConsole(const HANDLE hToken, @@ -41,14 +74,14 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty); HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show); HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent); -void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait); +void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds); -HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, - _In_ COORD size, - _In_ HANDLE hInput, - _In_ HANDLE hOutput, - _In_ DWORD dwFlags, - _Out_ HPCON* phPC); +HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, + _In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC); #ifdef __cplusplus } diff --git a/tools/CheckPSVersion.ps1 b/tools/CheckPSVersion.ps1 new file mode 100644 index 0000000000..ffde273393 --- /dev/null +++ b/tools/CheckPSVersion.ps1 @@ -0,0 +1,4 @@ +if($PSVersionTable.PSVersion.Major -lt 7){ + Write-Error "Incorrect version of PowerShell installed.`nMake sure you have at least PowerShell Core 7.0.0." + Exit 1 +} \ No newline at end of file diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index f7f2d6fc11..fa1975e3e4 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -10,21 +10,13 @@ - - {{FG: {_foreground}, BG: {_background}, Legacy: {_wAttrLegacy}, {_extendedAttrs}} - - _wAttrLegacy - _foreground - _background - _extendedAttrs - + {{FG: {_foreground}, BG: {_background}, Attr: {_attrs}} + {{FG: {_foreground}, BG: {_background}, Attr: {_attrs}, Hyperlink: {_hyperlinkId}} - {{LT({_sr.Left}, {_sr.Top}) RB({_sr.Right}, {_sr.Bottom}) [{_sr.Right-_sr.Left+1} x { _sr.Bottom-_sr.Top+1}]}} + {{LT({_sr.left}, {_sr.top}) RB({_sr.right}, {_sr.bottom}) [{_sr.right-_sr.left+1} x { _sr.bottom-_sr.top+1}]}} _sr @@ -38,31 +30,12 @@ {{LT({Left}, {Top}) RB({Right}, {Bottom}) In:[{Right-Left+1} x {Bottom-Top+1}] Ex:[{Right-Left} x {Bottom-Top}]}} - - Stored Glyph, go to UnicodeStorage. - {_wch,X} Single - {_wch,X} Lead - {_wch,X} Trail - - - - - _data - - - - - {{ wrap={_wrapForced} padded={_doubleBytePadded} }} - - _data - - - - {{ id={_id} width={_rowWidth} }} + {_chars.data(),[_charOffsets[_columnCount]]} + _chars.data(),[_charOffsets[_columnCount]] - _charRow - _attrRow + _chars.data(),[_charOffsets[_columnCount]] + _charOffsets.data(),[_charOffsets.size()] @@ -72,21 +45,40 @@ - - {{↓ 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} - {{W: {_width,d} x H: {_height,d} -> A: {_width * _height, d}}} + {{W: {width} x H: {height} -> A: {width * height}}} - {{X: {_x,d}, Y: {_y,d}}} + {{X: {x}, Y: {y}}} - {{L: {_topLeft._x}, T: {_topLeft._y}, R: {_bottomRight._x} B: {_bottomRight._y} [W: {_bottomRight._x - _topLeft._x} x H: {_bottomRight._y - _topLeft._y} -> A: {(_bottomRight._x - _topLeft._x) * (_bottomRight._y - _topLeft._y)}]}} + {{L: {left}, T: {top}, R: {right} B: {bottom} [W: {right - left} x H: {bottom - top} -> A: {(right - left) * (bottom - top)}]}} @@ -124,4 +116,67 @@ _ptr + + + + &_value + {{ generation={_generation._value} }} + + + + {{ size={_load / $T2} }} + + + _capacity + _map._Mypair._Myval2 + + + + + + {{ width={_width}, height={_height} }} + + _firstRow + *(ROW*)_buffer.__ptr_.__value_ + + (_commitWatermark - _buffer.__ptr_.__value_ - 1) / _bufferRowStride + *(ROW*)(_buffer.__ptr_.__value_ + _bufferRowStride * ($i + 1)) + + + + + + {{ size={_size} }} + + _size + + _size + _data + + + + + + {{ dirtyTop={dirtyTop}, dirtyBottom={dirtyBottom} }} + + + + (empty) + {glyphIndex} + + + + {(void*)fontFace.m_ptr}, {lineRendition} + + glyphs + + + + + (empty) + {*inner._Mypair._Myval2} + + *inner._Mypair._Myval2 + + diff --git a/tools/FeatureStagingSchema.xsd b/tools/FeatureStagingSchema.xsd index 49d4f7ce3d..7f0dc9103b 100644 --- a/tools/FeatureStagingSchema.xsd +++ b/tools/FeatureStagingSchema.xsd @@ -61,7 +61,7 @@ - + diff --git a/tools/Generate-CodepointWidthsFromUCD.ps1 b/tools/Generate-CodepointWidthsFromUCD.ps1 index d1eb56ea65..beee858abc 100644 --- a/tools/Generate-CodepointWidthsFromUCD.ps1 +++ b/tools/Generate-CodepointWidthsFromUCD.ps1 @@ -20,8 +20,8 @@ # significantly smaller, which would provide a performance win on the admittedly # extremely rare occasion that we should need to regenerate our table. # -# Invoke as ./Generate-xxx ucd.nounihan.flat.xml -Pack | Out-File -Encoding -# UTF-8 Temporary.cpp +# Invoke this script from the root of this repository as: +# .\tools\Generate-CodepointWidthsFromUCD.ps1 -Path .\path\to\ucd.nounihan.flat.xml -OverridePath .\src\types\unicode_width_overrides.xml -Pack # # [1]: https://www.unicode.org/Public/UCD/latest/ucdxml/ # [2]: https://www.unicode.org/reports/tr42/ @@ -43,15 +43,13 @@ Param( [string]$OverridePath = "overrides.xml", [switch]$Pack, # Pack tightly based on width - [switch]$NoOverrides, # Do not include overrides - [switch]$Full = $False # Include Narrow codepoints + [switch]$NoOverrides # Do not include overrides ) Enum CodepointWidth { Narrow; Wide; Ambiguous; - Invalid; } # UCD Functions {{{ @@ -82,8 +80,8 @@ Function Get-UCDEntryWidth($entry) { "W" { [CodepointWidth]::Wide; Return } "F" { [CodepointWidth]::Wide; Return } "A" { [CodepointWidth]::Ambiguous; Return } + default { throw "Unexpected East_Asian_Width property" } } - [CodepointWidth]::Invalid } Function Get-UCDEntryFlags($entry) { @@ -224,20 +222,10 @@ $UCDRepertoire = $InputObject.ucd.repertoire.ChildNodes | Sort-Object { } } -If (-not $Full) { - $UCDRepertoire = $UCDRepertoire | Where-Object { - # Select everything Wide/Ambiguous/Full OR Emoji w/ Emoji Presentation - ($_.ea -notin "N", "Na", "H") -or ($_.Emoji -eq "Y" -and $_.EPres -eq "Y") - } -} - $ranges = [UnicodeRangeList]::New(1024) -$c = 0 ForEach($v in $UCDRepertoire) { $range = [UnicodeRange]::new($v) - $c += $range.Length() - If ($ranges.Count -gt 0 -and $ranges[$ranges.Count - 1].Merge($range)) { # Merged into last entry Continue @@ -260,9 +248,16 @@ If (-not $NoOverrides) { } } +$ranges.RemoveAll({ $args[0].Width -eq [CodepointWidth]::Narrow }) | Out-Null + +$c = 0 +ForEach($_ in $ranges) { + $c += $_.End - $_.Start + 1 +} + # Emit Code " // Generated by {0} -Pack:{1} -Full:{2} -NoOverrides:{3}" -f $MyInvocation.MyCommand.Name, $Pack, $Full, $NoOverrides -" // on {0} (UTC) from {1}." -f (Get-Date -AsUTC), $InputObject.ucd.description +" // on {0} from {1}." -f (Get-Date -AsUTC -Format "u"), $InputObject.ucd.description " // {0} (0x{0:X}) codepoints covered." -f $c If (-not $NoOverrides) { " // {0} (0x{0:X}) codepoints overridden." -f $overrideCount @@ -270,11 +265,12 @@ If (-not $NoOverrides) { } " static constexpr std::array s_wideAndAmbiguousTable{{" -f $ranges.Count ForEach($_ in $ranges) { + $isAmbiguous = $_.Width -eq [CodepointWidth]::Ambiguous $comment = "" if ($null -ne $_.Comment) { # We only vend comments when we aren't packing tightly $comment = " // {0}" -f $_.Comment } -" UnicodeRange{{ 0x{0:x}, 0x{1:x}, CodepointWidth::{2} }},{3}" -f $_.Start, $_.End, $_.Width, $comment +" UnicodeRange{{ 0x{0:x}, 0x{1:x}, {2} }},{3}" -f $_.Start, $_.End, [int]$isAmbiguous, $comment } " };" diff --git a/tools/Generate-FeatureStagingHeader.ps1 b/tools/Generate-FeatureStagingHeader.ps1 index 8206b7bb10..cc78685b8b 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, diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index fd0ededc11..3dd1795716 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -367,7 +367,7 @@ function Test-XamlFormat() { $xamlsForStyler = (git ls-files "$root/**/*.xaml") -join "," dotnet tool run xstyler -- -c "$root\XamlStyler.json" -f "$xamlsForStyler" --passive - if ($lastExitCode -eq 1) { + if ($LASTEXITCODE -eq 1) { throw "Xaml formatting bad, run Invoke-XamlFormat on branch" } diff --git a/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 b/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 index 3620fb9abe..7f442f2ccf 100644 --- a/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 +++ b/tools/ReleaseEngineering/New-TerminalStackedChangelog.ps1 @@ -34,12 +34,12 @@ Function Test-MicrosoftPerson($email) { Function Generate-Thanks($Entry) { # We don't need to thank ourselves for doing our jobs - If ($_.Microsoft) { + If ($Entry.Microsoft) { "" - } ElseIf (-Not [string]::IsNullOrEmpty($_.PossibleUsername)) { - " (thanks @{0}!)" -f $_.PossibleUsername + } ElseIf (-Not [string]::IsNullOrEmpty($Entry.PossibleUsername)) { + " (thanks @{0}!)" -f $Entry.PossibleUsername } Else { - " (thanks @<{0}>!)" -f $_.Email + " (thanks @<{0}>!)" -f $Entry.Email } } @@ -54,6 +54,7 @@ Function Get-PossibleUserName($email) { $Entries = @() +$i = 0 ForEach ($RevisionRange in $RevisionRanges) { # --pretty=format notes: # - %an: author name @@ -70,15 +71,23 @@ ForEach ($RevisionRange in $RevisionRanges) { Subject = $_.Subject; Microsoft = (Test-MicrosoftPerson $_.Email); PossibleUsername = (Get-PossibleUserName $_.Email); + RevRangeID = $i; } } + $i++ } -$Unique = $Entries | Group-Object Subject | %{ $_.Group[0] | Add-Member Count $_.Count -Force -PassThru } +For($c = 0; $c -Lt $i; $c++) { + " " + ("|" * $c) + "/ " + $RevisionRanges[$c] +} + +$Unique = $Entries | Group-Object Subject $Unique | % { - $c = "" - If ($_.Count -Gt 1) { - $c = "[{0}] " -f $_.Count + $en = $_.Group[0] + $revSpec = (" " * $i) + $_.Group | % { + $revSpec = $revSpec.remove($_.RevRangeID, 1).insert($_.RevRangeID, "X") } - "* {0}{1}{2}" -f ($c, $_.Subject, (Generate-Thanks $_)) + $c = "[{0}] " -f $revSpec + "* {0}{1}{2}" -f ($c, $en.Subject, (Generate-Thanks $en)) } diff --git a/tools/WindowsCheckPSVersion.ps1 b/tools/WindowsCheckPSVersion.ps1 new file mode 100644 index 0000000000..cc2b78dbeb --- /dev/null +++ b/tools/WindowsCheckPSVersion.ps1 @@ -0,0 +1,3 @@ +if(-not (Get-Command "pwsh.exe" -ErrorAction SilentlyContinue)){ + Write-Error "Incorrect version of PowerShell installed.`nMake sure you have at least PowerShell Core 7.0.0." +} \ No newline at end of file diff --git a/tools/bcz.cmd b/tools/bcz.cmd index 46640a725f..72071fa7cc 100644 --- a/tools/bcz.cmd +++ b/tools/bcz.cmd @@ -32,6 +32,10 @@ if (%1) == (rel) ( echo Manually building release set _LAST_BUILD_CONF=Release ) +if (%1) == (audit) ( + echo Manually building audit mode + set _LAST_BUILD_CONF=AuditMode +) if (%1) == (no_clean) ( set _MSBUILD_TARGET=Build ) diff --git a/tools/packages.config b/tools/packages.config index f52e4f8eab..035bd89d02 100644 --- a/tools/packages.config +++ b/tools/packages.config @@ -1,4 +1,4 @@ - + diff --git a/tools/runut.cmd b/tools/runut.cmd index 48cb13a39e..02bcc47d19 100644 --- a/tools/runut.cmd +++ b/tools/runut.cmd @@ -4,7 +4,7 @@ rem Run the console unit tests. rem Keep this file in sync with tests.xml rem The TerminalApp.LocalTests are actually run from the TestHostApp path. -rem That's a cppwinrt project, that doesn't use %PLATFORM% in it's path when the +rem That's a cppwinrt project, that doesn't use %PLATFORM% in its path when the rem platform is Win32/x86. rem set a helper for us to find that test