diff --git a/.clang-format b/.clang-format index 9e5a7d13157..26a27f32ed1 100644 --- a/.clang-format +++ b/.clang-format @@ -1,17 +1,19 @@ AccessModifierOffset: -4 AlignAfterOpenBracket: Align -#AllowAllArgumentsOnNextLine: false +AllowAllArgumentsOnNextLine: true +AlignConsecutiveMacros: false AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false -#AllowAllConstructorInitializersOnNextLine: false +AllowAllConstructorInitializersOnNextLine: true AlignEscapedNewlines: Left AlignOperands: true AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never AllowShortFunctionsOnASingleLine: Inline AllowShortCaseLabelsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: Never #AllowShortLambdasOnASingleLine: Inline AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None @@ -20,6 +22,7 @@ AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BraceWrapping: + AfterCaseLabel: true AfterClass: true AfterControlStatement: true AfterEnum: true @@ -47,6 +50,7 @@ ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: false +DeriveLineEnding: true DerivePointerAlignment: false FixNamespaceComments: false IncludeBlocks: Regroup @@ -73,7 +77,7 @@ ReflowComments: false SortIncludes: false SortUsingDeclarations: true SpaceAfterCStyleCast: false -#SpaceAfterLogicalNot: false +SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: false @@ -88,6 +92,6 @@ SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false -Standard: Cpp11 +Standard: Latest TabWidth: 4 UseTab: Never diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..2c8e01bb659 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: true + +contact_links: + - name: Microsoft Security Response Center 🔐 + url: https://msrc.microsoft.com/create-report + about: Please report security vulnerabilities here. + - name: Windows Terminal Documentation issue 📄 + url: https://github.com/MicrosoftDocs/terminal/issues/new + about: Report issues with the documentation for the Windows Terminal (in docs.microsoft.com/windows/terminal) + - name: Console Documentation issue 📄 + url: https://github.com/MicrosoftDocs/console-docs/issues/new + about: Report issues with the documentation for the Console (in docs.microsoft.com/windows/console) diff --git a/.github/actions/spell-check/dictionary/README.md b/.github/actions/spell-check/dictionary/README.md index ac142e12f51..ad8e772c260 100644 --- a/.github/actions/spell-check/dictionary/README.md +++ b/.github/actions/spell-check/dictionary/README.md @@ -18,3 +18,4 @@ it'll be accepted). | [Microsoft](microsoft.txt) | Microsoft brand items | | [Fonts](fonts.txt) | Font names | | [Names](names.txt) | Names of people | +| [Colors](colors.txt) | Names of color | diff --git a/.github/actions/spell-check/dictionary/apis.txt b/.github/actions/spell-check/dictionary/apis.txt index 298b869c186..867d3c831f2 100644 --- a/.github/actions/spell-check/dictionary/apis.txt +++ b/.github/actions/spell-check/dictionary/apis.txt @@ -4,10 +4,20 @@ alignof bitfield bitfields CLASSNOTAVAILABLE +cmdletbinding +COLORPROPERTY +CXICON +CYICON +environstrings EXPCMDFLAGS EXPCMDSTATE fullkbd futex +GETDESKWALLPAPER +GETHIGHCONTRAST +Hashtable +HIGHCONTRASTON +HIGHCONTRASTW href IAsync IBind @@ -18,28 +28,52 @@ ICustom IDialog IDirect IExplorer +IInheritable IMap IObject IStorage +ITaskbar llabs LCID +llabs +localtime +lround LSHIFT +msappx NCHITTEST NCLBUTTONDBLCLK NCRBUTTONDBLCLK NOAGGREGATION +NOPROGRESS NOREDIRECTIONBITMAP oaidl ocidl +otms +OUTLINETEXTMETRICW +overridable PAGESCROLL RETURNCMD rfind roundf RSHIFT rx +schandle +semver +serializer SIZENS spsc +sregex STDCPP +strchr syscall +TBPF +THEMECHANGED tmp tx +UPDATEINIFILE +userenv +wcstoui +wsregex +XDocument +XElement +XParse diff --git a/.github/actions/spell-check/dictionary/colors.txt b/.github/actions/spell-check/dictionary/colors.txt new file mode 100644 index 00000000000..389e1754cd0 --- /dev/null +++ b/.github/actions/spell-check/dictionary/colors.txt @@ -0,0 +1,782 @@ +snow +ghost-white +ghostwhite +white-smoke +whitesmoke +gainsboro +floral-white +floralwhite +old-lace +oldlace +linen +antique-white +antiquewhite +papaya-whip +papayawhip +blanched-almond +blanchedalmond +bisque +peach-puff +peachpuff +navajo-white +navajowhite +moccasin +cornsilk +ivory +lemon-chiffon +lemonchiffon +seashell +honeydew +mint-cream +mintcream +azure +alice-blue +aliceblue +lavender +lavender-blush +lavenderblush +misty-rose +mistyrose +white +black +dark-slate-gray +darkslategray +dark-slate-grey +darkslategrey +dim-gray +dimgray +dim-grey +dimgrey +slate-gray +slategray +slate-grey +slategrey +light-slate-gray +lightslategray +light-slate-grey +lightslategrey +gray +grey +xray +x11gray +xrey +x11grey +web-gray +webgray +web-grey +webgrey +light-grey +lightgrey +light-gray +lightgray +midnight-blue +midnightblue +navy +navy-blue +navyblue +cornflower-blue +cornflowerblue +dark-slate-blue +darkslateblue +slate-blue +slateblue +medium-slate-blue +mediumslateblue +light-slate-blue +lightslateblue +medium-blue +mediumblue +royal-blue +royalblue +blue +dodger-blue +dodgerblue +deep-sky-blue +deepskyblue +sky-blue +skyblue +light-sky-blue +lightskyblue +steel-blue +steelblue +light-steel-blue +lightsteelblue +light-blue +lightblue +powder-blue +powderblue +pale-turquoise +paleturquoise +dark-turquoise +darkturquoise +medium-turquoise +mediumturquoise +turquoise +cyan +aqua +light-cyan +lightcyan +cadet-blue +cadetblue +medium-aquamarine +mediumaquamarine +aquamarine +dark-green +darkgreen +dark-olive-green +darkolivegreen +dark-sea-green +darkseagreen +sea-green +seagreen +medium-sea-green +mediumseagreen +light-sea-green +lightseagreen +pale-green +palegreen +spring-green +springgreen +lawn-green +lawngreen +green +lime +xreen +x11green +web-green +webgreen +chartreuse +medium-spring-green +mediumspringgreen +green-yellow +greenyellow +lime-green +limegreen +yellow-green +yellowgreen +forest-green +forestgreen +olive-drab +olivedrab +dark-khaki +darkkhaki +khaki +pale-goldenrod +palegoldenrod +light-goldenrod-yellow +lightgoldenrodyellow +light-yellow +lightyellow +yellow +gold +light-goldenrod +lightgoldenrod +goldenrod +dark-goldenrod +darkgoldenrod +rosy-brown +rosybrown +indian-red +indianred +saddle-brown +saddlebrown +sienna +peru +burlywood +beige +wheat +sandy-brown +sandybrown +tan +chocolate +firebrick +brown +dark-salmon +darksalmon +salmon +light-salmon +lightsalmon +orange +dark-orange +darkorange +coral +light-coral +lightcoral +tomato +orange-red +orangered +red +hot-pink +hotpink +deep-pink +deeppink +pink +light-pink +lightpink +pale-violet-red +palevioletred +maroon +xaroon +x11maroon +web-maroon +webmaroon +medium-violet-red +mediumvioletred +violet-red +violetred +magenta +fuchsia +violet +plum +orchid +medium-orchid +mediumorchid +dark-orchid +darkorchid +dark-violet +darkviolet +blue-violet +blueviolet +purple +xurple +x11purple +web-purple +webpurple +medium-purple +mediumpurple +thistle +snow1 +snow2 +snow3 +snow4 +seashell1 +seashell2 +seashell3 +seashell4 +antiquewhite1 +antiquewhite2 +antiquewhite3 +antiquewhite4 +bisque1 +bisque2 +bisque3 +bisque4 +peachpuff1 +peachpuff2 +peachpuff3 +peachpuff4 +navajowhite1 +navajowhite2 +navajowhite3 +navajowhite4 +lemonchiffon1 +lemonchiffon2 +lemonchiffon3 +lemonchiffon4 +cornsilk1 +cornsilk2 +cornsilk3 +cornsilk4 +ivory1 +ivory2 +ivory3 +ivory4 +honeydew1 +honeydew2 +honeydew3 +honeydew4 +lavenderblush1 +lavenderblush2 +lavenderblush3 +lavenderblush4 +mistyrose1 +mistyrose2 +mistyrose3 +mistyrose4 +azure1 +azure2 +azure3 +azure4 +slateblue1 +slateblue2 +slateblue3 +slateblue4 +royalblue1 +royalblue2 +royalblue3 +royalblue4 +blue1 +blue2 +blue3 +blue4 +dodgerblue1 +dodgerblue2 +dodgerblue3 +dodgerblue4 +steelblue1 +steelblue2 +steelblue3 +steelblue4 +deepskyblue1 +deepskyblue2 +deepskyblue3 +deepskyblue4 +skyblue1 +skyblue2 +skyblue3 +skyblue4 +lightskyblue1 +lightskyblue2 +lightskyblue3 +lightskyblue4 +slategray1 +slategray2 +slategray3 +slategray4 +lightsteelblue1 +lightsteelblue2 +lightsteelblue3 +lightsteelblue4 +lightblue1 +lightblue2 +lightblue3 +lightblue4 +lightcyan1 +lightcyan2 +lightcyan3 +lightcyan4 +paleturquoise1 +paleturquoise2 +paleturquoise3 +paleturquoise4 +cadetblue1 +cadetblue2 +cadetblue3 +cadetblue4 +turquoise1 +turquoise2 +turquoise3 +turquoise4 +cyan1 +cyan2 +cyan3 +cyan4 +darkslategray1 +darkslategray2 +darkslategray3 +darkslategray4 +aquamarine1 +aquamarine2 +aquamarine3 +aquamarine4 +darkseagreen1 +darkseagreen2 +darkseagreen3 +darkseagreen4 +seagreen1 +seagreen2 +seagreen3 +seagreen4 +palegreen1 +palegreen2 +palegreen3 +palegreen4 +springgreen1 +springgreen2 +springgreen3 +springgreen4 +green1 +green2 +green3 +green4 +chartreuse1 +chartreuse2 +chartreuse3 +chartreuse4 +olivedrab1 +olivedrab2 +olivedrab3 +olivedrab4 +darkolivegreen1 +darkolivegreen2 +darkolivegreen3 +darkolivegreen4 +khaki1 +khaki2 +khaki3 +khaki4 +lightgoldenrod1 +lightgoldenrod2 +lightgoldenrod3 +lightgoldenrod4 +lightyellow1 +lightyellow2 +lightyellow3 +lightyellow4 +yellow1 +yellow2 +yellow3 +yellow4 +gold1 +gold2 +gold3 +gold4 +goldenrod1 +goldenrod2 +goldenrod3 +goldenrod4 +darkgoldenrod1 +darkgoldenrod2 +darkgoldenrod3 +darkgoldenrod4 +rosybrown1 +rosybrown2 +rosybrown3 +rosybrown4 +indianred1 +indianred2 +indianred3 +indianred4 +sienna1 +sienna2 +sienna3 +sienna4 +burlywood1 +burlywood2 +burlywood3 +burlywood4 +wheat1 +wheat2 +wheat3 +wheat4 +tan1 +tan2 +tan3 +tan4 +chocolate1 +chocolate2 +chocolate3 +chocolate4 +firebrick1 +firebrick2 +firebrick3 +firebrick4 +brown1 +brown2 +brown3 +brown4 +salmon1 +salmon2 +salmon3 +salmon4 +lightsalmon1 +lightsalmon2 +lightsalmon3 +lightsalmon4 +orange1 +orange2 +orange3 +orange4 +darkorange1 +darkorange2 +darkorange3 +darkorange4 +coral1 +coral2 +coral3 +coral4 +tomato1 +tomato2 +tomato3 +tomato4 +orangered1 +orangered2 +orangered3 +orangered4 +red1 +red2 +red3 +red4 +deeppink1 +deeppink2 +deeppink3 +deeppink4 +hotpink1 +hotpink2 +hotpink3 +hotpink4 +pink1 +pink2 +pink3 +pink4 +lightpink1 +lightpink2 +lightpink3 +lightpink4 +palevioletred1 +palevioletred2 +palevioletred3 +palevioletred4 +maroon1 +maroon2 +maroon3 +maroon4 +violetred1 +violetred2 +violetred3 +violetred4 +magenta1 +magenta2 +magenta3 +magenta4 +orchid1 +orchid2 +orchid3 +orchid4 +plum1 +plum2 +plum3 +plum4 +mediumorchid1 +mediumorchid2 +mediumorchid3 +mediumorchid4 +darkorchid1 +darkorchid2 +darkorchid3 +darkorchid4 +purple1 +purple2 +purple3 +purple4 +mediumpurple1 +mediumpurple2 +mediumpurple3 +mediumpurple4 +thistle1 +thistle2 +thistle3 +thistle4 +gray0 +grey0 +gray1 +grey1 +gray2 +grey2 +gray3 +grey3 +gray4 +grey4 +gray5 +grey5 +gray6 +grey6 +gray7 +grey7 +gray8 +grey8 +gray9 +grey9 +gray10 +grey10 +gray11 +grey11 +gray12 +grey12 +gray13 +grey13 +gray14 +grey14 +gray15 +grey15 +gray16 +grey16 +gray17 +grey17 +gray18 +grey18 +gray19 +grey19 +gray20 +grey20 +gray21 +grey21 +gray22 +grey22 +gray23 +grey23 +gray24 +grey24 +gray25 +grey25 +gray26 +grey26 +gray27 +grey27 +gray28 +grey28 +gray29 +grey29 +gray30 +grey30 +gray31 +grey31 +gray32 +grey32 +gray33 +grey33 +gray34 +grey34 +gray35 +grey35 +gray36 +grey36 +gray37 +grey37 +gray38 +grey38 +gray39 +grey39 +gray40 +grey40 +gray41 +grey41 +gray42 +grey42 +gray43 +grey43 +gray44 +grey44 +gray45 +grey45 +gray46 +grey46 +gray47 +grey47 +gray48 +grey48 +gray49 +grey49 +gray50 +grey50 +gray51 +grey51 +gray52 +grey52 +gray53 +grey53 +gray54 +grey54 +gray55 +grey55 +gray56 +grey56 +gray57 +grey57 +gray58 +grey58 +gray59 +grey59 +gray60 +grey60 +gray61 +grey61 +gray62 +grey62 +gray63 +grey63 +gray64 +grey64 +gray65 +grey65 +gray66 +grey66 +gray67 +grey67 +gray68 +grey68 +gray69 +grey69 +gray70 +grey70 +gray71 +grey71 +gray72 +grey72 +gray73 +grey73 +gray74 +grey74 +gray75 +grey75 +gray76 +grey76 +gray77 +grey77 +gray78 +grey78 +gray79 +grey79 +gray80 +grey80 +gray81 +grey81 +gray82 +grey82 +gray83 +grey83 +gray84 +grey84 +gray85 +grey85 +gray86 +grey86 +gray87 +grey87 +gray88 +grey88 +gray89 +grey89 +gray90 +grey90 +gray91 +grey91 +gray92 +grey92 +gray93 +grey93 +gray94 +grey94 +gray95 +grey95 +gray96 +grey96 +gray97 +grey97 +gray98 +grey98 +gray99 +grey99 +gray100 +grey100 +dark-grey +darkgrey +dark-gray +darkgray +dark-blue +darkblue +dark-cyan +darkcyan +dark-magenta +darkmagenta +dark-red +darkred +light-green +lightgreen +crimson +indigo +olive +rebecca-purple +rebeccapurple +silver +teal diff --git a/.github/actions/spell-check/dictionary/dictionary.txt b/.github/actions/spell-check/dictionary/dictionary.txt index c01a75b1927..beddeeec9c0 100644 --- a/.github/actions/spell-check/dictionary/dictionary.txt +++ b/.github/actions/spell-check/dictionary/dictionary.txt @@ -20937,6 +20937,8 @@ apay Apayao APB APC +Apc +apc APDA APDU APE @@ -99401,6 +99403,8 @@ DCP DCPR DCPSK DCS +Dcs +dcs DCT DCTN DCTS @@ -152429,6 +152433,7 @@ ft-lb ftncmd ftnerr FTP +ftp ft-pdl FTPI FTS @@ -157074,6 +157079,7 @@ geek geekier geekiest geeks +geeksforgeeks geeky geelbec geelbeck @@ -186986,6 +186992,7 @@ hyperleucocytotic hyperleukocytosis hyperlexis hyperlink +hyperlinks hyperlinking hyperlipaemia hyperlipaemic @@ -313150,6 +313157,7 @@ post-Mishnical postmistress postmistresses postmistress-ship +postmodern postmortal post-mortem postmortem @@ -345227,6 +345235,7 @@ resequester resequestration reserate reserene +reserialization reserpine reserpinized reservable @@ -383886,6 +383895,7 @@ sorus sorva sory SOS +Sos sos Sosanna so-seeming @@ -418862,6 +418872,7 @@ time-shrouded time-space time-spirit timestamp +timestamped timestamps timet time-table diff --git a/.github/actions/spell-check/dictionary/microsoft.txt b/.github/actions/spell-check/dictionary/microsoft.txt index ef2c23c5ab6..e803c06f62e 100644 --- a/.github/actions/spell-check/dictionary/microsoft.txt +++ b/.github/actions/spell-check/dictionary/microsoft.txt @@ -1,18 +1,40 @@ ACLs altform +appendwttlogging backplating +CPRs DACL DACLs +dotnetfeed +DWINRT +enablewttlogging LKG mfcribbon microsoft microsoftonline +netcore osgvsowi +pgc +pgo +pgosweep powerrename powershell +propkey pscustomobject +robocopy SACLs +Shobjidl +Skype +sysnative +systemroot +taskkill +tasklist tdbuildteamid vcruntime visualstudio +VSTHRD +wlk wslpath +wtl +wtt +wttlog diff --git a/.github/actions/spell-check/dictionary/names.txt b/.github/actions/spell-check/dictionary/names.txt index 6d47c4e98e9..dd4eaae1812 100644 --- a/.github/actions/spell-check/dictionary/names.txt +++ b/.github/actions/spell-check/dictionary/names.txt @@ -8,6 +8,7 @@ dhowett Diviness dsafa duhowett +ekg ethanschoonover Firefox Gatta @@ -25,16 +26,21 @@ Kourosh kowalczyk leonmsft Lepilleur +lukesampson Manandhar mbadolato Mehrain mgravell michaelniksa +michkap migrie mikegr mikemaccana +miloush miniksa niksa +nvaccess +nvda oising oldnewthing osgwiki diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index 97eb58e3ff7..8639b6b91f0 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -45,6 +45,7 @@ SUMS$ \.tar$ \.tgz$ \.ttf$ +\.vsdx$ \.woff \.xcf$ \.xls @@ -61,3 +62,4 @@ SUMS$ ^src/tools/U8U16Test/(?:fr|ru|zh)\.txt$ ^\.github/actions/spell-check/ ^\.gitignore$ +^doc/reference/master-sequence-list.csv$ diff --git a/.github/actions/spell-check/expect/5757ec679b03a4240130c3c53766c91bbc5cd6a7.txt b/.github/actions/spell-check/expect/5757ec679b03a4240130c3c53766c91bbc5cd6a7.txt new file mode 100644 index 00000000000..696b9b1e499 --- /dev/null +++ b/.github/actions/spell-check/expect/5757ec679b03a4240130c3c53766c91bbc5cd6a7.txt @@ -0,0 +1 @@ +renamer diff --git a/.github/actions/spell-check/expect/655f007265b351e140d20b3976792523ad689241.txt b/.github/actions/spell-check/expect/655f007265b351e140d20b3976792523ad689241.txt new file mode 100644 index 00000000000..39f6c008254 --- /dev/null +++ b/.github/actions/spell-check/expect/655f007265b351e140d20b3976792523ad689241.txt @@ -0,0 +1,7 @@ +autogenerated +CPPCORECHECK +Debian +filepath +inplace +KEYBDINPUT +WINVER diff --git a/.github/actions/spell-check/expect/alphabet.txt b/.github/actions/spell-check/expect/alphabet.txt index 6d395ae7a5d..2f3072f0177 100644 --- a/.github/actions/spell-check/expect/alphabet.txt +++ b/.github/actions/spell-check/expect/alphabet.txt @@ -1,3 +1,4 @@ +abcd abcde abcdef ABCDEFG @@ -10,6 +11,8 @@ abcdefghijklmnopqrstuvwxyz ABE BBGGRR BBBBBBBBBBBBBBDDDD +EFG +EFGh QQQQQQQQQQABCDEFGHIJ QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ diff --git a/.github/actions/spell-check/expect/expect.txt b/.github/actions/spell-check/expect/expect.txt index 01b64c42d78..7f7b99e9401 100644 --- a/.github/actions/spell-check/expect/expect.txt +++ b/.github/actions/spell-check/expect/expect.txt @@ -6,7 +6,9 @@ ABANDONFONT ABCDEFGHIJKLMNO ABCG abf +abgr abi +ACCESSTOKEN acec acf acidev @@ -23,6 +25,7 @@ addressof ADDSTRING ADDTOOL AEnd +aef AFew AFill AFX @@ -30,7 +33,6 @@ AHelper ahz AImpl AInplace -akb ALIGNRIGHT alloc allocing @@ -59,7 +61,6 @@ apiset apos APPBARDATA appconsult -appdata APPICON appium applet @@ -71,6 +72,7 @@ apps APPWINDOW appx appxbundle +appxerror appxmanifest APrep apsect @@ -99,17 +101,14 @@ AStomps ASYNCWINDOWPOS atch ATest -atg attr ATTRCOLOR aumid Authenticode AUTOBUDDY AUTOCHECKBOX -Autogenerated autohide AUTOHSCROLL -autologin automagically autopositioning AUTORADIOBUTTON @@ -160,7 +159,6 @@ bitsavers bitset BKCOLOR BKGND -BKMK Bksp blog Blt @@ -190,6 +188,7 @@ buffersize buflen bugfix buildtransitive +BUILDURI burriter BValue byref @@ -295,7 +294,7 @@ codepage codepoint codeproject COINIT -colo +COLLECTIONURI colorizing colororacle colorref @@ -352,10 +351,11 @@ conpty conptylib consecteturadipiscingelit conserv -consoleaccessibility consoleapi CONSOLECONTROL CONSOLEENDTASK +consolegit +consolehost CONSOLEIME consoleinternal Consoleroot @@ -390,7 +390,6 @@ CPINFOEX cplinfo cplusplus cpp -cppcorecheck cppcorecheckrules cpprest cpprestsdk @@ -426,6 +425,7 @@ cstdlib cstr cstring cstyle +CSV CSwitch CText ctime @@ -506,7 +506,6 @@ DDESHARE DDevice DEADCHAR dealloc -debian debolden debounce DECALN @@ -516,11 +515,9 @@ DECAWM DECCKM DECCOLM DECEKBD -decf DECKPAM DECKPM DECKPNM -DECLL DECLRMM decls declspec @@ -532,6 +529,7 @@ DECOM deconstructed DECPCTERM DECRC +DECREQTPARM DECRLM DECRQM DECRST @@ -544,8 +542,8 @@ DECSCUSR DECSED DECSEL DECSET +DECSLPP DECSLRM -DECSMBV DECSMKR DECSR decstandar @@ -594,8 +592,10 @@ devops Dext df DFactory +DFF DFMT dh +dhandler dialogbox diffing DINLINE @@ -618,6 +618,8 @@ dllmain DLLVERSIONINFO DLOAD DLOOK +dmp +dnceng DOCTYPE docx DONTCARE @@ -635,12 +637,13 @@ dpiy DRAWFRAME DRAWITEM DRAWITEMSTRUCT +drcs dropdown DROPDOWNLIST DROPFILES drv dsm -Dst +dst DSwap DTest dtor @@ -683,7 +686,7 @@ Elems elif elseif emacs -emptybox +EMPTYBOX enabledelayedexpansion endian endif @@ -705,8 +708,8 @@ EPres ERASEBKGND errno errorlevel -esa ETB +etcoreapp ETW ETX EUDC @@ -752,7 +755,6 @@ fdw fea fesb FFDE -FFF FFrom FGCOLOR fgetc @@ -760,7 +762,6 @@ fgetwc fgidx FILEDESCRIPTION fileno -FILEPATH FILESUBTYPE FILESYSPATH filesystem @@ -769,7 +770,6 @@ FILETYPE FILEW FILLATTR FILLCONSOLEOUTPUT -Filledbox FILTERONPASTE finalizer FINDCASE @@ -821,7 +821,6 @@ fsproj fstream fte Ftm -fullcolor fullscreen fullwidth func @@ -850,6 +849,7 @@ GETAUTOHIDEBAREX GETCARETWIDTH getch getchar +GETCLIENTAREAANIMATION GETCOMMANDHISTORY GETCOMMANDHISTORYLENGTH GETCONSOLEINPUT @@ -892,9 +892,11 @@ GETWHEELSCROLLCHARACTERS GETWHEELSCROLLCHARS GETWHEELSCROLLLINES getwriter +GFEh Gfun gfx gh +gitfilters github gitlab gle @@ -920,7 +922,6 @@ GTP guc gui guidatom -guidgenerator GValue GWL GWLP @@ -951,6 +952,8 @@ hfile hfont hglobal hh +hhh +hhhh hhook hhx HIBYTE @@ -988,6 +991,7 @@ hpj hpp HPR HPROPSHEETPAGE +HProvider HREDRAW hresult HRSRC @@ -1011,7 +1015,6 @@ hwheel hwnd HWNDPARENT hxx -hyperlink IAccessibility IAction IApi @@ -1034,7 +1037,6 @@ ICore IData IDCANCEL IDD -IDefault IDesktop IDictionary IDISHWND @@ -1067,6 +1069,8 @@ IInteract IInteractivity IIo IList +imagemagick +Imatch ime Imm IMouse @@ -1083,7 +1087,6 @@ INITCOMMONCONTROLSEX INITDIALOG initguid INITMENU -imagemagick inkscape inl INLINEPREFIX @@ -1091,7 +1094,6 @@ Inlines INotify inout INPATHROOT -Inplace inproc Inputkeyinfo INPUTPROCESSORPROFILE @@ -1157,6 +1159,7 @@ iwch IWin IWindow IXaml +IXMP jconcpp JOBOBJECT JOBOBJECTINFOCLASS @@ -1179,11 +1182,9 @@ kcud kcuf kcuu Kd -keith kernelbase kernelbasestaging keybinding -keybound keychord keydown keyevent @@ -1239,6 +1240,7 @@ linputfile Linq linux listbox +listproperties listptr listptrsize lk @@ -1259,6 +1261,7 @@ locstudio Loewen LOGFONT LOGFONTW +logissue Loremipsumdolorsitamet lowercased loword @@ -1383,8 +1386,8 @@ mimetype mincore mindbogglingly mingw +minimizeall minkernel -minmax minwin minwindef Mip @@ -1454,9 +1457,9 @@ namestream Namquiseratal nano natvis -naws nbsp Nc +NCACTIVATE NCCALCSIZE NCCREATE NCLBUTTONDOWN @@ -1532,11 +1535,10 @@ nothrow NOTICKS NOTIMPL notin -NOTOPMOST NOTNULL +NOTOPMOST NOTRACK NOTSUPPORTED -notypeopt nouicompat nounihan NOUPDATE @@ -1545,7 +1547,7 @@ NOYIELD NOZORDER NPM npos -NRCS +nrcs NSTATUS ntapi ntcon @@ -1611,7 +1613,6 @@ opencon openconsole OPENIF OPENLINK -openlogo openps opensource openvt @@ -1623,8 +1624,8 @@ OSCBG OSCCT OSCFG OSCRCC -OSCSCC OSCSCB +OSCSCC OSCWT OSDEPENDSROOT osfhandle @@ -1722,7 +1723,6 @@ pguid pgup PHANDLE phhook -phsl phwnd pid pidl @@ -1752,7 +1752,6 @@ POBJECT Podcast POINTSLIST POLYTEXTW -popclip popd POPF poppack @@ -1790,9 +1789,11 @@ prect prefast prefilled prefs +preinstalled PRELOAD PREMULTIPLIED prepopulated +presorted PREVENTPINNING PREVIEWLABEL PREVIEWWINDOW @@ -1935,7 +1936,7 @@ REGSTR reingest Relayout RELBINPATH -remoting +Remoting renderengine rendersize reparent @@ -1943,7 +1944,6 @@ reparenting replatformed Replymessage repositorypath -rerendered rescap Resequence Reserialize @@ -1978,7 +1978,6 @@ rgw rgwch rhs ri -richturn RIGHTALIGN RIGHTBUTTON riid @@ -1989,10 +1988,10 @@ RMENU roadmap robomac roundtrip -ROWSTOSCROLL rparen RRF RRRGGGBB +rsas rtcore RTEXT rtf @@ -2009,6 +2008,7 @@ runformat runft RUNFULLSCREEN runsettings +runtests runtimeclass runuia runut @@ -2051,7 +2051,6 @@ SCROLLSCALE SCROLLSCREENBUFFER Scrollup Scrolluppage -SCS scursor sddl sdeleted @@ -2121,7 +2120,6 @@ Shl shlguid shlobj shlwapi -shobjidl SHORTPATH SHOWCURSOR SHOWMAXIMIZED @@ -2158,6 +2156,7 @@ SND SOLIDBOX Solutiondir somefile +SOURCEBRANCH sourced SOURCESDIRECTORY SPACEBAR @@ -2194,14 +2193,12 @@ STARTWPARMSW Statusline stdafx STDAPI -stdarg stdcall -stddef +stdcpp stderr stdexcept stdin stdio -stdlib STDMETHODCALLTYPE STDMETHODIMP stdout @@ -2228,17 +2225,16 @@ subkey SUBLANG sublicensable submenu -subnegotiation subresource subspan substr subsystemconsole subsystemwindows +suiteless svg swapchain swapchainpanel swappable -Switchto SWMR SWP swprintf @@ -2284,7 +2280,9 @@ tcome tcommandline tcommands tcon +TDelegated TDP +TEAMPROJECT tearoff Teb techcommunity @@ -2292,14 +2290,13 @@ technet tellp telnet telnetd -telnetpp templated terminalcore -terminalnuget TERMINALSCROLLING terminfo TEs testapp +testbuildplatform testcon testd testdlls @@ -2308,11 +2305,15 @@ testlab testlist testmd testmddefinition +testmode +testname +testnameprefix TESTNULL testpass testpasses testtestabc testtesttesttesttest +testtimeout TEXCOORD texel TExpected @@ -2351,7 +2352,6 @@ TMult tmultiple tmux todo -Tofill tofrom tokenhelpers tokenized @@ -2362,6 +2362,7 @@ Toolset tooltip TOPDOWNDIB TOPLEFT +toplevel TOPRIGHT TOpt tosign @@ -2423,6 +2424,7 @@ ucdxml uch UCHAR ucs +UDK UDKs UDM uer @@ -2447,7 +2449,6 @@ undef Unescape unexpand Unfocus -unfocuses unhighlighting unhosted unicode @@ -2470,6 +2471,7 @@ unpause Unregister Unregistering unte +untests untextured untimes UPDATEDISPLAY @@ -2567,6 +2569,7 @@ vstudio vswhere vtapp VTE +VTID vtio vtmode vtpipeterm @@ -2684,12 +2687,10 @@ wintelnet winternl winuser winuserp -winver wistd wixproj wline wlinestream -Wlk wmain WMSZ wnd @@ -2700,9 +2701,11 @@ WNDCLASSW Wndproc WNegative WNull +wnwb workarea workaround workflow +workitem wostream WOutside WOWARM @@ -2730,7 +2733,6 @@ WRunoff WScript wsl WSLENV -wslhome wsmatch WSpace wss @@ -2746,6 +2748,7 @@ wtof wtoi wtw wtypes +Wubi WUX WVerify wwaproj @@ -2753,33 +2756,29 @@ WWith wx wxh xa -xab xact xamarin xaml Xamlmeta xargs xaz -xb -xbc xbf xbutton XBUTTONDBLCLK XBUTTONDOWN XBUTTONUP -xca XCast -xce XCENTER XColors xcopy XCount -xdd xdy xe XEncoding +xes +Xes +XES xff -xffff XFile xlang XManifest @@ -2788,6 +2787,8 @@ XMFLOAT xml xmlns xor +xorg +XOrg Xpath XPosition XResource @@ -2798,6 +2799,7 @@ XSubstantial xtended xterm XTest +xunit xutr xvalue XVIRTUALSCREEN @@ -2828,3 +2830,9 @@ zsh zu zxcvbnm zy +AAAAABBBBBBCCC +AAAAA +BBBBBCCC +abcd +LPMINMAXINFO +MINMAXINFO diff --git a/.github/actions/spell-check/patterns/patterns.txt b/.github/actions/spell-check/patterns/patterns.txt index f8c3d65534a..394ae60f9af 100644 --- a/.github/actions/spell-check/patterns/patterns.txt +++ b/.github/actions/spell-check/patterns/patterns.txt @@ -16,7 +16,8 @@ Scro\&ll TestUtils::VerifyExpectedString\(tb, L"[^"]+" (?:hostSm|mach)\.ProcessString\(L"[^"]+" \b([A-Za-z])\1{3,}\b +0x[0-9A-Za-z]+ Base64::s_(?:En|De)code\(L"[^"]+" VERIFY_ARE_EQUAL\(L"[^"]+" -L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\+/" +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\+/" std::memory_order_[\w]+ diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml new file mode 100644 index 00000000000..9128b389dd6 --- /dev/null +++ b/.github/linters/.markdown-lint.yml @@ -0,0 +1,42 @@ +--- +########################### +########################### +## Markdown Linter rules ## +########################### +########################### + +# Linter rules doc: +# - https://github.com/DavidAnson/markdownlint +# +# Note: +# To comment out a single error: +# +# any violations you want +# +# +# To run the linter locally: +# 1. install the npm package: +# `npm install -g markdownlint-cli` +# 2. Then run it in the root of the repo with +# `markdownlint -c .github\linters\.markdown-lint.yml ./*.md` + +############### +# Rules by id # +############### +MD004: false # Unordered list style +MD007: + indent: 2 # Unordered list indentation +MD013: + line_length: 400 # Line length 80 is far to short +MD024: false # Allow multiple headings with same content +MD026: + punctuation: ".,;:!。,;:" # List of not allowed +MD029: false # Ordered list item prefix +MD033: false # Allow inline HTML +MD036: false # Emphasis used instead of a heading +MD040: false # Allow ``` blocks in md files with no language specified + +################# +# Rules by tags # +################# +blank_lines: false # Error on blank lines diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70099f116ec..84133983ed3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ The point of doing all this work in public is to ensure that we are holding ours The team triages new issues several times a week. During triage, the team uses labels to categorize, manage, and drive the project workflow. -We employ [a bot engine](https://github.com/microsoft/terminal/blob/master/doc/bot.md) to help us automate common processes within our workflow. +We employ [a bot engine](https://github.com/microsoft/terminal/blob/main/doc/bot.md) to help us automate common processes within our workflow. We drive the bot by tagging issues with specific labels which cause the bot engine to close issues, merge branches, etc. This bot engine helps us keep the repo clean by automating the process of notifying appropriate parties if/when information/follow-up is needed, and closing stale issues/PRs after reminders have remained unanswered for several days. @@ -140,6 +140,13 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and you 1. Create & push a feature branch 1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/) 1. Work on your changes +1. Build and see if it works. Consult [How to build OpenConsole](./doc/building.md) if you have problems. + +### Testing + +Testing is a key component in the development workflow. Both Windows Terminal and Windows Console use TAEF(the Test Authoring and Execution Framework) as the main framework for testing. + +If your changes affect existing test cases, or you're working on brand new features and also the accompanying test cases, see [TAEF](./doc/TAEF.md) for more information about how to validate your work locally. ### Code Review @@ -149,7 +156,7 @@ When you'd like the team to take a look, (even if the work is not yet fully-comp ### Merge -Once your code has been reviewed and approved by the requisite number of team members, it will be merged into the master branch. Once merged, your PR will be automatically closed. +Once your code has been reviewed and approved by the requisite number of team members, it will be merged into the main branch. Once merged, your PR will be automatically closed. --- diff --git a/NOTICE.md b/NOTICE.md index f5b54b42688..d8fd3e9b052 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -2,7 +2,7 @@ Do Not Translate or Localize This software incorporates material from third parties. Microsoft makes certain -open source code available at http://3rdpartysource.microsoft.com, or you may +open source code available at [http://3rdpartysource.microsoft.com](http://3rdpartysource.microsoft.com), or you may send a check or money order for US $5.00, including the product name, the open source component name, and version number, to: @@ -20,7 +20,7 @@ General Public License. ## jsoncpp -**Source**: https://github.com/open-source-parsers/jsoncpp +**Source**: [https://github.com/open-source-parsers/jsoncpp](https://github.com/open-source-parsers/jsoncpp) ### License @@ -48,39 +48,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## telnetpp - -**Source**: https://github.com/KazDragon/telnetpp - -### License - -``` -The MIT License (MIT) - -Copyright (c) 2015-2017 Matthew Chaplain a.k.a KazDragon - -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. -``` - ## chromium/base/numerics -**Source**: https://github.com/chromium/chromium/tree/master/base/numerics +**Source**: [https://github.com/chromium/chromium/tree/master/base/numerics](https://github.com/chromium/chromium/tree/master/base/numerics) ### License @@ -116,7 +86,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## kimwalisch/libpopcnt -**Source**: https://github.com/kimwalisch/libpopcnt +**Source**: [https://github.com/kimwalisch/libpopcnt](https://github.com/kimwalisch/libpopcnt) ### License @@ -152,7 +122,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## dynamic_bitset -**Source**: https://github.com/pinam45/dynamic_bitset +**Source**: [https://github.com/pinam45/dynamic_bitset](https://github.com/pinam45/dynamic_bitset) ### License @@ -181,9 +151,9 @@ SOFTWARE. ``` -## {fmt} +## \{fmt\} -**Source**: https://github.com/fmtlib/fmt +**Source**: [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt) ### License @@ -218,3 +188,32 @@ of this Software are embedded into a machine-executable object form of such source code, you may redistribute such embedded portions in such object form without including the above copyright and permission notices. ``` + +## interval_tree + +**Source**: [https://github.com/ekg/IntervalTree](https://github.com/ekg/IntervalTree) + +### License + +``` +Copyright (c) 2011 Erik Garrison + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` diff --git a/NuGet.Config b/NuGet.Config index 00b1de60c4f..e5cb393dc59 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -9,6 +9,11 @@ + + + + + + + + \ No newline at end of file diff --git a/build/Helix/UpdateUnreliableTests.ps1 b/build/Helix/UpdateUnreliableTests.ps1 new file mode 100644 index 00000000000..06af44f6950 --- /dev/null +++ b/build/Helix/UpdateUnreliableTests.ps1 @@ -0,0 +1,135 @@ +[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-RestMethod -Uri $queryUri -Method Get -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 -Uri "$($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-RestMethod -Uri $testRunResultsUri -Method Get -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. + $rerunResults = ConvertFrom-Json (New-Object System.Net.WebClient).DownloadString($testResult.errorMessage) + [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 new file mode 100644 index 00000000000..cec97c1d235 --- /dev/null +++ b/build/Helix/global.json @@ -0,0 +1,5 @@ +{ + "msbuild-sdks": { + "Microsoft.DotNet.Helix.Sdk": "5.0.0-beta.20277.5" + } +} diff --git a/build/Helix/packages.config b/build/Helix/packages.config new file mode 100644 index 00000000000..9d5e0fdc3a9 --- /dev/null +++ b/build/Helix/packages.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Helix/readme.md b/build/Helix/readme.md new file mode 100644 index 00000000000..d51fc973c1e --- /dev/null +++ b/build/Helix/readme.md @@ -0,0 +1,32 @@ +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 new file mode 100644 index 00000000000..ddf8c2d3d11 --- /dev/null +++ b/build/Helix/runtests.cmd @@ -0,0 +1,106 @@ +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 Conhost.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% +for /f "tokens=* delims=" %%a in ('dir /b *.pgc') do ren "%%a" "%testnameprefix%.%%~na.pgc" +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% \ No newline at end of file diff --git a/build/packages.config b/build/packages.config new file mode 100644 index 00000000000..11c8417e608 --- /dev/null +++ b/build/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/build/pipelines/ci.yml b/build/pipelines/ci.yml index 365e0f3abb1..5234f12b279 100644 --- a/build/pipelines/ci.yml +++ b/build/pipelines/ci.yml @@ -2,7 +2,8 @@ trigger: batch: true branches: include: - - master + - main + - feature/* paths: exclude: - doc/* @@ -12,7 +13,8 @@ trigger: pr: branches: include: - - master + - main + - feature/* paths: exclude: - doc/* diff --git a/build/pipelines/templates/build-console-audit-job.yml b/build/pipelines/templates/build-console-audit-job.yml index faf5ec67207..1c9a90d6e8f 100644 --- a/build/pipelines/templates/build-console-audit-job.yml +++ b/build/pipelines/templates/build-console-audit-job.yml @@ -8,7 +8,9 @@ jobs: variables: BuildConfiguration: AuditMode BuildPlatform: ${{ parameters.platform }} - pool: { vmImage: windows-2019 } + pool: "windevbuildagents" + # The public pool is also an option! + # pool: { vmImage: windows-2019 } steps: - checkout: self diff --git a/build/pipelines/templates/build-console-ci.yml b/build/pipelines/templates/build-console-ci.yml index 34c8d09add7..7c414af06b9 100644 --- a/build/pipelines/templates/build-console-ci.yml +++ b/build/pipelines/templates/build-console-ci.yml @@ -2,6 +2,8 @@ parameters: configuration: 'Release' platform: '' additionalBuildArguments: '' + minimumExpectedTestsExecutedCount: 10 # Sanity check for minimum expected tests to be reported + rerunPassesRequiredToAvoidFailure: 5 jobs: - job: Build${{ parameters.platform }}${{ parameters.configuration }} @@ -9,9 +11,27 @@ jobs: variables: BuildConfiguration: ${{ parameters.configuration }} BuildPlatform: ${{ parameters.platform }} - pool: { vmImage: windows-2019 } + pool: "windevbuildagents" + # The public pool is also an option! + # pool: { vmImage: windows-2019 } 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: and(succeeded(), and(eq('${{ parameters.platform }}', 'x64'), not(eq(variables['Build.Reason'], 'PullRequest')))) + testSuite: 'DevTestSuite' + rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} + +- template: helix-processtestresults-job.yml + parameters: + dependsOn: + - RunTestsInHelix + condition: and(succeededOrFailed(), and(eq('${{ parameters.platform }}', 'x64'), not(eq(variables['Build.Reason'], 'PullRequest')))) + rerunPassesRequiredToAvoidFailure: ${{ parameters.rerunPassesRequiredToAvoidFailure }} + minimumExpectedTestsExecutedCount: ${{ parameters.minimumExpectedTestsExecutedCount }} \ No newline at end of file diff --git a/build/pipelines/templates/build-console-steps.yml b/build/pipelines/templates/build-console-steps.yml index 0fd6c7b6878..4a3adcb4054 100644 --- a/build/pipelines/templates/build-console-steps.yml +++ b/build/pipelines/templates/build-console-steps.yml @@ -1,5 +1,6 @@ parameters: additionalBuildArguments: '' + testLogPath: '$(Build.BinariesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\testsOnBuildMachine.wtl' steps: - checkout: self @@ -7,23 +8,29 @@ steps: clean: true - task: NuGetToolInstaller@0 - displayName: Ensure NuGet 4.8.1 + displayName: 'Use NuGet 5.2.0' inputs: - versionSpec: 4.8.1 - -- task: VisualStudioTestPlatformInstaller@1 - displayName: Ensure VSTest Platform + versionSpec: 5.2.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 + displayName: Restore NuGet packages for solution inputs: command: restore feedsToUse: config configPath: NuGet.config restoreSolution: OpenConsole.sln restoreDirectory: '$(Build.SourcesDirectory)\packages' + +- 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: VSBuild@1 displayName: 'Build solution **\OpenConsole.sln' @@ -66,7 +73,7 @@ steps: inputs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + arguments: -MatchPattern '*unit.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86'))) - task: PowerShell@2 @@ -74,9 +81,41 @@ steps: inputs: targetType: filePath filePath: build\scripts\Run-Tests.ps1 - arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' + arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' -LogPath '${{ parameters.testLogPath }}' condition: and(succeeded(), eq(variables['BuildPlatform'], 'x64')) +- task: PowerShell@2 + displayName: 'Convert Test Logs from WTL to xUnit format' + 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: or(eq(variables['BuildPlatform'], 'x64'), eq(variables['BuildPlatform'], 'x86')) + +- task: PublishTestResults@2 + displayName: 'Upload converted test logs' + 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' + inputs: + Contents: | + **/*.wtl + **/*onBuildMachineResults.xml + ${{ parameters.testLogPath }} + TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test' + OverWrite: true + flattenFolders: true + - task: CopyFiles@2 displayName: 'Copy *.appx/*.msix to Artifacts (Non-PR builds only)' inputs: @@ -90,9 +129,22 @@ steps: flattenFolders: true condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) +- task: CopyFiles@2 + displayName: 'Copy outputs needed for test runs to Artifacts' + inputs: + Contents: | + $(Build.SourcesDirectory)/bin/$(BuildPlatform)/$(BuildConfiguration)/*.exe + $(Build.SourcesDirectory)/bin/$(BuildPlatform)/$(BuildConfiguration)/*.dll + $(Build.SourcesDirectory)/bin/$(BuildPlatform)/$(BuildConfiguration)/*.xml + **/Microsoft.VCLibs.*.appx + **/TestHostApp/* + TargetFolder: '$(Build.ArtifactStagingDirectory)/$(BuildConfiguration)/$(BuildPlatform)/test' + OverWrite: true + flattenFolders: true + condition: and(and(succeeded(), eq(variables['BuildPlatform'], 'x64')), ne(variables['Build.Reason'], 'PullRequest')) + - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact (appx) (Non-PR builds only)' + displayName: 'Publish All Build Artifacts' inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/appx' - ArtifactName: 'appx-$(BuildConfiguration)' - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' \ No newline at end of file diff --git a/build/pipelines/templates/helix-createprojfile-steps.yml b/build/pipelines/templates/helix-createprojfile-steps.yml new file mode 100644 index 00000000000..89818ae8e95 --- /dev/null +++ b/build/pipelines/templates/helix-createprojfile-steps.yml @@ -0,0 +1,15 @@ +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)\${{ parameters.outputProjFileName }}' -JobTestSuiteName '${{ parameters.testSuite }}' -TaefPath '$(Build.SourcesDirectory)\build\Helix\packages\taef.redist.wlk.10.57.200731005-develop\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 new file mode 100644 index 00000000000..9d301e9cb6e --- /dev/null +++ b/build/pipelines/templates/helix-processtestresults-job.yml @@ -0,0 +1,68 @@ +parameters: + condition: 'succeededOrFailed()' + dependsOn: '' + rerunPassesRequiredToAvoidFailure: 5 + minimumExpectedTestsExecutedCount: 10 + checkJobAttempt: false + pgoArtifact: '' + +jobs: +- job: ProcessTestResults + 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) + 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) + 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 new file mode 100644 index 00000000000..81125f3d988 --- /dev/null +++ b/build/pipelines/templates/helix-runtests-job.yml @@ -0,0 +1,131 @@ +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: '' + # if 'useBuildOutputFromBuildId' is set, we will default to using a build from this pipeline: + useBuildOutputFromPipeline: $(System.DefinitionId) + matrix: + # Release_x86: + # buildPlatform: 'x86' + # buildConfiguration: 'release' + # openHelixTargetQueues: 'windows.10.amd64.client19h1.open.xaml' + # closedHelixTargetQueues: 'windows.10.amd64.client19h1.xaml' + Release_x64: + buildPlatform: 'x64' + buildConfiguration: 'release' + openHelixTargetQueues: 'windows.10.amd64.client19h1.open.xaml' + closedHelixTargetQueues: 'windows.10.amd64.client19h1.xaml' + +jobs: +- job: ${{ parameters.name }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + pool: + vmImage: 'windows-2019' + timeoutInMinutes: 120 + strategy: + maxParallel: ${{ parameters.maxParallel }} + matrix: ${{ parameters.matrix }} + variables: + artifactsDir: $(Build.SourcesDirectory)\Artifacts + taefPath: $(Build.SourcesDirectory)\build\Helix\packages\taef.redist.wlk.10.57.200731005-develop\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 5.2.0' + inputs: + versionSpec: 5.2.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' + + - template: helix-createprojfile-steps.yml + parameters: + condition: and(succeeded(),ne('${{ parameters.testSuite }}','NugetTestSuite')) + 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(),ne('${{ parameters.testSuite }}','NugetTestSuite')) + testFilePath: '$(artifactsDir)\${{ parameters.artifactName }}\$(buildConfiguration)\$(buildPlatform)\Test\Conhost.UIA.Tests.dll' + outputProjFileName: 'RunTestsInHelix-HostTestsUIA.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)' + 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)' + diff --git a/build/rules/Branding.targets b/build/rules/Branding.targets new file mode 100644 index 00000000000..f72a2430a4d --- /dev/null +++ b/build/rules/Branding.targets @@ -0,0 +1,18 @@ + + + + <_WTBrandingPreprocessorToken Condition="'$(WindowsTerminalBranding)'=='Preview'">WT_BRANDING_PREVIEW + <_WTBrandingPreprocessorToken Condition="'$(WindowsTerminalBranding)'=='Release'">WT_BRANDING_RELEASE + <_WTBrandingPreprocessorToken Condition="'$(_WTBrandingPreprocessorToken)'==''">WT_BRANDING_DEV + + + + + $(_WTBrandingPreprocessorToken);%(PreprocessorDefinitions) + + + + $(_WTBrandingPreprocessorToken);%(PreprocessorDefinitions) + + + diff --git a/build/scripts/Get-WttLog.ps1 b/build/scripts/Get-WttLog.ps1 new file mode 100644 index 00000000000..46aab2f12aa --- /dev/null +++ b/build/scripts/Get-WttLog.ps1 @@ -0,0 +1,15 @@ +[CmdLetBinding()] +Param( + [Parameter(Mandatory=$true, Position=0)][string]$BuildPlatform, + [Parameter(Mandatory=$true, Position=1)][string]$RationalizedPlatform, + [Parameter(Mandatory=$true, Position=2)][string]$Configuration +) + + +$i = Get-Item .\packages\MuxCustomBuild* +$wtt = Join-Path -Path $i[0].FullName -ChildPath (Join-Path -Path 'tools' -ChildPath (Join-Path -Path $BuildPlatform -ChildPath 'wttlog.dll')) +$dest = Join-Path -Path .\bin -ChildPath (Join-Path -Path $RationalizedPlatform -ChildPath ($Configuration)) +copy $wtt $dest + + +Exit 0 diff --git a/build/scripts/Run-Tests.ps1 b/build/scripts/Run-Tests.ps1 index 28c9b5bd95f..377577a36c1 100644 --- a/build/scripts/Run-Tests.ps1 +++ b/build/scripts/Run-Tests.ps1 @@ -2,12 +2,24 @@ Param( [Parameter(Mandatory=$true, Position=0)][string]$MatchPattern, [Parameter(Mandatory=$true, Position=1)][string]$Platform, - [Parameter(Mandatory=$true, Position=2)][string]$Configuration + [Parameter(Mandatory=$true, Position=2)][string]$Configuration, + [Parameter(Mandatory=$false, Position=3)][string]$LogPath ) $testdlls = Get-ChildItem -Path ".\bin\$Platform\$Configuration" -Recurse -Filter $MatchPattern -&".\bin\$Platform\$Configuration\te.exe" $testdlls.FullName + +$args = @(); + +if ($LogPath) +{ + $args += '/enablewttlogging'; + $args += '/appendwttlogging'; + $args += "/logFile:$LogPath"; + Write-Host "Wtt Logging Enabled"; +} + +&".\bin\$Platform\$Configuration\te.exe" $args $testdlls.FullName if ($lastexitcode -Ne 0) { Exit $lastexitcode } diff --git a/build/scripts/Test-WindowsTerminalPackage.ps1 b/build/scripts/Test-WindowsTerminalPackage.ps1 index 88cb14bbf97..2f2b5cbc349 100644 --- a/build/scripts/Test-WindowsTerminalPackage.ps1 +++ b/build/scripts/Test-WindowsTerminalPackage.ps1 @@ -106,6 +106,10 @@ Try { Throw "Failed to find wt.exe/wtd.exe -- check the WAP packaging project" } + If ($null -eq (Get-Item "$AppxPackageRootPath\OpenConsole.exe" -EA:Ignore)) { + Throw "Failed to find OpenConsole.exe -- check the WAP packaging project" + } + } Finally { Remove-Item -Recurse -Force $AppxPackageRootPath } diff --git a/custom.props b/custom.props index c4bf1eefda0..38d0c629d53 100644 --- a/custom.props +++ b/custom.props @@ -5,7 +5,7 @@ true 2020 1 - 3 + 6 Windows Terminal diff --git a/dep/llvm/clang-format.exe b/dep/llvm/clang-format.exe deleted file mode 100644 index efed82cc09d..00000000000 Binary files a/dep/llvm/clang-format.exe and /dev/null differ diff --git a/doc/Niksa.md b/doc/Niksa.md index 4ad72148cf6..e543191a6c7 100644 --- a/doc/Niksa.md +++ b/doc/Niksa.md @@ -9,6 +9,8 @@ This document serves as a storage point for those posts. - [Output Processing between "Far East" and "Western"](#fesb) - [Why do we not backport things?](#backport) - [Why can't we have mixed elevated and non-elevated tabs in the Terminal?](#elevation) +- [What's the difference between a shell and a terminal?](#shell-vs-terminal) + ## Why do we avoid changing CMD.exe? `setlocal` doesn't behave the same way as an environment variable. It's a thing that would have to be put in at the top of the batch script that is `somefile.cmd` as one of its first commands to adjust the way that one specific batch file is processed by the `cmd.exe` engine. That's probably not suitable for your needs, but that's the way we have to go. @@ -179,3 +181,20 @@ Other platforms have accepted that risk in preference for user convenience. They Original Source: https://github.com/microsoft/terminal/issues/632#issuecomment-519375707 +## What's the difference between a shell and a terminal? + +_guest speaker @zadjii-msft_ + +I think there might be a bit of a misunderstanding here - there are two different kinds of applications we're talking about here: +* shell applications, like `cmd.exe`, `powershell`, `zsh`, etc. These are text-only applications that emit streams of characters. They don't care at all about how they're eventually rendered to the user. These are also sometimes referred to as "commandline client" applications. +* terminal applications, like the Windows Terminal, gnome-terminal, xterm, iterm2, hyper. These are graphical applications that can be used to render the output of commandline clients. + +On Windows, if you just run `cmd.exe` directly, the OS will create an instance of `conhost.exe` as the _terminal_ for `cmd.exe`. The same thing happens for `powershell.exe`, the system will creates a new conhost window for any client that's not already connected to a terminal of some sort. This has lead to an enormous amount of confusion for people thinking that a conhost window is actually a "`cmd` window". `cmd` can't have a window, it's just a commandline application. Its window is always some other terminal. + +Any terminal can run any commandline client application. So you can use the Windows Terminal to run whatever shell you want. I use mine for both `cmd` and `powershell`, and also WSL: + +![image](https://user-images.githubusercontent.com/18356694/89556758-79d27e80-d7d7-11ea-84e2-10710e09ef4a.png) + +It's not the Terminal's responsibility to remember the commands executed by a commandline client. That's the responsibility of the _shell_. How would the terminal remember commands executed by something like `emacs` or `vim`? Those are both applications where the user is typing input and hitting enter, like they would at a cmd prompt, but without something that resembles a command history. + +Original Source: https://github.com/microsoft/terminal/issues/6500#issuecomment-670035468 diff --git a/doc/TAEF.md b/doc/TAEF.md index 65d06c2f874..807d6957c00 100644 --- a/doc/TAEF.md +++ b/doc/TAEF.md @@ -1,9 +1,30 @@ -### TAEF ### +### TAEF Overview ### + TAEF, the Test Authoring and Execution Framework, is used extensively within the Windows organization to test the operating system code in a unified manner for system, driver, and application code. As the console is a Windows OS Component, we strive to continue using the same system such that tests can be ran in a unified manner both externally to Microsoft as well as inside the official OS Build/Test system. -The [official documentation](https://msdn.microsoft.com/en-us/library/windows/hardware/hh439725\(v=vs.85\).aspx) for TAEF describes the basic architecture, usage, and functionality of the test system. It is similar to Visual Studio test, but a bit more comprehensive and flexible. +The [official documentation](https://docs.microsoft.com/en-us/windows-hardware/drivers/taef/) for TAEF describes the basic architecture, usage, and functionality of the test system. It is similar to Visual Studio test, but a bit more comprehensive and flexible. + +### Writing Tests + +You may want to read the section [Authoring Tests in C++](https://docs.microsoft.com/en-us/windows-hardware/drivers/taef/authoring-tests-in-c--) before getting your hands dirty. Note that the quoted header name in `#include "WexTestClass.h"` might be a bit confusing. You are not required to copy TAEF headers into the project folder. + +Use the [TAEF Verify Macros for C++](https://docs.microsoft.com/en-us/windows-hardware/drivers/taef/verify) in your test code to perform verifications. + +### Running Tests + +If you have Visual Studio and related C++ components installed, and you have successfully restored NuGets, you should have the TAEF test runner `te.exe` available locally as part of the `Taef.Redist.Wlk` package. + +> Note that you cannot easily run TAEF tests directly through Visual Studio. The `Taef.Redist.Wlk` NuGet package comes with an adapter that will let you browse and execute TAEF tests inside of Visual Studio, but its performance and reliability prevent us from recommending it here. -For the purposes of the console project, you can run the tests using the *TE.exe* that matches the architecture for which the test was build (x86/x64) in the pattern +In a "normal" CMD environment, `te.exe` may not be directly available. Try the following command to set up the development enviroment first: + +```shell +.\tools\razzle.cmd +``` + +Then you should be able to use `%TAEF%` as an alias of the actual `te.exe`. + +For the purposes of the OpenConsole project, you can run the tests using the `te.exe` that matches the architecture for which the test was built (x86/x64): te.exe Console.Unit.Tests.dll @@ -15,6 +36,15 @@ Limiting the tests to be run is also useful with: Any pattern of class/method names can be specified after the */name:* flag with wildcard patterns. -For any further details on the functionality of the TAEF test runner, *TE.exe*, please see the documentation above or run the embedded help with +For any further details on the functionality of the TAEF test runner, please see the [Executing Tests](https://docs.microsoft.com/en-us/windows-hardware/drivers/taef/executing-tests) section in the official documentation. Or run the embedded help with te.exe /! + +If you use PowerShell, try the following command: + +```powershell +Import-Module .\tools\OpenConsole.psm1 +Invoke-OpenConsoleTests +``` + +`Invoke-OpenConsoleTests` supports a number of options, which you can enumerate by running `Invoke-OpenConsoleTests -?`. diff --git a/doc/building.md b/doc/building.md index 27c1e7d806e..9a3eba6c6d6 100644 --- a/doc/building.md +++ b/doc/building.md @@ -67,12 +67,12 @@ To update the version of a given package, use the following snippet where: - `$PackageName` is the name of the package, e.g. Microsoft.UI.Xaml -- `$OldVersionNumber` is the version number currently used, e.g. 2.5.0-prerelease.200609001 -- `$NewVersionNumber` is the version number you want to migrate to, e.g. 2.4.200117003-prerelease +- `$OldVersionNumber` is the version number currently used, e.g. 2.4.0-prerelease.200506002 +- `$NewVersionNumber` is the version number you want to migrate to, e.g. 2.5.0-prerelease.200812002 Example usage: -`git grep -z -l Microsoft.UI.Xaml | xargs -0 sed -i -e 's/2.5.0-prerelease.200609001/2.4.200117003-prerelease/g'` +`git grep -z -l Microsoft.UI.Xaml | xargs -0 sed -i -e 's/2.4.0-prerelease.200506002/2.5.0-prerelease.200812002/g'` ## Using .nupkg files instead of downloaded Nuget packages If you want to use .nupkg files instead of the downloaded Nuget package, you can do this with the following steps: diff --git a/doc/cascadia/Json-Utility-API.md b/doc/cascadia/Json-Utility-API.md index 465f3239adb..422a550bf2b 100644 --- a/doc/cascadia/Json-Utility-API.md +++ b/doc/cascadia/Json-Utility-API.md @@ -8,19 +8,20 @@ return a JSON value coerced into the specified type. When reading into existing storage, it returns a boolean indicating whether that storage was modified. If the JSON value cannot be converted to the specified type, an exception will be generated. +For non-nullable type conversions (most POD types), `null` is considered to be an invalid type. ```c++ std::string one; std::optional two; JsonUtils::GetValue(json, one); -// one is populated or unchanged. +// one is populated or an exception is thrown. JsonUtils::GetValue(json, two); -// two is populated, nullopt or unchanged +// two is populated, nullopt or an exception is thrown auto three = JsonUtils::GetValue(json); -// three is populated or zero-initialized +// three is populated or an exception is thrown auto four = JsonUtils::GetValue>(json); // four is populated or nullopt @@ -225,14 +226,14 @@ auto v = JsonUtils::GetValue(json, conv); -|json type invalid|json null|valid -|-|-|- -`T`|❌ exception|🔵 unchanged|✔ converted +`T`|❌ exception|❌ exception|✔ converted `std::optional`|❌ exception|🟨 `nullopt`|✔ converted ### GetValue<T>() (returning) -|json type invalid|json null|valid -|-|-|- -`T`|❌ exception|🟨 `T{}` (zero value)|✔ converted +`T`|❌ exception|❌ exception|✔ converted `std::optional`|❌ exception|🟨 `nullopt`|✔ converted ### GetValueForKey(T&) (type-deducing) @@ -242,14 +243,14 @@ a "key not found" state. The remaining three cases are the same. val type|key not found|_json type invalid_|_json null_|_valid_ -|-|-|-|- -`T`|🔵 unchanged|_❌ exception_|_🔵 unchanged_|_✔ converted_ -`std::optional`|_🔵 unchanged_|_❌ exception_|_🟨 `nullopt`_|_✔ converted_ +`T`|🔵 unchanged|_❌ exception_|_❌ exception_|_✔ converted_ +`std::optional`|🔵 unchanged|_❌ exception_|_🟨 `nullopt`_|_✔ converted_ ### GetValueForKey<T>() (return value) val type|key not found|_json type invalid_|_json null_|_valid_ -|-|-|-|- -`T`|🟨 `T{}` (zero value)|_❌ exception_|_🟨 `T{}` (zero value)_|_✔ converted_ +`T`|🟨 `T{}` (zero value)|_❌ exception_|_❌ exception_|_✔ converted_ `std::optional`|🟨 `nullopt`|_❌ exception_|_🟨 `nullopt`_|_✔ converted_ ### Future Direction diff --git a/doc/cascadia/SettingsSchema.md b/doc/cascadia/SettingsSchema.md index 9c58edc89f4..e34b5ba0d77 100644 --- a/doc/cascadia/SettingsSchema.md +++ b/doc/cascadia/SettingsSchema.md @@ -1,210 +1 @@ -# Settings.json Documentation - -## Globals - -Properties listed below affect the entire window, regardless of the profile settings. - -| Property | Necessity | Type | Default | Description | -| -------- | --------- | ---- | ------- | ----------- | -| `alwaysShowTabs` | _Required_ | Boolean | `true` | When set to `true`, tabs are always displayed. When set to `false` and `showTabsInTitlebar` is set to `false`, tabs only appear after typing Ctrl + T. | -| `copyOnSelect` | Optional | Boolean | `false` | When set to `true`, a selection is immediately copied to your clipboard upon creation. When set to `false`, the selection persists and awaits further action. | -| `copyFormatting` | Optional | Boolean | `false` | 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. | -| `largePasteWarning` | Optional | Boolean | `true` | When set to `true`, trying to paste text with more than 5 KiB of characters will display a warning asking you whether to continue or not with the paste. | -| `multiLinePasteWarning` | Optional | Boolean | `true` | When set to `true`, trying to paste text with a _new line_ character will display a warning asking you whether to continue or not with the paste. | -| `defaultProfile` | _Required_ | String | PowerShell guid | Sets the default profile. Opens by typing Ctrl + T or by clicking the '+' icon. The guid of the desired default profile is used as the value. | -| `initialCols` | _Required_ | Integer | `120` | The number of columns displayed in the window upon first load. | -| `initialPosition` | Optional | String | `","` | The position of the top left corner of the window upon first load. On a system with multiple displays, these coordinates are relative to the top left of the primary display. If `launchMode` is set to `"maximized"`, the window will be maximized on the monitor specified by those coordinates. | -| `initialRows` | _Required_ | Integer | `30` | The number of rows displayed in the window upon first load. | -| `launchMode` | Optional | String | `default` | Defines whether the Terminal will launch as maximized or not. Possible values: `"default"`, `"maximized"` | -| `theme` | _Required_ | String | `system` | Sets the theme of the application. Possible values: `"light"`, `"dark"`, `"system"` | -| `showTerminalTitleInTitlebar` | _Required_ | Boolean | `true` | When set to `true`, titlebar displays the title of the selected tab. When set to `false`, titlebar displays "Windows Terminal". | -| `showTabsInTitlebar` | Optional | Boolean | `true` | When set to `true`, the tabs are moved into the titlebar and the titlebar disappears. When set to `false`, the titlebar sits above the tabs. | -| `snapToGridOnResize` | Optional | Boolean | `false` | When set to `true`, the window will snap to the nearest character boundary on resize. When `false`, the window will resize "smoothly" | -| `tabWidthMode` | Optional | String | `equal` | Sets the width of the tabs. Possible values:
  • `"equal"`: sizes each tab to the same width
  • `"titleLength"`: sizes each tab to the length of its title
  • `"compact"`: sizes each tab to the length of its title when focused, and shrinks to the size of only the icon when the tab is unfocused.
| -| `wordDelimiters` | Optional | String |  /\()"'-:,.;<>~!@#$%^&*|+=[]{}~?│
_(`│` is `U+2502 BOX DRAWINGS LIGHT VERTICAL`)_ | Determines the delimiters used in a double click selection. | -| `confirmCloseAllTabs` | Optional | Boolean | `true` | When set to `true` closing a window with multiple tabs open WILL require confirmation. When set to `false` closing a window with multiple tabs open WILL NOT require confirmation. | -| `startOnUserLogin` | Optional | Boolean | `false` | When set to `true` enables the launch of Windows Terminal at startup. Setting to `false` will disable the startup task entry. Note: if the Windows Terminal startup task entry is disabled either by org policy or by user action this setting will have no effect. | -| `disabledProfileSources` | Optional | Array[String] | `[]` | Disables all the dynamic profile generators in this list, preventing them from adding their profiles to the list of profiles on startup. This array can contain any combination of `Windows.Terminal.Wsl`, `Windows.Terminal.Azure`, or `Windows.Terminal.PowershellCore`. For more information, see [UsingJsonSettings.md](https://github.com/microsoft/terminal/blob/master/doc/user-docs/UsingJsonSettings.md#dynamic-profiles) | -| `experimental.rendering.forceFullRepaint` | Optional | Boolean | `false` | When set to true, we will redraw the entire screen each frame. When set to false, we will render only the updates to the screen between frames. | -| `experimental.rendering.software` | Optional | Boolean | `false` | When set to true, we will use the software renderer (a.k.a. WARP) instead of the hardware one. | - -## Profiles - -Properties listed below are specific to each unique profile. - -| Property | Necessity | Type | Default | Description | -| -------- | --------- | ---- | ------- | ----------- | -| `guid` | _Required_ | String | | Unique identifier of the profile. Written in registry format: `"{00000000-0000-0000-0000-000000000000}"`. | -| `name` | _Required_ | String | | Name of the profile. Displays in the dropdown menu.
Additionally, this value will be used as the "title" to pass to the shell on startup. Some shells (like `bash`) may choose to ignore this initial value, while others (`cmd`, `powershell`) may use this value over the lifetime of the application. This "title" behavior can be overridden by using `tabTitle`. | -| `acrylicOpacity` | Optional | Number | `0.5` | When `useAcrylic` is set to `true`, it sets the transparency of the window for the profile. Accepts floating point values from 0-1. | -| `antialiasingMode` | Optional | String | `"grayscale"` | Controls how text is antialiased in the renderer. Possible values are "grayscale", "cleartype" and "aliased". Note that changing this setting will require starting a new terminal instance. | -| `background` | Optional | String | | Sets the background color of the profile. Overrides `background` set in color scheme if `colorscheme` is set. Uses hex color format: `"#rrggbb"`. | -| `backgroundImage` | Optional | String | | Sets the file location of the Image to draw over the window background. | -| `backgroundImageAlignment` | Optional | String | `center` | Sets how the background image aligns to the boundaries of the window. Possible values: `"center"`, `"left"`, `"top"`, `"right"`, `"bottom"`, `"topLeft"`, `"topRight"`, `"bottomLeft"`, `"bottomRight"` | -| `backgroundImageOpacity` | Optional | Number | `1.0` | Sets the transparency of the background image. Accepts floating point values from 0-1. | -| `backgroundImageStretchMode` | Optional | String | `uniformToFill` | Sets how the background image is resized to fill the window. Possible values: `"none"`, `"fill"`, `"uniform"`, `"uniformToFill"` | -| `closeOnExit` | Optional | String | `graceful` | Sets how the profile reacts to termination or failure to launch. Possible values: `"graceful"` (close when `exit` is typed or the process exits normally), `"always"` (always close) and `"never"` (never close). `true` and `false` are accepted as synonyms for `"graceful"` and `"never"` respectively. | -| `colorScheme` | Optional | String | `Campbell` | Name of the terminal color scheme to use. Color schemes are defined under `schemes`. | -| `commandline` | Optional | String | | Executable used in the profile. | -| `cursorColor` | Optional | String | | Sets the cursor color of the profile. Overrides `cursorColor` set in color scheme if `colorscheme` is set. Uses hex color format: `"#rrggbb"`. | -| `cursorHeight` | Optional | Integer | | Sets the percentage height of the cursor starting from the bottom. Only works when `cursorShape` is set to `"vintage"`. Accepts values from 25-100. | -| `cursorShape` | Optional | String | `bar` | Sets the cursor shape for the profile. Possible values: `"vintage"` ( ▃ ), `"bar"` ( ┃ ), `"underscore"` ( ▁ ), `"filledBox"` ( █ ), `"emptyBox"` ( ▯ ) | -| `fontFace` | Optional | String | `Cascadia Mono` | Name of the font face used in the profile. We will try to fallback to Consolas if this can't be found or is invalid. | -| `fontSize` | Optional | Integer | `12` | Sets the font size. | -| `fontWeight` | Optional | String | `normal` | Sets the weight (lightness or heaviness of the strokes) for the given font. Possible values: `"thin"`, `"extra-light"`, `"light"`, `"semi-light"`, `"normal"`, `"medium"`, `"semi-bold"`, `"bold"`, `"extra-bold"`, `"black"`, `"extra-black"`, or the corresponding numeric representation of OpenType font weight. | -| `foreground` | Optional | String | | Sets the foreground color of the profile. Overrides `foreground` set in color scheme if `colorscheme` is set. Uses hex color format: `#rgb` or `"#rrggbb"`. | -| `hidden` | Optional | Boolean | `false` | If set to true, the profile will not appear in the list of profiles. This can be used to hide default profiles and dynamically generated profiles, while leaving them in your settings file. | -| `historySize` | Optional | Integer | `9001` | The number of lines above the ones displayed in the window you can scroll back to. | -| `icon` | Optional | String | | Image file location of the icon used in the profile. Displays within the tab and the dropdown menu. | -| `padding` | Optional | String | `8, 8, 8, 8` | Sets the padding around the text within the window. Can have three different formats: `"#"` sets the same padding for all sides, `"#, #"` sets the same padding for left-right and top-bottom, and `"#, #, #, #"` sets the padding individually for left, top, right, and bottom. | -| `scrollbarState` | Optional | String | `"visible"` | Defines the visibility of the scrollbar. Possible values: `"visible"`, `"hidden"` | -| `selectionBackground` | Optional | String | | Sets the selection background color of the profile. Overrides `selectionBackground` set in color scheme if `colorscheme` is set. Uses hex color format: `"#rrggbb"`. | -| `snapOnInput` | Optional | Boolean | `true` | When set to `true`, the window will scroll to the command input line when typing. When set to `false`, the window will not scroll when you start typing. | -| `altGrAliasing` | Optional | Boolean | `true` | By default Windows treats Ctrl+Alt as an alias for AltGr. When altGrAliasing is set to false, this behavior will be disabled. | -| `source` | Optional | String | | Stores the name of the profile generator that originated this profile. _There are no discoverable values for this field._ | -| `startingDirectory` | Optional | String | `%USERPROFILE%` | The directory the shell starts in when it is loaded. | -| `suppressApplicationTitle` | Optional | Boolean | `false` | 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. | -| `tabTitle` | Optional | String | | If set, will replace the `name` as the title to pass to the shell on startup. Some shells (like `bash`) may choose to ignore this initial value, while others (`cmd`, `powershell`) may use this value over the lifetime of the application. | -| `useAcrylic` | Optional | Boolean | `false` | When set to `true`, the window will have an acrylic background. When set to `false`, the window will have a plain, untextured background. The transparency only applies to focused windows due to OS limitation. | -| `experimental.retroTerminalEffect` | Optional | Boolean | `false` | When set to `true`, enable retro terminal effects. This is an experimental feature, and its continued existence is not guaranteed. | - -## Schemes - -Properties listed below are specific to each color scheme. [ColorTool](https://github.com/microsoft/terminal/tree/master/src/tools/ColorTool) is a great tool you can use to create and explore new color schemes. All colors use hex color format. - -| Property | Necessity | Type | Description | -| -------- | ---- | ----------- | ----------- | -| `name` | _Required_ | String | Name of the color scheme. | -| `foreground` | _Required_ | String | Sets the foreground color of the color scheme. | -| `background` | _Required_ | String | Sets the background color of the color scheme. | -| `selectionBackground` | Optional | String | Sets the selection background color of the color scheme. | -| `cursorColor` | Optional | String | Sets the cursor color of the color scheme. | -| `black` | _Required_ | String | Sets the color used as ANSI black. | -| `blue` | _Required_ | String | Sets the color used as ANSI blue. | -| `brightBlack` | _Required_ | String | Sets the color used as ANSI bright black. | -| `brightBlue` | _Required_ | String | Sets the color used as ANSI bright blue. | -| `brightCyan` | _Required_ | String | Sets the color used as ANSI bright cyan. | -| `brightGreen` | _Required_ | String | Sets the color used as ANSI bright green. | -| `brightPurple` | _Required_ | String | Sets the color used as ANSI bright purple. | -| `brightRed` | _Required_ | String | Sets the color used as ANSI bright red. | -| `brightWhite` | _Required_ | String | Sets the color used as ANSI bright white. | -| `brightYellow` | _Required_ | String | Sets the color used as ANSI bright yellow. | -| `cyan` | _Required_ | String | Sets the color used as ANSI cyan. | -| `green` | _Required_ | String | Sets the color used as ANSI green. | -| `purple` | _Required_ | String | Sets the color used as ANSI purple. | -| `red` | _Required_ | String | Sets the color used as ANSI red. | -| `white` | _Required_ | String | Sets the color used as ANSI white. | -| `yellow` | _Required_ | String | Sets the color used as ANSI yellow. | - -## Keybindings - -Properties listed below are specific to each custom key binding. - -| Property | Necessity | Type | Description | -| -------- | ---- | ----------- | ----------- | -| `command` | _Required_ | String | The command executed when the associated key bindings are pressed. | -| `keys` | _Required_ | Array[String] or String | Defines the key combinations used to call the command. | -| `action` | Optional | String | Adds additional functionality to certain commands. | - -### Implemented Commands and Actions - -Commands listed below are per the implementation in [`src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp`](https://github.com/microsoft/terminal/blob/master/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp). - -Keybindings can be structured in the following manners: - -For commands without arguments: -
-`{ "command": "commandName", "keys": [ "modifiers+key" ] }` - -For commands with arguments: -
-`{ "command": { "action": "commandName", "argument": "value" }, "keys": ["modifiers+key"] }` - -| Command | Command Description | Action (*=required) | Action Arguments | Argument Descriptions | -| ------- | ------------------- | ------ | ---------------- | ----------------- | -| `adjustFontSize` | Change the text size by a specified point amount. | `delta` | integer | Amount of size change per command invocation. | -| `closePane` | Close the active pane. | | | | -| `closeTab` | Close the current tab. | | | | -| `closeWindow` | Close the current window and all tabs within it. | | | | -| `copy` | Copy the selected terminal content to your Windows Clipboard. | `singleLine` | boolean | When `true`, the copied content will be copied as a single line. When `false`, newlines persist from the selected text. | -| `duplicateTab` | Make a copy and open the current tab. | | | | -| `find` | Open the search dialog box. | | | | -| `moveFocus` | Focus on a different pane depending on direction. | `direction`* | `left`, `right`, `up`, `down` | Direction in which the focus will move. | -| `newTab` | Create a new tab. Without any arguments, this will open the default profile in a new tab. | 1. `commandLine`
2. `startingDirectory`
3. `tabTitle`
4. `index`
5. `profile` | 1. string
2. string
3. string
4. integer
5. string | 1. Executable run within the tab.
2. Directory in which the tab will open.
3. Title of the new tab.
4. Profile that will open based on its position in the dropdown (starting at 0).
5. Profile that will open based on its GUID or name. | -| `nextTab` | Open the tab to the right of the current one. | | | | -| `openNewTabDropdown` | Open the dropdown menu. | | | | -| `openSettings` | Open the settings file. | | | | -| `paste` | Insert the content that was copied onto the clipboard. | | | | -| `prevTab` | Open the tab to the left of the current one. | | | | -| `resetFontSize` | Reset the text size to the default value. | | | | -| `resizePane` | Change the size of the active pane. | `direction`* | `left`, `right`, `up`, `down` | Direction in which the pane will be resized. | -| `scrollDown` | Move the screen down. | | | | -| `scrollUp` | Move the screen up. | | | | -| `scrollUpPage` | Move the screen up a whole page. | | | | -| `scrollDownPage` | Move the screen down a whole page. | | | | -| `splitPane` | Halve the size of the active pane and open another. Without any arguments, this will open the default profile in the new pane. | 1. `split`*
2. `commandLine`
3. `startingDirectory`
4. `tabTitle`
5. `index`
6. `profile`
7. `splitMode` | 1. `vertical`, `horizontal`, `auto`
2. string
3. string
4. string
5. integer
6. string
7. string | 1. How the pane will split. `auto` will split in the direction that provides the most surface area.
2. Executable run within the pane.
3. Directory in which the pane will open.
4. Title of the tab when the new pane is focused.
5. Profile that will open based on its position in the dropdown (starting at 0).
6. Profile that will open based on its GUID or name.
7. Controls how the pane splits. Only accepts `duplicate` which will duplicate the focused pane's profile into a new pane. | -| `switchToTab` | Open a specific tab depending on index. | `index`* | integer | Tab that will open based on its position in the tab bar (starting at 0). | -| `toggleFullscreen` | Switch between fullscreen and default window sizes. | | | | -| `unbound` | Unbind the associated keys from any command. | | | | - -### Accepted Modifiers and Keys - -#### Modifiers -`ctrl+`, `shift+`, `alt+` - -#### Keys - -| Type | Keys | -| ---- | ---- | -| Function and Alphanumeric Keys | `f1-f24`, `a-z`, `0-9` | -| Symbols | ``` ` ```, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/` | -| Arrow Keys | `down`, `left`, `right`, `up`, `pagedown`, `pageup`, `pgdn`, `pgup`, `end`, `home`, `plus` | -| Action Keys | `tab`, `enter`, `esc`, `escape`, `space`, `backspace`, `delete`, `insert` | -| Numpad Keys | `numpad_0-numpad_9`, `numpad0-numpad9`, `numpad_add`, `numpad_plus`, `numpad_decimal`, `numpad_period`, `numpad_divide`, `numpad_minus`, `numpad_subtract`, `numpad_multiply` | - -## Background Images and Icons - -Some Terminal settings allow you to specify custom background images and icons. It is recommended that custom images and icons are stored in system-provided folders and are referred to using the correct [URI Schemes](https://docs.microsoft.com/en-us/windows/uwp/app-resources/uri-schemes). URI Schemes provide a way to reference files independent of their physical paths (which may change in the future). - -The most useful URI schemes to remember when customizing background images and icons are: - -| URI Scheme | Corresponding Physical Path | Use / description | -| --- | --- | ---| -| `ms-appdata:///Local/` | `%localappdata%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\` | Per-machine files | -| `ms-appdata:///Roaming/` | `%localappdata%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\RoamingState\` | Common files | - -> ⚠ Note: Do not rely on file references using the `ms-appx` URI Scheme (i.e. icons). These files are considered an internal implementation detail and may change name/location or may be omitted in the future. - -### Icons - -Terminal displays icons for each of your profiles which Terminal generates for any built-in shells - PowerShell Core, PowerShell, and any installed Linux/WSL distros. Each profile refers to a stock icon via the `ms-appx` URI Scheme. - -> ⚠ Note: Do not rely on the files referenced by the `ms-appx` URI Scheme - they are considered an internal implementation detail and may change name/location or may be omitted in the future. - -You can refer to you own icons if you wish, e.g.: - -```json - "icon" : "C:\\Users\\richturn\\OneDrive\\WindowsTerminal\\icon-ubuntu-32.png", -``` - -> 👉 Tip: Icons should be sized to 32x32px in an appropriate raster image format (e.g. .PNG, .GIF, or .ICO) to avoid having to scale your icons during runtime (causing a noticeable delay and loss of quality.) - -### Custom Background Images - -You can apply a background image to each of your profiles, allowing you to configure/brand/style each of your profiles independently from one another if you wish. - -To do so, specify your preferred `backgroundImage`, position it using `backgroundImageAlignment`, set its opacity with `backgroundImageOpacity`, and/or specify how your image fill the available space using `backgroundImageStretchMode`. - -For example: -```json - "backgroundImage": "C:\\Users\\richturn\\OneDrive\\WindowsTerminal\\bg-ubuntu-256.png", - "backgroundImageAlignment": "bottomRight", - "backgroundImageOpacity": 0.1, - "backgroundImageStretchMode": "none" -``` - -> 👉 Tip: You can easily roam your collection of images and icons across all your machines by storing your icons and images in OneDrive (as shown above). - -With these settings, your Terminal's Ubuntu profile would look similar to this: - -![Custom icon and background image](../images/custom-icon-and-background-image.jpg) +⚠ This document has moved to [the Customize Settings section of the Windows Terminal documentation](https://docs.microsoft.com/windows/terminal/customize-settings/global-settings). diff --git a/doc/cascadia/Unittesting-CppWinRT-Xaml.md b/doc/cascadia/Unittesting-CppWinRT-Xaml.md index 3cfb228d515..05e66994c1f 100644 --- a/doc/cascadia/Unittesting-CppWinRT-Xaml.md +++ b/doc/cascadia/Unittesting-CppWinRT-Xaml.md @@ -86,7 +86,7 @@ project from our `TerminalAppLib` project: duplicate type definitions)--> - $(SolutionDir)$(Platform)\$(Configuration)\TerminalSettings\Microsoft.Terminal.Settings.winmd + $(OpenConsoleCommonOutDir)\TerminalSettings\Microsoft.Terminal.Settings.winmd true false false @@ -122,7 +122,7 @@ dir to your `AdditionalLibraryDirectories`, and adding the lib to your - $(SolutionDir)\$(Platform)\$(Configuration)\TerminalAppLib;%(AdditionalLibraryDirectories) + $(OpenConsoleCommonOutDir)\TerminalAppLib;%(AdditionalLibraryDirectories) TerminalAppLib.lib;%(AdditionalDependencies) @@ -260,9 +260,9 @@ this: echo OutDir=$(OutDir) (xcopy /Y "$(SolutionDir)src\cascadia\ut_app\TerminalApp.Unit.Tests.manifest" "$(OutDir)\TerminalApp.Unit.Tests.manifest*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalConnection\TerminalConnection.dll" "$(OutDir)\TerminalConnection.dll*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalSettings\TerminalSettings.dll" "$(OutDir)\TerminalSettings.dll*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalControl\TerminalControl.dll" "$(OutDir)\TerminalControl.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalConnection\TerminalConnection.dll" "$(OutDir)\TerminalConnection.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalSettings\TerminalSettings.dll" "$(OutDir)\TerminalSettings.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalControl\TerminalControl.dll" "$(OutDir)\TerminalControl.dll*" ) @@ -446,9 +446,9 @@ before. The complete `PostBuildEvent` now looks like this: (xcopy /Y "$(SolutionDir)src\cascadia\ut_app\TerminalApp.Unit.Tests.AppxManifest.xml" "$(OutDir)\TerminalApp.Unit.Tests.AppxManifest.xml*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalConnection\TerminalConnection.dll" "$(OutDir)\TerminalConnection.dll*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalSettings\TerminalSettings.dll" "$(OutDir)\TerminalSettings.dll*" ) - (xcopy /Y "$(SolutionDir)$(Platform)\$(Configuration)\TerminalControl\TerminalControl.dll" "$(OutDir)\TerminalControl.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalConnection\TerminalConnection.dll" "$(OutDir)\TerminalConnection.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalSettings\TerminalSettings.dll" "$(OutDir)\TerminalSettings.dll*" ) + (xcopy /Y "$(OpenConsoleCommonOutDir)\TerminalControl\TerminalControl.dll" "$(OutDir)\TerminalControl.dll*" ) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 0ca40b685bd..aa5d18e44b3 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1,12 +1,12 @@ { - "$id": "https://github.com/microsoft/terminal/blob/master/doc/cascadia/profiles.schema.json", + "$id": "https://github.com/microsoft/terminal/blob/main/doc/cascadia/profiles.schema.json", "$schema": "https://json-schema.org/draft/2019-09/schema#", "title": "Microsoft's Windows Terminal Settings Profile Schema", "definitions": { "KeyChordSegment": { - "pattern": "^(?(ctrl|alt|shift)(?:\\+(ctrl|alt|shift)(?[^\\s+]|backspace|tab|enter|esc|escape|space|pgup|pageup|pgdn|pagedown|end|home|left|up|right|down|insert|delete|(?(ctrl|alt|shift)(?:\\+(ctrl|alt|shift)(?[^\\s+]|app|menu|backspace|tab|enter|esc|escape|space|pgup|pageup|pgdn|pagedown|end|home|left|up|right|down|insert|delete|(?\", where each modifier is optional, separated by + symbols, and keyName is either one of the names listed in the table below, or any single key character. The string should be written in full lowercase.\nbackspace\tBACKSPACE key\ntab\tTAB key\nenter\tENTER key\nesc, escape\tESC key\nspace\tSPACEBAR\npgup, pageup\tPAGE UP key\npgdn, pagedown\tPAGE DOWN key\nend\tEND key\nhome\tHOME key\nleft\tLEFT ARROW key\nup\tUP ARROW key\nright\tRIGHT ARROW key\ndown\tDOWN ARROW key\ninsert\tINS key\ndelete\tDEL key\nnumpad_0-numpad_9, numpad0-numpad9\tNumeric keypad keys 0 to 9. Can't be combined with the shift modifier.\nnumpad_multiply\tNumeric keypad MULTIPLY key (*)\nnumpad_plus, numpad_add\tNumeric keypad ADD key (+)\nnumpad_minus, numpad_subtract\tNumeric keypad SUBTRACT key (-)\nnumpad_period, numpad_decimal\tNumeric keypad DECIMAL key (.). Can't be combined with the shift modifier.\nnumpad_divide\tNumeric keypad DIVIDE key (/)\nf1-f24\tF1 to F24 function keys\nplus\tADD key (+)" + "description": "The string should fit the format \"[ctrl+][alt+][shift+]\", where each modifier is optional, separated by + symbols, and keyName is either one of the names listed in the table below, or any single key character. The string should be written in full lowercase.\napp, menu\tMENU key\nbackspace\tBACKSPACE key\ntab\tTAB key\nenter\tENTER key\nesc, escape\tESC key\nspace\tSPACEBAR\npgup, pageup\tPAGE UP key\npgdn, pagedown\tPAGE DOWN key\nend\tEND key\nhome\tHOME key\nleft\tLEFT ARROW key\nup\tUP ARROW key\nright\tRIGHT ARROW key\ndown\tDOWN ARROW key\ninsert\tINS key\ndelete\tDEL key\nnumpad_0-numpad_9, numpad0-numpad9\tNumeric keypad keys 0 to 9. Can't be combined with the shift modifier.\nnumpad_multiply\tNumeric keypad MULTIPLY key (*)\nnumpad_plus, numpad_add\tNumeric keypad ADD key (+)\nnumpad_minus, numpad_subtract\tNumeric keypad SUBTRACT key (-)\nnumpad_period, numpad_decimal\tNumeric keypad DECIMAL key (.). Can't be combined with the shift modifier.\nnumpad_divide\tNumeric keypad DIVIDE key (/)\nf1-f24\tF1 to F24 function keys\nplus\tADD key (+)" }, "Color": { "default": "#", @@ -26,43 +26,83 @@ ], "type": "string" }, + "BellStyle": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "audible", + "visual" + ] + } + }, + { + "type": "string", + "enum": [ + "audible", + "visual", + "all", + "none" + ] + } + ] + }, "ProfileGuid": { "default": "{}", "pattern": "^\\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\\}$", "type": "string" }, + "Icon": { + "description": "Image file location or an emoji to be used as an icon. Displays within the tab, the dropdown menu, and jumplist.", + "type": [ + "string", + "null" + ] + }, "ShortcutActionName": { "enum": [ "adjustFontSize", + "closeOtherTabs", "closePane", "closeTab", + "closeTabsAfter", "closeWindow", + "commandPalette", "copy", "duplicateTab", + "find", "moveFocus", "newTab", "nextTab", "openNewTabDropdown", "openSettings", + "openTabColorPicker", "paste", "prevTab", + "renameTab", + "openTabRenamer", "resetFontSize", "resizePane", "scrollDown", "scrollDownPage", "scrollUp", "scrollUpPage", + "sendInput", + "setColorScheme", + "setTabColor", "splitPane", "switchToTab", + "tabSearch", + "toggleAlwaysOnTop", "toggleFocusMode", "toggleFullscreen", - "toggleAlwaysOnTop", + "togglePaneZoom", "toggleRetroEffect", - "find", - "setTabColor", - "openTabColorPicker", - "renameTab", - "commandPalette", "wt", "unbound" ], @@ -85,6 +125,40 @@ ], "type": "string" }, + "CopyFormat": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "html", + "rtf" + ] + } + }, + { + "type": "string", + "enum": [ + "html", + "rtf", + "all", + "none" + ] + } + ] + }, + "AnchorKey": { + "enum": [ + "ctrl", + "alt", + "shift" + ], + "type": "string" + }, "NewTerminalArgs": { "properties": { "commandline": { @@ -106,6 +180,11 @@ "index": { "type": "integer", "description": "The index of the profile in the new tab dropdown (starting at 0)" + }, + "tabColor": { + "$ref": "#/definitions/Color", + "default": null, + "description": "If provided, will set the tab's color to the given value" } }, "type": "object" @@ -150,6 +229,18 @@ "type": "boolean", "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." + }, + "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.", + "oneOf": [ + { + "$ref": "#/definitions/CopyFormat" + }, + { + "type": "null" + } + ] } } } @@ -218,6 +309,28 @@ ], "required": [ "direction" ] }, + "SendInputAction": { + "description": "Arguments corresponding to a Send Input Action", + "allOf": [ + { + "$ref": "#/definitions/ShortcutAction" + }, + { + "properties": { + "action": { + "type": "string", + "pattern": "sendInput" + }, + "input": { + "type": "string", + "default": "", + "description": "The text input to feed into the shell. ANSI escape sequences may be used. Escape codes like \\x1b must be written as \\u001b." + } + } + } + ], + "required": [ "input" ] + }, "SplitPaneAction": { "description": "Arguments corresponding to a Split Pane Action", "allOf": [ @@ -281,6 +394,23 @@ } ] }, + "SetColorSchemeAction": { + "description": "Arguments corresponding to a Set Color Scheme Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "setColorScheme" }, + "colorScheme": { + "type": "string", + "default": "", + "description": "the name of the scheme to apply to the active pane" + } + } + } + ], + "required": [ "colorScheme" ] + }, "WtAction": { "description": "Arguments corresponding to a wt Action", "allOf": [ @@ -298,6 +428,76 @@ ], "required": [ "commandline" ] }, + "CloseOtherTabsAction": { + "description": "Arguments for a closeOtherTabs action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "closeOtherTabs" }, + "index": { + "oneOf": [ + { "type": "integer" }, + { "type": null } + ], + "default": "", + "description": "Close the tabs other than the one at this index. If no index is provided, use the focused tab's index." + } + } + } + ] + }, + "CloseTabsAfterAction": { + "description": "Arguments for a closeTabsAfter action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "closeTabsAfter" }, + "index": { + "oneOf": [ + { "type": "integer" }, + { "type": null } + ], + "default": "", + "description": "Close the tabs following the tab at this index. If no index is provided, use the focused tab's index." + } + } + } + ] + }, + "ScrollUpAction": { + "description": "Arguments for a scrollUp action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "scrollUp" }, + "rowsToScroll": { + "type": ["integer", "null"], + "default": null, + "description": "Scroll up rowsToScroll lines. If no value is provided, use the system-level defaults." + } + } + } + ] + }, + "ScrollDownAction": { + "description": "Arguments for a scrollDown action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { + "properties": { + "action": { "type": "string", "pattern": "scrollDown" }, + "rowsToScroll": { + "type": ["integer", "null"], + "default": null, + "description": "Scroll down rowsToScroll lines. If no value is provided, use the system-level defaults." + } + } + } + ] + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -311,10 +511,16 @@ { "$ref": "#/definitions/SwitchToTabAction" }, { "$ref": "#/definitions/MoveFocusAction" }, { "$ref": "#/definitions/ResizePaneAction" }, + { "$ref": "#/definitions/SendInputAction" }, { "$ref": "#/definitions/SplitPaneAction" }, { "$ref": "#/definitions/OpenSettingsAction" }, { "$ref": "#/definitions/SetTabColorAction" }, + { "$ref": "#/definitions/SetColorSchemeAction" }, { "$ref": "#/definitions/WtAction" }, + { "$ref": "#/definitions/CloseOtherTabsAction" }, + { "$ref": "#/definitions/CloseTabsAfterAction" }, + { "$ref": "#/definitions/ScrollUpAction" }, + { "$ref": "#/definitions/ScrollDownAction" }, { "type": "null" } ] }, @@ -332,6 +538,14 @@ "type": "array" } ] + }, + "icon": { "$ref": "#/definitions/Icon" }, + "name": { + "description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.", + "type": [ + "string", + "null" + ] } }, "required": [ @@ -361,7 +575,12 @@ }, "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.", + "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.", + "$ref": "#/definitions/CopyFormat" + }, + "disableAnimations": { + "default": false, + "description": "When set to `true`, visual animations will be disabled across the application.", "type": "boolean" }, "largePasteWarning": { @@ -395,28 +614,36 @@ }, "initialCols": { "default": 120, - "description": "The number of columns displayed in the window upon first load.", + "description": "The number of columns displayed in the window upon first load. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), this property is ignored.", "maximum": 999, "minimum": 1, "type": "integer" }, "initialPosition": { "$ref": "#/definitions/Coordinates", - "description": "The position of the top left corner of the window upon first load. On a system with multiple displays, these coordinates are relative to the top left of the primary display. If \"launchMode\" is set to maximized, the window will be maximized on the monitor specified by those coordinates." + "description": "The position of the top left corner of the window upon first load. On a system with multiple displays, these coordinates are relative to the top left of the primary display. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), the window will be maximized on the monitor specified by those coordinates." }, "initialRows": { "default": 30, - "description": "The number of rows displayed in the window upon first load.", + "description": "The number of rows displayed in the window upon first load. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), this property is ignored.", "maximum": 999, "minimum": 1, "type": "integer" }, + "startOnUserLogin": { + "default": false, + "description": "When set to true, this enables the launch of Windows Terminal at startup. Setting this to false will disable the startup task entry. If the Windows Terminal startup task entry is disabled either by org policy or by user action this setting will have no effect.", + "type": "boolean" + }, "launchMode": { "default": "default", - "description": "Defines whether the Terminal will launch as maximized or not.", + "description": "Defines whether the terminal will launch as maximized, full screen, or in a window. Setting this to \"focus\" is equivalent to launching the terminal in the \"default\" mode, but with the focus mode enabled. Similar, setting this to \"maximizedFocus\" will result in launching the terminal in a maximized window with the focus mode enabled.", "enum": [ + "fullscreen", "maximized", - "default" + "default", + "focus", + "maximizedFocus" ], "type": "string" }, @@ -479,6 +706,41 @@ "default": true, "description": "When set to \"true\" closing a window with multiple tabs open will require confirmation. When set to \"false\", the confirmation dialog will not appear.", "type": "boolean" + }, + "useTabSwitcher": { + "default": true, + "description": "Deprecated. Please use \"tabSwitcherMode\" instead.", + "oneOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "mru", + "inOrder", + "disabled" + ], + "type": "string" + } + ], + "deprecated": true + }, + "tabSwitcherMode": { + "default": "inOrder", + "description": "When set to \"true\" or \"mru\", the \"nextTab\" and \"prevTab\" commands will use the tab switcher UI, with most-recently-used ordering. When set to \"inOrder\", these actions will switch tabs in their current ordering. Set to \"false\" to disable the tab switcher.", + "oneOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "mru", + "inOrder", + "disabled" + ], + "type": "string" + } + ] } }, "required": [ @@ -515,7 +777,16 @@ }, "backgroundImage": { "description": "Sets the file location of the image to draw over the window background.", - "type": ["string", "null"] + "oneOf": [ + { + "type": ["string", null] + }, + { + "enum": [ + "desktopWallpaper" + ] + } + ] }, "backgroundImageAlignment": { "default": "center", @@ -551,6 +822,11 @@ ], "type": "string" }, + "bellStyle": { + "default": "audible", + "description": "Controls what happens when the application emits a BEL character. When set to \"all\", the Terminal will play a sound and flash the taskbar icon. An array of specific behaviors can also be used. Supported array values include `audible` and `visual`. When set to \"none\", nothing will happen.", + "$ref": "#/definitions/BellStyle" + }, "closeOnExit": { "default": "graceful", "description": "Sets how the profile reacts to termination or failure to launch. Possible values:\n -\"graceful\" (close when exit is typed or the process exits normally)\n -\"always\" (always close)\n -\"never\" (never close).\ntrue and false are accepted as synonyms for \"graceful\" and \"never\" respectively.", @@ -666,10 +942,7 @@ "minimum": -1, "type": "integer" }, - "icon": { - "description": "Image file location of the icon used in the profile. Displays within the tab and the dropdown menu.", - "type": ["string", "null"] - }, + "icon":{ "$ref": "#/definitions/Icon" }, "name": { "description": "Name of the profile. Displays in the dropdown menu.", "minLength": 1, @@ -678,8 +951,15 @@ "padding": { "default": "8, 8, 8, 8", "description": "Sets the padding around the text within the window. Can have three different formats:\n -\"#\" sets the same padding for all sides \n -\"#, #\" sets the same padding for left-right and top-bottom\n -\"#, #, #, #\" sets the padding individually for left, top, right, and bottom.", - "pattern": "^-?[0-9]+(\\.[0-9]+)?( *, *-?[0-9]+(\\.[0-9]+)?|( *, *-?[0-9]+(\\.[0-9]+)?){3})?$", - "type": "string" + "oneOf": [ + { + "pattern": "^-?[0-9]+(\\.[0-9]+)?( *, *-?[0-9]+(\\.[0-9]+)?|( *, *-?[0-9]+(\\.[0-9]+)?){3})?$", + "type": "string" + }, + { + "type": "integer" + } + ] }, "scrollbarState": { "default": "visible", diff --git a/doc/creating_a_new_project.md b/doc/creating_a_new_project.md new file mode 100644 index 00000000000..0f7f3bd880a --- /dev/null +++ b/doc/creating_a_new_project.md @@ -0,0 +1,54 @@ +# Creating a New Project + +## Creating a new WinRT Component DLL and referencing it in another project + +When creating a new DLL, it was really helpful to reference an existing DLL's `.vcxproj` like `TerminalControl.vcxproj`. While you should mostly try to copy what the existing `.vcxproj` has, here's a handful of things to double check for as you go along. + +- [ ] Make sure to `` our pre props at the _top_ of the vcxproj, and our post props at the _bottom_ of the vcxproj. +``` + + + + + + + + +``` +- [ ] Add a `` to your new `.vcxproj` in both `WindowsTerminal.vcxproj` and `TerminalApp.vcxproj` +- [ ] Add a `` to `TerminalAppLib.vcxproj` similar to this: +``` + + $(OpenConsoleCommonOutDir)\TerminalNewDLL\Microsoft.Terminal.NewDLL.winmd + true + false + false + +``` +- [ ] Make sure the project has a `.def` file with the following lines. The `WINRT_GetActivationFactory` part is important to expose the new DLL's activation factory so that other projects can successfully call the DLL's `GetActivationFactory` to get the DLL's classes. +``` +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE +``` +- For a bit more context on this whole process, the `AppXManifest.xml` file defines which classes belong to which DLLs. If your project wants class `X.Y.Z`, it can look it up in the manifest's definitions and see that it came from `X.Y.dll`. Then it'll load up the DLL, and call a particular function called `GetActivationFactory(L"X.Y.Z")` to get the class it wants. So, the definitions in `AppXManifest` are _required_ for this activation to work properly, and I found myself double checking the file to see that the definitions I expect are there. +- _Note_: If your new library eventually rolls up as a reference to our Centennial Packaging project `CascadiaPackage`, you don't have to worry about manually adding your definitions to the `AppXManifest.xml` because the Centennial Packaging project automatically enumerates the reference tree of WinMDs and stitches that information into the `AppXManifest.xml`. However, if your new project does _not_ ultimately roll up to a packaging project that will automatically put the references into `AppXManifest`, you will have to add them in manually. + +### Troubleshooting +- If you hit an error that looks like this: + ``` + X found processing metadata file ..\blah1\Microsoft.UI.Xaml.winmd, type already exists in file ..\blah\NewDLLProject\Microsoft.UI.Xaml.winmd. + ``` + The `Microsoft.UI.Xaml.winmd` is showing up in the output folder when it shouldn't. Try adding this block at the top of your `.vcxproj` + ``` + + + false + + + ``` + This will make all references non-private, meaning "don't copy it into my folder" by default. + +- If you hit a `Class not Registered` error, this might be because a class isn't getting registered in the app manifest. You can go check `src/cascadia/CascadiaPackage/bin/x64/Debug/AppX/AppXManifest.xml` to see if there exist entries to the classes of your newly created DLL. If the references aren't there, double check that you've added `` blocks to both `WindowsTerminal.vcxproj` and `TerminalApp.vcxproj`. + +- If you hit an extremely vague error along the lines of `Error in the DLL`, and right before that line you notice that your new DLL is loaded and unloaded right after each other, double check that your new DLL's definitions show up in the `AppXManifest.xml` file. If your new DLL is included as a reference to a project that rolls up to `CascadiaPackage`, double check that you've created a `.def` file for the project. Otherwise if your new project _does not_ roll up to a package that populates the `AppXManifest` references for you, you'll have to add those references yourself. diff --git a/doc/images/custom-icon-and-background-image.jpg b/doc/images/custom-icon-and-background-image.jpg deleted file mode 100644 index 540a8c7c7e2..00000000000 Binary files a/doc/images/custom-icon-and-background-image.jpg and /dev/null differ diff --git a/doc/images/new-issue-template.png b/doc/images/new-issue-template.png index 04a9a42c541..2c17811bea8 100644 Binary files a/doc/images/new-issue-template.png and b/doc/images/new-issue-template.png differ diff --git a/doc/images/panes.png b/doc/images/panes.png index b40c170dcc4..862738f8d26 100644 Binary files a/doc/images/panes.png and b/doc/images/panes.png differ diff --git a/doc/images/solution-platform.png b/doc/images/solution-platform.png index b763770f7b4..a595a68f443 100644 Binary files a/doc/images/solution-platform.png and b/doc/images/solution-platform.png differ diff --git a/doc/images/terminal-0.6.png b/doc/images/terminal-0.6.png index 9e6d0a807c6..4e94dfd96a1 100644 Binary files a/doc/images/terminal-0.6.png and b/doc/images/terminal-0.6.png differ diff --git a/doc/images/terminal-mockup.png b/doc/images/terminal-mockup.png index ece2670a404..744e58fe0b8 100644 Binary files a/doc/images/terminal-mockup.png and b/doc/images/terminal-mockup.png differ diff --git a/doc/reference/Build-SupportedSequenceIndex.ps1 b/doc/reference/Build-SupportedSequenceIndex.ps1 new file mode 100644 index 00000000000..0cf12568aa8 --- /dev/null +++ b/doc/reference/Build-SupportedSequenceIndex.ps1 @@ -0,0 +1,239 @@ +#requires -version 6.1 + +<# +.SYNOPSIS +Scan source code and build a list of supported VT sequences. +.DESCRIPTION +Scan source code and build a list of supported VT sequences. +TODO: add more details +#> +[cmdletbinding(DefaultParameterSetName="stdout")] +param( + [parameter(ParameterSetName="file", mandatory)] + [string]$OutFile, + [parameter(ParameterSetName="file")] + [switch]$Force, # for overwriting $OutFile if it exists + + [parameter(ParameterSetName="stdout")] + [parameter(ParameterSetName="file")] + [switch]$NoLogo, # no logo in summary + [parameter(ParameterSetName="stdout")] + [switch]$SummaryOnly, # no markdown generated + [parameter(ParameterSetName="stdout")] + [parameter(ParameterSetName="file")] + [switch]$Quiet, # no summary or logo + + [parameter(ParameterSetName="file")] + [parameter(ParameterSetName="stdout")] + [string]$SolutionRoot = "..\..", + [parameter(ParameterSetName="file")] + [parameter(ParameterSetName="stdout")] + [string]$InterfacePath = $(join-path $solutionRoot "src\terminal\adapter\ITermDispatch.hpp"), + [parameter(ParameterSetName="file")] + [parameter(ParameterSetName="stdout")] + [string]$ConsoleAdapterPath = $(join-path $solutionRoot "src\terminal\adapter\adaptDispatch.hpp"), + [parameter(ParameterSetName="file")] + [parameter(ParameterSetName="stdout")] + [string]$TerminalAdapterPath = $(join-path $solutionRoot "src\cascadia\terminalcore\terminalDispatch.hpp") +) + +if ($PSCmdlet.ParameterSetName -eq "stdout") { + Write-Verbose "Emitting markdown to STDOUT" +} + +<# + GLOBALS +#> + +[semver]$myVer = "0.6-beta" +$sequences = import-csv ".\master-sequence-list.csv" +$base = @{} +$conhost = @{} +$terminal = @{} +$prefix = "https://vt100.net/docs/vt510-rm/" +$repo = "https://github.com/oising/terminal/tree/master" +$conhostUrl = $ConsoleAdapterPath.TrimStart($SolutionRoot).replace("\", "/") +$terminalUrl = $TerminalAdapterPath.TrimStart($SolutionRoot).replace("\", "/") + +function Read-SourceFiles { + # extract base interface + $baseScanner = [regex]'(?x)virtual\s\w+\s(?\w+)(?s)[^;]+;(?-s).*?(?(?<=\/\/\s).+)' + + $baseScanner.Matches(($src = get-content -raw $interfacePath)) | foreach-object { + $match = $_ + #$line = (($src[0..$_.Index] -join "") -split "`n").Length + #$decl = $_.groups[0].value + $_.groups["seq"].value.split(",") | ForEach-Object { + $SCRIPT:base[$_.trim()] = $match.groups["method"].value + } + } + + # match overrides of ITermDispatch + $scanner = [regex]'(?x)\s+\w+\s(?\w+)(?s)[^;]+override;' + + $scanner.Matches(($src = Get-Content -raw $consoleAdapterPath)) | ForEach-Object { + $line = (($src[0..$_.Index] -join "") -split "`n").Length + $SCRIPT:conhost[$_.groups["method"].value] = $line + } + + $scanner.Matches(($src = Get-Content -raw $terminalAdapterPath)) | ForEach-Object { + $line = (($src[0..$_.Index] -join "") -split "`n").Length + #write-verbose $_.groups[0].value + $SCRIPT:terminal[$_.groups["method"].value] = $line + } +} + +function Get-SequenceIndexMarkdown { + # "Sequence","Parent","Description","Origin","Heading","Subheading", "ImplementedBy", "ConsoleHost","Terminal" + + $heading = $null + $subheading = $null +<# + Emit markdown + + TODO: + - auto-generate TOC +#> +@" +# VT Function Support + +## Table of Contents + +* [Code Extension Functions](#code-extension-functions) + * [Control Coding](#control-coding) + * [Character Coding](#character-coding) + * [Graphic Character Sets](#graphic-character-sets) +* [Terminal Management Functions](#terminal-management-functions) + * [Identification, status, and Initialization](#identification-status-and-initialization) + * [Emulations](#emulations) + * [Set-Up](#set-up) +* [Display Coordinate System and Addressing](#display-coordinate-system-and-addressing) + * [Active Position and Cursor](#active-position-and-cursor) + * [Margins and Scrolling](#margins-and-scrolling) + * [Cursor Movement](#cursor-movement) + * [Horizontal Tabulation](#horizontal-tabulation) + * [Page Size and Arrangement](#page-size-and-arrangement) + * [Page Movement](#page-movement) + * [Status Display](#status-display) + * [Right to Left](#right-to-left) +* [Window Management](#window-management) +* [Visual Attributes and Renditions](#visual-attributes-and-renditions) + * [Line Renditions](#line-renditions) + * [Character Renditions](#character-renditions) +* [Audible Indicators](#audible-indicators) +* [Mode States](#mode-states) + * [ANSI](#ansi) + * [DEC Private](#dec-private) +* [Editing Functions](#editing-functions) +* [OLTP Features](#OLTP-features) + * [Rectangular Area Operations](#rectangular-area-operations) + * [Data Integrity](#data-integrity) + * [Macros](#macros) +* [Saving and Restoring Terminal State](#saving-and-restoring-terminal-state) + * [Cursor Save Buffer](#cursor-save-buffer) + * [Terminal State Interrogation](#terminal-state-interrogation) +* [Keyboard Processing Functions](#keyboard-processing-functions) +* [Soft Key Mapping (UDK)](#soft-key-mapping-UDK) +* [Soft Fonts (DRCS)](#soft-fonts-drcs) +* [Printing](#printing) +* [Terminal Communication and Synchronization](#terminal-communication-and-synchronization) +* [Text Locator Extension](#text-locator-extension) +* [Session Management Extension](#session-management-extension) +* [Documented Exceptions](#documented-exceptions) + +$($sequences | ForEach-Object { + if ($method = $base[$_.sequence]) { + $_.ImplementedBy = $method + $_.ConsoleHost = $conhost[$method] + $_.Terminal = $terminal[$method] + } + # "Sequence","Associated","Description","Origin","Heading","Subheading", "ImplementedBy", "ConsoleHost","Terminal" + $c0 = "[$($_.Sequence)]($prefix$($_.sequence).html ""View page on vt100.net"")" + $c1 = "$($_.description)" + $c2 = "$($_.origin)" + $c3 = $(if ($_.consolehost) {"[✓](${repo}/${conhostUrl}#L$($_.consolehost) ""View console host implementation"")"}) + $c4 = $(if ($_.terminal) {"[✓](${repo}/${terminalUrl}#L$($_.terminal)} ""View windows terminal implementation"")"}) + + $shouldRenderHeader = $false + + if ($heading -ne $_.heading) { + $heading = $_.heading +@" + +## $heading + +"@ + $shouldRenderHeader = $true + } + + if ($subheading -ne $_.subheading) { + $subheading = $_.subheading +@" + +### $subheading + +"@ + $shouldRenderHeader = $true + } + + if ($shouldRenderHeader) { +@" + +|Symbol|Function|Origin 🖳|Console Host|Terminal| +|:-|:--|:--:|:--:|:--:| +"@ + } +@" + +|$c0|$c1|$c2|$c3|$c4| +"@ +}) + +--- +Generated on $(get-date -DisplayHint DateTime) +"@ +} + +function Show-Summary { + write-host "`n$(' '*7)Windows Terminal Sequencer v${myVer}" + if (-not $NoLogo.IsPresent) { + Get-Content .\windows-terminal-logo.ans | ForEach-Object { Write-Host $_ } + } + $summary = @" + `e[1mSequence Support:`e[0m + + `e[7m {0:000} `e[0m known in master-sequence-list.csv. + `e[7m {1:000} `e[0m common members in ITermDispatch base, of which: + `e[7m {2:000} `e[0m are implemented by ConsoleHost. + `e[7m {3:000} `e[0m are implemented by Windows Terminal. + +"@ -f $sequences.Count, $base.count, $conhost.count, $terminal.Count + + write-host $summary +} + +<# + Entry Point +#> + +Read-SourceFiles + +if (-not $SummaryOnly.IsPresent) { + + $markdown = Get-SequenceIndexMarkdown + + if ($PSCmdlet.ParameterSetName -eq "file") { + # send to file and overwrite + $markdown | Out-File -FilePath $OutFile -Force:$Force.IsPresent -Encoding utf8NoBOM + } else { + # send to STDOUT + $markdown + } + + if (-not $Quiet.IsPresent) { + Show-Summary + } +} else { + # summary only + Show-Summary +} diff --git a/doc/reference/master-sequence-list.csv b/doc/reference/master-sequence-list.csv new file mode 100644 index 00000000000..85f81dc7e9d --- /dev/null +++ b/doc/reference/master-sequence-list.csv @@ -0,0 +1,224 @@ +"Sequence","Parent","Description","Origin","Heading","Subheading","ImplementedBy","ConsoleHost","Terminal" +"CAN","","Cancel","`VT100`","Code Extension Functions","Control Coding","","","" +"SUB","","Substitute","`VT100`","Code Extension Functions","Control Coding","","","" +"ESC","","Escape","`VT100`","Code Extension Functions","Control Coding","","","" +"DCS","","Device Control String","`VT220`","Code Extension Functions","Control Coding","","","" +"CSI","","Control Sequence Introducer","`VT100`","Code Extension Functions","Control Coding","","","" +"ST","","String Terminator","`VT220`","Code Extension Functions","Control Coding","","","" +"OSC","","Operating System Command","`DECterm`","Code Extension Functions","Control Coding","","","" +"PM","","Privacy Message","``","Code Extension Functions","Control Coding","","","" +"APC","","Application Program Command","`VT420`","Code Extension Functions","Control Coding","","","" +"S7C1T","","Select 7-bit C1 Transmission","`VT220`","Code Extension Functions","Control Coding","","","" +"S8C1T","","Select 8-bit C1 Transmission","`VT220`","Code Extension Functions","Control Coding","","","" +"LS0","","Locking Shift Zero (SI)","`VT100`","Code Extension Functions","Character Coding","","","" +"LS1","","Locking Shift One (SO)","`VT100`","Code Extension Functions","Character Coding","","","" +"LS2","","Locking Shift Two","`VT220`","Code Extension Functions","Character Coding","","","" +"LS3","","Locking Shift Three","`VT220`","Code Extension Functions","Character Coding","","","" +"LS1R","","Locking Shift One Right","`VT220`","Code Extension Functions","Character Coding","","","" +"LS2R","","Locking Shift Two Right","`VT220`","Code Extension Functions","Character Coding","","","" +"LS3R","","Locking Shift Three Right","`VT220`","Code Extension Functions","Character Coding","","","" +"SS2","","Single Shift Two","`VT220`","Code Extension Functions","Character Coding","","","" +"SS3","","Single Shift Three","`VT220`","Code Extension Functions","Character Coding","","","" +"SCS","","Select Character Set","`VT100`","Code Extension Functions","Graphic Character Sets","","","" +"DECNRCM","","(National Replacement) Character Set Mode","`VT220`","Code Extension Functions","Graphic Character Sets","","","" +"DECAUPSS","","Assign User-Preference Supplemental Set","`VT320`","Code Extension Functions","Graphic Character Sets","","","" +"DECRQUPSS","","Request User-Preference Supplemental Set","`VT320`","Code Extension Functions","Graphic Character Sets","","","" +"DA1","","Primary Device Attributes","`VT100`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DA2","","Secondary Device Attributes","`VT220`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DA3","","Tertiary Device Attributes","`VT420`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DSR","","Device Status Report","`VT100`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECID","","Identify Device","`VT100`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECTID","","Select Terminal ID","`VT510`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECSCL","","Select Conformance Level","`VT220`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECSR","","Secure Reset","`VT420`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECSRC","","Secure Reset Confirmation","`VT420`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECSTR","","Soft Terminal Reset","`VT220`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECSTUI","","Set Terminal Unit ID (Restricted)","`VT420`","Terminal Management Functions","Identification, status, and Initialization","","","" +"RIS","","Reset to Initial state","`VT100`","Terminal Management Functions","Identification, status, and Initialization","","","" +"DECPCTERM","","Enter/Exit PC Term Mode from DEC VT mode","`VT420PC`","Terminal Management Functions","Emulations","","","" +"DECTME","","Terminal Mode Emulation","`VT510`","Terminal Management Functions","Emulations","","","" +"DECSSL","","Select Set-Up Language","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECCRTSM","","CRT Save Mode (not required)","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECOSCNM","","Overscan Mode","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECSRFR","","Select Refresh Rate","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECLTOD","","Load Time of Day","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECLBAN","","Load Banner Message","`VT510`","Terminal Management Functions","Set-Up","","","" +"DECTCEM","","Text Cursor Enable Mode","`VT220`","Display Coordinate System and Addressing","Active Position and Cursor","","","" +"DECSCUSR","","Set Cursor Style","`VT510`","Display Coordinate System and Addressing","Active Position and Cursor","","","" +"DECSTBM","","Set Top and Bottom Margin","`VT100`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECSLRM","","Set Left and Right Margin","`VT420`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECLRMM","","Left Right Margin Mode","`VT420`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECOM","","Origin Mode","`VT100`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECSCLM","","Scrolling Mode","`VT100`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"IND","","Index","`VT100`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"RI","","Reverse Index","`VT100`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECFI","","Forward Index","`VT420`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECBI","","Back Index","`VT420`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"DECSSCLS","","Set Scroll Speed","`VT510`","Display Coordinate System and Addressing","Margins and Scrolling","","","" +"BS","","Backspace","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"LF","","Line Feed","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"VT","","Vertical Tab","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"FF","","Form Feed","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CR","","Carriage Return","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"NEL","","Next Line","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"LNM","","Line Feed/New Line Mode","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CUU","","Cursor Up","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CUD","","Cursor Down","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CUF","","Cursor Forward","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CUB","","Cursor Backward","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CUP","","Cursor Position","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"HVP","","Horizontal/Vertical Position","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"DSR-CPR","","Device Status Report (Cursor Position Report)","`VT100`","Display Coordinate System and Addressing","Cursor Movement","","","" +"DSR-XCPR","","Device Status Report (Extended Cursor Position Report)","`VT340` `VT420`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CHA","","Cursor Horizontal Absolute","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CNL","","Cursor Next Line","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"CPL","","Cursor Previous Line","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"HPA","","Horizontal Position Absolute","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"HPR","","Horizontal Position Relative","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"VPA","","Vertical Line Position Absolute","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"VPR","","Vertical Position Relative","`VT510`","Display Coordinate System and Addressing","Cursor Movement","","","" +"HT","","Horizontal Tab","`VT100`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"HTS","","Horizontal Tabulation Set","`VT100`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"TBC","","Tabulation Clear","`VT100`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"CBT","","Cursor Backward Tabulation","`VT510`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"CHT","","Cursor Horizontal Forward Tabulation","`VT510`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"DECST8C","","Set Tab at every 8 columns","`VT420PC`","Display Coordinate System and Addressing","Horizontal Tabulation","","","" +"DECCOLM","","Column Mode","`VT100`","Display Coordinate System and Addressing","Page Size and Arrangement","","","" +"DECNCSM","","No Clear Screen on column Mode","`VT510`","Display Coordinate System and Addressing","Page Size and Arrangement","","","" +"DECSCPP","","Set Columns Per Page","`VT340` `VT420`","Display Coordinate System and Addressing","Page Size and Arrangement","","","" +"DECSLPP","","Set Lines Per Page","`VT340` `VT420`","Display Coordinate System and Addressing","Page Size and Arrangement","","","" +"NP","","Next Page","`VT340` `VT420`","Display Coordinate System and Addressing","Page Movement","","","" +"PP","","Preceding Page","`VT340` `VT420`","Display Coordinate System and Addressing","Page Movement","","","" +"PPA","","Page Position Absolute","`VT340` `VT420`","Display Coordinate System and Addressing","Page Movement","","","" +"PPR","","Page Position Relative","`VT340` `VT420`","Display Coordinate System and Addressing","Page Movement","","","" +"PPB","","Page Position Backward","`VT340` `VT420`","Display Coordinate System and Addressing","Page Movement","","","" +"DECSASD","","Select Active Status Display","`VT340` `VT320`","Display Coordinate System and Addressing","Status Display","","","" +"DECSSDT","","Select Status Display Type","`VT340` `VT320`","Display Coordinate System and Addressing","Status Display","","","" +"DECRLM","","Right to Left Mode","`VT510`","Display Coordinate System and Addressing","Right to Left","","","" +"DECRLCM","","Right to Left Copy Mode","`VT510`","Display Coordinate System and Addressing","Right to Left","","","" +"DDD1","","`VT100` mode Hebrew","`VT510`","Display Coordinate System and Addressing","Right to Left","","","" +"DDD2","","`VT100` mode Hebrew","`VT510`","Display Coordinate System and Addressing","Right to Left","","","" +"DDD3","","`VT100` mode Hebrew","`VT510`","Display Coordinate System and Addressing","Right to Left","","","" +"DECHCCM","","Horizontal Cursor Coupling Mode","`VT340` `VT420`","Window Management","Right to Left","","","" +"DECVCCM","","Vertical Cursor Coupling Mode","`VT340` `VT420`","Window Management","Right to Left","","","" +"DECPCCM","","Page Cursor Coupling Mode","`VT340` `VT420`","Window Management","Right to Left","","","" +"DECRQDE","","Request Displayed Extent","`VT340` `VT420`","Window Management","Right to Left","","","" +"DECSNLS","","Select Number of Lines per Screen (exception)","`VT420`","Window Management","Right to Left","","","" +"DECARSM","","Auto Resize Mode","`DECterm` `VT420`","Window Management","Right to Left","","","" +"SU","","Pan Down","`VT340` `VT420`","Window Management","Right to Left","","","" +"SD","","Pan Up","`VT340` `VT420`","Window Management","Right to Left","","","" +"DECSCNM","","Screen Mode","`VT100`","Visual Attributes and Renditions","Right to Left","","","" +"DECSWL","","Single Width Line","`VT100`","Visual Attributes and Renditions","Line Renditions","","","" +"DECDWL","","Double Width Line","`VT100`","Visual Attributes and Renditions","Line Renditions","","","" +"DECDHLT","","Double Height Line Top","`VT100`","Visual Attributes and Renditions","Line Renditions","","","" +"DECDHLB","","Double Height Line Bottom","`VT100`","Visual Attributes and Renditions","Line Renditions","","","" +"SGR","","Select Graphic Rendition","`VT100`","Visual Attributes and Renditions","Character Renditions","","","" +"BEL","","Warning Bell","`VT100`","Audible Indicators","Character Renditions","","","" +"DECSKCV","","Set Keyclick Volume","`VT510`","Audible Indicators","Character Renditions","","","" +"DECSWBV","","Set Warning Bell Volume","`VT510`","Audible Indicators","Character Renditions","","","" +"DECSMBV","","Set Margin Bell Volume","`VT510`","Audible Indicators","Character Renditions","","","" +"IRM","","Insert/Replacement Mode","`VT102`","Editing Functions","DEC Private","","","" +"ICH","","Insert Character","`VT102`","Editing Functions","DEC Private","","","" +"DCH","","Delete Character","`VT102`","Editing Functions","DEC Private","","","" +"IL","","Insert Line","`VT100`","Editing Functions","DEC Private","","","" +"DL","","Delete Line","`VT100`","Editing Functions","DEC Private","","","" +"DECIC","","Insert Column","`VT420`","Editing Functions","DEC Private","","","" +"DECDC","","Delete Column","`VT420`","Editing Functions","DEC Private","","","" +"ECH","","Erase Character","`VT100`","Editing Functions","DEC Private","","","" +"EL","","Erase in Line","`VT100`","Editing Functions","DEC Private","","","" +"DECSEL","","Selective Erase in Line","`VT220`","Editing Functions","DEC Private","","","" +"ED","","Erase in Display","`VT100`","Editing Functions","DEC Private","","","" +"DECSED","","Selective Erase in Display","`VT220`","Editing Functions","DEC Private","","","" +"DECSCA","","Select Character Attribute (selective erase)","`VT220`","Editing Functions","DEC Private","","","" +"DECCRA","","Copy Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECFRA","","Fill Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECERA","","Erase Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECSERA","","Selective Erase Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECCARA","","Change Attribute in Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECRARA","","Reverse Attribute in Rectangular Area","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECSACE","","Select Attribute Change Extent Mode","`VT420`","OLTP Features","Rectangular Area Operations","","","" +"DECRQCRA","","Request Checksum of Rectangular Area","`VT420`","OLTP Features","Data Integrity","","","" +"DSR-DECCKSR","","Device Status Report (Memory Checksum)","`VT420`","OLTP Features","Data Integrity","","","" +"DECDMAC","","Define Macro","`VT420`","OLTP Features","Macros","","","" +"DECINVM","","Invoke Macro","`VT420`","OLTP Features","Macros","","","" +"DSR-MSR","","Device Status Report (Macro Space Report)","`VT420`","OLTP Features","Macros","","","" +"DECSC","","Save Cursor","`VT100`","Saving and Restoring Terminal State","Cursor Save Buffer","","","" +"DECRC","","Restore Cursor","`VT100`","Saving and Restoring Terminal State","Cursor Save Buffer","","","" +"DECRQM","","Request Mode","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECNKM","","Numeric Keypad Mode","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECRQSS","","Request Selection or Setting","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECRQPSR","","Request Presentation State Report","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECRSPS","","Restore Presentation State","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECRQTSR","","Request Terminal State Report","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECRSTS","","Restore Terminal State","`VT320`","Saving and Restoring Terminal State","Terminal State Interrogation","","","" +"DECARM","","Autorepeat Mode","`VT100`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECBKM","","Backarrow Key Mode","`VT420`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECCKM","","Cursor Keys Mode","`VT100`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKBUM","","Keyboard Usage Mode","`VT320`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKPAM","","Keypad Application Mode","`VT100`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKPM","","Key Position Mode","`VT420`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKPNM","","Keypad Numeric Mode","`VT100`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECNKM","","Numeric Keypad Mode","`VT320`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DSR-KBD","","Device Status Report (keyboard status)","`VT220`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"KAM","","Keyboard Action Mode","`VT220`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECLFC","","Local Functions Control","`VT420`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECLFKC","","Local Function Key Control","`VT420`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECSMKR","","Select Modifier Key Reporting","`VT420`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECHEBM","","Hebrew Keyboard Map mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECHCEM","","Hebrew Encoding Mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECNAKB","","NA/Greek Selection","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECESKM","","Secondary Keyboard Language Mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECSLCK","","Set Lock Key Style","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKBD","","Keyboard Dialect Selection","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECNUMLK","","NumLock Mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECCAPSLK","","CapsLock Mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECKLHIM","","Keyboard LEDs Host Indicator Mode","`VT510`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECLL","","Load LEDs","`VT100`","Keyboard Processing Functions","Terminal State Interrogation","","","" +"DECUDK","","User Defined Keys","`VT220`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DSR-UDK","","Device Status Report (UDK lock)","`VT220`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECPKA","","Program Key Action","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECPFK","","Program Function Key","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECPAK","","Program Alphanumeric Key","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECCKD","","Copy Key Default","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECRQPKFM","","Program Key Free Memory Inquiry","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECRQKT","","Inquire a Key Type","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECRQKD","","Inquire a Key Definition","`VT510`","Soft Key Mapping (UDK)","Terminal State Interrogation","","","" +"DECDLD","","Downline Load","`VT220`","Soft Fonts (DRCS)","Terminal State Interrogation","","","" +"DECPEX","","Print Extent Mode","`VT220`","Printing","Terminal State Interrogation","","","" +"DECPFF","","Print Form Feed Mode","`VT220`","Printing","Terminal State Interrogation","","","" +"DSR-PP","","Device Status Report (printer port)","`VT220`","Printing","Terminal State Interrogation","","","" +"MC","","Media Copy","`VT220`","Printing","Terminal State Interrogation","","","" +"DECSPRTT","","Select Printer Type","`VT510`","Printing","Terminal State Interrogation","","","" +"DECSDPT","","Select Digital Printed Data Type","`VT510`","Printing","Terminal State Interrogation","","","" +"DECSPPCS","","Select Proprinter Character Set","`VT510`","Printing","Terminal State Interrogation","","","" +"BREAK","","BREAK","`VT100`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"XON","","XON","`VT100`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"XOFF","","XOFF","`VT100`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"ENQ","","Enquiry","`VT100`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"SRM","","Send Receive Mode","`VT220`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECXRLM","","Transmit Rate Limiting Mode","`VT420`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECMCM","","Modem Control Mode","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECAAM","","Auto Answerback Mode","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECLANS","","Load Answerback Message","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECCANSM","","Conceal Answerback Message Mode","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECNULM","","Ignore Null Mode","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECHPDXM","","Half Duplex Mode","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSFC","","Select Flow Control","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSDDT","","Select Disconnect Delay Time","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSTRL","","Set Transmit Rate Limit","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSCS","","Select Communication Speed","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSCP","","Select Communication Port","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECSPP","","Set Port Parameter","`VT510`","Terminal Communication and Synchronization","Terminal State Interrogation","","","" +"DECEFR","","Enable Filter Rectangle","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECELR","","Enable Locator Reports","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECLKD","","Locator Key Definition","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECLRP","","Locator Report","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECRQLP","","Request Locator Position","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECSLE","","Select Locator Events","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DSR-LS","","Device Status Report (Locator Status)","`UWS`","Text Locator Extension","Terminal State Interrogation","","","" +"DECES","","Enable Sessions","`VT340` `VT420`","Session Management Extension","Terminal State Interrogation","","","" +"DECANM","","`ANSI`/`VT52` Mode","`VT100`","Documented Exceptions","Terminal State Interrogation","","","" +"DECALN","","Screen Alignment","`VT100`","Documented Exceptions","Terminal State Interrogation","","","" +"DECAWM","","Autowrap Mode","`VT100`","Documented Exceptions","Terminal State Interrogation","","","" +"DECTST","","Invoke Confidence Test","`VT100`","Documented Exceptions","Terminal State Interrogation","","","" +"CRM","","Control Representation Mode","`VT100`","Documented Exceptions","Terminal State Interrogation","","","" diff --git a/doc/reference/solution-dependencygraph.png b/doc/reference/solution-dependencygraph.png index 62970e97000..9b586271f65 100644 Binary files a/doc/reference/solution-dependencygraph.png and b/doc/reference/solution-dependencygraph.png differ diff --git a/doc/reference/windows-terminal-logo.ans b/doc/reference/windows-terminal-logo.ans new file mode 100644 index 00000000000..5620900d99b --- /dev/null +++ b/doc/reference/windows-terminal-logo.ans @@ -0,0 +1,15 @@ +                                                   +       ▀                                  ▀        +                                                   +       ▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀  ▀  ▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀         +       ▀ ▀▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀         +        ▀▀▀▀▀▀▀▀▀▀  ▀  ▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀  ▀          +        ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀        +       ▀▀  ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ ▀  ▀ ▀▀ ▀▀        +       ▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ ▀  ▀  ▀         +       ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀  ▀ ▀▀ ▀▀ ▀▀ ▀▀         +        ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀        +       ▀▀▀▀▀▀▀▀▀▀▀▀  ▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀        +       ▀   ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀        +       ▀ ▀ ▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀▀        +                                                   diff --git a/doc/specs/#1235 - Azure cloud shell connector/images/azProf.png b/doc/specs/#1235 - Azure cloud shell connector/images/azProf.png index 9f07c354046..42a6a0c6b27 100644 Binary files a/doc/specs/#1235 - Azure cloud shell connector/images/azProf.png and b/doc/specs/#1235 - Azure cloud shell connector/images/azProf.png differ diff --git a/doc/specs/#1235 - Azure cloud shell connector/images/solDesign.png b/doc/specs/#1235 - Azure cloud shell connector/images/solDesign.png index 3f33fe76984..4ffe794d793 100644 Binary files a/doc/specs/#1235 - Azure cloud shell connector/images/solDesign.png and b/doc/specs/#1235 - Azure cloud shell connector/images/solDesign.png differ diff --git a/doc/specs/#1337 - Per-Profile Tab Colors/#1337 - Per-Profile Tab Colors.md b/doc/specs/#1337 - Per-Profile Tab Colors/#1337 - Per-Profile Tab Colors.md new file mode 100644 index 00000000000..e2830babbe7 --- /dev/null +++ b/doc/specs/#1337 - Per-Profile Tab Colors/#1337 - Per-Profile Tab Colors.md @@ -0,0 +1,146 @@ +--- +author: Mike Griese @zadjii-msft +created on: 2020-07-31 +last updated: 2020-08-03 +issue id: #1337 +--- +# Per-Profile Tab Colors + +## Abstract + +This spec describes a way to specify tab colors in a profile in a way that will +be forward compatible with theming the Terminal. This spec will be largely +dedicated to the design of a single setting, but within the context of theming. + +## Inspiration + +Following the addition of the Tab Color Picker in [#3789], we've had numerous +requests for the ability to set the color of a tab directly within a profile. +While largely we're tracking theming in [#3327], including the specification of +a tab color, the theming spec ([#5772] )is very large and will take a while to +revise and approve. This spec is intended to pull a single point out from that +spec to make it more easily reviewable, and implement it in a way that will +continue working when we add support for themes in the future. + +## Solution Design + +To enable per-profile tab colors, we'll add a single setting: `tabColor`. For +now[[1](#user-content-footnote-1)], this setting will accept any +`#rrggbb` color string. + +Since each profile creates a `Pane` with a `TermControl`, we'll need to store +this color not in the `Tab`, but somewhere below `Pane`, so that when you switch +between Panes in a tab with different `tabColor`s, the color will update +automatically. When a new `TermControl` is created, we'll store this color in the +`TermControl`'s `Terminal` core. This is to support the future possibility of +setting the tab color via VT sequences. + +A Tab's color will be the result of layering a variety of sources, from the bottom up: + +Color | | Set by +-- | -- | -- +Runtime Color | _optional_ |Color Picker / `setTabColor` action +Control Tab Color | _optional_ | Profile's `tabColor`, or a color set by VT +Theme Tab Background | _optional_ | `tab.backgroundColor` in the theme +Tab Default Color | **default** | TabView in XAML + +Some examples: +* **Scenario 1**: The user has set `"tabColor": "#ff0000"` in their profile. + When they create tabs with that profile, instead of appearing in the default + color for the TabView, they'll be `#ff0000`. +* **Scenario 2**: The user has set `"tabColor": "#ff0000"` in their profile. + When they try to set the color for that tab (with the color picker) to + `#0000ff`, the tab's color is updated to reflect this new blue color. When + they clear the runtime color (with the color picker), the tab will return to + `#ff0000`. +* **Scenario 3**: The user has two profiles with colors set, one to `"tabColor": + "#ff0000"`, and the other with `"tabColor": "#00ff00"`. If they open these + profiles in two panes side-by side, the tab's color will update to reflect the + color from the currently-focused control. +* **Scenario 4**: The user has two profiles with colors set, one to `"tabColor": + "#ff0000"`, and the other with `"tabColor": "#00ff00"`. If they open these + profiles in two panes side-by side, and try to set the color for that tab + (with the color picker) to `#0000ff`, the tab's color is updated to reflect + this new blue color. Regardless of which pane is focused, the tab will be + blue. +* **Scenario 5**: The user has set `"tabColor": "#ff0000"` in their profile + ("Profile A"), and `"tab.backgroundColor": "#00ff00"`in their theme. When they + create tabs with "Profile A", the tabs will appear red. Other tabs (for + profiles without `tabColor` set) will appear green, using the color from the + theme. + + +## UI/UX Design + +In general, this is going to look exactly like the colored tabs look now. + +![preview](profile-tabColor-000.gif) + +## Capabilities + + + + + + + + + + + + + + + + + + + + + + +
Accessibility +N/A +
Security +N/A +
Reliability +No expected change +
Compatibility +This entire spec outlines how this feature is designed with a emphasis on future +compatibility. As such, there are no expected regressions in the future when we +do add support for themes. +
Performance, Power, and Efficiency +No expected change +
+ +## Potential Issues + +None expected. + +## Footnotes + +[1]: When full theming support is added, themes will +provide support for setting colors as one of a variety of values: + + * An `#rrggbb` string + * The system accent color + * The current background color of the Terminal + * A value from a given resource key from XAML + +When support for these other types of "smart" colors is added, then the profile +`tabColor` setting will also gracefully accept these values. + +## Future considerations + +* It's not out of the realm of possibility that someone might want to color each + _pane_'s color at runtime. In that case, the runtime color would be stored in + the `Pane`, not the `Tab`. + + + + + +[#1337]: https://github.com/microsoft/terminal/issues/1337 +[#3789]: https://github.com/microsoft/terminal/issues/3789 +[#3327]: https://github.com/microsoft/terminal/issues/3327 +[#5772]: https://github.com/microsoft/terminal/pull/5772 diff --git a/doc/specs/#1337 - Per-Profile Tab Colors/profile-tabColor-000.gif b/doc/specs/#1337 - Per-Profile Tab Colors/profile-tabColor-000.gif new file mode 100644 index 00000000000..2b3e04a6513 Binary files /dev/null and b/doc/specs/#1337 - Per-Profile Tab Colors/profile-tabColor-000.gif differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/CommandPaletteExample.png b/doc/specs/#1502 - Advanced Tab Switcher/img/CommandPaletteExample.png index 93c4879203a..5946b11aeb7 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/CommandPaletteExample.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/CommandPaletteExample.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeMinimumTabSwitcherSize.png b/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeMinimumTabSwitcherSize.png index aba54c7125e..1f359a8cb13 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeMinimumTabSwitcherSize.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeMinimumTabSwitcherSize.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeTabSwitcher.png b/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeTabSwitcher.png index d7c5bc27140..c6d5772a9b9 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeTabSwitcher.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/VSCodeTabSwitcher.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSize.png b/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSize.png index c55f25a659b..34dde40a54e 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSize.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSize.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSizeWithTabSwitcher.png b/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSizeWithTabSwitcher.png index 3682c19e62b..f6b8603af58 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSizeWithTabSwitcher.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSizeWithTabSwitcher.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/VSTabSwitcher.png b/doc/specs/#1502 - Advanced Tab Switcher/img/VSTabSwitcher.png index cdbf8066b60..104d12d48bf 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/VSTabSwitcher.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/VSTabSwitcher.png differ diff --git a/doc/specs/#1502 - Advanced Tab Switcher/img/tmuxPaneSwitching.png b/doc/specs/#1502 - Advanced Tab Switcher/img/tmuxPaneSwitching.png index c4d2f0c2509..ea9c1b3fbea 100644 Binary files a/doc/specs/#1502 - Advanced Tab Switcher/img/tmuxPaneSwitching.png and b/doc/specs/#1502 - Advanced Tab Switcher/img/tmuxPaneSwitching.png differ diff --git a/doc/specs/#1564 - Settings UI/appearance.png b/doc/specs/#1564 - Settings UI/appearance.png new file mode 100644 index 00000000000..caaeabf1533 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/appearance.png differ diff --git a/doc/specs/#1564 - Settings UI/design.md b/doc/specs/#1564 - Settings UI/design.md new file mode 100644 index 00000000000..23ecaead011 --- /dev/null +++ b/doc/specs/#1564 - Settings UI/design.md @@ -0,0 +1,112 @@ +--- +author: Kayla Cinnamon @cinnamon-msft +created on: 2020-07-13 +last updated: 2020-08-11 +issue id: #1564 +--- + +# Settings UI Design + +## Abstract + +This design document describes how each page of the settings UI will be laid out along with design mockups to display how the UI will appear. The mock ups are for appearance purposes and some layouts and naming may be different in this doc. This doc should be considered the final say. + +## UI Design + +### Overall navigation with Startup page + +This is the list of the top-level navigation items that will appear on the left nav bar: + +- General + - Startup + - Interaction + - Rendering +- Appearance + - Global + - Color schemes + - Themes* +- Profiles + - Defaults + - Enumerate profiles + - Add new +- Keyboard +- Mouse* +- Command Palette* +- Marketplace* + +\* Themes, mouse, command palette, and marketplace will be added once they're implemented. + +![Overall navigation](./navigation-2.png) + +### Profile appearance page + +This page requires special design because it includes the TerminalControl window to preview appearance changes. This preview window will appear on the following pages: + +- Appearance - Color Schemes +- Profiles - Appearance + +![Appearance page](./appearance.png) + +### Keyboard page + +The keyboard page will list the enabled key bindings and provide a way for users to add and remove them. + +![Keyboard page](./keyboard.png) + +When someone hovers over one of the items in the table, the Edit and Delete buttons will appear. Below is what the modal looks like if they were to click Edit on a command that does not have any arguments/actions. In the future, we would want this text box to be able to listen for key combinations. This would add a "listen" button to the UI. + +![Keyboard page modal](./keyboard-modal.png) + +If the command they select has additional arguments/actions, the modal will dynamically size as arguments/actions are added. + +![Keyboard page modal add new arguments](./keyboard-modal-add.png) + +![Keyboard page modal arguments](./keyboard-modal-args.png) + +## Settings layout + +Below is the list of all settings on their respective pages in the settings UI. The title row aligns with the navigation view on the left of the UI. Bolded headers in those columns align with top nav on the page. + +| General - Startup | General - Interaction | General - Rendering | Appearance - Global | Appearance - Color Schemes | Profiles - Global | Profiles - Enumerate profiles | Profiles - Add new | +| ---------------- | --------------------- | ------------------- | ------------------- | -------------------------- | ----------------- | ----------------------------- | ------------------ | +| Default profile (dropdown) | Copy after selection is made (checkbox) | Software rendering (checkbox) | Theme (radio) | Name (text box) | **General** | **General** | **General** | **General** | +| Launch on startup (checkbox) | Copy formatting (checkbox) | Screen redrawing (checkbox) | Show/Hide the title bar (checkbox) | Cursor color (color picker) | Command line (text box) | Scrollbar visibility (radio) | Scrollbar visibility (radio) | +| Launch size (radio) | Word delimiters (text box) | | Show terminal title in title bar (checkbox) | Selection background (color picker) | Starting directory (browse button) | Command line (browse button) | Command line (browse button) | +| Launch position (text box) | Window resize behavior (checkbox) | | Always show tabs (checkbox) | Background (color picker) | Icon (browse button) | Starting directory (browse button) | Starting directory (browse button) | +| Columns on first launch (number picker) | | | Tab width mode (radio) | Foreground (color picker) | Tab title (text box) | Name (text box) | Name (text box) | +| Rows on first launch (number picker) | | | Hide close all tabs popup (checkbox) | Black (color picker) | Scrollbar visibility (radio) | Icon (browse button) | Icon (browse button) | +| Automatically create new profiles when new shells are installed (checkbox) | | | | Blue (color picker) | **Appearance** | Tab title (text box) | Tab title (text box) | +| | | | | Cyan (color picker) | Font face (text box) | **Appearance** | **Appearance** | +| | | | | Green (color picker) | Font size (number picker) | Retro terminal effects (checkbox) | Retro terminal effects (checkbox) | +| | | | | Purple (color picker) | Font weight (dropdown) | Font face (text box) | Font face (text box) | +| | | | | Red (color picker) | Padding (text box) | Font size (number picker) | Font size (number picker) | +| | | | | White (color picker) | Cursor shape (radio) | Font weight (dropdown) | Font weight (dropdown) | +| | | | | Yellow (color picker) | Cursor color (color picker) | Padding (text box) | Padding (text box) | +| | | | | Bright black (color picker) | Cursor height (number picker) | Cursor shape (radio) | Cursor shape (radio) | +| | | | | Bright blue (color picker) | Color scheme (dropdown) | Cursor color (color picker) | Cursor color (color picker) | +| | | | | Bright cyan (color picker) | Foreground color (color picker) | Cursor height (number picker) | Cursor height (number picker) | +| | | | | Bright green (color picker) | Background color (color picker) | Color scheme (dropdown) | Color scheme (dropdown) | +| | | | | Bright purple (color picker) | Selection background color (color picker) | Foreground color (color picker) | Foreground color (color picker) | +| | | | | Bright red (color picker) | Enable acrylic (checkbox) | Background color (color picker) | Background color (color picker) | +| | | | | Bright white (color picker) | Acrylic opacity (number picker) | Selection background color (color picker) | Selection background color (color picker) | +| | | | | Bright yellow (color picker) | Background image (browse button) | Enable acrylic (checkbox) | Enable acrylic (checkbox) | +| | | | | | Background image stretch mode (radio) | Acrylic opacity (number picker) | Acrylic opacity (number picker) | +| | | | | | Background image alignment (dropdown) | Background image (browse button) | Background image (browse button) | +| | | | | | Background image opacity (number picker) | Background image stretch mode (radio) | Background image stretch mode (radio) | +| | | | | | Retro terminal effects (checkbox) | Background image alignment (dropdown) | Background image alignment (dropdown) | +| | | | | | **Advanced** | Background image opacity (number picker) | Background image opacity (number picker) | +| | | | | | Hide profile from dropdown (checkbox) | **Advanced** | **Advanced** | +| | | | | | Suppress title changes (checkbox) | GUID (text box) | GUID (text box) | +| | | | | | Antialiasing text (radio) | Hide profile from dropdown (checkbox) | Hide profile from dropdown (checkbox) | +| | | | | | AltGr aliasing (checkbox) | Suppress title changes (checkbox) | Suppress title changes (checkbox) | +| | | | | | Scroll to input when typing (checkbox) | Antialiasing text (radio) | Antialiasing text (radio) | +| | | | | | History size (number picker) | AltGr aliasing (checkbox) | AltGr aliasing (checkbox) | +| | | | | | How the profile closes (radio) | Scroll to input when typing (checkbox) | Scroll to input when typing (checkbox) | +| | | | | | | History size (number picker) | History size (number picker) | +| | | | | | | How the profile closes (radio) | How the profile closes (radio) | + +## Potential Issues + +## Future considerations + +## Resources diff --git a/doc/specs/#1564 - Settings UI/keyboard-modal-add.png b/doc/specs/#1564 - Settings UI/keyboard-modal-add.png new file mode 100644 index 00000000000..137afee8cf0 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/keyboard-modal-add.png differ diff --git a/doc/specs/#1564 - Settings UI/keyboard-modal-args.png b/doc/specs/#1564 - Settings UI/keyboard-modal-args.png new file mode 100644 index 00000000000..9af6f3acb21 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/keyboard-modal-args.png differ diff --git a/doc/specs/#1564 - Settings UI/keyboard-modal.png b/doc/specs/#1564 - Settings UI/keyboard-modal.png new file mode 100644 index 00000000000..31c18e61f38 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/keyboard-modal.png differ diff --git a/doc/specs/#1564 - Settings UI/keyboard.png b/doc/specs/#1564 - Settings UI/keyboard.png new file mode 100644 index 00000000000..a470caa8709 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/keyboard.png differ diff --git a/doc/specs/#1564 - Settings UI/navigation-2.png b/doc/specs/#1564 - Settings UI/navigation-2.png new file mode 100644 index 00000000000..2b27658d533 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/navigation-2.png differ diff --git a/doc/specs/#1564 - Settings UI/navigation.png b/doc/specs/#1564 - Settings UI/navigation.png new file mode 100644 index 00000000000..5ce456469c2 Binary files /dev/null and b/doc/specs/#1564 - Settings UI/navigation.png differ diff --git a/doc/specs/#1564 - Settings UI/spec.md b/doc/specs/#1564 - Settings UI/spec.md new file mode 100644 index 00000000000..77fe70d7dfa --- /dev/null +++ b/doc/specs/#1564 - Settings UI/spec.md @@ -0,0 +1,122 @@ +--- +author: Kayla Cinnamon @cinnamon-msft +created on: 2020-06-29 +last updated: 2020-08-10 +issue id: #1564 +--- + +# Settings UI Implementation + +## Abstract + +This spec describes the basic functionality of the settings UI, including disabling it, the navigation items, launch methods, and editing of settings. The specific layout of each page will defined in later design reviews. + +## Inspiration + +We have been wanting a settings UI since the dawn of Terminal time, so we need to define how it will interact with the application and how users should expect to interact with it. + +## Solution Design + +The settings UI will be the default experience. We will provide users an option to skip the settings UI and edit the raw JSON file. + +### Ability to disable displaying the settings UI + +Some users don't want a UI for the settings. We can update the `openSettings` key binding with a `settingsUI` option. + +If people still like the UI but want to access the JSON file, we can provide an "Open the JSON file" button at the bottom of the navigation menu. + +### Launch method: launch in a new tab + +Clicking the settings button in the dropdown menu will open the settings UI in a new tab. This helps us take steps toward supporting non-terminal content in a tab. Users will be able to see their visual changes by using the preview window inside the settings UI on relevant pages. + +#### We also considered: launch in a new window + +Clicking the settings button in the dropdown menu will open the settings UI in a new window. This allows the user to edit their settings and see the Terminal live update with their changes. + +In the Windows taskbar, the icon will appear as if Terminal has multiple windows open. + +### Editing and saving settings: implement a save button + +Users will only see their settings changes take place once they click "Save". Clicking "Save" will write to the settings.json file. This aligns with the functionality that exists today by editing the settings.json file in a text editor and saving it. + +We will also be adding a TerminalControl inside the settings UI to preview what the changes will look like before actually saving them to the settings.json file. + +#### We also considered: automatically save settings + +As users edit fields in the settings UI, they are automatically saved and written to the JSON file. This allows the user to see their settings changes taking place in real time. + +## UI/UX Design + +Layout of all of the settings per page can be found in the [design doc](./design.md). + +### Top-level navigation: more descriptive navigation + +The navigation menu is broken up into more digestible sections. This aligns more closely to other terminals. The following are the proposed navigation items: + +- General + - Startup + - Interaction + - Rendering +- Appearance + - Global + - Color schemes + - Themes* +- Profiles + - Defaults + - Enumerate profiles + - Add new +- Keyboard +- Mouse* +- Command Palette* +- Marketplace* + +\* Themes, mouse, command palette, and marketplace will be added once they're implemented. + +![Settings UI navigation 2](./navigation-2.png) + +#### We also considered: align with JSON + +The settings UI could have top-level navigation that aligns with the overall structure of the settings.json file. The following are the proposed navigation items: + +- Globals +- Profiles +- Color schemes +- Bindings + +For Bindings, it would have key bindings, mouse bindings, and command palette inside it. + +![Settings UI navigation 1](./navigation.png) + +## Capabilities + +### Accessibility + +This will have to undergo full accessibility testing because it is a new UI element. All items inside the settings UI should be accessible by a screen reader and the keyboard. Additionally, all of the settings UI will have to be localized. + +### Security + +This does not impact security. + +### Reliability + +This will not improve reliability. + +### Compatibility + +This will change the default experience to open the UI, rather than the JSON file in a text editor. This behavior can be reverted with the setting listed [above](#ability-to-disable-displaying-the-settings-ui). + +### Performance, Power, and Efficiency + +This does not affect performance, power, nor efficiency. + +## Potential Issues + +## Future considerations + +- We will have to have design reviews for all of the content pages. +- The `hidden` property will need special consideration. Ideally, all profiles will appear in the settings regardless if `hidden` is set to `true`. +- We should have undo functionality. In a text editor, you can type `Ctrl+Z` however the settings UI is a bit more complex. +- Once we have a marketplace for themes and extensions, this should be added to the top-level navigation. +- As we add more features, the top-level navigation is subject to change in favor of improved usability. + +## Resources 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 new file mode 100644 index 00000000000..ee0f9ee79a8 --- /dev/null +++ b/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md @@ -0,0 +1,300 @@ +--- +author: Mike Griese @zadjii-msft +created on: 2020-5-13 +last updated: 2020-08-04 +issue id: 1571 +--- + +# New Tab Menu Customization + +## Abstract + +Many users have lots and _lots_ of profiles that they use. Some of these +profiles the user might not use that frequently. When that happens, the new tab +dropdown can become quite cluttered. + +A common ask is for the ability to reorder and reorganize this dropdown. This +spec provides a design for how the user might be able to specify the +customization in their settings. + +## Inspiration + +Largely, this spec was inspired by discussion in +[#1571](https://github.com/microsoft/terminal/issues/1571#issuecomment-519504048) +and the _many_ linked threads. + +## Solution Design + +This design proposes adding a new setting `"newTabMenu"`. When unset, (the +default), the new tab menu is populated with all the profiles, in the order they +appear in the users settings file. When set, this enables the user to control +the appearance of the new tab dropdown. Let's take a look at an example: + +```json +{ + "profiles":{ ... }, + "newTabMenu": [ + { "type":"profile", "profile": "cmd" }, + { "type":"profile", "profile": "Windows PowerShell" }, + { "type":"separator" }, + { + "type":"folder", + "name": "ssh", + "icon": "C:\\path\\to\\icon.png", + "entries":[ + { "type":"profile", "profile": "Host 1" }, + { "type":"profile", "profile": "8.8.8.8" }, + { "type":"profile", "profile": "Host 2" } + ] + }, + { "type":"separator" }, + { "type":"profile", "profile": "Ubuntu-18.04" }, + { "type":"profile", "profile": "Fedora" } + ] +} +``` + +If a user were to use this as their new tab menu, that they would get is a menu +that looks like this: + +![fig 1](Menu-Customization-000.png) + +_fig 1_: A _very rough_ mockup of what this feature might look like + +There are five `type`s of objects in this menu: +* `"type":"profile"`: This is a profile. Clicking on this entry will open a new + tab, with that profile. The profile is identified with the `"profile"` + parameter, which accepts either a profile `name` or GUID. The icon for this + entry will be the profile's icon, and the text on the entry will be the + profile's name. +* `"type":"separator"`: This represents a XAML `MenuFlyoutSeparator`, enabling + the user to visually space out entries. +* `"type":"folder"`: This represents a nested menu of entries. + - The `"name"` property provides a string of text to display for the group. + - The `"icon"` property provides a path to a image to use as the icon. This + property is optional. + - 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! +* `"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]) + to identify the action to perform when the user selects the entry. Actions + with invalid IDs will be ignored and omitted from the list. + - The text for this entry will be the action's label (which is + either provided as the `"name"` in the global list of actions, or the + generated name if no `name` was provided) + - The icon for this entry will similarly re-use the action's `icon`. +* `"type":"remainingProfiles"`: This is a special type of entry that will be + expanded to contain one `"type":"profile"` entry for every profile that was + not already listed in the menu. This will allow users to add one entry for + just "all the profiles they haven't manually added to the menu". + - This type of entry can only be specified once - trying to add it to the menu + twice will raise a warning, and ignore all but the first `remainingProfiles` + entry. + - This type of entry can also be set inside a `folder` entry, allowing users + to highlight only a couple profiles in the top-level of the menu, but + 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 + +The "default" new tab menu could be imagined as the following blob of json: + +```json +{ + "newTabMenu": [ + { "type":"remainingProfiles" } + ] +} +``` + +### Other considerations + +Also considered during the investigation for this feature was re-using the list +of profiles to expose the structure of the new tab menu. For example, doing +something like: + +```json +"profiles": { + "defaults": {}, + "list": + [ + { "name": "cmd" }, + { "name": "powershell" }, + { "type": "separator" }, + { + "type": "folder" , + "profiles": [ + { "name": "ubuntu" } + ] + } + ] +} +``` + +This option was not pursued because we felt that it needlessly complicated the +contents of the list of profiles objects. We'd rather have the `profiles` list +exclusively contain `Profile` objects, and have other elements of the json +_refer_ to those profiles. What if someone would like to have an action that +opened a new tab with profile index 4, and then they set that action as entry 4 +in the profile's list? That would certainly be some sort of unexpected behavior. + +Additionally, what if someone wants to have an entry that opens a tab with one +pane with one profile in it, and another pane with different profile in it? Or +what if they want the same profile to appear twice in the menu? + +By overloading the structure of the `profiles` list, we're forcing all other +consumers of the list of profiles to care about the structure of the elements of +the list. These other consumers should only really care about the list of +profiles, and not necessarily how they're structured in the new tab dropdown. +Furthermore, it complicates the list of profiles, by adding actions intermixed +with the profiles. + +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. + +## UI/UX Design + +See the above _figure 1_. + +The profile's `icon` will also appear as the icon on `profile` entries. If +there's a keybinding bound to open a new tab with that profile, then that will +also be added to the `MenuFlyoutItem` as the accelerator text, similar to the +text we have nowadays. + +Beneath the list of profiles will _always_ be the same "Settings", "Feedback" +and "About" entries, separated by a `MenuFlyoutSeparator`. This is consistent +with the UI as it exists with no customization. These entries cannot be removed +with this feature, only the list of profiles customized. + +## Capabilities + +### Accessibility + +This menu will be added to the XAML tree in the same fashion as the current new +tab flyout, so there should be no dramatic change here. + +### Security + +_(no change expected)_ + +### Reliability + +_(no change expected)_ + +### Compatibility + +_(no change expected)_ + +### Performance, Power, and Efficiency + +_(no change expected)_ + +## Potential Issues + +Currently, the `openTab` and `splitPane` keybindings will accept a `index` +parameter to say either: +* "Create a new tab/pane with the N'th profile" +* "Create a new tab/pane with the profile at index N in the new +tab dropdown". + +These two were previously synonymous, as the N'th profile was always the N'th in +the dropdown. However, with this change, we'll be changing the meaning of that +argument to mean explicitly the first option - "Open a tab/pane with the N'th +profile". + +A previous version of this spec considered changing the meaning of that +parameter to mean "open the entry at index N", the second option. However, in +[Command Palette, Addendum 1], we found that naming that command would become +unnecessarily complex. + +To cover that above scenario, we could consider adding an `index` parameter to +the `openNewTabDropdown` action. If specified, that would open either the N'th +action in the dropdown (ignoring separators), or open the dropdown with the n'th +item selected. + +The N'th entry in the menu won't always be a profile: it might be a folder with +more options, or it might be an action (that might not be opening a new tab/pane +at all). + +Given all the above scenarios, `openNewTabDropdown` with an `"index":N` +parameter will behave in the following ways. If the Nth top-level entry in the +new tab menu is a: +* `"type":"profile"`: perform the `newTab` or `splitPane` action with that profile. +* `"type":"folder"`: Focus the first element in the sub menu, so the user could + navigate it with the keyboard. +* `"type":"separator"`: Ignore these when counting top-level entries. +* `"type":"action"`: Perform the action. + +So for example: + +``` +New Tab Button ▽ +├─ Folder 1 +│ └─ Profile A +│ └─ Action B +├─ Separator +├─ Folder 2 +│ └─ Profile C +│ └─ Profile D +├─ Action E +└─ Profile F +``` + +And assuming the user has bound: +```json +{ + "bindings": + [ + { "command": { "action": "openNewTabDropdown", "index": 0 }, "keys": "ctrl+shift+1" }, + { "command": { "action": "openNewTabDropdown", "index": 1 }, "keys": "ctrl+shift+2" }, + { "command": { "action": "openNewTabDropdown", "index": 2 }, "keys": "ctrl+shift+3" }, + { "command": { "action": "openNewTabDropdown", "index": 3 }, "keys": "ctrl+shift+4" }, + ] +} +``` + +* ctrl+shift+1 focuses "Profile A", but the user needs to press + enter/space to creates a new tab/split +* ctrl+shift+2 focuses "Profile C", but the user needs to press + enter/space to creates a new tab/split +* ctrl+shift+3 performs Action E +* ctrl+shift+4 Creates a new tab/split with Profile F + +## Future considerations + +* The user could set a `"name"`/`"text"`, or `"icon"` property to these menu + items manually, to override the value from the profile or action. These + settings would be totally optional, but it's not unreasonable that someone + might want this. +* We may want to consider adding a default icon for all folders or actions in + the menu. For example, a folder (like 📁) for `folder` entries, or something + like ⚡ for actions. We'll leave these unset by default, and evaluate setting + these icons by default in the future. +* Something considered during review was a way to specify "All my WSL profiles". + Maybe the user wants to have all their profiles generated by the WSL Distro + Generator appear in a "WSL" folder. This would likely require a more elaborate + filtering syntax, to be able to select only profiles where a certain property + has a specific value. Consider the user who has multiple "SSH + me@\.com" profiles, and they want all their "SSH\*" profiles to + appear in an "SSH" folder. This feels out-of-scope for this spec. +* A similar structure could potentially also be used for customizing the context + menu within a control, or the context menu for the tab. (see [#3337]) + - In both of those cases, it might be important to somehow refer to the + context of the current tab or control in the json. Think for example about + "Close tab" or "Close other tabs" - currently, those work by _knowing_ which + tab the "action" is specified for, not by actually using a `closeTab` action. + In the future, they might need to be implemented as something like + - 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}" }` + + + +[#2046]: https://github.com/microsoft/terminal/issues/2046 +[Command Palette, Addendum 1]: https://github.com/microsoft/terminal/blob/main/doc/specs/%232046%20-%20Unified%20keybindings%20and%20commands%2C%20and%20synthesized%20action%20names.md + +[#3337]: https://github.com/microsoft/terminal/issues/3337 +[#6899]: https://github.com/microsoft/terminal/issues/6899 +[#7175]: https://github.com/microsoft/terminal/issues/7175 diff --git a/doc/specs/#1571 - New Tab Menu Customization/Menu-Customization-000.png b/doc/specs/#1571 - New Tab Menu Customization/Menu-Customization-000.png new file mode 100644 index 00000000000..d673d69169b Binary files /dev/null and b/doc/specs/#1571 - New Tab Menu Customization/Menu-Customization-000.png differ diff --git a/doc/specs/#2046 - Unified keybindings and commands, and synthesized action names.md b/doc/specs/#2046 - Unified keybindings and commands, and synthesized action names.md index 56cd658b3ff..de7832d7ed3 100644 --- a/doc/specs/#2046 - Unified keybindings and commands, and synthesized action names.md +++ b/doc/specs/#2046 - Unified keybindings and commands, and synthesized action names.md @@ -605,4 +605,4 @@ as well as 3 schemes: "Scheme 1", "Scheme 2", and "Scheme 3". -[Command Palette Spec]: https://github.com/microsoft/terminal/blob/master/doc/specs/%232046%20-%20Command%20Palette.md +[Command Palette Spec]: https://github.com/microsoft/terminal/blob/main/doc/specs/%232046%20-%20Command%20Palette.md diff --git a/doc/specs/#4191 - Formatted Copy/twitter-poll.png b/doc/specs/#4191 - Formatted Copy/twitter-poll.png index 8d9e50dd80d..9616ce627c3 100644 Binary files a/doc/specs/#4191 - Formatted Copy/twitter-poll.png and b/doc/specs/#4191 - Formatted Copy/twitter-poll.png differ diff --git a/doc/specs/#605 - Search/images/SearchBoxControl.png b/doc/specs/#605 - Search/images/SearchBoxControl.png index 27fd0292abb..529258dbd0c 100644 Binary files a/doc/specs/#605 - Search/images/SearchBoxControl.png and b/doc/specs/#605 - Search/images/SearchBoxControl.png differ diff --git a/doc/specs/#605 - Search/images/SearchBoxControlNoEnoughWidth.png b/doc/specs/#605 - Search/images/SearchBoxControlNoEnoughWidth.png index 05fba3c4ae4..278e664a97b 100644 Binary files a/doc/specs/#605 - Search/images/SearchBoxControlNoEnoughWidth.png and b/doc/specs/#605 - Search/images/SearchBoxControlNoEnoughWidth.png differ diff --git a/doc/specs/#605 - Search/images/SearchBoxUpSelected.png b/doc/specs/#605 - Search/images/SearchBoxUpSelected.png index b6c4faf4c2f..22e29bae9b2 100644 Binary files a/doc/specs/#605 - Search/images/SearchBoxUpSelected.png and b/doc/specs/#605 - Search/images/SearchBoxUpSelected.png differ diff --git a/doc/specs/#6899 - Action IDs/#6899 - Action IDs.md b/doc/specs/#6899 - Action IDs/#6899 - Action IDs.md new file mode 100644 index 00000000000..c3d2871cb36 --- /dev/null +++ b/doc/specs/#6899 - Action IDs/#6899 - Action IDs.md @@ -0,0 +1,228 @@ +--- +author: Mike Griese @zadjii-msft +created on: 2020-07-13 +last updated: 2020-07-22 +issue id: 6899 +--- + +# Action IDs + +## Abstract + +This document is intended to serve as an addition to the [Command Palette Spec], +as well as the [New Tab Menu Customization Spec]. + +As we come to rely more on actions being a mechanism by which the user defines +"do something in the Terminal", we'll want to make it even easier for users to +re-use the actions that they've already defined, as to reduce duplicated json as +much as possible. This spec proposes a mechanism by which actions could be +uniquely identifiable, so that the user could refer to bindings in other +contexts without needing to replicate an entire json blob. + +## Solution Design + +This spec was largely inspired by the following diagram from @DHowett: + +![figure 1](data-mockup.png) + +The goal is to introduce an `id` parameter by which actions could be uniquely +refered to. If we'd ever like to use an action outside the list of `actions`, we +can simply refer to the action's ID, allowing the user to only define the action +_once_. + +We'll start by renaming `bindings` to `actions`. `bindings` was suggested as a +rename for `keybindings` in [#6532], as a way to make the name more generic. +Discussion with the team lead to the understanding that the name `actions` would +be even better, as a way of making the meaning of the "list of actions" more +obvious. + +When we're parsing `actions`, we'll make three passes: +* The first pass will scan the list for objects with an `id` property. We'll + attempt to parse those entries into `ActionAndArgs` which we'll store in the + global `id->ActionAndArgs` map. If any entry doesn't have an `id` set, we'll + skip it in this phase. If an entry doesn't have a `command` set, we'll ignore + it in this pass. +* The second pass will scan for _keybindings_. Any entries with `keys` set will + create a `KeyChord->ActionAndArgs` entry in the keybindings map. If the entry + has an `id` set, then we'll simply re-use the action we've already parsed for + the `id`, from the action map. If there isn't an `id`, then we'll parse the + action manually at this time. Entries without a `keys` set will be ignored in + this pass. +* The final pass will be to generate _commands_. Similar to the keybindings + pass, we'll attempt to lookup actions for entries with an `id` set. If there + isn't an `id`, then we'll parse the action manually at this time. We'll then + get the name for the entry, either from the `name` property if it's set, or + the action's `GenerateName` method. + +For a visual representation, let's assume the user has the following in their +`actions`: + +![figure 2](data-mockup-actions.png) + +We'll first parse the `actions` to generate the mapping of `id`->`Actions`: + +![figure 3](data-mockup-actions-and-ids.png) + +Then, we'll parse the `actions` to generate the mapping of keys to actions, with +some actions already being defined in the map of `id`->`Actions`: + +![figure 4](data-mockup-actions-and-ids-and-keys.png) + + +When layering `actions`, if a later settings file contains an action with the +same `id`, it will replace the current value. In this way, users can redefine +actions, or remove default ones (with something like `{ "id": +"Terminal.OpenTab", "command":null }` + +We'd maintain a large list of default actions, each with unique `id`s set. These +are all given `id`'s with a `Terminal.` prefix, to easily identify them as +built-in, default actions. Not all of these actions will be given keys, but they +will all be given `id`s. + +> 👉 NOTE: The IDs for the default actions will need to be manually created, not +> autogenerated. These `id`s are not strings displayed in the user interface, so +> localization is not a concern. + +As we add additional menus to the Terminal, like the customization for the new +tab dropdown, or the tab context menu, or the `TermControl` context menu, they +could all refer to these actions by `id`, rather than duplicating the same json. + + +### Existing Scenarios + +Keybindings will still be stored as a `keys->Action` mapping, so the user will +still be able to override default keybindings exactly the same as before. + +Similarly, commands in the Command Palette will continue using their existing +`name->Action` mapping they're currently using. For a binding like + +```json +{ "keys": "ctrl+alt+x", "id": "Terminal.OpenDefaultSettings" }, +``` +* We'll bind whatever action is defined as `Terminal.OpenDefaultSettings` to + ctrl+alt+x. +* We'll use whatever action is defined as `Terminal.OpenDefaultSettings` to + generate a name for the command palette. + +### Future Context Menus + +In [New Tab Menu Customization Spec], we discuss allowing the user to bind +actions to the new tab menu. In that spec, they can do so with something like +the following: + +```json +{ + "newTabMenu": [ + { "type":"action", "command": { "action": "adjustFontSize", "delta": 1 }, } + { "type":"action", "command": { "action": "adjustFontSize", "delta": -1 }, } + { "type":"action", "command": "resetFontSize", } + { "type":"profile", "profile": "cmd" }, + { "type":"profile", "profile": "Windows PowerShell" }, + { "type":"separator" }, + { + "type":"folder", + "name": "Settings...", + "icon": "C:\\path\\to\\icon.png", + "entries":[ + { "type":"action", "command": "openSettings" }, + { "type":"action", "command": { "action": "openSettings", "target": "defaultsFile" } }, + ] + } + ] +} +``` + +In this example, the user has also exposed the "Increase font size", "Decrease +font size", and "Reset font size" actions, as well as the settings files in a +submenu. With this proposal, the above could instead be re-written as: + +```json +{ + "newTabMenu": [ + { "type":"action", "id": "Terminal.IncreaseFontSize" }, + { "type":"action", "id": "Terminal.DecreaseFontSize" }, + { "type":"action", "id": "Terminal.ResetFontSize" }, + { "type":"profile", "profile": "cmd" }, + { "type":"profile", "profile": "Windows PowerShell" }, + { "type":"separator" }, + { + "type":"folder", + "name": "Settings...", + "icon": "C:\\path\\to\\icon.png", + "entries":[ + { "type":"action", "id": "Terminal.OpenDefaultSettings" }, + { "type":"action", "id": "Terminal.OpenSettings" }, + ] + } + ] +} +``` + +In this example, the actions are looked up from the global map using the `id` +provided, enabling the user to re-use their existing definitions. If the user +re-defined the `Terminal.IncreaseFontSize` action to mean something else, then +the action in the new tab menu will also be automatically updated. + +Furthermore, when additional menus are added (such as the tab context menu, or +the `TermControl` context menu), these could also leverage a similar syntax to +the above to allow re-use of the `id` parameter. + +Discussion with the team also suggested that users shouldn't be able to define +actions in these menus _at all_. The actions should exclusively be defined in +`actions`, and other menus should only be able to refer to these actions by +`id`. + +## UI/UX Design + +There's not a whole lot of UI for this feature specifically. This is largely +behind-the-scenes refactoring of how actions can be defined. + +## Capabilities + +### Accessibility + +_(not applicable)_ + +### Security + +_(no change expected)_ + +### Reliability + +_(no change expected)_ + +### Compatibility + +_(no change expected)_ + +### Performance, Power, and Efficiency + +_(no change expected)_ + +## Potential Issues + +This won't necessarily play well with iterable commands in the Command Palette, +but that's okay. For iterable commands, users will still need to define the +actions manually. + +## Future considerations + +* See the following issues for other places where this might be useful: + - [#1912] - Context Menu for Tabs + * See also [#5524], [#5025], [#5633] + - [#3337] - Right-click menu inside TerminalControl (w/ Copy & Paste?) + * See also [#5633] and [#5025], both those actions seem reasonable in either + the tab context menu or the control context menu. + + +[Command Palette Spec]: https://github.com/microsoft/terminal/blob/main/doc/specs/%232046%20-%20Command%20Palette.md +[New Tab Menu Customization Spec]: https://github.com/microsoft/terminal/blob/main/doc/specs/%231571%20-%20New%20Tab%20Menu%20Customization.md + +[#1571]: https://github.com/microsoft/terminal/issues/1571 +[#1912]: https://github.com/microsoft/terminal/issues/1912 +[#3337]: https://github.com/microsoft/terminal/issues/3337 +[#5025]: https://github.com/microsoft/terminal/issues/5025 +[#5524]: https://github.com/microsoft/terminal/issues/5524 +[#5633]: https://github.com/microsoft/terminal/issues/5633 +[#6532]: https://github.com/microsoft/terminal/issues/6532 +[#6899]: https://github.com/microsoft/terminal/issues/6899 diff --git a/doc/specs/#6899 - Action IDs/data-mockup-002.png b/doc/specs/#6899 - Action IDs/data-mockup-002.png new file mode 100644 index 00000000000..5f57a5faa07 Binary files /dev/null and b/doc/specs/#6899 - Action IDs/data-mockup-002.png differ diff --git a/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids-and-keys.png b/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids-and-keys.png new file mode 100644 index 00000000000..c324c6936c3 Binary files /dev/null and b/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids-and-keys.png differ diff --git a/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids.png b/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids.png new file mode 100644 index 00000000000..93ec7670609 Binary files /dev/null and b/doc/specs/#6899 - Action IDs/data-mockup-actions-and-ids.png differ diff --git a/doc/specs/#6899 - Action IDs/data-mockup-actions.png b/doc/specs/#6899 - Action IDs/data-mockup-actions.png new file mode 100644 index 00000000000..b32fca23a96 Binary files /dev/null and b/doc/specs/#6899 - Action IDs/data-mockup-actions.png differ diff --git a/doc/specs/#6899 - Action IDs/data-mockup.png b/doc/specs/#6899 - Action IDs/data-mockup.png new file mode 100644 index 00000000000..c070cd4f768 Binary files /dev/null and b/doc/specs/#6899 - Action IDs/data-mockup.png differ 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 new file mode 100644 index 00000000000..37b1fe4cb29 --- /dev/null +++ b/doc/specs/#885 - Terminal Settings Model/#885 - Terminal Settings Model.md @@ -0,0 +1,423 @@ +--- +author: Carlos Zamora @carlos-zamora +created on: 2020-07-10 +last updated: 2020-07-10 +issue id: [#885](https://github.com/microsoft/terminal/issues/885) +--- + +# Terminal Settings Model + +## Abstract + +This spec proposes a major refactor and repurposing of the TerminalSettings project as the TerminalSettingsModel. + TerminalSettingsModel would be responsible for exposing, serializing, and deserializing settings as WinRT objects + for Windows Terminal. In doing so, Terminal's settings model is accessible as WinRT objects to existing components + like TerminalApp, TerminalControl, and TerminalCore. Additionally, Terminal Settings can be used by the Settings UI or + Shell Extensions to modify or reference Terminal's settings respectively. + +## Inspiration + +The main driver for this change is the Settings UI. The Settings UI will need to read and modify Terminal's settings + objects. At the time of writing this spec, the Terminal's settings are serialized as objects in the TerminalApp project. + To access these objects via XAML, the Settings UI needs them to be WinRT objects. Additional features that need the + settings objects to be WinRT objects include future shell extensions, like jumplist. + +## Solution Design + +### Terminal Settings Model: Objects and Projections + +The following TerminalApp objects will become WinRT objects and will be moved to the TerminalSettingsModel project +(formerly TerminalSettings): +- ColorScheme +- Profile +- GlobalAppSettings +- CascadiaSettings + +The TerminalSettingsModel project will have a root namespace of `Microsoft.Terminal.Settings.Model`. + +Adjacent to the introduction of these settings objects, `IControlSettings` and `ICoreSettings` will be moved + to the `Microsoft.Terminal.TerminalControl` namespace. This allows for a better consumption of the + settings model that is covered later in the (Consumption section)[#terminal-settings-model:-consumption]. + +#### Moving/Splitting the Action Model + +Windows Terminal represents actions via several objects: +- `AppKeyBindings`: a map of all the defined keybindings and their corresponding actions +- `ActionAndArgs`: a (de)serializable action (this holds more objects inside of it, but we won't focus on that for now) +- `ShortcutActionDispatch`: responsible for dispatching events pertinent to a given ActionAndArgs object +`TerminalApp`'s `TerminalPage` handles any events dispatched by the `ShortcutActionDispatch`. + +With the introduction of the TerminalSettingsModel, we will split `AppKeyBindings` using a `KeyMapping` class. + This separation will look something like the following: +```c++ +namespace TerminalApp +{ + [default_interface] runtimeclass AppKeyBindings : Microsoft.Terminal.TerminalControl.IKeyBindings + { + AppKeyBindings(); + + // NOTE: It may be possible to move both of these to the constructor instead + void SetDispatch(ShortcutActionDispatch dispatch); + void SetKeyMap(KeyMapping keymap); + } +} + +namespace TerminalSettingsModel +{ + [default_interface] runtimeclass KeyMapping + { + void SetKeyBinding(ActionAndArgs actionAndArgs, Microsoft.Terminal.TerminalControl.KeyChord chord); + void ClearKeyBinding(Microsoft.Terminal.TerminalControl.KeyChord chord); + + Microsoft.Terminal.TerminalControl.KeyChord GetKeyBindingForAction(ShortcutAction action); + Microsoft.Terminal.TerminalControl.KeyChord GetKeyBindingForActionWithArgs(ActionAndArgs actionAndArgs); + } +} +``` +This separation leaves `AppKeyBindings` with the responsibility of detecting and dispatching actions, whereas + `KeyMapping` handles the (de)serialization and navigation of the key bindings. + + +### Fallback Value + +Cascading settings allows our settings model to be constructed in layers (i.e. settings.json values override defaults.json values). With the upcoming introduction of the Settings UI and serialization, it is important to know where a setting value comes from. Consider a Settings UI displaying the following information: +```json + // : + "defaults": "Solarized", // profiles.defaults + "A": "Raspberry", // profile A + "B": "Tango", // profile B + "C": "Solarized" // profile C +``` +If `profiles.defaults` gets changed to `"Tango"` via the Settings UI, it is unclear if profile C's value should be updated as well. We need profile C to record if it's value is inherited from profile.defaults or explicitly set by the user. + +#### Object Model Inheritance + +To start, each settings object will now have a `CreateChild()` function. For `GlobalAppSettings`, it will look something like this: +```c++ +GlobalAppSettings GlobalAppSettings::CreateChild() const +{ + GlobalAppSettings child {}; + child._parents.append(this); + return child; +} +``` +`std::vector _parents` serves as a reference for who to ask if a settings value was not provided by the user. `LaunchMode`, for example, will now have a getter/setter that looks similar to this: +```c++ +// _LaunchMode will now be a std::optional instead of a LaunchMode +// - std::nullopt will mean that there is no user-set value +// - otherwise, the value was explicitly set by the user + +// returns the resolved value for this setting +LaunchMode GlobalAppSettings::LaunchMode() +{ + // fallback tree: + // - user set value + // - inherited value + // - system set value + return til::coalesce_value(_LaunchMode, _parents[0].LaunchMode(), _parents[1].LaunchMode(), ..., LaunchMode::DefaultMode); +} + +// explicitly set the user-set value +void GlobalAppSettings::LaunchMode(LaunchMode val) +{ + _LaunchMode = val; +} + +// check if there is a user-set value +// NOTE: This is important for the Settings UI to identify whether the user explicitly or implicitly set the presented value +bool GlobalAppSettings::HasLaunchMode() +{ + return _LaunchMode.has_value(); +} + +// explicitly unset the user-set value (we want the inherited value) +void GlobalAppSettings::ClearLaunchMode() +{ + return _LaunchMode = std::nullopt; +} +``` + +As a result, the tracking and functionality of cascading settings is moved into the object model instead of keeping it as a json-only concept. + +#### Updates to CascadiaSettings + +As `CascadiaSettings` loads the settings model, it will create children for each component of the settings model and layer the new values on top of it. Thus, `LayerJson` will look something like this: +```c++ +void CascadiaSettings::LayerJson(const Json::Value& json) +{ + _globals = _globals.CreateChild(); + _globals->LayerJson(json); + + // repeat the same for Profiles... +} +``` +For `defaults.json`, `_globals` will now hold all of the values set in `defaults.json`. If any settings were omitted from the `defaults.json`, `_globals` will fallback to its parent (a `GlobalAppSettings` consisting purely of system-defined values). + +For `settings.json`, `_globals` will only hold the values set in `settings.json`. If any settings were omitted from `settings.json`, `_globals` will fallback to its parent (the `GlobalAppSettings` built from `defaults.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 +4. `defaults.json` profile + +`CascadiaSettings` must do the following... +1. load `defaults.json` + - append newly created profiles to `_profiles` (unchanged) +2. load dynamic profiles + - append newly created profiles to `_profiles` (unchanged) +3. load `settings.json` `profiles.defaults` + - construct a `Profile` from `profiles.defaults`. Save as `Profile _profileDefaults`. + - `CreateChild()` for each existing profile + - add `_profileDefaults` as the first parent to each child (`_parents=[_profileDefaults, ]`) + - replace each `Profile` in `_profiles` with the child +4. load `settings.json` `profiles.list` + - if a matching profile exists, `CreateChild` from the matching profile, and layer the json onto the child. + - NOTE: we do _not_ include `_profileDefaults` as a parent here, because it is already an ancestor + - otherwise, `CreateChild()` from `_profileDefaults`, and layer the json onto the child. + - As before, `_profiles` must be updated such that the parent is removed + +Additionally, `_profileDefaults` will be exposed by `Profile CascadiaSettings::ProfileDefaults()`. This will enable [#7414](https://github.com/microsoft/terminal/pull/7414)'s implementation to spawn incoming commandline app tabs with the "Default" profile (as opposed to the "default profile"). + + +#### Nullable Settings +Some settings are explicitly allowed to be nullable (i.e. `Profile` `Foreground`). These settings will be stored as the following struct instead of a `std::optional`: +```c++ +template +struct NullableSetting +{ + IReference setting{ nullptr }; + bool set{ false }; +}; +``` +where... +- `set` determines if the value was explicitly set by the user (if false, we should fall back) +- `setting` records the actual user-set value (`nullptr` represents an explicit set to null) + +The API surface will experience the following small changes: +- the getter/setter will output/input an `IReference` instead of `T` +- `Has...()` and `Clear...()` will reference/modify `set` + + +### CreateChild() vs Copy() + +Settings objects will have `CreateChild()` and `Copy()`. `CreateChild()` is responsible for creating a new settings object that inherits undefined values from its parent. `Copy()` is responsible for recreating the contents of the settings object, including a reference to a copied parent (not the original parent). + +`CreateChild()` will only be used during (de)serialization to adequately interpret and update the JSON. `CreateChild()` enables, but is not explicitly used, for retrieving a value from a settings object. It can also be used to enable larger hierarchies for inheritance within the settings model. + +The Settings UI will use `Copy()` to get a deep copy of `CascadiaSettings` and data bind the UI to that copy. Thus, `Copy()` needs to be exposed in the IDL. + +#### Copying _parents +It is important that `_parents` is handled properly when performing a deep copy. We need to be aware of the following errors: +- referencing `_parents` will result in inheriting from an obsolete object tree +- referencing a copy of `_parents` can result in losing the meaning of a reference + - For example, `profile.defaults` is a parent to each presented profile. When a change occurs to `profile.defaults`, that change should impact all profiles. An improper copy may only apply the change to one of the presented profiles + +The hierarchy we have created has evolved into a directed acyclic graph (DAG). For example, the hierarchy for profiles will appear similar to the following: + +![Profile Inheritance DAG Example](Inheritance-DAG.png) + +In order to preserve `profile.defaults` as a referenced parent to each profile, a copy of the DAG can be performed using the following algorithm: +```python +# Function to clone a graph. To do this, we start +# reading the original graph depth-wise, recursively +# If we encounter an unvisited node in original graph, +# we initialize a new instance of Node for +# cloned graph with key of original node +def cloneGraph(oldSource, newSource, visited): + clone = None + if visited[oldSource.key] is False and oldSource.adj is not None: + for old in oldSource.adj: + + # Below check is for backtracking, so new + # nodes don't get initialized everytime + if clone is None or(clone is not None and clone.key != old.key): + clone = Node(old.key, []) + newSource.adj.append(clone) + cloneGraph(old, clone, visited) + + # Once, all neighbors for that particular node + # are created in cloned graph, code backtracks + # and exits from that node, mark the node as + # visited in original graph, and traverse the + # next unvisited + visited[old.key] = True + return newSource +``` +Source: https://www.geeksforgeeks.org/clone-directed-acyclic-graph/ + +This algorithm operates in O(n) time and space where `n` is the number of profiles presented. The above algorithm will be slightly modified to... +- hold a separate reference to profile.defaults `Profile` in the `CascadiaSettings` clone +- visited will be a map of pointers to the cloned `Profile`. This ensures that profiles reference the same `Profile`, over creating a new copy + +### Terminal Settings Model: Serialization and Deserialization + +Introducing these `Microsoft.Terminal.Settings.Model` WinRT objects also allow the serialization and deserialization + logic from TerminalApp to be moved to TerminalSettings. `JsonUtils` introduces several quick and easy methods + for setting deserialization. This will be moved into the `Microsoft.Terminal.Settings.Model` namespace too. + +Serialization will be an extension of the existing `JsonUtils` `ConversionTrait` struct template. `ConversionTrait` + already includes `FromJson` and `CanConvert`. Serialization would be handled by a `ToJson` function. + + +### Terminal Settings Model: Warnings and Serialization Errors + +Today, if the deserialization of `CascadiaSettings` encounters any errors, an exception is thrown and caught/handled + by falling back to a simple `CascadiaSettings` object. However, WinRT does not support exceptions. + +To get around this issue, when `CascadiaSettings` encounters a serialization error, it must internally record + any pertinent information for that error, and return the simple `CascadiaSettings` as if nothing happened. + The consumer must then call `CascadiaSettings::GetErrors()` and `CascadiaSettings::GetWarnings()` to properly + understand whether an error ocurred and how to present that to the user. + + +#### TerminalApp: Loading and Reloading Changes + +TerminalApp will construct and reference a `CascadiaSettings settings` as follows: +- TerminalApp will have a global reference to the "settings.json" filepath +- construct an `CascadiaSettings` using `CascadiaSettings("settings.json")`. This builds an `CascadiaSettings` + from the "defaults.json" file data (which is already compiled as a string literal) + 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 + 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 + Model, if so desired. This could be helpful when importing additional settings files from an external location + such as a marketplace. + +When TerminalApp detects a change to settings.json, it'll repeat the steps above. We could cache the result from + constructing an `CascadiaSettings` from "defaults.json" data to improve performance. + + +#### TerminalControl: Acquiring and Applying the Settings + +At the time of writing this spec, TerminalApp constructs `TerminalControl.TerminalSettings` WinRT objects + to expose `IControlSettings` and `ICoreSettings` to any hosted terminals. In moving `IControlSettings` + and `ICoreSettings` down to the TerminalControl layer, TerminalApp can now have better control over + how to expose relevant settings to a TerminalControl instance. + +`TerminalSettings` (which implements `IControlSettings` and `ICoreSettings`) will be moved to + TerminalApp and act as a bridge connecting `CascadiaSettings` to the TermControl. It will operate + very similarly as it does today. On construction of the TermControl or hot-reload, + `TerminalSettings` will be constructed by copying the relevant values of `CascadiaSettings`. + Then, it will be passed to TermControl (and TermCore by extension). + + +## UI/UX Design + +N/A + +## Capabilities + +### Accessibility + +N/A + +### Security + +N/A + +### Reliability + +N/A + +### Compatibility + +N/A + +### Performance, Power, and Efficiency + +## Potential Issues + +N/A + +## Future considerations + +### TerminalSettings: passing by reference + +`TermApp` synthesizes a `TerminalSettings` by copying the relevant values of `CascadiaSettings`, + then giving it to a Terminal Control. Some visual keybindings and interactions like ctrl+scroll + and ctrl+shift+scroll to change the font size and acrylic opacity operate by directly modifying + the value of the instantiated `TerminalSettings`. However, when a settings reload occurs, + these instanced changes are lost. + +`TerminalSettings` can be used as a WinRT object that references (instead of copies) the relevant + values of `CascadiaSettings`. This would prevent those instanced changes from being lost on a settings + reload. + +Since previewing commands like `setColorScheme` would require a clone of the existing `TerminalSettings`, + a `Clone` API can be added on `TerminalSettings` to accomplish that. When passing by value, + `TerminalSettings` can just overwrite the existing property (i.e.: color scheme). When passing + by reference, a slightly more complex mechanism is required to override the value. + +Now, instead of overwriting the value, we need to override the reference to a constant value +(i.e.: `snapOnInput=true`) or a referenced value (i.e.: `colorScheme`). + +### Layering Additional Settings +As we begin to introduce more sources that affect the settings (via extensions or themes), + we can introduce a `LayerSettings(String path)`. This layers the new settings file + onto the existing `CascadiaSettings`. This is already done internally, we would just expose + it via C++/WinRT. + +```c++ +runtimeclass CascadiaSettings +{ + // Load a settings file, and layer those changes on top of the existing CascadiaSettings + void LayerSettings(String path); +} +``` + +### Settings UI: Modifying and Applying the Settings (DRAFT) + +```c++ +runtimeclass CascadiaSettings +{ + // Create a copy of the existing CascadiaSettings + CascadiaSettings Clone(); + + // Compares object to "source" and applies changes to + // the settings file at "outPath" + void Save(String outPath); +} +``` + +The Settings UI will also have a reference to the `CascadiaSettings settings` from TerminalApp + as `settingsSource`. When the Settings UI is opened up, the Settings UI will also have its own `CascadiaSettings settingsClone` + that is a clone of TerminalApp's `CascadiaSettings`. +```c++ +settingsClone = settingsSource.Clone() +``` + +As the user navigates the Settings UI, the relevant contents of `settingsClone` will be retrieved and presented. + As the user makes changes to the Settings UI, XAML will update `settingsClone` using XAML data binding. + When the user saves/applies the changes in the XAML, `settingsClone.Save("settings.json")` is called; + this compares the changes between `settingsClone` and `settingsSource`, then injects the changes (if any) to `settings.json`. + +As mentioned earlier, TerminalApp detects a change to "settings.json" to update its `CascadiaSettings`. + Since the above triggers a change to `settings.json`, TerminalApp will also update itself. When + something like this occurs, `settingsSource` will automatically be updated too. + +In the case that a user is simultaneously updating the settings file directly and the Settings UI, + `settingsSource` and `settingsClone` can be compared to ensure that the Settings UI, the TerminalApp, + and the settings files are all in sync. + + **NOTE:** In the event that the user would want to export their current configuration, `Save` + can be used to export the changes to a new file. + + ### Reserialization (DRAFT) + +After deserializing the settings, injecting the new json into settings.json + should not remove the existing comments or formatting. + +The reserialization process takes place right after comparing the `settingsSource` and `settingsClone` objects. + For each setting found in the diff, we go to the relevant part of the JSON and see if the key is already there. + If it is, we update the value to be the one from `settingsClone`. Otherwise, we append the key/value pair + at the end of the section (much like we do with dynamic profiles in `profiles`). + +## Resources + +- [Preview Commands](https://github.com/microsoft/terminal/issues/6689) +- [New JSON Utils](https://github.com/microsoft/terminal/pull/6590) +- [Spec: Settings UI](https://github.com/microsoft/terminal/pull/6720) diff --git a/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.png b/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.png new file mode 100644 index 00000000000..a794de8e2b0 Binary files /dev/null and b/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.png differ diff --git a/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.vsdx b/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.vsdx new file mode 100644 index 00000000000..8c34c59afaa Binary files /dev/null and b/doc/specs/#885 - Terminal Settings Model/Inheritance-DAG.vsdx differ diff --git a/doc/specs/Proto extensions-spec.md b/doc/specs/Proto extensions-spec.md new file mode 100644 index 00000000000..039da21d532 --- /dev/null +++ b/doc/specs/Proto extensions-spec.md @@ -0,0 +1,385 @@ +--- +author: <@PankajBhojwani> +created on: <2020-9-9> +last updated: <2020-10-15> +--- + +# Proto extensions + +## Abstract + +This spec outlines adding support for proto extensions. This would allow other apps/programs +to add json snippets to our json files, and will be used when we generate settings for our various profiles. + +## Inspiration + +### Goal: Allow programs to have a say in the settings for their profiles + +Currently, Ubuntu/WSL/Powershell etc are unable to provide any specifications for how they want +their profiles in Terminal to look - only we and the users can modify the profiles. We want to provide +these installations with the functionality to have a say in this, allowing them to specify things like +their icon, their font and so on. However, we want to maintain that the user has final say over all of +these settings. + +## Solution Design + +Currently, when we load the settings we perform the following steps (this is a simplified description, +for the full version see the [spec for cascading default + user settings](https://github.com/microsoft/terminal/blob/master/doc/specs/%23754%20-%20Cascading%20Default%20Settings.md)): + +1. Generate profiles from the defaults file +2. Generate profiles from the dynamic profile generators +3. Layer the user settings on top of all the profiles created in steps 1 and 2 +4. Validate the settings + +To allow for installations to add in their snippets of json, I propose the addition of a new step +in between 2 and 3: + +1. Generate profiles from the defaults file +2. Generate profiles from the dynamic profile generators +3. **Incorporate the additional provided json stubs** - these stubs could be: + 1. modifications to existing profiles + 2. additions of full profiles + 3. additions of colour schemes +4. Layer the user settings on top of all the profiles created in steps 1 through 3 (this includes the user-defined default settings which they wish to apply to all profiles) +5. Validate the settings + +### Specifications of the json stubs + +As written above, the json stubs could be modifications to existing profiles, additions to full profiles, or additions of colour schemes. + +To allow modifications/additions of several profiles in one file and to allow the addition of several colour schemes in one json file, +these stubs should be put into lists in the json file: one list named ```profiles``` and the other list named ```schemes```. For example: + +```js +{ + "profiles": [ modifications and additions of profiles go here ], + "schemes": [ additions of colour schemes go here ] +} +``` + +An example of a full json file with these fields filled out is shown at the end of this section. + +#### Modifications to existing profiles + +The main thing to note for modification of existing profiles is that this will only be used for modifying the +default profiles (cmd/powershell) or the dynamically generated profiles. + +For modifications to existing profiles, the json stub would need to indicate which profile it wishes to modify. It will +do this by providing the corresponding guid in the `"updates"` field of the json stub. The reason we use an `"updates"` +field rather than a `"guid"` field (like the way the user settings are eventually layered onto profiles) is because we +do not want to mistakenly create a new profile when the stub was meant to update a profile that did not exist. + +Note that currently, we generate a GUID for dynamic profiles using the "initial" name of the profile (i.e. before +any user changes are applied). For example, the "initial" name of a WSL profile is the \ argument to +"wsl.exe -d \", and the "initial" name of a Powershell profile is something like "Powershell (ARM)" - depending +on the version. Thus, the stub creator could simply use the same uuidv5 GUID generator we do on that name to obtain the +GUID. Then, in the stub they provide the GUID can be used to identify which profile to modify. + +Naturally, we will have to provide documentation on how they could generate the desired GUID themselves. In that documentation, +we will also provide examples showing how the current GUIDs are generated for clarity's sake. + +We need to inform developers that **we will not** prescribe any ordering to the way in which these modifications will be +applied - thus, they should not rely on their stub being the first/last to modify a particular profile (in the event that +there is more than one json stub modifying the same profile). + +Here is an example of a json file that modifies an existing profile (specifically the Azure cloud shell profile): + +```js +{ + "profiles": [ + { + "updates": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}", + "fontSize": 16, + "fontWeight": "thin" + } + ] +} +``` + +**NOTE**: This will *not* change the way the profile looks in the user's settings file. The Azure cloud shell profile +in their settings file (assuming they have made no changes) will still be as below. + +```js +{ + "guid": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}", + "hidden": false, + "name": "Azure Cloud Shell", + "source": "Windows.Terminal.Azure" +} +``` +However, the user is free to make changes to it as usual and those changes will have the 'final say'. + +#### Full profile stubs + +Technically, full profile stubs do not need to contain anything (they could just be '\{\}'). However we should +have some qualifying minimum criteria before we accept a stub as a full profile. I suggest that we only create +new profile objects from stubs that contain at least the following + +* A name + +As in the case of the dynamic profile generator, if we create a profile that did not exist before (i.e. does not +exist in the user settings), we need to add the profile to the user settings file and re-save that file. + +Furthermore, we will add a source field to profiles created this way (again, similar to what we do for dynamic profiles). +The source field value is dependent on how we obtained this json file, and will be touched on in more detail [later in the spec](#the-source-field). + +Here is an example of a json file that contains a full profile: + +```js +{ + "profiles": [ + { + "guid": "{a821ae62-9d4a-3e34-b989-0a998ec283e6}", + "name": "Cool Profile", + "commandline": "powershell.exe", + "antialiasingMode": "aliased", + "fontWeight": "bold", + "scrollbarState": "hidden" + } + ] +} +``` + +When this profile gets added back to the user's settings file, it will look similar to the way we currently add +new dynamic profiles to the user's settings file. Going along with the example above, the corresponding +json stub in the user's settings file will be: + +```js +{ + "guid": "{a821ae62-9d4a-3e34-b989-0a998ec283e6}", + "name": "Cool Profile", + "hidden": "false", + "source": "local" +} +``` +Again, the user will be able to make changes to it as they do for all other profiles. + +#### Creation of colour schemes + +As with full profiles, we will have some qualifying criteria for what we accept as full colour schemes: color schemes +must have _all_ the fields that define a colour scheme - see the +[docs](https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes) +on creating new colour schemes for what this means. + +This may cause the issue of colour schemes being overridden if there are many creations of a colour scheme with the +same name. Since for now all we have to uniquely identify colour schemes *is* the name, we will just let this be. + +Here is an example of a json stub that contains a colour scheme: + +```js +{ + "schemes": [ + { + "name": "Postmodern Tango Light", + + "cursorColor": "#FFFFFF", + "selectionBackground": "#FFFFFF", + + "background": '#61D6D6', + "foreground": '#E74856', + + "black" : "#0C0C0C", + "blue" : "#0037DA", + "cyan" : "#3A96DD", + "green" : "#13A10E", + "purple" : "#881798", + "red" : "#C50F1F", + "white" : "#CCCCCC", + "yellow" : "#C19C00", + "brightBlack" : "#767676", + "brightBlue" : "#3B78FF", + "brightCyan" : "#61D6D6", + "brightGreen" : "#16C60C", + "brightPurple" : "#B4009E", + "brightRed" : "#E74856", + "brightWhite" : "#F2F2F2", + "brightYellow" : "#F9F1A5" + } + ] +} +``` + +This stub will *not* show up in the users settings file, similar to the way our default colour schemes do not show up. + +#### Example of a full json file +This is an example of a json file that combines all the above examples. Thus, this json file modifies the Azure Cloud +Shell profile, creates a new profile called 'Cool Profile' and creates a new colour scheme called 'Postmodern Tango Light'. + +```js +{ + "profiles": [ + { + "updates": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}", + "fontSize": 16, + "fontWeight": "thin" + }, + { + "guid": "{a821ae62-9d4a-3e34-b989-0a998ec283e6}", + "name": "Cool Profile", + "commandline": "powershell.exe", + "antialiasingMode": "aliased", + "fontWeight": "bold", + "scrollbarState": "hidden" + } + ], + "schemes": [ + { + "name": "Postmodern Tango Light", + + "cursorColor": "#FFFFFF", + "selectionBackground": "#FFFFFF", + + "background": '#61D6D6', + "foreground": '#E74856', + + "black" : "#0C0C0C", + "blue" : "#0037DA", + "cyan" : "#3A96DD", + "green" : "#13A10E", + "purple" : "#881798", + "red" : "#C50F1F", + "white" : "#CCCCCC", + "yellow" : "#C19C00", + "brightBlack" : "#767676", + "brightBlue" : "#3B78FF", + "brightCyan" : "#61D6D6", + "brightGreen" : "#16C60C", + "brightPurple" : "#B4009E", + "brightRed" : "#E74856", + "brightWhite" : "#F2F2F2", + "brightYellow" : "#F9F1A5" + } + ] +} +``` + +### Creation and location(s) of the json files + +In this section, we cover where an app that wants to use this proto-extension functionality should put the json +files. Once we implement this feature, we will need to provide documentation on this for app developers. + +#### For apps installed through Microsoft store (or similar) + +For apps that are installed through something like the Microsoft Store, they will need to declare themselves to +be an app extension or write a separate app extension. In the app extension, they will need to create a public +folder, and create a subdirectory within that folder called `Fragments`. The json files should be inserted into the +`Fragments` subdirectory. The specifics of how they can declare themselves as an app extension are provided in the +[Microsoft Docs](https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-an-extension), and I will +replicate the necessary section here. + +In the appxmanifest file of the package: + +```js + + ... + + + ... + + ... + + + + + + + + ... + +``` + +Note that the name field **must** be `com.microsoft.windows.terminal.settings` for us to detect this extension. The `Id` field +can be filled out as the app wishes. The `PublicFolder` field should have the name of the folder, relative to +the package root, where the `json` files they wish to share with us are stored (this folder is typically named +`Public` but can be renamed as long as it matches the relevant folder). + +During our profile generation, we will probe the OS for app extensions with the name `com.microsoft.windows.terminal.settings` +and obtain the json files stored in the `Fragments` subdirectories of the public folders of those app extensions. + +#### For apps installed 'traditionally' + +For apps that are installed 'traditionally', there are 2 cases. The first is that this installation is for all +the users of the system - in this case, the installer should add their json files to the global folder: + +```js +C:\ProgramData\Microsoft\Windows Terminal\Fragments\{app-name} +``` + +Note: `C:\ProgramData` is a [known folder](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/bb776911(v=vs.85)) +that apps should be able to access. If the folder `Windows Terminal` in `C:\ProgramData\Microsoft\Windows` +does not already exist, the installer should create it. **Note:** the installer must create a subdirectory within +the `Fragments` folder with their app name, we will use that name for the [`source` field](#the-source-field). + +In the second case, the installation is only for the current user. For this case, the installer should add the +json files to the local folder: + +```js +C:\Users\\AppData\Local\Microsoft\Windows Terminal\Fragments\{app-name} +``` + +We will look through both folders mentioned above during profile generation. + +#### The source field + +Currently, we allow users an easy way to disable/enable profiles that are generated from certain sources. For +example, a user can easily hide all dynamic profiles with the source `"Windows.Terminal.Wsl"` if they wish to. +To retain this functionality, we will add source fields to profiles we create through proto-extensions. + +For full profiles that came from apps installed 'traditionally', we will use the name of the subdirectory where +the json file was found to fill out the source field. + +For full profiles that came from app extensions, we will use the app package name to fill out the source field. + + + +## UI/UX Design + +This feature will allow other installations a level of control over how their profiles look in Terminal. For example, +if Ubuntu gets a new icon or a new font they can have those changes be reflected in Terminal users' Ubuntu profiles. + +## Capabilities + +### Accessibility + +This change should not affect accessibility. + +### Security + +Opening a profile causes its commandline argument to be automatically run. Thus, if malicious modifications are made +to existing profiles or new profiles with malicious commandline arguments are added, users could be tricked into running +things they do not want to. + +### Reliability + +This should not affect reliability - most of what its doing is simply layering json which we already do. + +### Compatibility + +Again, there should not be any issues here - the user settings can still be layered after this layering for the user +to have the final say. + +### Performance, Power, and Efficiency + +Looking through the additional json files could negatively impact startup time. + +## Potential Issues + +* An installer dumps a _lot_ of json files into the folder which we need to look through. +* When a `.json` files is deleted, any new profiles that were generated from it remain in the user's settings file (though they no longer appear in the tab dropdown). + +## Future considerations + +We need to consider how an app extension provides the path to an image (for the icon source or background image of a profile for example) + +This will likely be a stepping stone for the theme marketplace. + +This will also likely be a stepping stone to allowing users to specify other files to import settings from. + +## Resources + +N/A diff --git a/doc/submitting_code.md b/doc/submitting_code.md index 2e81141b027..36fa8303dfc 100644 --- a/doc/submitting_code.md +++ b/doc/submitting_code.md @@ -1,11 +1,11 @@ # Branches in Openconsole -In Openconsole, `dev/main` is the master branch for the repo. +In OpenConsole, `dev/main` is the primary branch for the repo. Any branch that begins with `dev/` is recognized by our CI system and will automatically run x86 and amd64 builds and run our unit and feature tests. For feature branches the pattern we use is `dev//`. ex. `dev/austdi/SomeCoolUnicodeFeature`. The important parts are the dev prefix and your alias. -`inbox` is a special branch that coordinates Openconsole code to the main OS repo. +`inbox` is a special branch that coordinates OpenConsole code to the main OS repo. The code will be checked into the OS repo at `/onecore/windows/core/console/open`. It would be prudent to make sure that directory builds in razzle with your submitted changes. diff --git a/doc/terminal-v2-roadmap.md b/doc/terminal-v2-roadmap.md index 187733ee309..dcfaffb9cd3 100644 --- a/doc/terminal-v2-roadmap.md +++ b/doc/terminal-v2-roadmap.md @@ -22,15 +22,14 @@ Below is the schedule for when milestones will be included in release builds of | Milestone End Date | Milestone Name | Preview Release Blog Post | | ------------------ | -------------- | ------------------------- | | 2020-06-18 | [1.1] in Windows Terminal Preview | [Windows Terminal Preview 1.1 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-1-release/) | -| 2020-07-31 | [1.2] in Windows Terminal Preview
[1.1] in Windows Terminal | | -| 2020-08-31 | 1.3 in Windows Terminal Preview
[1.2] in Windows Terminal | | -| 2020-09-30 | 1.4 in Windows Terminal Preview
1.3 in Windows Terminal | | -| 2020-10-31 | 1.5 in Windows Terminal Preview
1.4 in Windows Terminal | | -| 2020-11-30 | 1.6 in Windows Terminal Preview
1.5 in Windows Terminal | | -| 2020-12-31 | 1.7 in Windows Terminal Preview
1.6 in Windows Terminal | | -| 2021-01-31 | 1.8 in Windows Terminal Preview
1.7 in Windows Terminal | | -| 2021-02-28 | 1.9 in Windows Terminal Preview
1.8 in Windows Terminal | | -| 2021-03-31 | 1.10 in Windows Terminal Preview
1.9 in Windows Terminal | | +| 2020-07-31 | [1.2] in Windows Terminal Preview
[1.1] in Windows Terminal | [Windows Terminal Preview 1.2 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-2-release/) | +| 2020-08-31 | [1.3] in Windows Terminal Preview
[1.2] in Windows Terminal | [Windows Terminal Preview 1.3 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-3-release/) | +| 2020-09-30 | [1.4] in Windows Terminal Preview
[1.3] in Windows Terminal | [Windows Terminal Preview 1.4 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-4-release/) | +| 2020-11-30 | [1.5] in Windows Terminal Preview
[1.4] in Windows Terminal | [Windows Terminal Preview 1.5 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-5-release/) | +| 2020-12-31 | [1.6] in Windows Terminal Preview
[1.5] in Windows Terminal | | +| 2021-01-31 | 1.7 in Windows Terminal Preview
[1.6] in Windows Terminal | | +| 2021-02-28 | 1.8 in Windows Terminal Preview
1.8 in Windows Terminal | | +| 2021-03-31 | 1.9 in Windows Terminal Preview
1.9 in Windows Terminal | | | 2021-04-30 | 2.0 RC in Windows Terminal Preview
2.0 RC in Windows Terminal | | | 2021-05-31 | [2.0] in Windows Terminal Preview
[2.0] in Windows Terminal | | @@ -50,11 +49,11 @@ The following are a list of the key scenarios we're aiming to deliver for Termin | Priority\* | Scenario | Description/Notes | | ---------- | -------- | ----------------- | -| 0 | Settings UI | A user interface that connects to settings.json. This provides a way for people to edit their settings without having to edit a JSON file.

Issue: [#1564] | -| 0 | Command palette | A popup menu to list possible actions and commands.

Issues: [#5400], [#2046]
Spec: [#2193] | +| 0 | Settings UI | A user interface that connects to settings.json. This provides a way for people to edit their settings without having to edit a JSON file.

Issue: [#1564]
Specs: [#6720], [#6904]
Implementation: [#7283], [#7370] | +| 0 | Command palette | A popup menu to list possible actions and commands.

Issues: [#5400], [#2046]
Spec: [#2193]
Implementation: [#6635] | | 1 | Tab tear-off | The ability to tear a tab out of the current window and spawn a new window or attach it to a separate window.

Issue: [#1256]
Spec: [#2080] | -| 1 | Clickable links | Hyperlinking any links that appear in the text buffer. When clicking on the link, the link will open in your default browser.

Issue: [#574] | -| 1 | Default terminal | If a command-line application is spawned, it should open in Windows Terminal (if installed) or your preferred terminal

Issue: [#492]
Spec: [#2080] | +| 1 | Clickable links | Hyperlinking any links that appear in the text buffer. When clicking on the link, the link will open in your default browser.

Issue: [#574]
Implementation: [#7251] | +| 1 | Default terminal | If a command-line application is spawned, it should open in Windows Terminal (if installed) or your preferred terminal

Issue: [#492]
Spec: [#2080], [#7414] | | 1 | Overall theme support | Tab coloring, title bar coloring, pane border coloring, pane border width, definition of what makes a theme

Issue: [#3327]
Spec: [#5772] | | 1 | Open tab as admin/other user | Open tab in existing Windows Terminal instance as admin (if Terminal was run unelevated) or as another user.

Issue: [#5000] | | 1 | Traditional opacity | Have a transparent background without the acrylic blur.

Issue: [#603] | @@ -62,7 +61,7 @@ The following are a list of the key scenarios we're aiming to deliver for Termin | 2 | Infinite scrollback | Have an infinite history for the text buffer.

Issue: [#1410] | | 2 | Pane management | All issues listed out in the original issue. Some features include pane resizing with mouse, pane zooming, and opening a pane by prompting which profile to use.

Issue: [#1000] | | 2 | Theme marketplace | Marketplace for creation and distribution of themes.
Dependent on overall theming | -| 2 | Jump list | Show profiles from task bar (on right click)/start menu.

Issue: [#576] | +| 2 | Jump list | Show profiles from task bar (on right click)/start menu.

Issue: [#576]
Implementation: [#7515] | | 2 | Open with multiple tabs | A setting that allows Windows Terminal to launch with a specific tab configuration (not using only command line arguments).

Issue: [#756] | | 3 | Open in Windows Terminal | Functionality to right click on a file or folder and select Open in Windows Terminal.

Issue: [#1060]
Implementation: [#6100] | | 3 | Session restoration | Launch Windows Terminal and the previous session is restored with the proper tab and pane configuration and starting directories.

Issues: [#961], [#960], [#766] | @@ -80,16 +79,27 @@ Feature Notes: [1.1]: https://github.com/microsoft/terminal/milestone/24 [1.2]: https://github.com/microsoft/terminal/milestone/25 +[1.3]: https://github.com/microsoft/terminal/milestone/26 +[1.4]: https://github.com/microsoft/terminal/milestone/28 +[1.5]: https://github.com/microsoft/terminal/milestone/30 +[1.6]: https://github.com/microsoft/terminal/milestone/31 [2.0]: https://github.com/microsoft/terminal/milestone/22 [#1564]: https://github.com/microsoft/terminal/issues/1564 +[#6720]: https://github.com/microsoft/terminal/pull/6720 +[#6904]: https://github.com/microsoft/terminal/pull/6904 +[#7283]: https://github.com/microsoft/terminal/pull/7283 +[#7370]: https://github.com/microsoft/terminal/pull/7370 [#5400]: https://github.com/microsoft/terminal/issues/5400 [#2046]: https://github.com/microsoft/terminal/issues/2046 [#2193]: https://github.com/microsoft/terminal/pull/2193 +[#6635]: https://github.com/microsoft/terminal/pull/6635 [#1256]: https://github.com/microsoft/terminal/issues/1256 [#2080]: https://github.com/microsoft/terminal/pull/2080 [#574]: https://github.com/microsoft/terminal/issues/574 +[#7251]: https://github.com/microsoft/terminal/pull/7251 [#492]: https://github.com/microsoft/terminal/issues/492 [#2080]: https://github.com/microsoft/terminal/pull/2080 +[#7414]: https://github.com/microsoft/terminal/pull/7414 [#3327]: https://github.com/microsoft/terminal/issues/3327 [#5772]: https://github.com/microsoft/terminal/pull/5772 [#5000]: https://github.com/microsoft/terminal/issues/5000 @@ -100,6 +110,7 @@ Feature Notes: [#1410]: https://github.com/microsoft/terminal/issues/1410 [#1000]: https://github.com/microsoft/terminal/issues/1000 [#576]: https://github.com/microsoft/terminal/issues/576 +[#7515]: https://github.com/microsoft/terminal/pull/7515 [#756]: https://github.com/microsoft/terminal/issues/756 [#1060]: https://github.com/microsoft/terminal/issues/1060 [#6100]: https://github.com/microsoft/terminal/pull/6100 diff --git a/doc/user-docs/ThirdPartyToolProfiles.md b/doc/user-docs/ThirdPartyToolProfiles.md index 9da658dc075..4aca496f75b 100644 --- a/doc/user-docs/ThirdPartyToolProfiles.md +++ b/doc/user-docs/ThirdPartyToolProfiles.md @@ -1,13 +1,7 @@ # Adding profiles for third-party tools -This doc will hopefully provide a useful guide for adding profiles for common -third-party tools to your -[settings.json](https://github.com/microsoft/terminal/blob/master/doc/user-docs/UsingJsonSettings.md) -file. - -All of these profiles are provided _without_ their `guid` set. If you'd like to -set any of these profiles as your _default_ profile, you'll need to make sure to -[generate a unique guid](https://www.guidgenerator.com/) for them manually. +This doc will hopefully provide a useful guide for adding profiles for common third-party tools to your +[settings.json](https://docs.microsoft.com/en-us/windows/terminal/customize-settings/profile-settings) file. ## Anaconda @@ -15,10 +9,10 @@ Assuming that you've installed Anaconda into `%USERPROFILE%\Anaconda3`: ```json { - "commandline" : "cmd.exe /k \"%USERPROFILE%\\Anaconda3\\Scripts\\activate.bat %USERPROFILE%\\Anaconda3\"", - "icon" : "%USERPROFILE%/Anaconda3/Menu/anaconda-navigator.ico", - "name" : "Anaconda3", - "startingDirectory" : "%USERPROFILE%" + "commandline": "cmd.exe /k \"%USERPROFILE%\\Anaconda3\\Scripts\\activate.bat %USERPROFILE%\\Anaconda3\"", + "icon": "%USERPROFILE%\\Anaconda3\\Menu\\anaconda-navigator.ico", + "name": "Anaconda3", + "startingDirectory": "%USERPROFILE%" } ``` @@ -28,23 +22,23 @@ Assuming that you've installed cmder into `%CMDER_ROOT%`: ```json { - "commandline" : "cmd.exe /k \"%CMDER_ROOT%\\vendor\\init.bat\"", - "name" : "cmder", - "icon" : "%CMDER_ROOT%/icons/cmder.ico", - "startingDirectory" : "%USERPROFILE%" + "commandline": "cmd.exe /k \"%CMDER_ROOT%\\vendor\\init.bat\"", + "name": "cmder", + "icon": "%CMDER_ROOT%\\icons\\cmder.ico", + "startingDirectory": "%USERPROFILE%" } ``` ## Cygwin -Assuming that you've installed Cygwin into `C:/Cygwin`: +Assuming that you've installed Cygwin into `C:\Cygwin`: ```json { - "name" : "Cygwin", - "commandline" : "C:/Cygwin/bin/bash --login -i", - "icon" : "C:/Cygwin/Cygwin.ico", - "startingDirectory" : "C:/Cygwin/bin" + "name": "Cygwin", + "commandline": "C:\\Cygwin\\bin\\bash --login -i", + "icon": "C:\\Cygwin\\Cygwin.ico", + "startingDirectory": "C:\\Cygwin\\bin" } ``` @@ -58,51 +52,53 @@ Assuming that you've installed Far into `c:\Program Files\Far Manager`: ```json { - "name" : "Far", - "commandline" : "\"c:\\program files\\far manager\\far.exe\"", - "startingDirectory" : "%USERPROFILE%", - "useAcrylic" : false + "name": "Far", + "commandline": "\"c:\\program files\\far manager\\far.exe\"", + "startingDirectory": "%USERPROFILE%", + "useAcrylic": false }, ``` ## Git Bash -Assuming that you've installed Git Bash into `C:/Program Files/Git`: +Assuming that you've installed Git Bash into `C:\\Program Files\\Git`: ```json { - "name" : "Git Bash", - "commandline" : "C:/Program Files/Git/bin/bash.exe -li", - "icon" : "C:/Program Files/Git/mingw64/share/git/git-for-windows.ico", - "startingDirectory" : "%USERPROFILE%" + "name": "Git Bash", + "commandline": "C:\\Program Files\\Git\\bin\\bash.exe -li", + "icon": "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico", + "startingDirectory": "%USERPROFILE%" } ```` ## Git Bash (WOW64) -Assuming that you've installed Git Bash into `C:/Program Files (x86)/Git`: +Assuming that you've installed Git Bash into `C:\\Program Files (x86)\\Git`: ```json { - "name" : "Git Bash", - "commandline" : "%ProgramFiles(x86)%/Git/bin/bash.exe -li", - "icon" : "%ProgramFiles(x86)%/Git/mingw32/share/git/git-for-windows.ico", - "startingDirectory" : "%USERPROFILE%" + "name": "Git Bash", + "commandline": "%ProgramFiles(x86)%\\Git\\bin\\bash.exe -li", + "icon": "%ProgramFiles(x86)%\\Git\\mingw32\\share\\git\\git-for-windows.ico", + "startingDirectory": "%USERPROFILE%" } ``` ## MSYS2 -Assuming that you've installed MSYS2 into `C:/msys64`: +Assuming that you've installed MSYS2 into `C:\\msys64`: ```json { - "name" : "MSYS2", - "commandline" : "C:/msys64/msys2_shell.cmd -defterm -no-start -mingw64", - "icon": "C:/msys64/msys2.ico", - "startingDirectory" : "C:/msys64/home/user" + "name": "MSYS2", + "commandline": "C:\\msys64\\msys2_shell.cmd -defterm -no-start -mingw64", + "icon": "C:\\msys64\\msys2.ico", + "startingDirectory": "C:\\msys64\\home\\user" } -```` +``` + +For more details, see [this page](https://www.msys2.org/docs/terminals/#windows-terminal) on the MSYS2 documentation. ## Developer Command Prompt for Visual Studio @@ -110,9 +106,9 @@ Assuming that you've installed VS 2019 Professional: ```json { - "name" : "Developer Command Prompt for VS 2019", - "commandline" : "cmd.exe /k \"C:/Program Files (x86)/Microsoft Visual Studio/2019/Professional/Common7/Tools/VsDevCmd.bat\"", - "startingDirectory" : "%USERPROFILE%" + "name": "Developer Command Prompt for VS 2019", + "commandline": "cmd.exe /k \"C:/Program Files (x86)/Microsoft Visual Studio/2019/Professional/Common7/Tools/VsDevCmd.bat\"", + "startingDirectory": "%USERPROFILE%" } ``` diff --git a/doc/user-docs/UsingCommandlineArguments.md b/doc/user-docs/UsingCommandlineArguments.md index 63de50d6d2a..64677f48840 100644 --- a/doc/user-docs/UsingCommandlineArguments.md +++ b/doc/user-docs/UsingCommandlineArguments.md @@ -1,201 +1 @@ ---- -author: Mike Griese @zadjii-msft -created on: 2020-01-16 -last updated: 2020-01-17 ---- - -# Using the `wt.exe` Commandline - -As of [#4023], the Windows Terminal now supports accepting arguments on the -commandline, to enable launching the Terminal in a non-default configuration. -This document serves as a reference for all the parameters you can currently -pass, and gives some examples of how to use the `wt` commandline. - -> NOTE: If you're running the Terminal built straight from the repo, you'll need -> to use `wtd.exe` and `wtd` instead of `wt.exe` and `wt`. - -1. [Commandline Reference](#Reference) -1. [Commandline Examples](#Examples) - -## Reference - -### Options - -#### `--help,-h,-?,/?,` - -Display the help message. - -## Subcommands - -#### `new-tab` - -`new-tab [terminal_parameters]` - -Opens a new tab with the given customizations. On its _first_ invocation, also -opens a new window. Subsequent `new-tab` commands will all open new tabs in the -same window. [[1](#footnote-1)] - -**Parameters**: - -* `[terminal_parameters]`: See [[terminal_parameters]](#terminal_parameters). - -#### `split-pane` - -`split-pane [-H,--horizontal|-V,--vertical] [terminal_parameters]` - -Creates a new pane in the currently focused tab by splitting the given pane -vertically or horizontally. [[1](#footnote-1)] - -**Parameters**: - -* `-H,--horizontal`, `-V,--vertical`: Used to indicate which direction to split - the pane. `-V` is "vertically" (think `[|]`), and `-H` is "horizontally" - (think `[-]`). If omitted, defaults to "auto", which splits the current pane - in whatever the larger dimension is. If both `-H` and `-V` are provided, - defaults to vertical. -* `[terminal_parameters]`: See [[terminal_parameters]](#terminal_parameters). - -#### `focus-tab` - -`focus-tab [--target,-t tab-index]|[--next,-n]|[--previous,-p]` - -Moves focus to a given tab. - -**Parameters**: - -* `--target,-t tab-index`: moves focus to the tab at index `tab-index`. If - omitted, defaults to `0` (the first tab). Will display an error if combined - with either of `--next` or `--previous`. -* `-n,--next`: Move focus to the next tab. Will display an error if combined - with either of `--previous` or `--target`. -* `-p,--previous`: Move focus to the previous tab. Will display an error if - combined with either of `--next` or `--target`. - -#### `[terminal_parameters]` - -Some of the preceding commands are used to create a new terminal instance. -These commands are listed above as accepting `[terminal_parameters]` as a -parameter. For these commands, `[terminal_parameters]` can be any of the -following: - -`[--profile,-p profile-name] [--startingDirectory,-d starting-directory] [commandline]` - -* `--profile,-p profile-name`: Use the given profile to open the new tab/pane, - where `profile-name` is the `name` or `guid` of a profile. If `profile-name` - does not match _any_ profiles, uses the default. -* `--startingDirectory,-d starting-directory`: Overrides the value of - `startingDirectory` of the specified profile, to start in `starting-directory` - instead. -* `commandline`: A commandline to replace the default commandline of the - selected profile. If the user wants to use a `;` in this commandline, it - should be escaped as `\;`. - -### Notes - -* [1]: If you try to run a `wt` commandline while running in a Windows Terminal window, the commandline will _always_ create a new window by default. Being able to run `wt` commandlines in the _current_ window is planned in the future - for more information, refer to [#4472]. - -## Examples - -### Open Windows Terminal in the current directory - -```powershell -wt -d . -``` - -This will launch a new Windows Terminal window in the current working directory. -It will use your default profile, but instead of using the `startingDirectory` -property from that it will use the current path. This is especially useful for -launching the Windows Terminal in a directory you currently have open in an -`explorer.exe` window. - -### Opening with multiple panes - -If you want to open with multiple panes in the same tab all at once, you can use -the `split-pane` command to create new panes. - -Consider the following commandline: - -```powershell -wt ; split-pane -p "Windows PowerShell" ; split-pane -H wsl.exe -``` - -This creates a new Windows Terminal window with one tab, and 3 panes: - -* `wt`: Creates the new tab with the default profile -* `split-pane -p "Windows PowerShell"`: This will create a new pane, split from - the parent with the default profile. This pane will open with the "Windows - PowerShell" profile -* `split-pane -H wsl.exe`: This will create a third pane, split _horizontally_ - from the "Windows PowerShell" pane. It will be running the default profile, - and will use `wsl.exe` as the commandline (instead of the default profile's - `commandline`). - - -### Using multiple commands from PowerShell - -The Windows Terminal uses the semicolon character `;` as a delimiter for -separating subcommands in the `wt` commandline. Unfortunately, `powershell` also -uses `;` as a command separator. To work around this you can use the following -tricks to help run multiple wt sub commands from powershell. In all the -following examples, we'll be creating a new Terminal window with three panes - -one running `cmd`, one with `powershell`, and a last one running `wsl`. - -In each of the following examples, we're using the `Start-Process` command to -run `wt`. For more information on why we're using `Start-Process`, see ["Using -`start`"](#using-start) below. - -#### Single quoted parameters (if you aren't calculating anything): - -In this example, we'll wrap all the parameters to `wt` in single quotes (`'`) - -```PowerShell -start wt 'new-tab "cmd"; split-pane -p "Windows PowerShell" ; split-pane -H wsl.exe' -``` - -#### Escaped quotes (if you need variables): - -If you'd like to pass a value contained in a variable to the `wt` commandline, -instead use the following syntax: - -```PowerShell -$ThirdPane = "wsl.exe" -start wt "new-tab cmd; split-pane -p `"Windows PowerShell`" ; split-pane -H $ThirdPane" -``` - -Note the usage of `` ` `` to escape the double-quotes (`"`) around "Windows -Powershell" in the `-p` parameter to the `split-pane` sub-command. - -#### Using `start` - -All the above examples explicitly used `start` to launch the Terminal. - -In the following examples, we're going to not use `start` to run the -commandline. Instead, we'll try two other methods of escaping the commandline: -* Only escaping the semicolons so that `powershell` will ignore them and pass - them straight to `wt`. -* Using `--%`, so powershell will treat the rest of the commandline as arguments - to the application. - -```PowerShell -wt new-tab "cmd" `; split-pane -p "Windows PowerShell" `; split-pane -H wsl.exe -``` - -```Powershell -wt --% new-tab cmd ; split-pane -p "Windows PowerShell" ; split-pane -H wsl.exe -``` - -In both these examples, the newly created Windows Terminal window will create -the window by correctly parsing all the provided commandline arguments. - -However, these methods are _not_ recommended currently, as Powershell will wait -for the newly-created Terminal window to be closed before returning control to -Powershell. By default, Powershell will always wait for Windows Store -applications (like the Windows Terminal) to close before returning to the -prompt. Note that this is different than the behavior of `cmd`, which will return -to the prompt immediately. See -[Powershell/PowerShell#9970](https://github.com/PowerShell/PowerShell/issues/9970) -for more details on this bug. - - -[#4023]: https://github.com/microsoft/terminal/pull/4023 -[#4472]: https://github.com/microsoft/terminal/issues/4472 +⚠ This document has moved to [Using command-line arguments for Windows Terminal](https://docs.microsoft.com/windows/terminal/command-line-arguments). diff --git a/doc/user-docs/UsingJsonSettings.md b/doc/user-docs/UsingJsonSettings.md index 5fab48d289c..dc967085ea1 100644 --- a/doc/user-docs/UsingJsonSettings.md +++ b/doc/user-docs/UsingJsonSettings.md @@ -1,483 +1 @@ -# Editing Windows Terminal JSON Settings - -One way (currently the only way) to configure Windows Terminal is by editing the -`settings.json` settings file. At the time of writing you can open the settings -file in your default editor by selecting `Settings` from the WT pull down menu. - -The settings are stored in the file `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json`. - -As of [#2515](https://github.com/microsoft/terminal/pull/2515), the settings are -split into _two_ files: a hardcoded `defaults.json`, and `settings.json`, which -contains the user settings. Users should only be concerned with the contents of -the `settings.json`, which contains their customizations. The `defaults.json` -file is only provided as a reference of what the default settings are. For more -details on how these two files work, see [Settings -Layering](#settings-layering). To view the default settings file, click on the -"Settings" button while holding the Alt key. - -Details of specific settings can be found [here](../cascadia/SettingsSchema.md). -A general introduction is provided below. - -The settings are grouped under four headings: - -1. Global: Settings that apply to the whole application e.g. Default profile, initial size etc. -2. Key Bindings: Actually a sub field of the global settings, but worth discussing separately -3. Profiles: A group of settings to be applied to a tab when it is opened using that profile. E.g. shell to use, cursor shape etc. -4. Schemes: Sets of colors for background, text etc. that can be used by profiles - -## Global Settings - -These settings define startup defaults, and application-wide settings that might -not affect a particular terminal instance. - -* Theme -* Title Bar options -* Initial size -* Default profile used when the Windows Terminal is started - -Example settings include - -```json -{ - "defaultProfile" : "{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}", - "initialCols" : 120, - "initialRows" : 50, - "theme" : "system", - "keybindings" : [] - ... -} -``` - -These global properties should exist in the root json object. - -## Key Bindings - -This is an array of key chords and shortcuts to invoke various commands. -Each command can have more than one key binding. - -> 👉 **Note**: Key bindings is a subfield of the global settings and -> key bindings apply to all profiles in the same manner. - -For example, here's a sample of the default keybindings: - -```json -{ - "keybindings": - [ - { "command": "closePane", "keys": ["ctrl+shift+w"] }, - { "command": "copy", "keys": ["ctrl+shift+c"] }, - { "command": "newTab", "keys": ["ctrl+shift+t"] }, - // etc. - ] -} -``` - -You can also use a single key chord string as the value of `"keys"`. -It will be treated as a chord of length one. -This will allow you to simplify the above snippet as follows: - -```json -{ - "keybindings": - [ - { "command": "closePane", "keys": "ctrl+shift+w" }, - { "command": "copy", "keys": "ctrl+shift+c" }, - { "command": "newTab", "keys": "ctrl+shift+t" }, - // etc. - ] -} -``` - -A list of default key bindings is available [here](https://github.com/microsoft/terminal/blob/master/src/cascadia/TerminalApp/defaults.json#L204). - -### Unbinding keys - -If you ever come across a key binding that you're unhappy with, it's possible to -easily change the keybindings. For example, vim uses Ctrl+^ as a -binding for "switch to previous buffer", which conflicts with the Terminal's -default keybinding for "open a new tab with the sixth profile". If you'd like to -unbind that keybinding, and allow the keystroke to fall through to vim, you can -add the following to your keybindings: - -```json -{ - "command" : null, "keys" : ["ctrl+shift+6"] -}, -``` - -This will _unbind_ Ctrl+Shift+6, allowing vim to use the keystroke -instead of the terminal. - -### Binding multiple keys - -You can have multiple key chords bound to the same action. To do this, simply -add multiple bindings for the same action. For example: - -```json - "keybindings" : - [ - { "command": "copy", "keys": "ctrl+shift+c" }, - { "command": "copy", "keys": "ctrl+c" }, - { "command": "copy", "keys": "enter" } - ] -``` - -In this snippet, all three of ctrl+shift+c, ctrl+c and enter are bound to `copy`. - -## Profiles - -A profile contains the settings applied when a new WT tab is opened. Each -profile is identified by a GUID and contains a number of other fields. - -> 👉 **Note**: The `guid` property is the unique identifier for a profile. If -> multiple profiles all have the same `guid` value, you may see unexpected -> behavior. - -* Which command to execute on startup - this can include arguments. -* Starting directory -* Which color scheme to use (see Schemes below) -* Font face and size -* Various settings to control appearance. E.g. Opacity, icon, cursor appearance, display name etc. -* Other behavioral settings. E.g. Close on exit, snap on input, ..... - -Example settings include - -```json - "closeOnExit" : true, - "colorScheme" : "Campbell", - "commandline" : "wsl.exe -d Debian", - "cursorColor" : "#FFFFFF", - "cursorShape" : "bar", - "fontFace" : "Hack", - "fontSize" : 9, - "guid" : "{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}", - "name" : "Debian", - "startingDirectory" : "%USERPROFILE%\\wslhome" - .... -``` - -> 👉 **Note**: To use backslashes in any path field, you'll need to escape them following JSON escaping rules (like shown above). As an alternative, you can use forward slashes ("%USERPROFILE%/wslhome"). - -The profile GUID is used to reference the default profile in the global settings. - -The values for background image stretch mode are documented [here](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.stretch). - -### Hiding a profile - -If you want to remove a profile from the list of profiles in the new tab -dropdown, but keep the profile around in your `settings.json` file, you can add -the property `"hidden": true` to the profile's json. This can also be used to -remove the default `cmd` and PowerShell profiles, if the user does not wish to -see them. - -## Color Schemes - -Each scheme defines the color values to be used for various terminal escape sequences. -Each schema is identified by the name field. Examples include - -```json - "name" : "Campbell", - "background" : "#0C0C0C", - "black" : "#0C0C0C", - "blue" : "#0037DA", - "foreground" : "#F2F2F2", - "green" : "#13A10E", - "red" : "#C50F1F", - "white" : "#CCCCCC", - "yellow" : "#C19C00" - ... -``` - -The schema name can then be referenced in one or more profiles. - -## Settings layering - -The runtime settings are actually constructed from _three_ sources: - -* The default settings, which are hardcoded into the application, and available - in `defaults.json`. This includes the default keybindings, color schemes, and - profiles for both Windows PowerShell and Command Prompt (`cmd.exe`). -* Dynamic Profiles, which are generated at runtime. These include Powershell - Core, the Azure Cloud Shell connector, and profiles for and WSL distros. -* The user settings from `settings.json`. - -Settings from each of these sources are "layered" upon the settings from -previous sources. In this manner, the user settings in `settings.json` can -contain _only the changes from the default settings_. For example, if a user -would like to only change the color scheme of the default `cmd` profile to -"Solarized Dark", you could change your cmd profile to the following: - -```js - { - // Make changes here to the cmd.exe profile - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "colorScheme": "Solarized Dark" - } -``` - -Here, we know we're changing the `cmd` profile, because the `guid` -`"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"` is `cmd`'s unique GUID. Any profiles -with that GUID will all be treated as the same object. Any changes in that -profile will overwrite those from the defaults. - -Similarly, you can overwrite settings from a color scheme by defining a color -scheme in `settings.json` with the same name as a default color scheme. - -If you'd like to unbind a keystroke that's bound to an action in the default -keybindings, you can set the `"command"` to `"unbound"` or `null`. This will -allow the keystroke to fallthrough to the commandline application instead of -performing the default action. - -### Dynamic Profiles - -When dynamic profiles are created at runtime, they'll be added to the -`settings.json` file. You can identify these profiles by the presence of a -`"source"` property. These profiles are tied to their source - if you uninstall -a linux distro, then the profile will remain in your `settings.json` file, but -the profile will be hidden. - -The Windows Terminal uses the `guid` property of these dynamically-generated -profiles to uniquely identify them. If you try to change the `guid` of a -dynamically-generated profile, the Terminal will automatically recreate a new -entry for that profile. - -If you'd like to disable a particular dynamic profile source, you can add that -`source` to the global `"disabledProfileSources"` array. For example, if you'd -like to hide all the WSL profiles, you could add the following setting: - -```json - - "disabledProfileSources": ["Windows.Terminal.WSL"], - ... - -``` - -> 👉 **NOTE**: On launch, if a dynamic profile generator is enabled, it will -> always add new profiles it detects to your list of profiles. If you delete a -> dynamically generated profile from your list of profiles, it will just get -> re-added the next time the Terminal is launched! To remove a dynamic profile -> from your list of profiles, make sure to set `"hidden": true` in the profile. - -### Default settings - -In [#2325](https://github.com/microsoft/terminal/issues/2325), we introduced the -concept of "Default Profile Settings". These are settings that will apply to all -of your profiles by default. Profiles can still override these settings -individually. With default profile settings, you can easily make changes to all -your profiles at once. For example, given the following settings: - -```json - "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "profiles": - [ - { - "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "name": "Windows PowerShell", - "commandline": "powershell.exe", - "fontFace": "Cascadia Code", - "fontSize": 14 - }, - { - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "cmd", - "commandline": "cmd.exe", - "fontFace": "Cascadia Code", - "fontSize": 14 - }, - { - "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", - "name" : "cmder", - "startingDirectory" : "%USERPROFILE%", - "fontFace": "Cascadia Code", - "fontSize": 14 - } - ], -``` - -All three of these profiles are using "Cascadia Code" as their `"fontFace"`, and -14 as their `fontSize`. With default profile settings, you can easily set these -properties for all your profiles, like so: - -```json - "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "profiles": { - "defaults": - { - "fontFace": "Cascadia Code", - "fontSize": 14 - }, - "list": [ - { - "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "name": "Windows PowerShell", - "commandline": "powershell.exe" - }, - { - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "cmd", - "commandline": "cmd.exe" - }, - { - "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", - "name" : "cmder", - "startingDirectory" : "%USERPROFILE%" - } - ] - }, -``` - -Note that the `profiles` property has changed in this example from a _list_ of -profiles, to an _object_ with two properties: - -* a `list` that contains the list of all the profiles -* the new `defaults` object, which contains all the settings that should apply to - every profile. - -What if I wanted a profile to have a different value for a property other than -the default? Simply set the property in the profile's entry to override the -value from `defaults`. Let's say you want the `cmd` profile to have _"Consolas"_ -as the font, but the rest of your profiles to still have _"Cascadia Code"_. You -could achieve that with the following: - -```json - "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "profiles": { - "defaults": - { - "fontFace": "Cascadia Code", - "fontSize": 14 - }, - "list": [ - { - "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", - "name": "Windows PowerShell", - "commandline": "powershell.exe" - }, - { - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "cmd", - "commandline": "cmd.exe", - "fontFace": "Consolas" - }, - { - "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", - "name" : "cmder", - "startingDirectory" : "%USERPROFILE%" - } - ] - }, -``` - -In the above settings, the `"fontFace"` in the `cmd.exe` profile overrides the -`"fontFace"` from the `defaults`. - -## Configuration Examples - -### Add a custom background to the WSL Debian terminal profile - -1. Download the [Debian JPG logo](https://www.debian.org/logos/openlogo-100.jpg) -2. Put the image in the - `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_\LocalState\` - directory (same directory as your `settings.json` file). - - __NOTE__: You can put the image anywhere you like, the above suggestion happens to be convenient. -3. Open your WT json properties file. -4. Under the Debian Linux profile, add the following fields: - -```json - "backgroundImage": "ms-appdata:///Local/openlogo-100.jpg", - "backgroundImageOpacity": 1, - "backgroundImageStretchMode" : "none", - "backgroundImageAlignment" : "topRight", -``` - -5. Make sure that `useAcrylic` is `false`. -6. Save the file. -7. Jump over to WT and verify your changes. - -Notes: - -1. You will need to experiment with different color settings -and schemes to make your terminal text visible on top of your image -2. If you store the image in the UWP directory (the same directory as your settings.json file), -then you should use the URI style path name given in the above example. -More information about UWP URI schemes [here](https://docs.microsoft.com/en-us/windows/uwp/app-resources/uri-schemes). -3. Instead of using a UWP URI you can use a: - 1. URL such as -`http://open.esa.int/files/2017/03/Mayer_and_Bond_craters_seen_by_SMART-1-350x346.jpg` - 2. Local file location such as `C:\Users\Public\Pictures\openlogo.jpg` - -### Adding Copy and Paste Keybindings - -As of [#1093](https://github.com/microsoft/terminal/pull/1093) (first available -in Windows Terminal v0.3), the Windows Terminal now supports copy and paste -keyboard shortcuts. However, if you installed and ran the terminal before that, -you won't automatically get the new keybindings added to your settings. If you'd -like to add shortcuts for copy and paste, you can do so by inserting the -following objects into your `globals.keybindings` array: - -```json -{ "command": "copy", "keys": ["ctrl+shift+c"] }, -{ "command": "paste", "keys": ["ctrl+shift+v"] } -``` - -> 👉 **Note**: you can also add a keybinding for the `copy` command with the argument `"trimWhitespace": true`. This removes newlines as the text is copied to your clipboard. - -This will add copy and paste on ctrl+shift+c -and ctrl+shift+v respectively. - -You can set the keybindings to whatever you'd like. If you prefer -ctrl+c to copy, then set the `keys` to `"ctrl+c"`. - -You can even set multiple keybindings for a single action if you'd like. For example: - -```json - - { - "command" : "paste", - "keys" : - [ - "ctrl+shift+v" - ] - }, - { - "command" : "paste", - "keys" : - [ - "shift+insert" - ] - } -``` - -will bind both ctrl+shift+v and -shift+Insert to `paste`. - -> 👉 **Note**: If you set your copy keybinding to `"ctrl+c"`, you'll only be able to send -an interrupt to the commandline application using Ctrl+C when there's -no text selection. Additionally, if you set `paste` to `"ctrl+v"`, commandline -applications won't be able to read a ctrl+v from the input. For these reasons, -we suggest `"ctrl+shift+c"` and `"ctrl+shift+v"` - -### Setting the `startingDirectory` of WSL Profiles to `~` - -By default, the `startingDirectory` of a profile is `%USERPROFILE%` -(`C:\Users\`). This is a Windows path. However, for WSL, you might -want to use the WSL home path instead. At the time of writing (26decf1 / Nov. -1st, 2019), `startingDirectory` only accepts a Windows-style path, so setting it -to start within the WSL distro can be a little tricky. - -Fortunately, with Windows 1903, the filesystems of WSL distros can easily be -addressed using the `\\wsl$\` prefix. For any WSL distro whose name is -`DistroName`, you can use `\\wsl$\DistroName` as a Windows path that points to -the root of that distro's filesystem. - -For example, the following works as a profile to launch the "Ubuntu-18.04" -distro in it's home path: - -```json -{ - "name": "Ubuntu-18.04", - "commandline" : "wsl -d Ubuntu-18.04", - "startingDirectory" : "//wsl$/Ubuntu-18.04/home/", -} -``` +⚠ This document has moved to [the Customize Settings section of the Windows Terminal documentation](https://docs.microsoft.com/windows/terminal/customize-settings/global-settings). diff --git a/doc/user-docs/index.md b/doc/user-docs/index.md index 80e48522d5a..63e4df92006 100644 --- a/doc/user-docs/index.md +++ b/doc/user-docs/index.md @@ -1,91 +1 @@ -# Windows Terminal User Documentation - -NOTE: At the time of writing Windows Terminal is still under active development and many things will -change. If you notice an error in the docs, please raise an issue. Or better yet, please file a PR with an appropriate update! - -## Installing Windows Terminal - -### From Source Code - -To compile Windows Terminal yourself using the source code, follow the instructions in the [README](/README.md#developer-guidance). - -### From the Microsoft Store - -1. Make sure you have upgraded to the current Windows 10 release (at least build `1903`). To determine your build number, see [winver](https://docs.microsoft.com/en-us/windows/client-management/windows-version-search). -2. Open the Windows Terminal listing in the [Microsoft Store](https://aka.ms/install-terminal). -3. Review the minimum system requirements to confirm you can successfully install Windows Terminal. -4. Click `Get` to begin the installation process. - -## Starting Windows Terminal - -1. Locate the _Windows Terminal_ app in your Start menu. -2. Click _Windows Terminal_ to launch the app. If you need administrative privileges, right-click the entry and click `Run as administrator`. Alternatively, you can highlight the app and press `Ctrl`+`Shift`+`Enter`. - -NOTE: The default shell is PowerShell; you can change this using the _Running a Different Shell_ procedure. - -### Command line options - -Windows Terminal has implemented a rich set of command-line options in part as response to issue [#607](https://github.com/microsoft/terminal/issues/607). See [UsingCommandlineArguments.md](https://github.com/microsoft/terminal/blob/master/doc/user-docs/UsingCommandlineArguments.md) for details. - -## Multiple Tabs - -Additional shells can be started by hitting the `+` button from the tab bar -- a new instance of the -default shell is displayed (default shortcut: Ctrl+Shift+1). - -## Running a Different Shell - -Note: This section assumes you already have _Windows Subsystem for Linux_ (WSL) installed. For more information, see [the installation guide](https://docs.microsoft.com/en-us/windows/wsl/install-win10). - -Windows Terminal uses PowerShell as its default shell. You can also use Windows Terminal to launch other shells, such as `cmd.exe` or WSL's `bash`: - -1. In the tab bar, click the `⌵` button to view the available shells. -2. Choose your shell from the dropdown list. The new shell session will open in a new tab. - -To customize the shell list, see the _Configuring Windows Terminal_ section below. - -## Starting a new PowerShell tab with admin privilege - -There is no current plan to support this feature for security reasons. See issue [#632](https://github.com/microsoft/terminal/issues/632) - -## Selecting and Copying Text in Windows Terminal - -As in ConHost, a selection can be made by left-clicking and dragging the mouse across the terminal. This is a line selection by default, meaning that the selection will wrap to the end of the line and the beginning of the next one. You can select in block mode by holding down the Alt key when starting a selection. - -To copy the text to your clipboard, you can right-click the terminal when a selection is active. As of [#1224](https://github.com/microsoft/terminal/pull/1224) (first available in Windows Terminal v0.4), the Windows Terminal now supports HTML copy. The HTML is automatically copied to your clipboard along with the regular text in any copy operation. - -If there is not an active selection, a right-click will paste the text content from your clipboard to the terminal. - -Copy and paste operations can also be keybound. For more information on how to bind keys, see [Using Json Settings](UsingJsonSettings.md#adding-copy-and-paste-keybindings). - -> 👉 **Note**: If you have the `copyOnSelect` global setting enabled, a selection will persist and immediately copy the selected text to your clipboard. Right-clicking will always paste your clipboard data. - -## Add a "Open Windows Terminal Here" to File Explorer - -Not currently supported "out of the box" (See issue [#1060](https://github.com/microsoft/terminal/issues/1060)). However, you can open Windows Terminal in current directory by typing `wt -d .` in the Explorer address bar. - -## Configuring Windows Terminal - -All Windows Terminal settings are currently managed using the `settings.json` file, located within `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState`. - -To open the settings file from Windows Terminal: - -1. Click the `⌵` button in the top bar. -2. From the dropdown list, click `Settings`. You can also use a shortcut: Ctrl+,. -3. Your default `json` editor will open the settings file. - -For an introduction to the various settings, see [Using Json Settings](UsingJsonSettings.md). The list of valid settings can be found in the [settings.json documentation](../cascadia/SettingsSchema.md) section. - -## Tips and Tricks - -1. In PowerShell you can discover if the Windows Terminal is being used by checking for the existence of the environment variable `WT_SESSION`. - - Under pwsh you can also use -`(Get-Process -Id $pid).Parent.ProcessName -eq 'WindowsTerminal'` - - (ref [https://twitter.com/r_keith_hill/status/1142871145852440576](https://twitter.com/r_keith_hill/status/1142871145852440576)) - -2. Terminal zoom can be changed by holding Ctrl and scrolling with mouse. -3. Background opacity can be changed by holding Ctrl+Shift and scrolling with mouse. Note that acrylic transparency is limited by the OS only to focused windows. -4. Open Windows Terminal in current directory by typing `wt -d .` in the address bar. -5. Pin the Windows Terminal to the taskbar. Now it can be launched using the Windows shortcut Win+Number (e.g. Win+1 or any other number based on the position in the taskbar!). Press Win+Shift+Number to always launch a new window. -6. Please add more Tips and Tricks. +⚠ Our user-facing documentation has moved to the [Windows Terminal documentation page](https://docs.microsoft.com/windows/terminal/). diff --git a/oss/interval_tree/IntervalTree.h b/oss/interval_tree/IntervalTree.h new file mode 100644 index 00000000000..372258bfca3 --- /dev/null +++ b/oss/interval_tree/IntervalTree.h @@ -0,0 +1,434 @@ +#ifndef __INTERVAL_TREE_H +#define __INTERVAL_TREE_H + +#include +#include +#include +#include +#include + +#ifdef USE_INTERVAL_TREE_NAMESPACE +namespace interval_tree +{ +#endif + template + class Interval + { + public: + Scalar start; + Scalar stop; + Value value; + Interval(const Scalar& s, const Scalar& e, const Value& v) : + start(std::min(s, e)), stop(std::max(s, e)), value(v) + { + } + + Interval() + { + } + + constexpr bool operator==(const Interval& other) const noexcept + { + return start == other.start && + stop == other.stop && + value == other.value; + } + + constexpr bool operator!=(const Interval& other) const noexcept + { + return !(*this == other); + } + }; + + template + Value intervalStart(const Interval& i) + { + return i.start; + } + + template + Value intervalStop(const Interval& i) + { + return i.stop; + } + + template + std::ostream& operator<<(std::ostream& out, const Interval& i) + { + out << "Interval(" << i.start << ", " << i.stop << "): " << i.value; + return out; + } + + template + class IntervalTree + { + public: + typedef Interval interval; + typedef std::vector interval_vector; + + struct IntervalStartCmp + { + bool operator()(const interval& a, const interval& b) + { + return a.start < b.start; + } + }; + + struct IntervalStopCmp + { + bool operator()(const interval& a, const interval& b) + { + return a.stop < b.stop; + } + }; + + IntervalTree() : + left(nullptr), right(nullptr), center() + { + } + + ~IntervalTree() = default; + + std::unique_ptr clone() const + { + return std::unique_ptr(new IntervalTree(*this)); + } + + IntervalTree(const IntervalTree& other) : + intervals(other.intervals), + left(other.left ? other.left->clone() : nullptr), + right(other.right ? other.right->clone() : nullptr), + center(other.center) + { + } + + IntervalTree& operator=(IntervalTree&&) = default; + IntervalTree(IntervalTree&&) = default; + + IntervalTree& operator=(const IntervalTree& other) + { + center = other.center; + intervals = other.intervals; + left = other.left ? other.left->clone() : nullptr; + right = other.right ? other.right->clone() : nullptr; + return *this; + } + + IntervalTree( + interval_vector&& ivals, + std::size_t depth = 16, + std::size_t minbucket = 64, + std::size_t maxbucket = 512, + Scalar leftextent = {}, + Scalar rightextent = {}) : + left(nullptr), right(nullptr) + { + --depth; + const auto minmaxStop = std::minmax_element(ivals.begin(), ivals.end(), IntervalStopCmp()); + const auto minmaxStart = std::minmax_element(ivals.begin(), ivals.end(), IntervalStartCmp()); + if (!ivals.empty()) + { + center = (minmaxStart.first->start + minmaxStop.second->stop) / 2; + } + if (leftextent == Scalar{} && rightextent == Scalar{}) + { + // sort intervals by start + std::sort(ivals.begin(), ivals.end(), IntervalStartCmp()); + } + else + { + assert(std::is_sorted(ivals.begin(), ivals.end(), IntervalStartCmp())); + } + if (depth == 0 || (ivals.size() < minbucket && ivals.size() < maxbucket)) + { + std::sort(ivals.begin(), ivals.end(), IntervalStartCmp()); + intervals = std::move(ivals); + assert(is_valid().first); + return; + } + else + { + Scalar leftp = Scalar{}; + Scalar rightp = Scalar{}; + + if (leftextent != Scalar{} || rightextent != Scalar{}) + { + leftp = leftextent; + rightp = rightextent; + } + else + { + leftp = ivals.front().start; + rightp = std::max_element(ivals.begin(), ivals.end(), IntervalStopCmp())->stop; + } + + interval_vector lefts; + interval_vector rights; + + for (typename interval_vector::const_iterator i = ivals.begin(); + i != ivals.end(); + ++i) + { + const interval& interval = *i; + if (interval.stop < center) + { + lefts.push_back(interval); + } + else if (interval.start > center) + { + rights.push_back(interval); + } + else + { + assert(interval.start <= center); + assert(center <= interval.stop); + intervals.push_back(interval); + } + } + + if (!lefts.empty()) + { + left.reset(new IntervalTree(std::move(lefts), + depth, + minbucket, + maxbucket, + leftp, + center)); + } + if (!rights.empty()) + { + right.reset(new IntervalTree(std::move(rights), + depth, + minbucket, + maxbucket, + center, + rightp)); + } + } + assert(is_valid().first); + } + + // Call f on all intervals near the range [start, stop]: + template + void visit_near(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + if (!intervals.empty() && !(stop < intervals.front().start)) + { + for (auto& i : intervals) + { + f(i); + } + } + if (left && start <= center) + { + left->visit_near(start, stop, f); + } + if (right && stop >= center) + { + right->visit_near(start, stop, f); + } + } + + // Call f on all intervals crossing pos + template + void visit_overlapping(const Scalar& pos, UnaryFunction f) const + { + visit_overlapping(pos, pos, f); + } + + // Call f on all intervals overlapping [start, stop] + template + void visit_overlapping(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + auto filterF = [&](const interval& interval) { + if (interval.stop >= start && interval.start <= stop) + { + // Only apply f if overlapping + f(interval); + } + }; + visit_near(start, stop, filterF); + } + + // Call f on all intervals contained within [start, stop] + template + void visit_contained(const Scalar& start, const Scalar& stop, UnaryFunction f) const + { + auto filterF = [&](const interval& interval) { + if (start <= interval.start && interval.stop <= stop) + { + f(interval); + } + }; + visit_near(start, stop, filterF); + } + + interval_vector findOverlapping(const Scalar& start, const Scalar& stop) const + { + interval_vector result; + visit_overlapping(start, stop, [&](const interval& interval) { + result.emplace_back(interval); + }); + return result; + } + + interval_vector findContained(const Scalar& start, const Scalar& stop) const + { + interval_vector result; + visit_contained(start, stop, [&](const interval& interval) { + result.push_back(interval); + }); + return result; + } + bool empty() const + { + if (left && !left->empty()) + { + return false; + } + if (!intervals.empty()) + { + return false; + } + if (right && !right->empty()) + { + return false; + } + return true; + } + + template + void visit_all(UnaryFunction f) const + { + if (left) + { + left->visit_all(f); + } + std::for_each(intervals.begin(), intervals.end(), f); + if (right) + { + right->visit_all(f); + } + } + + std::pair extentBruitForce() const + { + struct Extent + { + std::pair x = { std::numeric_limits::max(), + std::numeric_limits::min() }; + void operator()(const interval& interval) + { + x.first = std::min(x.first, interval.start); + x.second = std::max(x.second, interval.stop); + } + }; + Extent extent; + + visit_all([&](const interval& interval) { extent(interval); }); + return extent.x; + } + + // Check all constraints. + // If first is false, second is invalid. + std::pair> is_valid() const + { + const auto minmaxStop = std::minmax_element(intervals.begin(), intervals.end(), IntervalStopCmp()); + const auto minmaxStart = std::minmax_element(intervals.begin(), intervals.end(), IntervalStartCmp()); + + std::pair> result = { true, { std::numeric_limits::max(), std::numeric_limits::min() } }; + if (!intervals.empty()) + { + result.second.first = std::min(result.second.first, minmaxStart.first->start); + result.second.second = std::min(result.second.second, minmaxStop.second->stop); + } + if (left) + { + auto valid = left->is_valid(); + result.first &= valid.first; + result.second.first = std::min(result.second.first, valid.second.first); + result.second.second = std::min(result.second.second, valid.second.second); + if (!result.first) + { + return result; + } + if (valid.second.second >= center) + { + result.first = false; + return result; + } + } + if (right) + { + auto valid = right->is_valid(); + result.first &= valid.first; + result.second.first = std::min(result.second.first, valid.second.first); + result.second.second = std::min(result.second.second, valid.second.second); + if (!result.first) + { + return result; + } + if (valid.second.first <= center) + { + result.first = false; + return result; + } + } + if (!std::is_sorted(intervals.begin(), intervals.end(), IntervalStartCmp())) + { + result.first = false; + } + return result; + } + + friend std::ostream& operator<<(std::ostream& os, const IntervalTree& itree) + { + return writeOut(os, itree); + } + + friend std::ostream& writeOut(std::ostream& os, const IntervalTree& itree, std::size_t depth = 0) + { + auto pad = [&]() { for (std::size_t i = 0; i != depth; ++i) { os << ' '; } }; + pad(); + os << "center: " << itree.center << '\n'; + for (const interval& inter : itree.intervals) + { + pad(); + os << inter << '\n'; + } + if (itree.left) + { + pad(); + os << "left:\n"; + writeOut(os, *itree.left, depth + 1); + } + else + { + pad(); + os << "left: nullptr\n"; + } + if (itree.right) + { + pad(); + os << "right:\n"; + writeOut(os, *itree.right, depth + 1); + } + else + { + pad(); + os << "right: nullptr\n"; + } + return os; + } + + private: + interval_vector intervals; + std::unique_ptr left; + std::unique_ptr right; + Scalar center; + }; +#ifdef USE_INTERVAL_TREE_NAMESPACE +} +#endif + +#endif diff --git a/oss/interval_tree/MAINTAINER_README.md b/oss/interval_tree/MAINTAINER_README.md new file mode 100644 index 00000000000..648db77d6a3 --- /dev/null +++ b/oss/interval_tree/MAINTAINER_README.md @@ -0,0 +1,17 @@ +# Notes for Future Maintainers + +This was originally imported by @PankajBhojwani in September 2020. + +The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme. +Please update the provenance information in that file when ingesting an updated version of the dependent library. +That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards. + +## What should be done to update this in the future? + +1. Go to ekg/intervaltreerepository on GitHub. +2. Take the file IntervalTree.h wholesale and drop it into the directory here. +3. Don't change anything about it. +4. Validate that the license in the root of the repository didn't change and update it if so. It is sitting in the same directory as this readme. + If it changed dramatically, ensure that it is still compatible with our license scheme. Also update the NOTICE file in the root of our repository to declare the third-party usage. +5. Submit the pull. + diff --git a/oss/interval_tree/cgmanifest.json b/oss/interval_tree/cgmanifest.json new file mode 100644 index 00000000000..b6c5b21999e --- /dev/null +++ b/oss/interval_tree/cgmanifest.json @@ -0,0 +1,13 @@ +{"Registrations":[ + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/ekg/intervaltree", + "commitHash": "b90527f9e6d51cd36ecbb50429e4524d3a418ea5" + } + } + } + ], + "Version": 1 +} diff --git a/oss/xorg_apps_rgb/MAINTAINER_README.md b/oss/xorg_apps_rgb/MAINTAINER_README.md new file mode 100644 index 00000000000..b675bdf9423 --- /dev/null +++ b/oss/xorg_apps_rgb/MAINTAINER_README.md @@ -0,0 +1,7 @@ +### Notes for Future Maintainers + +This manifest anchors our usage of rgb.txt from the X11 distribution. + +The provenance information (where it came from and which commit) is stored in the file `cgmanifest.json` in the same directory as this readme. +Please update the provenance information in that file when ingesting an updated version of the dependent library. +That provenance file is automatically read and inventoried by Microsoft systems to ensure compliance with appropiate governance standards. diff --git a/oss/xorg_apps_rgb/cgmanifest.json b/oss/xorg_apps_rgb/cgmanifest.json new file mode 100644 index 00000000000..c5d7fb8e9a8 --- /dev/null +++ b/oss/xorg_apps_rgb/cgmanifest.json @@ -0,0 +1,13 @@ +{"Registrations":[ + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://gitlab.freedesktop.org/xorg/app/rgb.git", + "commitHash": "97820e748eb496a1f6d3fc3bf89688f0ce1f64f9" + } + } + } + ], + "Version": 1 +} diff --git a/res/Cascadia.ttf b/res/Cascadia.ttf index 48acfd4e676..7ef47ce5a6e 100644 Binary files a/res/Cascadia.ttf and b/res/Cascadia.ttf differ diff --git a/res/CascadiaMono.ttf b/res/CascadiaMono.ttf index e90a0f58b82..152a74ef349 100644 Binary files a/res/CascadiaMono.ttf and b/res/CascadiaMono.ttf differ diff --git a/res/README.md b/res/README.md index f0929cfff73..2b372414581 100644 --- a/res/README.md +++ b/res/README.md @@ -2,20 +2,20 @@ ## Images -The images in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/master/LICENSE) as the rest +The images in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/main/LICENSE) as the rest of the Windows Terminal code. Please consult the [license](./LICENSE) in this directory for terms applicable to the image assets in this directory. ## Fonts -The fonts in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/master/LICENSE) as the rest +The fonts in this directory do not fall under the same [license](https://raw.githubusercontent.com/microsoft/terminal/main/LICENSE) as the rest of the Windows Terminal code. -Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadia-code/master/LICENSE) in the +Please consult the [license](https://raw.githubusercontent.com/microsoft/cascadia-code/main/LICENSE) in the [microsoft/cascadia-code](https://github.com/microsoft/cascadia-code) repository for terms applicable to the fonts in this directory. ### Fonts Included -* Cascadia Code, Cascadia Mono (2007.15) - * from microsoft/cascadia-code@2a54363b2c867f7ae811b9a034c0024cef67de96 +* Cascadia Code, Cascadia Mono (2009.21) + * from microsoft/cascadia-code@32f84124db1970fa5d032f0fe9019e6922961beb diff --git a/res/terminal/Generate-TerminalAssets.ps1 b/res/terminal/Generate-TerminalAssets.ps1 index f6520f3b3b9..3fd98547a4f 100644 --- a/res/terminal/Generate-TerminalAssets.ps1 +++ b/res/terminal/Generate-TerminalAssets.ps1 @@ -24,6 +24,7 @@ Param( [string]$Path, [string]$Destination, [int[]]$Altforms = (16, 20, 24, 30, 32, 36, 40, 48, 60, 64, 72, 80, 96, 256), + [int[]]$Win32IconSizes = (16, 20, 24, 32, 48, 64, 256), [switch]$Unplated = $true, [float[]]$Scales = (1.0, 1.25, 1.5, 2.0, 4.0), [string]$HighContrastPath = "", @@ -115,6 +116,7 @@ If (-Not [string]::IsNullOrEmpty($Destination)) { $TranslatedOutDir = "." } +$intermediates = [System.Collections.Concurrent.ConcurrentBag[PSCustomObject]]::new() $intermediateFiles = [System.Collections.Concurrent.ConcurrentBag[string]]::new() # Generate the base icons @@ -136,6 +138,11 @@ $allSizes | ForEach-Object -Parallel { } ($using:intermediateFiles).Add($intermediateStandardNt) + ($using:intermediates).Add([PSCustomObject]@{ + Contrast = "standard" + Size = $sz + PathWSL = $intermediateStandardWsl + }) If ($svgContrastWsl -Ne $null) { $intermediateBlackNt = "$destinationNt\_intermediate.black.$($sz).png" @@ -158,7 +165,26 @@ $allSizes | ForEach-Object -Parallel { ($using:intermediateFiles).Add($intermediateBlackNt) ($using:intermediateFiles).Add($intermediateWhiteNt) + ($using:intermediates).Add([PSCustomObject]@{ + Contrast = "black" + Size = $sz + PathWSL = $intermediateBlackWsl + }) + ($using:intermediates).Add([PSCustomObject]@{ + Contrast = "white" + Size = $sz + PathWSL = $intermediateWhiteWsl + }) + } +} + +$intermediates | ? { $_.Size -In $Win32IconSizes } | Group-Object Contrast | ForEach-Object -Parallel { + $assetName = "terminal.ico" + If ($_.Name -Ne "standard") { + $assetName = "terminal_contrast-$($_.Name).ico" } + Write-Host "Producing win32 .ico for contrast=$($_.Name) as $assetName" + wsl convert $_.Group.PathWSL "$($using:TranslatedOutDir)/$assetName" } # Once the base icons are done, splat them into the middles of larger canvases. diff --git a/res/terminal/images-Dev/LargeTile.scale-100.png b/res/terminal/images-Dev/LargeTile.scale-100.png index d409ee0c2d2..1168e1aa2a1 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-100.png and b/res/terminal/images-Dev/LargeTile.scale-100.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-100_contrast-black.png b/res/terminal/images-Dev/LargeTile.scale-100_contrast-black.png index 4f9f6ceeec5..8b5a6e5503c 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-100_contrast-black.png and b/res/terminal/images-Dev/LargeTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-100_contrast-white.png b/res/terminal/images-Dev/LargeTile.scale-100_contrast-white.png index 3dba00e203b..b1bf688340a 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-100_contrast-white.png and b/res/terminal/images-Dev/LargeTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-125.png b/res/terminal/images-Dev/LargeTile.scale-125.png index 18e903ea459..70c1c53979c 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-125.png and b/res/terminal/images-Dev/LargeTile.scale-125.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-125_contrast-black.png b/res/terminal/images-Dev/LargeTile.scale-125_contrast-black.png index 43fe337ac29..1d6a78d1f56 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-125_contrast-black.png and b/res/terminal/images-Dev/LargeTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-125_contrast-white.png b/res/terminal/images-Dev/LargeTile.scale-125_contrast-white.png index 893a64f8ec5..112900eced2 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-125_contrast-white.png and b/res/terminal/images-Dev/LargeTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-150.png b/res/terminal/images-Dev/LargeTile.scale-150.png index cb3b390f2ef..477c7904d35 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-150.png and b/res/terminal/images-Dev/LargeTile.scale-150.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-150_contrast-black.png b/res/terminal/images-Dev/LargeTile.scale-150_contrast-black.png index 2b51fefb028..72eaf7187cf 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-150_contrast-black.png and b/res/terminal/images-Dev/LargeTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-150_contrast-white.png b/res/terminal/images-Dev/LargeTile.scale-150_contrast-white.png index 602efd44f01..1c11fe8e212 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-150_contrast-white.png and b/res/terminal/images-Dev/LargeTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-200.png b/res/terminal/images-Dev/LargeTile.scale-200.png index dc151aa71d4..9261b9c7cbe 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-200.png and b/res/terminal/images-Dev/LargeTile.scale-200.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-200_contrast-black.png b/res/terminal/images-Dev/LargeTile.scale-200_contrast-black.png index 07c342a30c5..ba1e5cc5c23 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-200_contrast-black.png and b/res/terminal/images-Dev/LargeTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-200_contrast-white.png b/res/terminal/images-Dev/LargeTile.scale-200_contrast-white.png index 1ef86a82225..e765d441ef9 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-200_contrast-white.png and b/res/terminal/images-Dev/LargeTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-400.png b/res/terminal/images-Dev/LargeTile.scale-400.png index 941f0e05728..2265ee7870b 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-400.png and b/res/terminal/images-Dev/LargeTile.scale-400.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-400_contrast-black.png b/res/terminal/images-Dev/LargeTile.scale-400_contrast-black.png index dcbc2dfec17..8b10ef9a607 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-400_contrast-black.png and b/res/terminal/images-Dev/LargeTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/LargeTile.scale-400_contrast-white.png b/res/terminal/images-Dev/LargeTile.scale-400_contrast-white.png index bc6097de8b4..dd6349b4e2c 100644 Binary files a/res/terminal/images-Dev/LargeTile.scale-400_contrast-white.png and b/res/terminal/images-Dev/LargeTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-100.png b/res/terminal/images-Dev/LockScreenLogo.scale-100.png index ac7f437a26e..9c57ca1a088 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-100.png and b/res/terminal/images-Dev/LockScreenLogo.scale-100.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-black.png b/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-black.png index 931b46f2318..b044178bdc5 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-black.png and b/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-white.png b/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-white.png index 41c3aee3502..82000e14aca 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-white.png and b/res/terminal/images-Dev/LockScreenLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-125.png b/res/terminal/images-Dev/LockScreenLogo.scale-125.png index 48cba1f3883..2f10331d204 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-125.png and b/res/terminal/images-Dev/LockScreenLogo.scale-125.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-black.png b/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-black.png index d0c78011e6c..e4355ea0f17 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-black.png and b/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-white.png b/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-white.png index 1788b2253da..b5f7ef5195b 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-white.png and b/res/terminal/images-Dev/LockScreenLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-150.png b/res/terminal/images-Dev/LockScreenLogo.scale-150.png index 6574eef4b16..67fca3576fb 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-150.png and b/res/terminal/images-Dev/LockScreenLogo.scale-150.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-black.png b/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-black.png index 153e67c4ebb..1387f8a96fc 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-black.png and b/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-white.png b/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-white.png index 79ee49b96db..ce385eb837e 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-white.png and b/res/terminal/images-Dev/LockScreenLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-200.png b/res/terminal/images-Dev/LockScreenLogo.scale-200.png index 90603165de5..b01f3662a44 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-200.png and b/res/terminal/images-Dev/LockScreenLogo.scale-200.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-black.png b/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-black.png index 03201095aec..0159d4f074b 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-black.png and b/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-white.png b/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-white.png index 9c5b7161150..c345cb05885 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-white.png and b/res/terminal/images-Dev/LockScreenLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-400.png b/res/terminal/images-Dev/LockScreenLogo.scale-400.png index 60e2f47e425..69030497ddc 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-400.png and b/res/terminal/images-Dev/LockScreenLogo.scale-400.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-black.png b/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-black.png index 563108e90e9..b1aa59b8242 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-black.png and b/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-white.png b/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-white.png index 0f9c7950c71..c1ce213fbbc 100644 Binary files a/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-white.png and b/res/terminal/images-Dev/LockScreenLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-100.png b/res/terminal/images-Dev/SmallTile.scale-100.png index d57197710a6..acd1aea080c 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-100.png and b/res/terminal/images-Dev/SmallTile.scale-100.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-100_contrast-black.png b/res/terminal/images-Dev/SmallTile.scale-100_contrast-black.png index 3b4d5619397..f0f7dd6ce62 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-100_contrast-black.png and b/res/terminal/images-Dev/SmallTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-100_contrast-white.png b/res/terminal/images-Dev/SmallTile.scale-100_contrast-white.png index 110e3de75f0..2c165afd642 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-100_contrast-white.png and b/res/terminal/images-Dev/SmallTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-125.png b/res/terminal/images-Dev/SmallTile.scale-125.png index facd14b3bb0..93a9918b4c0 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-125.png and b/res/terminal/images-Dev/SmallTile.scale-125.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-125_contrast-black.png b/res/terminal/images-Dev/SmallTile.scale-125_contrast-black.png index 473ec5122af..52a55a5e989 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-125_contrast-black.png and b/res/terminal/images-Dev/SmallTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-125_contrast-white.png b/res/terminal/images-Dev/SmallTile.scale-125_contrast-white.png index d864ab7131c..8f7bdfbc0f3 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-125_contrast-white.png and b/res/terminal/images-Dev/SmallTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-150.png b/res/terminal/images-Dev/SmallTile.scale-150.png index b09b52967a5..569f7fb51fa 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-150.png and b/res/terminal/images-Dev/SmallTile.scale-150.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-150_contrast-black.png b/res/terminal/images-Dev/SmallTile.scale-150_contrast-black.png index c9f872090e0..3dcca6c5953 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-150_contrast-black.png and b/res/terminal/images-Dev/SmallTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-150_contrast-white.png b/res/terminal/images-Dev/SmallTile.scale-150_contrast-white.png index 6f029a17a15..8e6dcffaefd 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-150_contrast-white.png and b/res/terminal/images-Dev/SmallTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-200.png b/res/terminal/images-Dev/SmallTile.scale-200.png index 0bd0bbd9fc7..1e8da1a949f 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-200.png and b/res/terminal/images-Dev/SmallTile.scale-200.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-200_contrast-black.png b/res/terminal/images-Dev/SmallTile.scale-200_contrast-black.png index ee72b0ec3ee..33d85436fb4 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-200_contrast-black.png and b/res/terminal/images-Dev/SmallTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-200_contrast-white.png b/res/terminal/images-Dev/SmallTile.scale-200_contrast-white.png index af58357987f..7f17128bc80 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-200_contrast-white.png and b/res/terminal/images-Dev/SmallTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-400.png b/res/terminal/images-Dev/SmallTile.scale-400.png index e2b5edb96bf..b907c64f38f 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-400.png and b/res/terminal/images-Dev/SmallTile.scale-400.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-400_contrast-black.png b/res/terminal/images-Dev/SmallTile.scale-400_contrast-black.png index c636747380b..1898166a6a9 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-400_contrast-black.png and b/res/terminal/images-Dev/SmallTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/SmallTile.scale-400_contrast-white.png b/res/terminal/images-Dev/SmallTile.scale-400_contrast-white.png index cfa8ce7e4c7..753aa5fbde8 100644 Binary files a/res/terminal/images-Dev/SmallTile.scale-400_contrast-white.png and b/res/terminal/images-Dev/SmallTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-100.png b/res/terminal/images-Dev/SplashScreen.scale-100.png index db55993a98f..085a90578af 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-100.png and b/res/terminal/images-Dev/SplashScreen.scale-100.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-100_contrast-black.png b/res/terminal/images-Dev/SplashScreen.scale-100_contrast-black.png index a4480138b64..c76035d7c95 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-100_contrast-black.png and b/res/terminal/images-Dev/SplashScreen.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-100_contrast-white.png b/res/terminal/images-Dev/SplashScreen.scale-100_contrast-white.png index 09b65238b44..a44427ca69c 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-100_contrast-white.png and b/res/terminal/images-Dev/SplashScreen.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-125.png b/res/terminal/images-Dev/SplashScreen.scale-125.png index 1723102ecb4..1851b5893f7 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-125.png and b/res/terminal/images-Dev/SplashScreen.scale-125.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-125_contrast-black.png b/res/terminal/images-Dev/SplashScreen.scale-125_contrast-black.png index d827b3a621c..ba23f9a9ffe 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-125_contrast-black.png and b/res/terminal/images-Dev/SplashScreen.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-125_contrast-white.png b/res/terminal/images-Dev/SplashScreen.scale-125_contrast-white.png index 53057953a8f..3e2e389aac6 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-125_contrast-white.png and b/res/terminal/images-Dev/SplashScreen.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-150.png b/res/terminal/images-Dev/SplashScreen.scale-150.png index 4af9cccc109..5e72e6f5569 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-150.png and b/res/terminal/images-Dev/SplashScreen.scale-150.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-150_contrast-black.png b/res/terminal/images-Dev/SplashScreen.scale-150_contrast-black.png index 9137cacbf18..c766e8ea14a 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-150_contrast-black.png and b/res/terminal/images-Dev/SplashScreen.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-150_contrast-white.png b/res/terminal/images-Dev/SplashScreen.scale-150_contrast-white.png index 06a70ddc7dd..ecd2c17d1b4 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-150_contrast-white.png and b/res/terminal/images-Dev/SplashScreen.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-200.png b/res/terminal/images-Dev/SplashScreen.scale-200.png index 8660c3288d4..97380029565 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-200.png and b/res/terminal/images-Dev/SplashScreen.scale-200.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-200_contrast-black.png b/res/terminal/images-Dev/SplashScreen.scale-200_contrast-black.png index 38ad367e109..2b0bbe005e0 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-200_contrast-black.png and b/res/terminal/images-Dev/SplashScreen.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-200_contrast-white.png b/res/terminal/images-Dev/SplashScreen.scale-200_contrast-white.png index e910cabe62c..9b11bbbddae 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-200_contrast-white.png and b/res/terminal/images-Dev/SplashScreen.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-400.png b/res/terminal/images-Dev/SplashScreen.scale-400.png index e326020d5c6..e326fa8eeac 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-400.png and b/res/terminal/images-Dev/SplashScreen.scale-400.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-400_contrast-black.png b/res/terminal/images-Dev/SplashScreen.scale-400_contrast-black.png index 40c8b3d2429..d080a34010c 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-400_contrast-black.png and b/res/terminal/images-Dev/SplashScreen.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/SplashScreen.scale-400_contrast-white.png b/res/terminal/images-Dev/SplashScreen.scale-400_contrast-white.png index bdf7b5b7cb8..b044661d960 100644 Binary files a/res/terminal/images-Dev/SplashScreen.scale-400_contrast-white.png and b/res/terminal/images-Dev/SplashScreen.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-100.png b/res/terminal/images-Dev/Square150x150Logo.scale-100.png index 08df3c058d2..a6fe137a6b7 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-100.png and b/res/terminal/images-Dev/Square150x150Logo.scale-100.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-black.png b/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-black.png index a94e09558d5..72652fc544f 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-black.png and b/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-white.png b/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-white.png index 8b15f746632..20323ffed6a 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-white.png and b/res/terminal/images-Dev/Square150x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-125.png b/res/terminal/images-Dev/Square150x150Logo.scale-125.png index 2bf2012a23a..5e718af42cb 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-125.png and b/res/terminal/images-Dev/Square150x150Logo.scale-125.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-black.png b/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-black.png index 52a0961f9c1..e04a03369cf 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-black.png and b/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-white.png b/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-white.png index 1f2b02d1b32..6933f0f1015 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-white.png and b/res/terminal/images-Dev/Square150x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-150.png b/res/terminal/images-Dev/Square150x150Logo.scale-150.png index fe0f613df1f..f9dc809b099 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-150.png and b/res/terminal/images-Dev/Square150x150Logo.scale-150.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-black.png b/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-black.png index e709c17c3b7..6ef64f61336 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-black.png and b/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-white.png b/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-white.png index 00fb42558e0..c6ea6b4e18c 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-white.png and b/res/terminal/images-Dev/Square150x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-200.png b/res/terminal/images-Dev/Square150x150Logo.scale-200.png index ca73cfeb68e..3efc58299dc 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-200.png and b/res/terminal/images-Dev/Square150x150Logo.scale-200.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-black.png b/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-black.png index da75c53210c..adfac09934e 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-black.png and b/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-white.png b/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-white.png index f56a69cf10f..9944dfaf2e3 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-white.png and b/res/terminal/images-Dev/Square150x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-400.png b/res/terminal/images-Dev/Square150x150Logo.scale-400.png index fbf26770810..d888e70dfd3 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-400.png and b/res/terminal/images-Dev/Square150x150Logo.scale-400.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-black.png b/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-black.png index 3bb828ac8d5..8f840d0e993 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-black.png and b/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-white.png b/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-white.png index 99fc2a7ff4d..a21e4162f5d 100644 Binary files a/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-white.png and b/res/terminal/images-Dev/Square150x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-100.png b/res/terminal/images-Dev/Square44x44Logo.scale-100.png index b80c6f0999d..cc307445be3 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-100.png and b/res/terminal/images-Dev/Square44x44Logo.scale-100.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-black.png index 9f13cded565..bafed8ebac9 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-white.png index 7786af596e0..c91e7702d21 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-125.png b/res/terminal/images-Dev/Square44x44Logo.scale-125.png index 04cec312ab6..5c3a37e3272 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-125.png and b/res/terminal/images-Dev/Square44x44Logo.scale-125.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-black.png index eed2b33a67a..261d7479631 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-white.png index 86948693b8a..49be1b29a41 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-150.png b/res/terminal/images-Dev/Square44x44Logo.scale-150.png index 08841ccd584..67085809e14 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-150.png and b/res/terminal/images-Dev/Square44x44Logo.scale-150.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-black.png index 085efc7b6af..70af3251a08 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-white.png index c6ef1dafa8e..db8db65fd5a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-200.png b/res/terminal/images-Dev/Square44x44Logo.scale-200.png index df80737862b..e97cdc0ddb2 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-200.png and b/res/terminal/images-Dev/Square44x44Logo.scale-200.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-black.png index 16469eb5bd3..deb2c0d4a66 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-white.png index f9a6ae53359..8f579e72752 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-400.png b/res/terminal/images-Dev/Square44x44Logo.scale-400.png index af9be2cd111..0bbbdcd6865 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-400.png and b/res/terminal/images-Dev/Square44x44Logo.scale-400.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-black.png index 0b8ca35feb0..4e4d0074186 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-white.png index fa3109e7e22..00c699f697b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16.png index 53a57cfe600..208d101e329 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated.png index 53a57cfe600..208d101e329 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png index 26ae28f2a0b..5c168d971fb 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png index d1a2951126a..5b34fac8631 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-black.png index 26ae28f2a0b..5c168d971fb 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-white.png index d1a2951126a..04233897ae5 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-16_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20.png index 34b6f72788f..574c9a582c7 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated.png index 34b6f72788f..574c9a582c7 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png index a036a06ee23..0e967fa57bd 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png index 42d68e5f6f5..c1e799baf8e 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-black.png index a036a06ee23..0e967fa57bd 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-white.png index 42d68e5f6f5..79e1689dfe8 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-20_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24.png index ac7f437a26e..9c57ca1a088 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated.png index ac7f437a26e..9c57ca1a088 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png index 931b46f2318..cc9b13b02f5 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png index 41c3aee3502..a76fe401fd1 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-black.png index 931b46f2318..6b0019b6ff6 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-white.png index 41c3aee3502..790390a52b5 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-24_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256.png index 6478ae9ed44..8e9e3eeea7b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated.png index 6478ae9ed44..8e9e3eeea7b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png index f89467e7320..7c7d169d77b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png index 665dfbe3a37..f8a1d558b6a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-black.png index f89467e7320..b77782684c7 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-white.png index 665dfbe3a37..d64132d39d7 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-256_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30.png index 48cba1f3883..2f10331d204 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated.png index 48cba1f3883..2f10331d204 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png index d0c78011e6c..963c1617974 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png index 1788b2253da..cfd8d2e2393 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-black.png index d0c78011e6c..0002876e56f 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-white.png index 1788b2253da..92351241e9d 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-30_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32.png index d3d7c109d0f..88e8019f48a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated.png index d3d7c109d0f..88e8019f48a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png index e195a888f5f..27c89352cd3 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png index efc56018094..3c3e48a5aa0 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-black.png index e195a888f5f..536f5db541a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-white.png index efc56018094..0313c6116bc 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-32_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36.png index 6574eef4b16..67fca3576fb 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated.png index 6574eef4b16..67fca3576fb 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png index 153e67c4ebb..5672aa91200 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png index 79ee49b96db..93958bd9d9b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-black.png index 153e67c4ebb..82eacd71758 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-white.png index 79ee49b96db..fe44e9572f4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-36_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40.png index 7e6342060ab..87866f4a1ed 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated.png index 7e6342060ab..87866f4a1ed 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png index 65b26da3965..ea920ed09b6 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png index 5869c620465..1839861b9b4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-black.png index 65b26da3965..c8bce31d37b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-white.png index 5869c620465..208d7dc5139 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-40_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48.png index 90603165de5..b01f3662a44 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated.png index 90603165de5..b01f3662a44 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png index 03201095aec..029b13435d4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png index 9c5b7161150..f85e7d9102e 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-black.png index 03201095aec..366e2213ee1 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-white.png index 9c5b7161150..d26e851ba3b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-48_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60.png index a2b72777749..bd2e845e14f 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated.png index a2b72777749..bd2e845e14f 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png index acc3310df63..566909126f3 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png index aecda0f11e3..1dbec60f196 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-black.png index acc3310df63..a4dd468691a 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-white.png index aecda0f11e3..b27bab03095 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-60_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64.png index 0fb95dcd8bd..188291dd2a4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated.png index 0fb95dcd8bd..188291dd2a4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png index 01686eaccf7..8ceca111a29 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png index 63fb5b06cd7..89f3f524eaf 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-black.png index 01686eaccf7..eb7e3a7a83f 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-white.png index 63fb5b06cd7..34a153a5c9b 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-64_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72.png index 1fa68bbd07e..41f490db98c 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated.png index 1fa68bbd07e..41f490db98c 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png index 110249863df..f18d6884b41 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png index fa7c0bb9cf5..1bc9f94dda3 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-black.png index 110249863df..30949cbdd3e 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-white.png index fa7c0bb9cf5..e925f5f3be8 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-72_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80.png index 99abe57df37..8a7d7d060fa 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated.png index 99abe57df37..8a7d7d060fa 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png index 81f6b28749e..91c14f3a38f 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png index e5e2dacc31d..ea900daf66d 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-black.png index 81f6b28749e..923c93034a9 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-white.png index e5e2dacc31d..753563be216 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-80_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96.png index 60e2f47e425..69030497ddc 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated.png index 60e2f47e425..69030497ddc 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png index 563108e90e9..a2bf507caaa 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png index 0f9c7950c71..6df5d7fc545 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-black.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-black.png index 563108e90e9..604fe8ca0d4 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-black.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-black.png differ diff --git a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-white.png b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-white.png index 0f9c7950c71..bcb8d37442d 100644 Binary files a/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-white.png and b/res/terminal/images-Dev/Square44x44Logo.targetsize-96_contrast-white.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-100.png b/res/terminal/images-Dev/StoreLogo.scale-100.png index dbaf5d0935a..add36063b50 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-100.png and b/res/terminal/images-Dev/StoreLogo.scale-100.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-100_contrast-black.png b/res/terminal/images-Dev/StoreLogo.scale-100_contrast-black.png index c52aafcc5dd..74cc2f68ad5 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-100_contrast-black.png and b/res/terminal/images-Dev/StoreLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-100_contrast-white.png b/res/terminal/images-Dev/StoreLogo.scale-100_contrast-white.png index f3c68fb688c..87432f9e814 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-100_contrast-white.png and b/res/terminal/images-Dev/StoreLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-125.png b/res/terminal/images-Dev/StoreLogo.scale-125.png index ca2f2431624..ba42d76a64d 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-125.png and b/res/terminal/images-Dev/StoreLogo.scale-125.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-125_contrast-black.png b/res/terminal/images-Dev/StoreLogo.scale-125_contrast-black.png index a84d1ff188e..66e9fcd9b6d 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-125_contrast-black.png and b/res/terminal/images-Dev/StoreLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-125_contrast-white.png b/res/terminal/images-Dev/StoreLogo.scale-125_contrast-white.png index 5e741ef242f..a9ebeb75264 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-125_contrast-white.png and b/res/terminal/images-Dev/StoreLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-150.png b/res/terminal/images-Dev/StoreLogo.scale-150.png index 2a5cea070b0..a3db5b02ae6 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-150.png and b/res/terminal/images-Dev/StoreLogo.scale-150.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-150_contrast-black.png b/res/terminal/images-Dev/StoreLogo.scale-150_contrast-black.png index d843bd16ae1..a2d9194fbdc 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-150_contrast-black.png and b/res/terminal/images-Dev/StoreLogo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-150_contrast-white.png b/res/terminal/images-Dev/StoreLogo.scale-150_contrast-white.png index 8dac64a4fe8..432a8aa3acd 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-150_contrast-white.png and b/res/terminal/images-Dev/StoreLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-200.png b/res/terminal/images-Dev/StoreLogo.scale-200.png index 3529ce28184..4bcd7647d2f 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-200.png and b/res/terminal/images-Dev/StoreLogo.scale-200.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-200_contrast-black.png b/res/terminal/images-Dev/StoreLogo.scale-200_contrast-black.png index 4fb88aad7d1..ee5848ff561 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-200_contrast-black.png and b/res/terminal/images-Dev/StoreLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-200_contrast-white.png b/res/terminal/images-Dev/StoreLogo.scale-200_contrast-white.png index 5a1b5727e69..ad004da8616 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-200_contrast-white.png and b/res/terminal/images-Dev/StoreLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-400.png b/res/terminal/images-Dev/StoreLogo.scale-400.png index dd5be08ea13..0bfa64deaff 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-400.png and b/res/terminal/images-Dev/StoreLogo.scale-400.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-400_contrast-black.png b/res/terminal/images-Dev/StoreLogo.scale-400_contrast-black.png index d6616c8ab55..d8127f75d56 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-400_contrast-black.png and b/res/terminal/images-Dev/StoreLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/StoreLogo.scale-400_contrast-white.png b/res/terminal/images-Dev/StoreLogo.scale-400_contrast-white.png index 8c622de1d48..713389facc5 100644 Binary files a/res/terminal/images-Dev/StoreLogo.scale-400_contrast-white.png and b/res/terminal/images-Dev/StoreLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-100.png b/res/terminal/images-Dev/Wide310x150Logo.scale-100.png index 53ae47c1baf..226ab2fe73d 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-100.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-100.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-black.png b/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-black.png index e001af83cd8..b4ee2bb8004 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-black.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-white.png b/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-white.png index 0ce5b040316..3bb8cb95899 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-white.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-125.png b/res/terminal/images-Dev/Wide310x150Logo.scale-125.png index d92d52ec304..1f1fb392189 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-125.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-125.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-black.png b/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-black.png index 33010325e98..d5283f0da15 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-black.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-white.png b/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-white.png index b5745ff70f6..42c5b74bf88 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-white.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-150.png b/res/terminal/images-Dev/Wide310x150Logo.scale-150.png index 35de1db3d08..b09e84fbad5 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-150.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-150.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-black.png b/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-black.png index 7844cda30cf..47a76ef3247 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-black.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-white.png b/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-white.png index 9f62ef7fd38..a2d9c68ed14 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-white.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-200.png b/res/terminal/images-Dev/Wide310x150Logo.scale-200.png index db55993a98f..41eb4b388ed 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-200.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-200.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-black.png b/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-black.png index a4480138b64..d35cea16b90 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-black.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-white.png b/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-white.png index 09b65238b44..ba1aab9da0b 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-white.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-400.png b/res/terminal/images-Dev/Wide310x150Logo.scale-400.png index 8660c3288d4..cd912633b72 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-400.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-400.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-black.png b/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-black.png index 38ad367e109..9be71df8a6f 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-black.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-white.png b/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-white.png index e910cabe62c..71fb13cb425 100644 Binary files a/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-white.png and b/res/terminal/images-Dev/Wide310x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Dev/terminal.ico b/res/terminal/images-Dev/terminal.ico new file mode 100644 index 00000000000..cd3cabf67ef Binary files /dev/null and b/res/terminal/images-Dev/terminal.ico differ diff --git a/res/terminal/images-Dev/terminal_contrast-black.ico b/res/terminal/images-Dev/terminal_contrast-black.ico new file mode 100644 index 00000000000..3f5fc32c4ff Binary files /dev/null and b/res/terminal/images-Dev/terminal_contrast-black.ico differ diff --git a/res/terminal/images-Dev/terminal_contrast-white.ico b/res/terminal/images-Dev/terminal_contrast-white.ico new file mode 100644 index 00000000000..dfb487211e7 Binary files /dev/null and b/res/terminal/images-Dev/terminal_contrast-white.ico differ diff --git a/res/terminal/images-Pre/LargeTile.scale-100.png b/res/terminal/images-Pre/LargeTile.scale-100.png index ff19cc37d1b..87420f9c47a 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-100.png and b/res/terminal/images-Pre/LargeTile.scale-100.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-100_contrast-black.png b/res/terminal/images-Pre/LargeTile.scale-100_contrast-black.png index bbcfc696a9e..a11241559ac 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-100_contrast-black.png and b/res/terminal/images-Pre/LargeTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-100_contrast-white.png b/res/terminal/images-Pre/LargeTile.scale-100_contrast-white.png index 9b6b05881de..872ba2d5766 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-100_contrast-white.png and b/res/terminal/images-Pre/LargeTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-125.png b/res/terminal/images-Pre/LargeTile.scale-125.png index 6dbcfc80529..b339ffa30c9 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-125.png and b/res/terminal/images-Pre/LargeTile.scale-125.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-125_contrast-black.png b/res/terminal/images-Pre/LargeTile.scale-125_contrast-black.png index daa163d30e5..99bb866e0ef 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-125_contrast-black.png and b/res/terminal/images-Pre/LargeTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-125_contrast-white.png b/res/terminal/images-Pre/LargeTile.scale-125_contrast-white.png index e675b860dc3..7474440c1ba 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-125_contrast-white.png and b/res/terminal/images-Pre/LargeTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-150.png b/res/terminal/images-Pre/LargeTile.scale-150.png index 4f613a2e5d1..6b5d23cbc63 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-150.png and b/res/terminal/images-Pre/LargeTile.scale-150.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-150_contrast-black.png b/res/terminal/images-Pre/LargeTile.scale-150_contrast-black.png index 9de6e5ae4ef..ed38993441f 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-150_contrast-black.png and b/res/terminal/images-Pre/LargeTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-150_contrast-white.png b/res/terminal/images-Pre/LargeTile.scale-150_contrast-white.png index f3c3a8d6ce3..759109171e3 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-150_contrast-white.png and b/res/terminal/images-Pre/LargeTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-200.png b/res/terminal/images-Pre/LargeTile.scale-200.png index 5180f07084f..49b7e666054 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-200.png and b/res/terminal/images-Pre/LargeTile.scale-200.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-200_contrast-black.png b/res/terminal/images-Pre/LargeTile.scale-200_contrast-black.png index ac9b19f44ee..0196e45f576 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-200_contrast-black.png and b/res/terminal/images-Pre/LargeTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-200_contrast-white.png b/res/terminal/images-Pre/LargeTile.scale-200_contrast-white.png index 57e420e9974..c2849340cca 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-200_contrast-white.png and b/res/terminal/images-Pre/LargeTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-400.png b/res/terminal/images-Pre/LargeTile.scale-400.png index 59dc0864eb8..e2922a1f427 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-400.png and b/res/terminal/images-Pre/LargeTile.scale-400.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-400_contrast-black.png b/res/terminal/images-Pre/LargeTile.scale-400_contrast-black.png index dcb433a1084..43caa45c32f 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-400_contrast-black.png and b/res/terminal/images-Pre/LargeTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/LargeTile.scale-400_contrast-white.png b/res/terminal/images-Pre/LargeTile.scale-400_contrast-white.png index 0e6aefd75a6..5267377e379 100644 Binary files a/res/terminal/images-Pre/LargeTile.scale-400_contrast-white.png and b/res/terminal/images-Pre/LargeTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-black.png b/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-black.png index ce51243c628..3fb991d36db 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-black.png and b/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-white.png b/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-white.png index 2b9d1a06b62..4d768914b9f 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-white.png and b/res/terminal/images-Pre/LockScreenLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-black.png b/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-black.png index f7094e3374b..f2cadaf6093 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-black.png and b/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-white.png b/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-white.png index 788d405f453..0d5aced8769 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-white.png and b/res/terminal/images-Pre/LockScreenLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-black.png b/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-black.png index 62f84ba9aab..30c415ee7cb 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-black.png and b/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-white.png b/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-white.png index 61be432569a..1c0715711ee 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-white.png and b/res/terminal/images-Pre/LockScreenLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-black.png b/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-black.png index 8427e96d130..943b165b649 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-black.png and b/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-white.png b/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-white.png index f490a9aa080..b27565afc7c 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-white.png and b/res/terminal/images-Pre/LockScreenLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-400.png b/res/terminal/images-Pre/LockScreenLogo.scale-400.png index f3d81998c82..23ac9060328 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-400.png and b/res/terminal/images-Pre/LockScreenLogo.scale-400.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-black.png b/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-black.png index 217ea2d31a0..49b7c748775 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-black.png and b/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-white.png b/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-white.png index b2f0a14d558..a587d0d2e13 100644 Binary files a/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-white.png and b/res/terminal/images-Pre/LockScreenLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-100_contrast-black.png b/res/terminal/images-Pre/SmallTile.scale-100_contrast-black.png index 0d2591b8a46..9883df75ae5 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-100_contrast-black.png and b/res/terminal/images-Pre/SmallTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-100_contrast-white.png b/res/terminal/images-Pre/SmallTile.scale-100_contrast-white.png index 66a16a331f9..e9e03f17f4e 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-100_contrast-white.png and b/res/terminal/images-Pre/SmallTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-125_contrast-black.png b/res/terminal/images-Pre/SmallTile.scale-125_contrast-black.png index be14cbfa973..62f3fd7da7c 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-125_contrast-black.png and b/res/terminal/images-Pre/SmallTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-125_contrast-white.png b/res/terminal/images-Pre/SmallTile.scale-125_contrast-white.png index 5f8bfad946f..de9b4b4d95e 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-125_contrast-white.png and b/res/terminal/images-Pre/SmallTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-150.png b/res/terminal/images-Pre/SmallTile.scale-150.png index 4de7ce947a4..946b80179ac 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-150.png and b/res/terminal/images-Pre/SmallTile.scale-150.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-150_contrast-black.png b/res/terminal/images-Pre/SmallTile.scale-150_contrast-black.png index 3d9acb361c3..7eb40ee9f1b 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-150_contrast-black.png and b/res/terminal/images-Pre/SmallTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-150_contrast-white.png b/res/terminal/images-Pre/SmallTile.scale-150_contrast-white.png index caf834d6c22..31c958beaee 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-150_contrast-white.png and b/res/terminal/images-Pre/SmallTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-200.png b/res/terminal/images-Pre/SmallTile.scale-200.png index cb5aa5b8b26..65fb0ebe15f 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-200.png and b/res/terminal/images-Pre/SmallTile.scale-200.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-200_contrast-black.png b/res/terminal/images-Pre/SmallTile.scale-200_contrast-black.png index 0f7481d481b..4483f7a8a70 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-200_contrast-black.png and b/res/terminal/images-Pre/SmallTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-200_contrast-white.png b/res/terminal/images-Pre/SmallTile.scale-200_contrast-white.png index b321705e93c..1ab8cb00cb7 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-200_contrast-white.png and b/res/terminal/images-Pre/SmallTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-400.png b/res/terminal/images-Pre/SmallTile.scale-400.png index 802b5697ba6..2c1a4e65f35 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-400.png and b/res/terminal/images-Pre/SmallTile.scale-400.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-400_contrast-black.png b/res/terminal/images-Pre/SmallTile.scale-400_contrast-black.png index 27dca17f1d1..1a7663d4a4a 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-400_contrast-black.png and b/res/terminal/images-Pre/SmallTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/SmallTile.scale-400_contrast-white.png b/res/terminal/images-Pre/SmallTile.scale-400_contrast-white.png index de3c606cda7..8e9fafa22dd 100644 Binary files a/res/terminal/images-Pre/SmallTile.scale-400_contrast-white.png and b/res/terminal/images-Pre/SmallTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-100.png b/res/terminal/images-Pre/SplashScreen.scale-100.png index 7b9aa833627..f2d28837d19 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-100.png and b/res/terminal/images-Pre/SplashScreen.scale-100.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-100_contrast-black.png b/res/terminal/images-Pre/SplashScreen.scale-100_contrast-black.png index c33de15b3ba..e4ad08bf376 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-100_contrast-black.png and b/res/terminal/images-Pre/SplashScreen.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-100_contrast-white.png b/res/terminal/images-Pre/SplashScreen.scale-100_contrast-white.png index 26ebbbf09e7..abd1a4757a8 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-100_contrast-white.png and b/res/terminal/images-Pre/SplashScreen.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-125.png b/res/terminal/images-Pre/SplashScreen.scale-125.png index 6f40d5dbac5..0c49e60951f 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-125.png and b/res/terminal/images-Pre/SplashScreen.scale-125.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-125_contrast-black.png b/res/terminal/images-Pre/SplashScreen.scale-125_contrast-black.png index 1c1f57c92bb..8cf0bc59f6b 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-125_contrast-black.png and b/res/terminal/images-Pre/SplashScreen.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-125_contrast-white.png b/res/terminal/images-Pre/SplashScreen.scale-125_contrast-white.png index dd4d19a7cc9..07ab5545a70 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-125_contrast-white.png and b/res/terminal/images-Pre/SplashScreen.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-150.png b/res/terminal/images-Pre/SplashScreen.scale-150.png index 979920c7041..1ebf72ee3f2 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-150.png and b/res/terminal/images-Pre/SplashScreen.scale-150.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-150_contrast-black.png b/res/terminal/images-Pre/SplashScreen.scale-150_contrast-black.png index 12a8a36a798..be6b6bf30d1 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-150_contrast-black.png and b/res/terminal/images-Pre/SplashScreen.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-150_contrast-white.png b/res/terminal/images-Pre/SplashScreen.scale-150_contrast-white.png index 093903c8ad0..234a75c986b 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-150_contrast-white.png and b/res/terminal/images-Pre/SplashScreen.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-200.png b/res/terminal/images-Pre/SplashScreen.scale-200.png index 2b3700d954a..52f63856fb8 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-200.png and b/res/terminal/images-Pre/SplashScreen.scale-200.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-200_contrast-black.png b/res/terminal/images-Pre/SplashScreen.scale-200_contrast-black.png index 3f8fc2d06f5..d6d7449c721 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-200_contrast-black.png and b/res/terminal/images-Pre/SplashScreen.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-200_contrast-white.png b/res/terminal/images-Pre/SplashScreen.scale-200_contrast-white.png index 357a83f1325..e836c743366 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-200_contrast-white.png and b/res/terminal/images-Pre/SplashScreen.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-400.png b/res/terminal/images-Pre/SplashScreen.scale-400.png index cfa18af7afb..20c8517ac39 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-400.png and b/res/terminal/images-Pre/SplashScreen.scale-400.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-400_contrast-black.png b/res/terminal/images-Pre/SplashScreen.scale-400_contrast-black.png index 24ea7722a7d..41ca12dc4cf 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-400_contrast-black.png and b/res/terminal/images-Pre/SplashScreen.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/SplashScreen.scale-400_contrast-white.png b/res/terminal/images-Pre/SplashScreen.scale-400_contrast-white.png index 5568cc8615d..1b2f5532ac3 100644 Binary files a/res/terminal/images-Pre/SplashScreen.scale-400_contrast-white.png and b/res/terminal/images-Pre/SplashScreen.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-black.png b/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-black.png index 2a82392f5f6..aea541e5c5d 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-black.png and b/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-white.png b/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-white.png index 3a93c3c213b..b17e332259f 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-white.png and b/res/terminal/images-Pre/Square150x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-125.png b/res/terminal/images-Pre/Square150x150Logo.scale-125.png index b5f246453c9..c690ad4349e 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-125.png and b/res/terminal/images-Pre/Square150x150Logo.scale-125.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-black.png b/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-black.png index f2031a207aa..431b99bc669 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-black.png and b/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-white.png b/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-white.png index 24caab8deaf..17920dab16e 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-white.png and b/res/terminal/images-Pre/Square150x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-150.png b/res/terminal/images-Pre/Square150x150Logo.scale-150.png index 56a588cdb41..6dacb121498 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-150.png and b/res/terminal/images-Pre/Square150x150Logo.scale-150.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-black.png b/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-black.png index e03041227d9..2e80f0fbe1b 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-black.png and b/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-white.png b/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-white.png index 6c2e8f38cb2..2e0b6a00178 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-white.png and b/res/terminal/images-Pre/Square150x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-200.png b/res/terminal/images-Pre/Square150x150Logo.scale-200.png index 8e474e250ee..ab346527cb1 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-200.png and b/res/terminal/images-Pre/Square150x150Logo.scale-200.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-black.png b/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-black.png index 4b35315c7ef..3a4e911f70a 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-black.png and b/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-white.png b/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-white.png index 47154776d69..c66df81b9af 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-white.png and b/res/terminal/images-Pre/Square150x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-400.png b/res/terminal/images-Pre/Square150x150Logo.scale-400.png index 6506347b70c..6c4d6cbae83 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-400.png and b/res/terminal/images-Pre/Square150x150Logo.scale-400.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-black.png b/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-black.png index 8ad38f6a9e4..def0b6e9af9 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-black.png and b/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-white.png b/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-white.png index 9f5c525348d..4080f0a90ab 100644 Binary files a/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-white.png and b/res/terminal/images-Pre/Square150x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-black.png index a9a5b046d86..666d975b4e5 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-white.png index a7550ffadf2..df498a04d5c 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-black.png index ce820d9be4d..b4f0b2f6d4f 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-white.png index d05e4bd2a68..07804a1ba9f 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-black.png index b4a2e6cc928..ed7dd271dc0 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-white.png index 2df02c39299..9365b282383 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-black.png index 07bb68ba944..5b7928ea30d 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-white.png index b71dd10ecb5..bfa39cdb9f2 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-400.png b/res/terminal/images-Pre/Square44x44Logo.scale-400.png index bb2b49e617e..81854d275a7 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-400.png and b/res/terminal/images-Pre/Square44x44Logo.scale-400.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-black.png index 52b835c05b8..7e8c6e755cb 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-white.png index 451c5d8a5be..6bec41429ce 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png index f068d41135d..ecfbda1cf42 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-16_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-16_contrast-white.png index f068d41135d..a4d62d9a45d 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-16_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-16_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png index 59f4b0be724..64afde06d6a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-20_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-20_contrast-white.png index 59f4b0be724..787743b58be 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-20_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-20_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png index ce51243c628..48cd28d398b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png index 2b9d1a06b62..a2ac2feb984 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-black.png index ce51243c628..86d0b183a2a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-white.png index 2b9d1a06b62..5b6da5b5629 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-24_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256.png index 672edff53c8..772cf72503a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated.png index 672edff53c8..772cf72503a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png index 1ecd842894d..00ce3b2788b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png index 55fe0139f27..6c1d2d62a9b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-black.png index 1ecd842894d..c910f0fa7f9 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-white.png index 55fe0139f27..5d29a762772 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-256_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png index f7094e3374b..74dd8f6759f 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png index 788d405f453..c64a10704d6 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-black.png index f7094e3374b..d042231faf2 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-white.png index 788d405f453..35f9f76fc00 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-30_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png index 7363adf4cf4..48ca81e2639 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png index 80df0b81f93..72ae74119a2 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-black.png index 7363adf4cf4..1da424d51ff 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-white.png index 80df0b81f93..23b45c728f3 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-32_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png index 62f84ba9aab..c2ebcfeb1d2 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png index 61be432569a..35595a8d015 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-black.png index 62f84ba9aab..222a2d11cbb 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-white.png index 61be432569a..2d07f5dfa32 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-36_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png index 93ef2fac9c5..2679d7dc1ae 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png index 59cef34ed0b..8825addbd90 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-black.png index 93ef2fac9c5..a0052777cf8 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-white.png index 59cef34ed0b..049439c12bf 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-40_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png index 8427e96d130..a8dfce17e9a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png index f490a9aa080..8da058e265d 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-black.png index 8427e96d130..a2673419987 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-white.png index f490a9aa080..3185d533f83 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-48_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png index d742b734893..231be8c2dbb 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png index edb9d45d24e..c0f8b219fc0 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-black.png index d742b734893..721bb85003a 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-white.png index edb9d45d24e..ed5f4fe5a89 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-60_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png index 4d97ed69bd3..11a81deccd9 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png index 833f8150167..d78ae88daa5 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-black.png index 4d97ed69bd3..310101b6b48 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-white.png index 833f8150167..b9b318e456b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-64_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png index bb27466d5a1..c89d331e8eb 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png index c337b4be5ee..505c5b73f5b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-black.png index bb27466d5a1..22f5ab9e1d2 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-white.png index c337b4be5ee..c9e5d1cabad 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-72_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80.png index 7b7cb73ff32..e7f68ba3cb4 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated.png index 7b7cb73ff32..a5794ae4a2d 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png index e636f00d75c..42cd409a9ee 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png index d687f9d73d1..8479bcb238c 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-black.png index e636f00d75c..0ef6790f471 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-white.png index d687f9d73d1..7b8888fdc19 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-80_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96.png index f3d81998c82..55f3eb7baee 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated.png index f3d81998c82..241bae35cc0 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png index 217ea2d31a0..82988f2f6d1 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png index b2f0a14d558..c9b505b97cb 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-black.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-black.png index 217ea2d31a0..f13d1603a4b 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-black.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-black.png differ diff --git a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-white.png b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-white.png index b2f0a14d558..4132ed24f35 100644 Binary files a/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-white.png and b/res/terminal/images-Pre/Square44x44Logo.targetsize-96_contrast-white.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-100_contrast-black.png b/res/terminal/images-Pre/StoreLogo.scale-100_contrast-black.png index 26f94d563de..12c5c018965 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-100_contrast-black.png and b/res/terminal/images-Pre/StoreLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-100_contrast-white.png b/res/terminal/images-Pre/StoreLogo.scale-100_contrast-white.png index 59318f1e47d..b8bd06c7e20 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-100_contrast-white.png and b/res/terminal/images-Pre/StoreLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-125_contrast-black.png b/res/terminal/images-Pre/StoreLogo.scale-125_contrast-black.png index f6c85246064..d83abba28b3 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-125_contrast-black.png and b/res/terminal/images-Pre/StoreLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-125_contrast-white.png b/res/terminal/images-Pre/StoreLogo.scale-125_contrast-white.png index fc296cb2f9a..98a8aae5450 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-125_contrast-white.png and b/res/terminal/images-Pre/StoreLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-150_contrast-black.png b/res/terminal/images-Pre/StoreLogo.scale-150_contrast-black.png index fd6c20ce317..65f06b77e5b 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-150_contrast-black.png and b/res/terminal/images-Pre/StoreLogo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-150_contrast-white.png b/res/terminal/images-Pre/StoreLogo.scale-150_contrast-white.png index 66fd5655058..c3027d0270f 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-150_contrast-white.png and b/res/terminal/images-Pre/StoreLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-200.png b/res/terminal/images-Pre/StoreLogo.scale-200.png index cb927be574a..214d571dbe8 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-200.png and b/res/terminal/images-Pre/StoreLogo.scale-200.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-200_contrast-black.png b/res/terminal/images-Pre/StoreLogo.scale-200_contrast-black.png index 60eb894b35a..00682579b91 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-200_contrast-black.png and b/res/terminal/images-Pre/StoreLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-200_contrast-white.png b/res/terminal/images-Pre/StoreLogo.scale-200_contrast-white.png index 93713970ec5..bf8706cf005 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-200_contrast-white.png and b/res/terminal/images-Pre/StoreLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-400.png b/res/terminal/images-Pre/StoreLogo.scale-400.png index 378428959af..4257e8f8741 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-400.png and b/res/terminal/images-Pre/StoreLogo.scale-400.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-400_contrast-black.png b/res/terminal/images-Pre/StoreLogo.scale-400_contrast-black.png index b1d9835eb78..086fd52cfc5 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-400_contrast-black.png and b/res/terminal/images-Pre/StoreLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/StoreLogo.scale-400_contrast-white.png b/res/terminal/images-Pre/StoreLogo.scale-400_contrast-white.png index e2025f22550..4859edc9eac 100644 Binary files a/res/terminal/images-Pre/StoreLogo.scale-400_contrast-white.png and b/res/terminal/images-Pre/StoreLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-100.png b/res/terminal/images-Pre/Wide310x150Logo.scale-100.png index bc80585c0f2..7ec239549bb 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-100.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-100.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-black.png b/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-black.png index b1efc07df3a..1e679dab3a2 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-black.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-white.png b/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-white.png index 066fceb3401..f944ae16af4 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-white.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-125.png b/res/terminal/images-Pre/Wide310x150Logo.scale-125.png index d6f75cb9549..87460af2507 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-125.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-125.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-black.png b/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-black.png index f1b0c364f9d..be13f63fbce 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-black.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-white.png b/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-white.png index 6671495b6c0..88a42db767e 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-white.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-150.png b/res/terminal/images-Pre/Wide310x150Logo.scale-150.png index f73fb44f273..740ace0c423 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-150.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-150.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-black.png b/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-black.png index 2729e37f318..62da0c03893 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-black.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-white.png b/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-white.png index 7dc7b1a55c2..cf567a37c16 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-white.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-200.png b/res/terminal/images-Pre/Wide310x150Logo.scale-200.png index 7b9aa833627..2598cb6426a 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-200.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-200.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-black.png b/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-black.png index c33de15b3ba..72db9d5516d 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-black.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-white.png b/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-white.png index 26ebbbf09e7..ba4a1aa8184 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-white.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-400.png b/res/terminal/images-Pre/Wide310x150Logo.scale-400.png index 2b3700d954a..a960fea03de 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-400.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-400.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-black.png b/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-black.png index 3f8fc2d06f5..af69ac9cc1a 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-black.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-white.png b/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-white.png index 357a83f1325..7c2083e67f2 100644 Binary files a/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-white.png and b/res/terminal/images-Pre/Wide310x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images-Pre/terminal.ico b/res/terminal/images-Pre/terminal.ico new file mode 100644 index 00000000000..eaffee3bf7d Binary files /dev/null and b/res/terminal/images-Pre/terminal.ico differ diff --git a/res/terminal/images-Pre/terminal_contrast-black.ico b/res/terminal/images-Pre/terminal_contrast-black.ico new file mode 100644 index 00000000000..f25ecd5dbe6 Binary files /dev/null and b/res/terminal/images-Pre/terminal_contrast-black.ico differ diff --git a/res/terminal/images-Pre/terminal_contrast-white.ico b/res/terminal/images-Pre/terminal_contrast-white.ico new file mode 100644 index 00000000000..5d196e711de Binary files /dev/null and b/res/terminal/images-Pre/terminal_contrast-white.ico differ diff --git a/res/terminal/images-Universal/LargeTile.scale-100.png b/res/terminal/images-Universal/LargeTile.scale-100.png index 20d9f18169f..89c73fc6614 100644 Binary files a/res/terminal/images-Universal/LargeTile.scale-100.png and b/res/terminal/images-Universal/LargeTile.scale-100.png differ diff --git a/res/terminal/images-Universal/LargeTile.scale-125.png b/res/terminal/images-Universal/LargeTile.scale-125.png index fd77a3192ec..38a7e663d27 100644 Binary files a/res/terminal/images-Universal/LargeTile.scale-125.png and b/res/terminal/images-Universal/LargeTile.scale-125.png differ diff --git a/res/terminal/images-Universal/LargeTile.scale-150.png b/res/terminal/images-Universal/LargeTile.scale-150.png index 57d81682890..49574a0e4e3 100644 Binary files a/res/terminal/images-Universal/LargeTile.scale-150.png and b/res/terminal/images-Universal/LargeTile.scale-150.png differ diff --git a/res/terminal/images-Universal/LargeTile.scale-200.png b/res/terminal/images-Universal/LargeTile.scale-200.png index ed031de1ea2..112af439d1a 100644 Binary files a/res/terminal/images-Universal/LargeTile.scale-200.png and b/res/terminal/images-Universal/LargeTile.scale-200.png differ diff --git a/res/terminal/images-Universal/LargeTile.scale-400.png b/res/terminal/images-Universal/LargeTile.scale-400.png index 04e8afad1bd..c4c464a4fa2 100644 Binary files a/res/terminal/images-Universal/LargeTile.scale-400.png and b/res/terminal/images-Universal/LargeTile.scale-400.png differ diff --git a/res/terminal/images-Universal/LockScreenLogo.scale-400.png b/res/terminal/images-Universal/LockScreenLogo.scale-400.png index 3ad06c9d2f4..1856d7160f5 100644 Binary files a/res/terminal/images-Universal/LockScreenLogo.scale-400.png and b/res/terminal/images-Universal/LockScreenLogo.scale-400.png differ diff --git a/res/terminal/images-Universal/SmallTile.scale-100.png b/res/terminal/images-Universal/SmallTile.scale-100.png index cde85ebbac9..cbbe8b85660 100644 Binary files a/res/terminal/images-Universal/SmallTile.scale-100.png and b/res/terminal/images-Universal/SmallTile.scale-100.png differ diff --git a/res/terminal/images-Universal/SmallTile.scale-125.png b/res/terminal/images-Universal/SmallTile.scale-125.png index 6394934ec3e..63cc5ca208a 100644 Binary files a/res/terminal/images-Universal/SmallTile.scale-125.png and b/res/terminal/images-Universal/SmallTile.scale-125.png differ diff --git a/res/terminal/images-Universal/SmallTile.scale-150.png b/res/terminal/images-Universal/SmallTile.scale-150.png index fd231248ff4..00771f7314a 100644 Binary files a/res/terminal/images-Universal/SmallTile.scale-150.png and b/res/terminal/images-Universal/SmallTile.scale-150.png differ diff --git a/res/terminal/images-Universal/SmallTile.scale-200.png b/res/terminal/images-Universal/SmallTile.scale-200.png index ff24b499048..9cb038020d9 100644 Binary files a/res/terminal/images-Universal/SmallTile.scale-200.png and b/res/terminal/images-Universal/SmallTile.scale-200.png differ diff --git a/res/terminal/images-Universal/SmallTile.scale-400.png b/res/terminal/images-Universal/SmallTile.scale-400.png index 1f8c99b2c4c..0a4fcac3744 100644 Binary files a/res/terminal/images-Universal/SmallTile.scale-400.png and b/res/terminal/images-Universal/SmallTile.scale-400.png differ diff --git a/res/terminal/images-Universal/SplashScreen.scale-100.png b/res/terminal/images-Universal/SplashScreen.scale-100.png index 4cb4d9f457e..07b5a2d3d70 100644 Binary files a/res/terminal/images-Universal/SplashScreen.scale-100.png and b/res/terminal/images-Universal/SplashScreen.scale-100.png differ diff --git a/res/terminal/images-Universal/SplashScreen.scale-125.png b/res/terminal/images-Universal/SplashScreen.scale-125.png index 226a590769e..937e9fa9583 100644 Binary files a/res/terminal/images-Universal/SplashScreen.scale-125.png and b/res/terminal/images-Universal/SplashScreen.scale-125.png differ diff --git a/res/terminal/images-Universal/SplashScreen.scale-150.png b/res/terminal/images-Universal/SplashScreen.scale-150.png index 225b1736625..b59a5106091 100644 Binary files a/res/terminal/images-Universal/SplashScreen.scale-150.png and b/res/terminal/images-Universal/SplashScreen.scale-150.png differ diff --git a/res/terminal/images-Universal/SplashScreen.scale-200.png b/res/terminal/images-Universal/SplashScreen.scale-200.png index 71fffea6564..d8bab400046 100644 Binary files a/res/terminal/images-Universal/SplashScreen.scale-200.png and b/res/terminal/images-Universal/SplashScreen.scale-200.png differ diff --git a/res/terminal/images-Universal/SplashScreen.scale-400.png b/res/terminal/images-Universal/SplashScreen.scale-400.png index 36e19921a3b..346b691db02 100644 Binary files a/res/terminal/images-Universal/SplashScreen.scale-400.png and b/res/terminal/images-Universal/SplashScreen.scale-400.png differ diff --git a/res/terminal/images-Universal/Square150x150Logo.scale-100.png b/res/terminal/images-Universal/Square150x150Logo.scale-100.png index 7807fe391e4..8c3345a03b6 100644 Binary files a/res/terminal/images-Universal/Square150x150Logo.scale-100.png and b/res/terminal/images-Universal/Square150x150Logo.scale-100.png differ diff --git a/res/terminal/images-Universal/Square150x150Logo.scale-125.png b/res/terminal/images-Universal/Square150x150Logo.scale-125.png index 772069b0a8e..14f9082018a 100644 Binary files a/res/terminal/images-Universal/Square150x150Logo.scale-125.png and b/res/terminal/images-Universal/Square150x150Logo.scale-125.png differ diff --git a/res/terminal/images-Universal/Square150x150Logo.scale-150.png b/res/terminal/images-Universal/Square150x150Logo.scale-150.png index e10142d18ef..48f858457a3 100644 Binary files a/res/terminal/images-Universal/Square150x150Logo.scale-150.png and b/res/terminal/images-Universal/Square150x150Logo.scale-150.png differ diff --git a/res/terminal/images-Universal/Square150x150Logo.scale-200.png b/res/terminal/images-Universal/Square150x150Logo.scale-200.png index 3675e159767..f85e3a6abc2 100644 Binary files a/res/terminal/images-Universal/Square150x150Logo.scale-200.png and b/res/terminal/images-Universal/Square150x150Logo.scale-200.png differ diff --git a/res/terminal/images-Universal/Square150x150Logo.scale-400.png b/res/terminal/images-Universal/Square150x150Logo.scale-400.png index ab2fb81fa42..8b29d666ec1 100644 Binary files a/res/terminal/images-Universal/Square150x150Logo.scale-400.png and b/res/terminal/images-Universal/Square150x150Logo.scale-400.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.scale-100.png b/res/terminal/images-Universal/Square44x44Logo.scale-100.png index 5add3816c1a..d6a0d529a45 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.scale-100.png and b/res/terminal/images-Universal/Square44x44Logo.scale-100.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.scale-125.png b/res/terminal/images-Universal/Square44x44Logo.scale-125.png index 09eaa01cb07..e4b611269a2 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.scale-125.png and b/res/terminal/images-Universal/Square44x44Logo.scale-125.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.scale-150.png b/res/terminal/images-Universal/Square44x44Logo.scale-150.png index 6fce68f3c26..ab058ef4a27 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.scale-150.png and b/res/terminal/images-Universal/Square44x44Logo.scale-150.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.scale-200.png b/res/terminal/images-Universal/Square44x44Logo.scale-200.png index 8fbe3f6aaf5..e4420aa8ce7 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.scale-200.png and b/res/terminal/images-Universal/Square44x44Logo.scale-200.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.scale-400.png b/res/terminal/images-Universal/Square44x44Logo.scale-400.png index 630456e0b76..9059b2e3df8 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.scale-400.png and b/res/terminal/images-Universal/Square44x44Logo.scale-400.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.targetsize-256.png b/res/terminal/images-Universal/Square44x44Logo.targetsize-256.png index 59f4296e235..1255f30764f 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.targetsize-256.png and b/res/terminal/images-Universal/Square44x44Logo.targetsize-256.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.targetsize-256_altform-unplated.png b/res/terminal/images-Universal/Square44x44Logo.targetsize-256_altform-unplated.png index 59f4296e235..1255f30764f 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.targetsize-256_altform-unplated.png and b/res/terminal/images-Universal/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.targetsize-96.png b/res/terminal/images-Universal/Square44x44Logo.targetsize-96.png index 3ad06c9d2f4..1856d7160f5 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.targetsize-96.png and b/res/terminal/images-Universal/Square44x44Logo.targetsize-96.png differ diff --git a/res/terminal/images-Universal/Square44x44Logo.targetsize-96_altform-unplated.png b/res/terminal/images-Universal/Square44x44Logo.targetsize-96_altform-unplated.png index 3ad06c9d2f4..1856d7160f5 100644 Binary files a/res/terminal/images-Universal/Square44x44Logo.targetsize-96_altform-unplated.png and b/res/terminal/images-Universal/Square44x44Logo.targetsize-96_altform-unplated.png differ diff --git a/res/terminal/images-Universal/StoreLogo.scale-100.png b/res/terminal/images-Universal/StoreLogo.scale-100.png index fa575eb48c7..2016a9eb5cc 100644 Binary files a/res/terminal/images-Universal/StoreLogo.scale-100.png and b/res/terminal/images-Universal/StoreLogo.scale-100.png differ diff --git a/res/terminal/images-Universal/StoreLogo.scale-125.png b/res/terminal/images-Universal/StoreLogo.scale-125.png index 4ab038b6aab..6bef531470d 100644 Binary files a/res/terminal/images-Universal/StoreLogo.scale-125.png and b/res/terminal/images-Universal/StoreLogo.scale-125.png differ diff --git a/res/terminal/images-Universal/StoreLogo.scale-150.png b/res/terminal/images-Universal/StoreLogo.scale-150.png index 5ea72b4b8fd..c5b6f52a036 100644 Binary files a/res/terminal/images-Universal/StoreLogo.scale-150.png and b/res/terminal/images-Universal/StoreLogo.scale-150.png differ diff --git a/res/terminal/images-Universal/StoreLogo.scale-200.png b/res/terminal/images-Universal/StoreLogo.scale-200.png index f9dce98de97..52604acc924 100644 Binary files a/res/terminal/images-Universal/StoreLogo.scale-200.png and b/res/terminal/images-Universal/StoreLogo.scale-200.png differ diff --git a/res/terminal/images-Universal/StoreLogo.scale-400.png b/res/terminal/images-Universal/StoreLogo.scale-400.png index a51e51d618d..5dfd992be16 100644 Binary files a/res/terminal/images-Universal/StoreLogo.scale-400.png and b/res/terminal/images-Universal/StoreLogo.scale-400.png differ diff --git a/res/terminal/images-Universal/Wide310x150Logo.scale-100.png b/res/terminal/images-Universal/Wide310x150Logo.scale-100.png index 172b7523bf7..a9b6af59b0c 100644 Binary files a/res/terminal/images-Universal/Wide310x150Logo.scale-100.png and b/res/terminal/images-Universal/Wide310x150Logo.scale-100.png differ diff --git a/res/terminal/images-Universal/Wide310x150Logo.scale-125.png b/res/terminal/images-Universal/Wide310x150Logo.scale-125.png index b17bd824365..8158441c45a 100644 Binary files a/res/terminal/images-Universal/Wide310x150Logo.scale-125.png and b/res/terminal/images-Universal/Wide310x150Logo.scale-125.png differ diff --git a/res/terminal/images-Universal/Wide310x150Logo.scale-150.png b/res/terminal/images-Universal/Wide310x150Logo.scale-150.png index 60798baec5f..7014db99bac 100644 Binary files a/res/terminal/images-Universal/Wide310x150Logo.scale-150.png and b/res/terminal/images-Universal/Wide310x150Logo.scale-150.png differ diff --git a/res/terminal/images-Universal/Wide310x150Logo.scale-200.png b/res/terminal/images-Universal/Wide310x150Logo.scale-200.png index 4cb4d9f457e..e8b4b724f89 100644 Binary files a/res/terminal/images-Universal/Wide310x150Logo.scale-200.png and b/res/terminal/images-Universal/Wide310x150Logo.scale-200.png differ diff --git a/res/terminal/images-Universal/Wide310x150Logo.scale-400.png b/res/terminal/images-Universal/Wide310x150Logo.scale-400.png index 71fffea6564..c76d438dbfe 100644 Binary files a/res/terminal/images-Universal/Wide310x150Logo.scale-400.png and b/res/terminal/images-Universal/Wide310x150Logo.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/LargeTile.scale-100.png b/res/terminal/images-UniversalDev/LargeTile.scale-100.png index c4b6e2f0eb3..b9c7deea425 100644 Binary files a/res/terminal/images-UniversalDev/LargeTile.scale-100.png and b/res/terminal/images-UniversalDev/LargeTile.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/LargeTile.scale-125.png b/res/terminal/images-UniversalDev/LargeTile.scale-125.png index e319702d763..960b04d734c 100644 Binary files a/res/terminal/images-UniversalDev/LargeTile.scale-125.png and b/res/terminal/images-UniversalDev/LargeTile.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/LargeTile.scale-150.png b/res/terminal/images-UniversalDev/LargeTile.scale-150.png index ca28d81bddb..9dec2a6ac2d 100644 Binary files a/res/terminal/images-UniversalDev/LargeTile.scale-150.png and b/res/terminal/images-UniversalDev/LargeTile.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/LargeTile.scale-200.png b/res/terminal/images-UniversalDev/LargeTile.scale-200.png index db918a66451..aa60bbb671e 100644 Binary files a/res/terminal/images-UniversalDev/LargeTile.scale-200.png and b/res/terminal/images-UniversalDev/LargeTile.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/LargeTile.scale-400.png b/res/terminal/images-UniversalDev/LargeTile.scale-400.png index 24ff15394e1..7c2f3b1a7d3 100644 Binary files a/res/terminal/images-UniversalDev/LargeTile.scale-400.png and b/res/terminal/images-UniversalDev/LargeTile.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/LockScreenLogo.scale-400.png b/res/terminal/images-UniversalDev/LockScreenLogo.scale-400.png index 931051b650a..7497bb33bf2 100644 Binary files a/res/terminal/images-UniversalDev/LockScreenLogo.scale-400.png and b/res/terminal/images-UniversalDev/LockScreenLogo.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/SmallTile.scale-100.png b/res/terminal/images-UniversalDev/SmallTile.scale-100.png index 6d9c851f2db..73ebc5a8737 100644 Binary files a/res/terminal/images-UniversalDev/SmallTile.scale-100.png and b/res/terminal/images-UniversalDev/SmallTile.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/SmallTile.scale-125.png b/res/terminal/images-UniversalDev/SmallTile.scale-125.png index 9b7068355cd..8ef0c3f1c0e 100644 Binary files a/res/terminal/images-UniversalDev/SmallTile.scale-125.png and b/res/terminal/images-UniversalDev/SmallTile.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/SmallTile.scale-150.png b/res/terminal/images-UniversalDev/SmallTile.scale-150.png index af51a9aa0a3..fe59476cfcf 100644 Binary files a/res/terminal/images-UniversalDev/SmallTile.scale-150.png and b/res/terminal/images-UniversalDev/SmallTile.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/SmallTile.scale-200.png b/res/terminal/images-UniversalDev/SmallTile.scale-200.png index afa5b128723..256c84aeee0 100644 Binary files a/res/terminal/images-UniversalDev/SmallTile.scale-200.png and b/res/terminal/images-UniversalDev/SmallTile.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/SmallTile.scale-400.png b/res/terminal/images-UniversalDev/SmallTile.scale-400.png index 1f5ca05e572..4f44bf14d49 100644 Binary files a/res/terminal/images-UniversalDev/SmallTile.scale-400.png and b/res/terminal/images-UniversalDev/SmallTile.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/SplashScreen.scale-100.png b/res/terminal/images-UniversalDev/SplashScreen.scale-100.png index ac502d7abd9..fe2b7a8b398 100644 Binary files a/res/terminal/images-UniversalDev/SplashScreen.scale-100.png and b/res/terminal/images-UniversalDev/SplashScreen.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/SplashScreen.scale-125.png b/res/terminal/images-UniversalDev/SplashScreen.scale-125.png index fa975cb3e5f..81af6374687 100644 Binary files a/res/terminal/images-UniversalDev/SplashScreen.scale-125.png and b/res/terminal/images-UniversalDev/SplashScreen.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/SplashScreen.scale-150.png b/res/terminal/images-UniversalDev/SplashScreen.scale-150.png index e17c759f58e..5de685c37fc 100644 Binary files a/res/terminal/images-UniversalDev/SplashScreen.scale-150.png and b/res/terminal/images-UniversalDev/SplashScreen.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/SplashScreen.scale-200.png b/res/terminal/images-UniversalDev/SplashScreen.scale-200.png index 853ba2e553e..f0bf5ec3e30 100644 Binary files a/res/terminal/images-UniversalDev/SplashScreen.scale-200.png and b/res/terminal/images-UniversalDev/SplashScreen.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/SplashScreen.scale-400.png b/res/terminal/images-UniversalDev/SplashScreen.scale-400.png index 659ae08735e..17237111653 100644 Binary files a/res/terminal/images-UniversalDev/SplashScreen.scale-400.png and b/res/terminal/images-UniversalDev/SplashScreen.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/Square150x150Logo.scale-100.png b/res/terminal/images-UniversalDev/Square150x150Logo.scale-100.png index 3315504af23..0924e7c94ec 100644 Binary files a/res/terminal/images-UniversalDev/Square150x150Logo.scale-100.png and b/res/terminal/images-UniversalDev/Square150x150Logo.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/Square150x150Logo.scale-125.png b/res/terminal/images-UniversalDev/Square150x150Logo.scale-125.png index 93512364414..1c28b7b834e 100644 Binary files a/res/terminal/images-UniversalDev/Square150x150Logo.scale-125.png and b/res/terminal/images-UniversalDev/Square150x150Logo.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/Square150x150Logo.scale-150.png b/res/terminal/images-UniversalDev/Square150x150Logo.scale-150.png index 3b07d3ca2ae..221dcc0e54a 100644 Binary files a/res/terminal/images-UniversalDev/Square150x150Logo.scale-150.png and b/res/terminal/images-UniversalDev/Square150x150Logo.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/Square150x150Logo.scale-200.png b/res/terminal/images-UniversalDev/Square150x150Logo.scale-200.png index 141f2154112..fba9de01b75 100644 Binary files a/res/terminal/images-UniversalDev/Square150x150Logo.scale-200.png and b/res/terminal/images-UniversalDev/Square150x150Logo.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/Square150x150Logo.scale-400.png b/res/terminal/images-UniversalDev/Square150x150Logo.scale-400.png index 5e7b5c6327e..5caba1140e0 100644 Binary files a/res/terminal/images-UniversalDev/Square150x150Logo.scale-400.png and b/res/terminal/images-UniversalDev/Square150x150Logo.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.scale-100.png b/res/terminal/images-UniversalDev/Square44x44Logo.scale-100.png index 132d5d38866..62c47eb44d6 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.scale-100.png and b/res/terminal/images-UniversalDev/Square44x44Logo.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.scale-125.png b/res/terminal/images-UniversalDev/Square44x44Logo.scale-125.png index 44acfbf8744..2f48b6e2860 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.scale-125.png and b/res/terminal/images-UniversalDev/Square44x44Logo.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.scale-150.png b/res/terminal/images-UniversalDev/Square44x44Logo.scale-150.png index bfda7012e59..431dd47b270 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.scale-150.png and b/res/terminal/images-UniversalDev/Square44x44Logo.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.scale-200.png b/res/terminal/images-UniversalDev/Square44x44Logo.scale-200.png index fab57281ba8..a4987d889f3 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.scale-200.png and b/res/terminal/images-UniversalDev/Square44x44Logo.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.scale-400.png b/res/terminal/images-UniversalDev/Square44x44Logo.scale-400.png index 7814eab2a4c..346eb7117a2 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.scale-400.png and b/res/terminal/images-UniversalDev/Square44x44Logo.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256.png b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256.png index 0a5942634fa..1197dc83184 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256.png and b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256_altform-unplated.png b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256_altform-unplated.png index 0a5942634fa..1197dc83184 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256_altform-unplated.png and b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96.png b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96.png index 931051b650a..7497bb33bf2 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96.png and b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96.png differ diff --git a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96_altform-unplated.png b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96_altform-unplated.png index 931051b650a..7497bb33bf2 100644 Binary files a/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96_altform-unplated.png and b/res/terminal/images-UniversalDev/Square44x44Logo.targetsize-96_altform-unplated.png differ diff --git a/res/terminal/images-UniversalDev/StoreLogo.scale-100.png b/res/terminal/images-UniversalDev/StoreLogo.scale-100.png index f9656b5e1d9..74a6a044e3f 100644 Binary files a/res/terminal/images-UniversalDev/StoreLogo.scale-100.png and b/res/terminal/images-UniversalDev/StoreLogo.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/StoreLogo.scale-125.png b/res/terminal/images-UniversalDev/StoreLogo.scale-125.png index 3a032967e37..255eb135a13 100644 Binary files a/res/terminal/images-UniversalDev/StoreLogo.scale-125.png and b/res/terminal/images-UniversalDev/StoreLogo.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/StoreLogo.scale-150.png b/res/terminal/images-UniversalDev/StoreLogo.scale-150.png index 2b614d6df74..99b42403084 100644 Binary files a/res/terminal/images-UniversalDev/StoreLogo.scale-150.png and b/res/terminal/images-UniversalDev/StoreLogo.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/StoreLogo.scale-200.png b/res/terminal/images-UniversalDev/StoreLogo.scale-200.png index 6f9cb96a685..a45e1bc4b4f 100644 Binary files a/res/terminal/images-UniversalDev/StoreLogo.scale-200.png and b/res/terminal/images-UniversalDev/StoreLogo.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/StoreLogo.scale-400.png b/res/terminal/images-UniversalDev/StoreLogo.scale-400.png index 3c4887caafc..141757ee260 100644 Binary files a/res/terminal/images-UniversalDev/StoreLogo.scale-400.png and b/res/terminal/images-UniversalDev/StoreLogo.scale-400.png differ diff --git a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-100.png b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-100.png index 2003e0c724d..366b0a1b1ba 100644 Binary files a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-100.png and b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-100.png differ diff --git a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-125.png b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-125.png index 34a309ad210..52c7cab5690 100644 Binary files a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-125.png and b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-125.png differ diff --git a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-150.png b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-150.png index d389d1ae4cd..badca03183a 100644 Binary files a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-150.png and b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-150.png differ diff --git a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-200.png b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-200.png index ac502d7abd9..1baac4e6b1f 100644 Binary files a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-200.png and b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-200.png differ diff --git a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-400.png b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-400.png index 853ba2e553e..f350971ac76 100644 Binary files a/res/terminal/images-UniversalDev/Wide310x150Logo.scale-400.png and b/res/terminal/images-UniversalDev/Wide310x150Logo.scale-400.png differ diff --git a/res/terminal/images/LargeTile.scale-100_contrast-black.png b/res/terminal/images/LargeTile.scale-100_contrast-black.png index 4f9f6ceeec5..4d0af95c8cd 100644 Binary files a/res/terminal/images/LargeTile.scale-100_contrast-black.png and b/res/terminal/images/LargeTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images/LargeTile.scale-100_contrast-white.png b/res/terminal/images/LargeTile.scale-100_contrast-white.png index 3dba00e203b..cb790aaacd4 100644 Binary files a/res/terminal/images/LargeTile.scale-100_contrast-white.png and b/res/terminal/images/LargeTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images/LargeTile.scale-125_contrast-black.png b/res/terminal/images/LargeTile.scale-125_contrast-black.png index 43fe337ac29..aece26fcfc9 100644 Binary files a/res/terminal/images/LargeTile.scale-125_contrast-black.png and b/res/terminal/images/LargeTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images/LargeTile.scale-125_contrast-white.png b/res/terminal/images/LargeTile.scale-125_contrast-white.png index 893a64f8ec5..382f81e7204 100644 Binary files a/res/terminal/images/LargeTile.scale-125_contrast-white.png and b/res/terminal/images/LargeTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images/LargeTile.scale-150_contrast-black.png b/res/terminal/images/LargeTile.scale-150_contrast-black.png index 2b51fefb028..6c75a46f8af 100644 Binary files a/res/terminal/images/LargeTile.scale-150_contrast-black.png and b/res/terminal/images/LargeTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images/LargeTile.scale-150_contrast-white.png b/res/terminal/images/LargeTile.scale-150_contrast-white.png index 602efd44f01..2ceb8babcd6 100644 Binary files a/res/terminal/images/LargeTile.scale-150_contrast-white.png and b/res/terminal/images/LargeTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images/LargeTile.scale-200_contrast-black.png b/res/terminal/images/LargeTile.scale-200_contrast-black.png index 07c342a30c5..936966c9f7a 100644 Binary files a/res/terminal/images/LargeTile.scale-200_contrast-black.png and b/res/terminal/images/LargeTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images/LargeTile.scale-200_contrast-white.png b/res/terminal/images/LargeTile.scale-200_contrast-white.png index 1ef86a82225..0d2be111df1 100644 Binary files a/res/terminal/images/LargeTile.scale-200_contrast-white.png and b/res/terminal/images/LargeTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images/LargeTile.scale-400_contrast-black.png b/res/terminal/images/LargeTile.scale-400_contrast-black.png index dcbc2dfec17..c77991840e6 100644 Binary files a/res/terminal/images/LargeTile.scale-400_contrast-black.png and b/res/terminal/images/LargeTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images/LargeTile.scale-400_contrast-white.png b/res/terminal/images/LargeTile.scale-400_contrast-white.png index bc6097de8b4..9076b3abd8b 100644 Binary files a/res/terminal/images/LargeTile.scale-400_contrast-white.png and b/res/terminal/images/LargeTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-100_contrast-white.png b/res/terminal/images/LockScreenLogo.scale-100_contrast-white.png index 41c3aee3502..d84f7fdc104 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-100_contrast-white.png and b/res/terminal/images/LockScreenLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-125_contrast-black.png b/res/terminal/images/LockScreenLogo.scale-125_contrast-black.png index d0c78011e6c..f325909bfdf 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-125_contrast-black.png and b/res/terminal/images/LockScreenLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-125_contrast-white.png b/res/terminal/images/LockScreenLogo.scale-125_contrast-white.png index 1788b2253da..f04486617c3 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-125_contrast-white.png and b/res/terminal/images/LockScreenLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-150_contrast-white.png b/res/terminal/images/LockScreenLogo.scale-150_contrast-white.png index 79ee49b96db..83d215daefc 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-150_contrast-white.png and b/res/terminal/images/LockScreenLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-200_contrast-black.png b/res/terminal/images/LockScreenLogo.scale-200_contrast-black.png index 03201095aec..0a9912cdfc2 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-200_contrast-black.png and b/res/terminal/images/LockScreenLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-200_contrast-white.png b/res/terminal/images/LockScreenLogo.scale-200_contrast-white.png index 9c5b7161150..006de607c65 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-200_contrast-white.png and b/res/terminal/images/LockScreenLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-400_contrast-black.png b/res/terminal/images/LockScreenLogo.scale-400_contrast-black.png index 563108e90e9..3520a74d313 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-400_contrast-black.png and b/res/terminal/images/LockScreenLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images/LockScreenLogo.scale-400_contrast-white.png b/res/terminal/images/LockScreenLogo.scale-400_contrast-white.png index 0f9c7950c71..82c2d4a7e0d 100644 Binary files a/res/terminal/images/LockScreenLogo.scale-400_contrast-white.png and b/res/terminal/images/LockScreenLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images/SmallTile.scale-100_contrast-black.png b/res/terminal/images/SmallTile.scale-100_contrast-black.png index 3b4d5619397..57858ce9bea 100644 Binary files a/res/terminal/images/SmallTile.scale-100_contrast-black.png and b/res/terminal/images/SmallTile.scale-100_contrast-black.png differ diff --git a/res/terminal/images/SmallTile.scale-100_contrast-white.png b/res/terminal/images/SmallTile.scale-100_contrast-white.png index 110e3de75f0..ca6b810b694 100644 Binary files a/res/terminal/images/SmallTile.scale-100_contrast-white.png and b/res/terminal/images/SmallTile.scale-100_contrast-white.png differ diff --git a/res/terminal/images/SmallTile.scale-125_contrast-black.png b/res/terminal/images/SmallTile.scale-125_contrast-black.png index 473ec5122af..9af3a012a21 100644 Binary files a/res/terminal/images/SmallTile.scale-125_contrast-black.png and b/res/terminal/images/SmallTile.scale-125_contrast-black.png differ diff --git a/res/terminal/images/SmallTile.scale-125_contrast-white.png b/res/terminal/images/SmallTile.scale-125_contrast-white.png index d864ab7131c..4e0d4f1dd63 100644 Binary files a/res/terminal/images/SmallTile.scale-125_contrast-white.png and b/res/terminal/images/SmallTile.scale-125_contrast-white.png differ diff --git a/res/terminal/images/SmallTile.scale-150_contrast-black.png b/res/terminal/images/SmallTile.scale-150_contrast-black.png index c9f872090e0..b9143958dde 100644 Binary files a/res/terminal/images/SmallTile.scale-150_contrast-black.png and b/res/terminal/images/SmallTile.scale-150_contrast-black.png differ diff --git a/res/terminal/images/SmallTile.scale-150_contrast-white.png b/res/terminal/images/SmallTile.scale-150_contrast-white.png index 6f029a17a15..20ce68eb11d 100644 Binary files a/res/terminal/images/SmallTile.scale-150_contrast-white.png and b/res/terminal/images/SmallTile.scale-150_contrast-white.png differ diff --git a/res/terminal/images/SmallTile.scale-200_contrast-black.png b/res/terminal/images/SmallTile.scale-200_contrast-black.png index ee72b0ec3ee..19d5cd21afd 100644 Binary files a/res/terminal/images/SmallTile.scale-200_contrast-black.png and b/res/terminal/images/SmallTile.scale-200_contrast-black.png differ diff --git a/res/terminal/images/SmallTile.scale-200_contrast-white.png b/res/terminal/images/SmallTile.scale-200_contrast-white.png index af58357987f..2a74998b1a0 100644 Binary files a/res/terminal/images/SmallTile.scale-200_contrast-white.png and b/res/terminal/images/SmallTile.scale-200_contrast-white.png differ diff --git a/res/terminal/images/SmallTile.scale-400_contrast-black.png b/res/terminal/images/SmallTile.scale-400_contrast-black.png index c636747380b..3f5dd44149b 100644 Binary files a/res/terminal/images/SmallTile.scale-400_contrast-black.png and b/res/terminal/images/SmallTile.scale-400_contrast-black.png differ diff --git a/res/terminal/images/SmallTile.scale-400_contrast-white.png b/res/terminal/images/SmallTile.scale-400_contrast-white.png index cfa8ce7e4c7..e72b1212ffb 100644 Binary files a/res/terminal/images/SmallTile.scale-400_contrast-white.png and b/res/terminal/images/SmallTile.scale-400_contrast-white.png differ diff --git a/res/terminal/images/SplashScreen.scale-100_contrast-black.png b/res/terminal/images/SplashScreen.scale-100_contrast-black.png index a4480138b64..60d238b185d 100644 Binary files a/res/terminal/images/SplashScreen.scale-100_contrast-black.png and b/res/terminal/images/SplashScreen.scale-100_contrast-black.png differ diff --git a/res/terminal/images/SplashScreen.scale-100_contrast-white.png b/res/terminal/images/SplashScreen.scale-100_contrast-white.png index 09b65238b44..e78f61c2440 100644 Binary files a/res/terminal/images/SplashScreen.scale-100_contrast-white.png and b/res/terminal/images/SplashScreen.scale-100_contrast-white.png differ diff --git a/res/terminal/images/SplashScreen.scale-125_contrast-black.png b/res/terminal/images/SplashScreen.scale-125_contrast-black.png index d827b3a621c..867ca3c24df 100644 Binary files a/res/terminal/images/SplashScreen.scale-125_contrast-black.png and b/res/terminal/images/SplashScreen.scale-125_contrast-black.png differ diff --git a/res/terminal/images/SplashScreen.scale-125_contrast-white.png b/res/terminal/images/SplashScreen.scale-125_contrast-white.png index 53057953a8f..7a475d0a329 100644 Binary files a/res/terminal/images/SplashScreen.scale-125_contrast-white.png and b/res/terminal/images/SplashScreen.scale-125_contrast-white.png differ diff --git a/res/terminal/images/SplashScreen.scale-150_contrast-black.png b/res/terminal/images/SplashScreen.scale-150_contrast-black.png index 9137cacbf18..58fdd8bf608 100644 Binary files a/res/terminal/images/SplashScreen.scale-150_contrast-black.png and b/res/terminal/images/SplashScreen.scale-150_contrast-black.png differ diff --git a/res/terminal/images/SplashScreen.scale-150_contrast-white.png b/res/terminal/images/SplashScreen.scale-150_contrast-white.png index 06a70ddc7dd..bb84c55a96c 100644 Binary files a/res/terminal/images/SplashScreen.scale-150_contrast-white.png and b/res/terminal/images/SplashScreen.scale-150_contrast-white.png differ diff --git a/res/terminal/images/SplashScreen.scale-200_contrast-black.png b/res/terminal/images/SplashScreen.scale-200_contrast-black.png index 38ad367e109..cbebaf69aff 100644 Binary files a/res/terminal/images/SplashScreen.scale-200_contrast-black.png and b/res/terminal/images/SplashScreen.scale-200_contrast-black.png differ diff --git a/res/terminal/images/SplashScreen.scale-200_contrast-white.png b/res/terminal/images/SplashScreen.scale-200_contrast-white.png index e910cabe62c..db95aef875e 100644 Binary files a/res/terminal/images/SplashScreen.scale-200_contrast-white.png and b/res/terminal/images/SplashScreen.scale-200_contrast-white.png differ diff --git a/res/terminal/images/SplashScreen.scale-400_contrast-black.png b/res/terminal/images/SplashScreen.scale-400_contrast-black.png index 40c8b3d2429..bfc6b8c7c48 100644 Binary files a/res/terminal/images/SplashScreen.scale-400_contrast-black.png and b/res/terminal/images/SplashScreen.scale-400_contrast-black.png differ diff --git a/res/terminal/images/SplashScreen.scale-400_contrast-white.png b/res/terminal/images/SplashScreen.scale-400_contrast-white.png index bdf7b5b7cb8..e55edf370a7 100644 Binary files a/res/terminal/images/SplashScreen.scale-400_contrast-white.png and b/res/terminal/images/SplashScreen.scale-400_contrast-white.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-100_contrast-black.png b/res/terminal/images/Square150x150Logo.scale-100_contrast-black.png index a94e09558d5..0dbe8515427 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-100_contrast-black.png and b/res/terminal/images/Square150x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-100_contrast-white.png b/res/terminal/images/Square150x150Logo.scale-100_contrast-white.png index 8b15f746632..f6a92baac2a 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-100_contrast-white.png and b/res/terminal/images/Square150x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-125_contrast-black.png b/res/terminal/images/Square150x150Logo.scale-125_contrast-black.png index 52a0961f9c1..dc2793d77c8 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-125_contrast-black.png and b/res/terminal/images/Square150x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-125_contrast-white.png b/res/terminal/images/Square150x150Logo.scale-125_contrast-white.png index 1f2b02d1b32..02a0838b2fa 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-125_contrast-white.png and b/res/terminal/images/Square150x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-150_contrast-black.png b/res/terminal/images/Square150x150Logo.scale-150_contrast-black.png index e709c17c3b7..f07adbc4450 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-150_contrast-black.png and b/res/terminal/images/Square150x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-150_contrast-white.png b/res/terminal/images/Square150x150Logo.scale-150_contrast-white.png index 00fb42558e0..baeda25b5b6 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-150_contrast-white.png and b/res/terminal/images/Square150x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-200_contrast-black.png b/res/terminal/images/Square150x150Logo.scale-200_contrast-black.png index da75c53210c..5b45122adce 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-200_contrast-black.png and b/res/terminal/images/Square150x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-200_contrast-white.png b/res/terminal/images/Square150x150Logo.scale-200_contrast-white.png index f56a69cf10f..3a8273318d8 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-200_contrast-white.png and b/res/terminal/images/Square150x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-400_contrast-black.png b/res/terminal/images/Square150x150Logo.scale-400_contrast-black.png index 3bb828ac8d5..ebd90e2f2ed 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-400_contrast-black.png and b/res/terminal/images/Square150x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images/Square150x150Logo.scale-400_contrast-white.png b/res/terminal/images/Square150x150Logo.scale-400_contrast-white.png index 99fc2a7ff4d..8f1492a1b32 100644 Binary files a/res/terminal/images/Square150x150Logo.scale-400_contrast-white.png and b/res/terminal/images/Square150x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-100_contrast-black.png b/res/terminal/images/Square44x44Logo.scale-100_contrast-black.png index 9f13cded565..ee19eba946f 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-100_contrast-black.png and b/res/terminal/images/Square44x44Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-100_contrast-white.png b/res/terminal/images/Square44x44Logo.scale-100_contrast-white.png index 7786af596e0..5f98a1d0b55 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-100_contrast-white.png and b/res/terminal/images/Square44x44Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-125_contrast-black.png b/res/terminal/images/Square44x44Logo.scale-125_contrast-black.png index eed2b33a67a..c59713c97d2 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-125_contrast-black.png and b/res/terminal/images/Square44x44Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-125_contrast-white.png b/res/terminal/images/Square44x44Logo.scale-125_contrast-white.png index 86948693b8a..66bc74e816d 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-125_contrast-white.png and b/res/terminal/images/Square44x44Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-150_contrast-black.png b/res/terminal/images/Square44x44Logo.scale-150_contrast-black.png index 085efc7b6af..c6085c384e9 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-150_contrast-black.png and b/res/terminal/images/Square44x44Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-150_contrast-white.png b/res/terminal/images/Square44x44Logo.scale-150_contrast-white.png index c6ef1dafa8e..2fe6f8ff931 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-150_contrast-white.png and b/res/terminal/images/Square44x44Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-200_contrast-black.png b/res/terminal/images/Square44x44Logo.scale-200_contrast-black.png index 16469eb5bd3..b85d42d135c 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-200_contrast-black.png and b/res/terminal/images/Square44x44Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-200_contrast-white.png b/res/terminal/images/Square44x44Logo.scale-200_contrast-white.png index f9a6ae53359..c070593f85c 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-200_contrast-white.png and b/res/terminal/images/Square44x44Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-400_contrast-black.png b/res/terminal/images/Square44x44Logo.scale-400_contrast-black.png index 0b8ca35feb0..308021302a1 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-400_contrast-black.png and b/res/terminal/images/Square44x44Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.scale-400_contrast-white.png b/res/terminal/images/Square44x44Logo.scale-400_contrast-white.png index fa3109e7e22..e0df53b0c40 100644 Binary files a/res/terminal/images/Square44x44Logo.scale-400_contrast-white.png and b/res/terminal/images/Square44x44Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png index d1a2951126a..65add3bf752 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-16_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-16_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-16_contrast-white.png index d1a2951126a..df742d5cd07 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-16_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-16_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png index 42d68e5f6f5..e41478d78cc 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-20_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-20_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-20_contrast-white.png index 42d68e5f6f5..0cb7c182470 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-20_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-20_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png index 41c3aee3502..4d947a5e72d 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-24_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-24_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-24_contrast-white.png index 41c3aee3502..8c88dcf1ba6 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-24_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-24_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png index f89467e7320..490e06c7ab5 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png index 665dfbe3a37..47464c42e61 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-256_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-256_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-256_contrast-black.png index f89467e7320..0b92377d068 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-256_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-256_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-256_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-256_contrast-white.png index 665dfbe3a37..5ae4590aa12 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-256_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-256_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png index d0c78011e6c..a8f3261c8e0 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png index 1788b2253da..7bf0377549f 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-30_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-30_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-30_contrast-black.png index d0c78011e6c..f325909bfdf 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-30_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-30_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-30_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-30_contrast-white.png index 1788b2253da..f76efdfc4f7 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-30_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-30_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png index efc56018094..8f4b55c9d92 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-32_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-32_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-32_contrast-white.png index efc56018094..baa8459e345 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-32_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-32_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png index 79ee49b96db..15c35cf659f 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-36_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-36_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-36_contrast-white.png index 79ee49b96db..665eeb09e87 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-36_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-36_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png index 5869c620465..a3d592a7c7a 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-40_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-40_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-40_contrast-white.png index 5869c620465..8454561099c 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-40_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-40_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png index 03201095aec..c46cd93faa9 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png index 9c5b7161150..95d62f504d2 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-48_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-48_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-48_contrast-black.png index 03201095aec..d2f42d7323c 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-48_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-48_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-48_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-48_contrast-white.png index 9c5b7161150..553f9956e99 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-48_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-48_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png index acc3310df63..624a4d6a90f 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png index aecda0f11e3..163ad6476c5 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-60_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-60_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-60_contrast-black.png index acc3310df63..f7e5f24cdb9 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-60_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-60_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-60_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-60_contrast-white.png index aecda0f11e3..fcda1dba631 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-60_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-60_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png index 01686eaccf7..f62ca0322ac 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png index 63fb5b06cd7..5f8f396267c 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-64_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-64_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-64_contrast-black.png index 01686eaccf7..e679191546a 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-64_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-64_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-64_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-64_contrast-white.png index 63fb5b06cd7..cdcfd20ed68 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-64_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-64_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png index 110249863df..4eeb6aa8190 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png index fa7c0bb9cf5..ef4a7001577 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-72_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-72_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-72_contrast-black.png index 110249863df..437228a83e5 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-72_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-72_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-72_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-72_contrast-white.png index fa7c0bb9cf5..c83269b1ba3 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-72_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-72_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png index 81f6b28749e..e280e5d0564 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png index e5e2dacc31d..c4997c1eb0b 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-80_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-80_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-80_contrast-black.png index 81f6b28749e..977fdb859e3 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-80_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-80_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-80_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-80_contrast-white.png index e5e2dacc31d..4dae3175cfc 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-80_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-80_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png index 563108e90e9..85f1165729d 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png index 0f9c7950c71..fa979f2949f 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-96_altform-unplated_contrast-white.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-96_contrast-black.png b/res/terminal/images/Square44x44Logo.targetsize-96_contrast-black.png index 563108e90e9..3520a74d313 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-96_contrast-black.png and b/res/terminal/images/Square44x44Logo.targetsize-96_contrast-black.png differ diff --git a/res/terminal/images/Square44x44Logo.targetsize-96_contrast-white.png b/res/terminal/images/Square44x44Logo.targetsize-96_contrast-white.png index 0f9c7950c71..a6deff3cd2d 100644 Binary files a/res/terminal/images/Square44x44Logo.targetsize-96_contrast-white.png and b/res/terminal/images/Square44x44Logo.targetsize-96_contrast-white.png differ diff --git a/res/terminal/images/StoreLogo.scale-100_contrast-black.png b/res/terminal/images/StoreLogo.scale-100_contrast-black.png index c52aafcc5dd..b1910907c56 100644 Binary files a/res/terminal/images/StoreLogo.scale-100_contrast-black.png and b/res/terminal/images/StoreLogo.scale-100_contrast-black.png differ diff --git a/res/terminal/images/StoreLogo.scale-100_contrast-white.png b/res/terminal/images/StoreLogo.scale-100_contrast-white.png index f3c68fb688c..27cdaab077d 100644 Binary files a/res/terminal/images/StoreLogo.scale-100_contrast-white.png and b/res/terminal/images/StoreLogo.scale-100_contrast-white.png differ diff --git a/res/terminal/images/StoreLogo.scale-125_contrast-black.png b/res/terminal/images/StoreLogo.scale-125_contrast-black.png index a84d1ff188e..f5d8dd45706 100644 Binary files a/res/terminal/images/StoreLogo.scale-125_contrast-black.png and b/res/terminal/images/StoreLogo.scale-125_contrast-black.png differ diff --git a/res/terminal/images/StoreLogo.scale-125_contrast-white.png b/res/terminal/images/StoreLogo.scale-125_contrast-white.png index 5e741ef242f..585b966d642 100644 Binary files a/res/terminal/images/StoreLogo.scale-125_contrast-white.png and b/res/terminal/images/StoreLogo.scale-125_contrast-white.png differ diff --git a/res/terminal/images/StoreLogo.scale-150_contrast-black.png b/res/terminal/images/StoreLogo.scale-150_contrast-black.png index d843bd16ae1..270f930efae 100644 Binary files a/res/terminal/images/StoreLogo.scale-150_contrast-black.png and b/res/terminal/images/StoreLogo.scale-150_contrast-black.png differ diff --git a/res/terminal/images/StoreLogo.scale-150_contrast-white.png b/res/terminal/images/StoreLogo.scale-150_contrast-white.png index 8dac64a4fe8..d8df44c7a50 100644 Binary files a/res/terminal/images/StoreLogo.scale-150_contrast-white.png and b/res/terminal/images/StoreLogo.scale-150_contrast-white.png differ diff --git a/res/terminal/images/StoreLogo.scale-200_contrast-black.png b/res/terminal/images/StoreLogo.scale-200_contrast-black.png index 4fb88aad7d1..313f2cd20d4 100644 Binary files a/res/terminal/images/StoreLogo.scale-200_contrast-black.png and b/res/terminal/images/StoreLogo.scale-200_contrast-black.png differ diff --git a/res/terminal/images/StoreLogo.scale-200_contrast-white.png b/res/terminal/images/StoreLogo.scale-200_contrast-white.png index 5a1b5727e69..2c86be61e02 100644 Binary files a/res/terminal/images/StoreLogo.scale-200_contrast-white.png and b/res/terminal/images/StoreLogo.scale-200_contrast-white.png differ diff --git a/res/terminal/images/StoreLogo.scale-400_contrast-black.png b/res/terminal/images/StoreLogo.scale-400_contrast-black.png index d6616c8ab55..97f23e84a48 100644 Binary files a/res/terminal/images/StoreLogo.scale-400_contrast-black.png and b/res/terminal/images/StoreLogo.scale-400_contrast-black.png differ diff --git a/res/terminal/images/StoreLogo.scale-400_contrast-white.png b/res/terminal/images/StoreLogo.scale-400_contrast-white.png index 8c622de1d48..1cf73c931aa 100644 Binary files a/res/terminal/images/StoreLogo.scale-400_contrast-white.png and b/res/terminal/images/StoreLogo.scale-400_contrast-white.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-100_contrast-black.png b/res/terminal/images/Wide310x150Logo.scale-100_contrast-black.png index e001af83cd8..2eea6d40441 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-100_contrast-black.png and b/res/terminal/images/Wide310x150Logo.scale-100_contrast-black.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-100_contrast-white.png b/res/terminal/images/Wide310x150Logo.scale-100_contrast-white.png index 0ce5b040316..b3b0ef2195d 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-100_contrast-white.png and b/res/terminal/images/Wide310x150Logo.scale-100_contrast-white.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-125_contrast-black.png b/res/terminal/images/Wide310x150Logo.scale-125_contrast-black.png index 33010325e98..184dd17977a 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-125_contrast-black.png and b/res/terminal/images/Wide310x150Logo.scale-125_contrast-black.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-125_contrast-white.png b/res/terminal/images/Wide310x150Logo.scale-125_contrast-white.png index b5745ff70f6..4a74f2a5f75 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-125_contrast-white.png and b/res/terminal/images/Wide310x150Logo.scale-125_contrast-white.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-150_contrast-black.png b/res/terminal/images/Wide310x150Logo.scale-150_contrast-black.png index 7844cda30cf..d35476df94f 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-150_contrast-black.png and b/res/terminal/images/Wide310x150Logo.scale-150_contrast-black.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-150_contrast-white.png b/res/terminal/images/Wide310x150Logo.scale-150_contrast-white.png index 9f62ef7fd38..bbab255e7ad 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-150_contrast-white.png and b/res/terminal/images/Wide310x150Logo.scale-150_contrast-white.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-200_contrast-black.png b/res/terminal/images/Wide310x150Logo.scale-200_contrast-black.png index a4480138b64..3f1d770daef 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-200_contrast-black.png and b/res/terminal/images/Wide310x150Logo.scale-200_contrast-black.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-200_contrast-white.png b/res/terminal/images/Wide310x150Logo.scale-200_contrast-white.png index 09b65238b44..edce87d1559 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-200_contrast-white.png and b/res/terminal/images/Wide310x150Logo.scale-200_contrast-white.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-400_contrast-black.png b/res/terminal/images/Wide310x150Logo.scale-400_contrast-black.png index 38ad367e109..31bad44084e 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-400_contrast-black.png and b/res/terminal/images/Wide310x150Logo.scale-400_contrast-black.png differ diff --git a/res/terminal/images/Wide310x150Logo.scale-400_contrast-white.png b/res/terminal/images/Wide310x150Logo.scale-400_contrast-white.png index e910cabe62c..a2dcbf94a12 100644 Binary files a/res/terminal/images/Wide310x150Logo.scale-400_contrast-white.png and b/res/terminal/images/Wide310x150Logo.scale-400_contrast-white.png differ diff --git a/res/terminal/images/terminal_contrast-black.ico b/res/terminal/images/terminal_contrast-black.ico new file mode 100644 index 00000000000..ce7e33198fd Binary files /dev/null and b/res/terminal/images/terminal_contrast-black.ico differ diff --git a/res/terminal/images/terminal_contrast-white.ico b/res/terminal/images/terminal_contrast-white.ico new file mode 100644 index 00000000000..2af1825967e Binary files /dev/null and b/res/terminal/images/terminal_contrast-white.ico differ diff --git a/src/buffer/out/AttrRow.cpp b/src/buffer/out/AttrRow.cpp index 5df4da3978d..ffd95ad3f09 100644 --- a/src/buffer/out/AttrRow.cpp +++ b/src/buffer/out/AttrRow.cpp @@ -175,6 +175,23 @@ size_t ATTR_ROW::FindAttrIndex(const size_t index, size_t* const pApplies) const return runPos - _list.cbegin(); } +// Routine Description: +// - Finds the hyperlink IDs present in this row and returns them +// Return value: +// - An unordered set containing the hyperlink IDs present in this row +std::unordered_set ATTR_ROW::GetHyperlinks() +{ + std::unordered_set ids; + for (const auto& run : _list) + { + if (run.GetAttributes().IsHyperlink()) + { + ids.emplace(run.GetAttributes().GetHyperlinkId()); + } + } + return ids; +} + // Routine Description: // - Sets the attributes (colors) of all character positions from the given position through the end of the row. // Arguments: diff --git a/src/buffer/out/AttrRow.hpp b/src/buffer/out/AttrRow.hpp index 2f0a07794c3..a0802a74591 100644 --- a/src/buffer/out/AttrRow.hpp +++ b/src/buffer/out/AttrRow.hpp @@ -41,6 +41,8 @@ class ATTR_ROW final size_t FindAttrIndex(const size_t index, size_t* const pApplies) const; + std::unordered_set GetHyperlinks(); + bool SetAttrToEnd(const UINT iStart, const TextAttribute attr); void ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith) noexcept; diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index 371fedb2b64..dd6be654efa 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -88,16 +88,18 @@ bool TextAttribute::IsLegacy() const noexcept // - defaultFgColor: the default foreground color rgb value. // - defaultBgColor: the default background color rgb value. // - reverseScreenMode: true if the screen mode is reversed. +// - blinkingIsFaint: true if blinking should be interpreted as faint. // Return Value: // - the foreground and background colors that should be displayed. std::pair TextAttribute::CalculateRgbColors(const gsl::span colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, - const bool reverseScreenMode) const noexcept + const bool reverseScreenMode, + const bool blinkingIsFaint) const noexcept { auto fg = _foreground.GetColor(colorTable, defaultFgColor, IsBold()); auto bg = _background.GetColor(colorTable, defaultBgColor); - if (IsFaint()) + if (IsFaint() || (IsBlinking() && blinkingIsFaint)) { fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two. } @@ -112,6 +114,17 @@ std::pair TextAttribute::CalculateRgbColors(const gsl::span< return { fg, bg }; } +// Method description: +// - Tells us whether the text is a hyperlink or not +// Return value: +// - True if it is a hyperlink, false otherwise +bool TextAttribute::IsHyperlink() const noexcept +{ + // All non-hyperlink text have a default hyperlinkId of 0 while + // all hyperlink text have a non-zero hyperlinkId + return _hyperlinkId != 0; +} + TextColor TextAttribute::GetForeground() const noexcept { return _foreground; @@ -122,6 +135,15 @@ TextColor TextAttribute::GetBackground() const noexcept return _background; } +// Method description: +// - Retrieves the hyperlink ID of the text +// Return value: +// - The hyperlink ID +uint16_t TextAttribute::GetHyperlinkId() const noexcept +{ + return _hyperlinkId; +} + void TextAttribute::SetForeground(const TextColor foreground) noexcept { _foreground = foreground; @@ -174,6 +196,15 @@ void TextAttribute::SetColor(const COLORREF rgbColor, const bool fIsForeground) } } +// Method description: +// - Sets the hyperlink ID of the text +// Arguments: +// - id - the id we wish to set +void TextAttribute::SetHyperlinkId(uint16_t id) noexcept +{ + _hyperlinkId = id; +} + bool TextAttribute::IsLeadingByte() const noexcept { return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_LEADING_BYTE); @@ -246,8 +277,12 @@ bool TextAttribute::IsCrossedOut() const noexcept bool TextAttribute::IsUnderlined() const noexcept { - // TODO:GH#2915 Treat underline separately from LVB_UNDERSCORE - return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_UNDERSCORE); + return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Underlined); +} + +bool TextAttribute::IsDoublyUnderlined() const noexcept +{ + return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::DoublyUnderlined); } bool TextAttribute::IsOverlined() const noexcept @@ -292,8 +327,12 @@ void TextAttribute::SetCrossedOut(bool isCrossedOut) noexcept void TextAttribute::SetUnderlined(bool isUnderlined) noexcept { - // TODO:GH#2915 Treat underline separately from LVB_UNDERSCORE - WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_UNDERSCORE, isUnderlined); + WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Underlined, isUnderlined); +} + +void TextAttribute::SetDoublyUnderlined(bool isDoublyUnderlined) noexcept +{ + WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::DoublyUnderlined, isDoublyUnderlined); } void TextAttribute::SetOverlined(bool isOverlined) noexcept @@ -328,6 +367,14 @@ void TextAttribute::SetDefaultBackground() noexcept _background = TextColor(); } +// Method description: +// - Resets only the meta and extended attributes +void TextAttribute::SetDefaultMetaAttrs() noexcept +{ + _extendedAttrs = ExtendedAttributes::Normal; + _wAttrLegacy = 0; +} + // Method Description: // - Returns true if this attribute indicates its background is the "default" // background. Its _rgbBackground will contain the actual value of the @@ -348,6 +395,6 @@ bool TextAttribute::BackgroundIsDefault() const noexcept // requires for most erasing and filling operations. void TextAttribute::SetStandardErase() noexcept { - _extendedAttrs = ExtendedAttributes::Normal; - _wAttrLegacy = 0; + SetDefaultMetaAttrs(); + _hyperlinkId = 0; } diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 7cec03f75e6..ef140e39b77 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -36,7 +36,8 @@ class TextAttribute final _wAttrLegacy{ 0 }, _foreground{}, _background{}, - _extendedAttrs{ ExtendedAttributes::Normal } + _extendedAttrs{ ExtendedAttributes::Normal }, + _hyperlinkId{ 0 } { } @@ -44,7 +45,8 @@ class TextAttribute final _wAttrLegacy{ gsl::narrow_cast(wLegacyAttr & META_ATTRS) }, _foreground{ s_LegacyIndexOrDefault(wLegacyAttr & FG_ATTRS, s_legacyDefaultForeground) }, _background{ s_LegacyIndexOrDefault((wLegacyAttr & BG_ATTRS) >> 4, s_legacyDefaultBackground) }, - _extendedAttrs{ ExtendedAttributes::Normal } + _extendedAttrs{ ExtendedAttributes::Normal }, + _hyperlinkId{ 0 } { // If we're given lead/trailing byte information with the legacy color, strip it. WI_ClearAllFlags(_wAttrLegacy, COMMON_LVB_SBCSDBCS); @@ -55,7 +57,8 @@ class TextAttribute final _wAttrLegacy{ 0 }, _foreground{ rgbForeground }, _background{ rgbBackground }, - _extendedAttrs{ ExtendedAttributes::Normal } + _extendedAttrs{ ExtendedAttributes::Normal }, + _hyperlinkId{ 0 } { } @@ -66,7 +69,8 @@ class TextAttribute final std::pair CalculateRgbColors(const gsl::span colorTable, const COLORREF defaultFgColor, const COLORREF defaultBgColor, - const bool reverseScreenMode = false) const noexcept; + const bool reverseScreenMode = false, + const bool blinkingIsFaint = false) const noexcept; bool IsLeadingByte() const noexcept; bool IsTrailingByte() const noexcept; @@ -95,6 +99,7 @@ class TextAttribute final bool IsInvisible() const noexcept; bool IsCrossedOut() const noexcept; bool IsUnderlined() const noexcept; + bool IsDoublyUnderlined() const noexcept; bool IsOverlined() const noexcept; bool IsReverseVideo() const noexcept; @@ -105,13 +110,17 @@ class TextAttribute final void SetInvisible(bool isInvisible) noexcept; void SetCrossedOut(bool isCrossedOut) noexcept; void SetUnderlined(bool isUnderlined) noexcept; + void SetDoublyUnderlined(bool isDoublyUnderlined) noexcept; void SetOverlined(bool isOverlined) noexcept; void SetReverseVideo(bool isReversed) noexcept; ExtendedAttributes GetExtendedAttributes() const noexcept; + bool IsHyperlink() const noexcept; + TextColor GetForeground() const noexcept; TextColor GetBackground() const noexcept; + uint16_t GetHyperlinkId() const noexcept; void SetForeground(const TextColor foreground) noexcept; void SetBackground(const TextColor background) noexcept; void SetForeground(const COLORREF rgbForeground) noexcept; @@ -121,9 +130,11 @@ class TextAttribute final void SetIndexedForeground256(const BYTE fgIndex) noexcept; void SetIndexedBackground256(const BYTE bgIndex) noexcept; void SetColor(const COLORREF rgbColor, const bool fIsForeground) noexcept; + void SetHyperlinkId(uint16_t id) noexcept; void SetDefaultForeground() noexcept; void SetDefaultBackground() noexcept; + void SetDefaultMetaAttrs() noexcept; bool BackgroundIsDefault() const noexcept; @@ -141,11 +152,14 @@ class TextAttribute final return !IsAnyGridLineEnabled() && // grid lines have a visual representation // crossed out, doubly and singly underlined have a visual representation WI_AreAllFlagsClear(_extendedAttrs, ExtendedAttributes::CrossedOut | ExtendedAttributes::DoublyUnderlined | ExtendedAttributes::Underlined) && + // hyperlinks have a visual representation + !IsHyperlink() && // all other attributes do not have a visual representation (_wAttrLegacy & META_ATTRS) == (other._wAttrLegacy & META_ATTRS) && ((checkForeground && _foreground == other._foreground) || (!checkForeground && _background == other._background)) && - _extendedAttrs == other._extendedAttrs; + _extendedAttrs == other._extendedAttrs && + IsHyperlink() == other.IsHyperlink(); } constexpr bool IsAnyGridLineEnabled() const noexcept @@ -167,6 +181,8 @@ class TextAttribute final TextColor _background; ExtendedAttributes _extendedAttrs; + uint16_t _hyperlinkId; + #ifdef UNIT_TESTING friend class TextBufferTests; friend class TextAttributeTests; @@ -180,7 +196,7 @@ class TextAttribute final // 4 for _foreground // 4 for _background // 1 for _extendedAttrs -static_assert(sizeof(TextAttribute) <= 11 * sizeof(BYTE), "We should only need 11B for an entire TextColor. Any more than that is just waste"); +static_assert(sizeof(TextAttribute) <= 13 * sizeof(BYTE), "We should only need 13B for an entire TextAttribute. We may need to increment this in the future as we add additional attributes"); enum class TextAttributeBehavior { @@ -194,7 +210,8 @@ constexpr bool operator==(const TextAttribute& a, const TextAttribute& b) noexce return a._wAttrLegacy == b._wAttrLegacy && a._foreground == b._foreground && a._background == b._background && - a._extendedAttrs == b._extendedAttrs; + a._extendedAttrs == b._extendedAttrs && + a._hyperlinkId == b._hyperlinkId; } constexpr bool operator!=(const TextAttribute& a, const TextAttribute& b) noexcept diff --git a/src/buffer/out/UnicodeStorage.cpp b/src/buffer/out/UnicodeStorage.cpp index 1f4c1831ae2..7de3c11025a 100644 --- a/src/buffer/out/UnicodeStorage.cpp +++ b/src/buffer/out/UnicodeStorage.cpp @@ -35,7 +35,7 @@ void UnicodeStorage::StoreGlyph(const key_type key, const mapped_type& glyph) // - erases key and its associated data from the storage // Arguments: // - key - the key to remove -void UnicodeStorage::Erase(const key_type key) +void UnicodeStorage::Erase(const key_type key) noexcept { _map.erase(key); } diff --git a/src/buffer/out/UnicodeStorage.hpp b/src/buffer/out/UnicodeStorage.hpp index d37a01a2aeb..e1bd2ff3659 100644 --- a/src/buffer/out/UnicodeStorage.hpp +++ b/src/buffer/out/UnicodeStorage.hpp @@ -53,7 +53,7 @@ class UnicodeStorage final void StoreGlyph(const key_type key, const mapped_type& glyph); - void Erase(const key_type key); + void Erase(const key_type key) noexcept; void Remap(const std::unordered_map& rowMap, const std::optional width); diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 6ecb989f2f0..24ef10da53c 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -8,12 +8,15 @@ #include "../types/inc/utils.hpp" #include "../types/inc/convert.hpp" +#include "../../types/inc/GlyphWidth.hpp" #pragma hdrstop using namespace Microsoft::Console; using namespace Microsoft::Console::Types; +using PointTree = interval_tree::IntervalTree; + // Routine Description: // - Creates a new instance of TextBuffer // Arguments: @@ -34,7 +37,9 @@ TextBuffer::TextBuffer(const COORD screenBufferSize, _storage{}, _unicodeStorage{}, _renderTarget{ renderTarget }, - _size{} + _size{}, + _currentHyperlinkId{ 1 }, + _currentPatternId{ 0 } { // initialize ROWs for (size_t i = 0; i < static_cast(screenBufferSize.Y); ++i) @@ -551,7 +556,10 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) // to the logical position 0 in the window (cursor coordinates and all other coordinates). _renderTarget.TriggerCircling(); - // First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. + // Prune hyperlinks to delete obsolete references + _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) { @@ -992,19 +1000,29 @@ const COORD TextBuffer::GetWordStart(const COORD target, const std::wstring_view // so the words in the example include ["word ", "other "] // NOTE: the start anchor (this one) is inclusive, whereas the end anchor (GetWordEnd) is exclusive - // can't expand left - if (target.X == GetSize().Left()) +#pragma warning(suppress : 26496) + // GH#7664: Treat EndExclusive as EndInclusive so + // that it actually points to a space in the buffer + auto copy{ target }; + const auto bufferSize{ GetSize() }; + if (target == bufferSize.Origin()) { + // can't expand left return target; } + else if (target == bufferSize.EndExclusive()) + { + // treat EndExclusive as EndInclusive + copy = { bufferSize.RightInclusive(), bufferSize.BottomInclusive() }; + } if (accessibilityMode) { - return _GetWordStartForAccessibility(target, wordDelimiters); + return _GetWordStartForAccessibility(copy, wordDelimiters); } else { - return _GetWordStartForSelection(target, wordDelimiters); + return _GetWordStartForSelection(copy, wordDelimiters); } } @@ -1104,9 +1122,16 @@ const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view w // so the words in the example include ["word ", "other "] // NOTE: the end anchor (this one) is exclusive, whereas the start anchor (GetWordStart) is inclusive + // Already at the end. Can't move forward. + if (target == GetSize().EndExclusive()) + { + return target; + } + if (accessibilityMode) { - return _GetWordEndForAccessibility(target, wordDelimiters); + const auto lastCharPos{ GetLastNonSpaceCharacter() }; + return _GetWordEndForAccessibility(target, wordDelimiters, lastCharPos); } else { @@ -1119,13 +1144,20 @@ const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view w // Arguments: // - target - a COORD on the word you are currently on // - wordDelimiters - what characters are we considering for the separation of words +// - lastCharPos - the position of the last nonspace character in the text buffer (to improve performance) // Return Value: // - The COORD for the first character of the next readable "word". If no next word, return one past the end of the buffer -const COORD TextBuffer::_GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const +const COORD TextBuffer::_GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters, const COORD lastCharPos) const { const auto bufferSize = GetSize(); COORD result = target; + // Check if we're already on/past the last RegularChar + if (bufferSize.CompareInBounds(result, lastCharPos, true) >= 0) + { + return bufferSize.EndExclusive(); + } + // ignore right boundary. Continue through readable text found while (_GetDelimiterClassAt(result, wordDelimiters) == DelimiterClass::RegularChar) { @@ -1135,6 +1167,12 @@ const COORD TextBuffer::_GetWordEndForAccessibility(const COORD target, const st } } + // we are already on/past the last RegularChar + if (bufferSize.CompareInBounds(result, lastCharPos, true) >= 0) + { + return bufferSize.EndExclusive(); + } + // make sure we expand to the beginning of the NEXT word while (_GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar) { @@ -1185,6 +1223,46 @@ const COORD TextBuffer::_GetWordEndForSelection(const COORD target, const std::w return result; } +void TextBuffer::_PruneHyperlinks() +{ + // Check the old first row for hyperlink references + // If there are any, search the entire buffer for the same reference + // 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 + auto firstRowRefs = _storage.at(_firstRow).GetAttrRow().GetHyperlinks(); + if (!firstRowRefs.empty()) + { + const auto total = TotalRowCount(); + // Loop through all the rows in the buffer except the first row - + // we have found all hyperlink references in the first row and put them in refs, + // now we need to search the rest of the buffer (i.e. all the rows except the first) + // to see if those references are anywhere else + for (size_t i = 1; i != total; ++i) + { + const auto nextRowRefs = GetRowByOffset(i).GetAttrRow().GetHyperlinks(); + for (auto id : nextRowRefs) + { + if (firstRowRefs.find(id) != firstRowRefs.end()) + { + firstRowRefs.erase(id); + } + } + if (firstRowRefs.empty()) + { + // No more hyperlink references left to search for, terminate early + break; + } + } + } + + // Now delete obsolete references from our map + for (auto hyperlinkReference : firstRowRefs) + { + RemoveHyperlinkFromMap(hyperlinkReference); + } +} + // Method Description: // - Update pos to be the position of the first character of the next word. This is used for accessibility // Arguments: @@ -1196,38 +1274,16 @@ const COORD TextBuffer::_GetWordEndForSelection(const COORD target, const std::w // - pos - The COORD for the first character on the "word" (inclusive) bool TextBuffer::MoveToNextWord(COORD& pos, const std::wstring_view wordDelimiters, COORD lastCharPos) const { - auto copy = pos; - const auto bufferSize = GetSize(); + // move to the beginning of the next word + // NOTE: _GetWordEnd...() returns the exclusive position of the "end of the word" + // This is also the inclusive start of the next word. + auto copy{ _GetWordEndForAccessibility(pos, wordDelimiters, lastCharPos) }; - // started on a word, continue until the end of the word - while (_GetDelimiterClassAt(copy, wordDelimiters) == DelimiterClass::RegularChar) - { - if (!bufferSize.IncrementInBounds(copy)) - { - // last char in buffer is a RegularChar - // thus there is no next word - return false; - } - } - - // we are already on/past the last RegularChar - if (bufferSize.CompareInBounds(copy, lastCharPos) >= 0) + if (copy == GetSize().EndExclusive()) { return false; } - // on whitespace, continue until the beginning of the next word - while (_GetDelimiterClassAt(copy, wordDelimiters) != DelimiterClass::RegularChar) - { - if (!bufferSize.IncrementInBounds(copy)) - { - // last char in buffer is a DelimiterChar or ControlChar - // there is no next word - return false; - } - } - - // successful move, copy result out pos = copy; return true; } @@ -1242,33 +1298,17 @@ bool TextBuffer::MoveToNextWord(COORD& pos, const std::wstring_view wordDelimite // - pos - The COORD for the first character on the "word" (inclusive) bool TextBuffer::MoveToPreviousWord(COORD& pos, std::wstring_view wordDelimiters) const { - auto copy = pos; - auto bufferSize = GetSize(); + // move to the beginning of the current word + auto copy{ GetWordStart(pos, wordDelimiters, true) }; - // started on whitespace/delimiter, continue until the end of the previous word - while (_GetDelimiterClassAt(copy, wordDelimiters) != DelimiterClass::RegularChar) + if (!GetSize().DecrementInBounds(copy, true)) { - if (!bufferSize.DecrementInBounds(copy)) - { - // first char in buffer is a DelimiterChar or ControlChar - // there is no previous word - return false; - } - } - - // on a word, continue until the beginning of the word - while (_GetDelimiterClassAt(copy, wordDelimiters) == DelimiterClass::RegularChar) - { - if (!bufferSize.DecrementInBounds(copy)) - { - // first char in buffer is a RegularChar - // there is no previous word - return false; - } + // can't move behind current word + return false; } - // successful move, copy result out - pos = copy; + // move to the beginning of the previous word + pos = GetWordStart(copy, wordDelimiters, true); return true; } @@ -1281,8 +1321,13 @@ bool TextBuffer::MoveToPreviousWord(COORD& pos, std::wstring_view wordDelimiters const til::point TextBuffer::GetGlyphStart(const til::point pos) const { COORD resultPos = pos; - const auto bufferSize = GetSize(); + + if (resultPos == bufferSize.EndExclusive()) + { + bufferSize.DecrementInBounds(resultPos, true); + } + if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsTrailing()) { bufferSize.DecrementInBounds(resultPos, true); @@ -1323,9 +1368,15 @@ const til::point TextBuffer::GetGlyphEnd(const til::point pos) const bool TextBuffer::MoveToNextGlyph(til::point& pos, bool allowBottomExclusive) const { COORD resultPos = pos; + const auto bufferSize = GetSize(); + + if (resultPos == GetSize().EndExclusive()) + { + // we're already at the end + return false; + } // try to move. If we can't, we're done. - const auto bufferSize = GetSize(); const bool success = bufferSize.IncrementInBounds(resultPos, allowBottomExclusive); if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsTrailing()) { @@ -1340,20 +1391,19 @@ bool TextBuffer::MoveToNextGlyph(til::point& pos, bool allowBottomExclusive) con // - Update pos to be the beginning of the previous glyph/character. This is used for accessibility // Arguments: // - pos - a COORD on the word you are currently on -// - allowBottomExclusive - allow the nonexistent end-of-buffer cell to be encountered // Return Value: // - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary) // - pos - The COORD for the first cell of the previous glyph (inclusive) -bool TextBuffer::MoveToPreviousGlyph(til::point& pos, bool allowBottomExclusive) const +bool TextBuffer::MoveToPreviousGlyph(til::point& pos) const { COORD resultPos = pos; // try to move. If we can't, we're done. const auto bufferSize = GetSize(); - const bool success = bufferSize.DecrementInBounds(resultPos, allowBottomExclusive); + const bool success = bufferSize.DecrementInBounds(resultPos, true); if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsLeading()) { - bufferSize.DecrementInBounds(resultPos, allowBottomExclusive); + bufferSize.DecrementInBounds(resultPos, true); } pos = resultPos; @@ -2142,6 +2192,8 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, { // 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. @@ -2207,3 +2259,202 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, return hr; } + +// Method Description: +// - Adds or updates a hyperlink in our hyperlink table +// Arguments: +// - The hyperlink URI, the hyperlink id (could be new or old) +void TextBuffer::AddHyperlinkToMap(std::wstring_view uri, uint16_t id) +{ + _hyperlinkMap[id] = uri; +} + +// Method Description: +// - Retrieves the URI associated with a particular hyperlink ID +// Arguments: +// - The hyperlink ID +// Return Value: +// - The URI +std::wstring TextBuffer::GetHyperlinkUriFromId(uint16_t id) const +{ + return _hyperlinkMap.at(id); +} + +// Method description: +// - Provides the hyperlink ID to be assigned as a text attribute, based on the optional custom id provided +// Arguments: +// - The user-defined id +// Return value: +// - The internal hyperlink ID +uint16_t TextBuffer::GetHyperlinkId(std::wstring_view uri, std::wstring_view id) +{ + uint16_t numericId = 0; + if (id.empty()) + { + // no custom id specified, return our internal count + numericId = _currentHyperlinkId; + ++_currentHyperlinkId; + } + else + { + // assign _currentHyperlinkId if the custom id does not already exist + std::wstring newId{ id }; + // hash the URL and add it to the custom ID - GH#7698 + newId += L"%" + std::to_wstring(std::hash{}(uri)); + const auto result = _hyperlinkCustomIdMap.emplace(newId, _currentHyperlinkId); + if (result.second) + { + // the custom id did not already exist + ++_currentHyperlinkId; + } + numericId = (*(result.first)).second; + } + // _currentHyperlinkId could overflow, make sure its not 0 + if (_currentHyperlinkId == 0) + { + ++_currentHyperlinkId; + } + return numericId; +} + +// Method Description: +// - Removes a hyperlink from the hyperlink map and the associated +// user defined id from the custom id map (if there is one) +// Arguments: +// - The ID of the hyperlink to be removed +void TextBuffer::RemoveHyperlinkFromMap(uint16_t id) +{ + _hyperlinkMap.erase(id); + for (const auto& customIdPair : _hyperlinkCustomIdMap) + { + if (customIdPair.second == id) + { + _hyperlinkCustomIdMap.erase(customIdPair.first); + break; + } + } +} + +// Method Description: +// - Obtains the custom ID, if there was one, associated with the +// uint16_t id of a hyperlink +// Arguments: +// - The uint16_t id of the hyperlink +// Return Value: +// - The custom ID if there was one, empty string otherwise +std::wstring TextBuffer::GetCustomIdFromId(uint16_t id) const +{ + for (auto customIdPair : _hyperlinkCustomIdMap) + { + if (customIdPair.second == id) + { + return customIdPair.first; + } + } + return {}; +} + +// Method Description: +// - Copies the hyperlink/customID maps of the old buffer into this one, +// also copies currentHyperlinkId +// Arguments: +// - The other buffer +void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other) +{ + _hyperlinkMap = other._hyperlinkMap; + _hyperlinkCustomIdMap = other._hyperlinkCustomIdMap; + _currentHyperlinkId = other._currentHyperlinkId; +} + +// Method Description: +// - Adds a regex pattern we should search for +// - The searching does not happen here, we only search when asked to by TerminalCore +// Arguments: +// - The regex pattern +// Return value: +// - An ID that the caller should associate with the given pattern +const size_t TextBuffer::AddPatternRecognizer(const std::wstring_view regexString) +{ + ++_currentPatternId; + _idsAndPatterns.emplace(std::make_pair(_currentPatternId, regexString)); + return _currentPatternId; +} + +// Method Description: +// - 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; +} + +// 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 size_t firstRow, const size_t lastRow) const +{ + PointTree::interval_vector intervals; + + std::wstring concatAll; + const auto rowSize = GetRowByOffset(0).size(); + concatAll.reserve(rowSize * (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 (auto i = firstRow; i <= lastRow; ++i) + { + auto row = GetRowByOffset(i); + concatAll += row.GetCharRow().GetText(); + } + + // for each pattern we know of, iterate through the string + for (const auto& idAndPattern : _idsAndPatterns) + { + 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(); + + size_t lenUpToThis = 0; + for (auto i = words_begin; i != words_end; ++i) + { + // 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 + size_t prefixSize = 0; + + for (const auto ch : i->prefix().str()) + { + prefixSize += IsGlyphFullWidth(ch) ? 2 : 1; + } + const auto start = lenUpToThis + prefixSize; + size_t matchSize = 0; + for (const auto ch : i->str()) + { + matchSize += IsGlyphFullWidth(ch) ? 2 : 1; + } + const auto end = start + matchSize; + lenUpToThis = end; + + const til::point startCoord{ gsl::narrow(start % rowSize), gsl::narrow(start / rowSize) }; + const til::point endCoord{ gsl::narrow(end % rowSize), gsl::narrow(end / rowSize) }; + + // store the intervals + // NOTE: these intervals are relative to the VIEWPORT not the buffer + // Keeping these relative to the viewport for now because its the renderer + // that actually uses these locations and the renderer works relative to + // the viewport + intervals.push_back(PointTree::interval(startCoord, endCoord, idAndPattern.first)); + } + } + PointTree result(std::move(intervals)); + return result; +} diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 9ed2ce8b1af..b1e0be1c783 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -137,10 +137,17 @@ class TextBuffer final const til::point GetGlyphStart(const til::point pos) const; const til::point GetGlyphEnd(const til::point pos) const; bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false) const; - bool MoveToPreviousGlyph(til::point& pos, bool allowBottomExclusive = false) const; + bool MoveToPreviousGlyph(til::point& pos) const; const std::vector GetTextRects(COORD start, COORD end, bool blockSelection = false) const; + void AddHyperlinkToMap(std::wstring_view uri, uint16_t id); + std::wstring GetHyperlinkUriFromId(uint16_t id) const; + uint16_t GetHyperlinkId(std::wstring_view uri, std::wstring_view id); + void RemoveHyperlinkFromMap(uint16_t id); + std::wstring GetCustomIdFromId(uint16_t id) const; + void CopyHyperlinkMaps(const TextBuffer& OtherBuffer); + class TextAndColor { public: @@ -175,6 +182,10 @@ class TextBuffer final const std::optional lastCharacterViewport, std::optional> positionInfo); + const size_t AddPatternRecognizer(const std::wstring_view regexString); + void CopyPatterns(const TextBuffer& OtherBuffer); + interval_tree::IntervalTree GetPatterns(const size_t firstRow, const size_t lastRow) const; + private: void _UpdateSize(); Microsoft::Console::Types::Viewport _size; @@ -188,6 +199,10 @@ class TextBuffer final // storage location for glyphs that can't fit into the buffer normally UnicodeStorage _unicodeStorage; + std::unordered_map _hyperlinkMap; + std::unordered_map _hyperlinkCustomIdMap; + uint16_t _currentHyperlinkId; + void _RefreshRowIDs(std::optional newRowWidth); Microsoft::Console::Render::IRenderTarget& _renderTarget; @@ -213,9 +228,14 @@ class TextBuffer final const DelimiterClass _GetDelimiterClassAt(const COORD pos, const std::wstring_view wordDelimiters) const; const COORD _GetWordStartForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const; const COORD _GetWordStartForSelection(const COORD target, const std::wstring_view wordDelimiters) const; - const COORD _GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const; + const COORD _GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters, const COORD lastCharPos) const; const COORD _GetWordEndForSelection(const COORD target, const std::wstring_view wordDelimiters) const; + void _PruneHyperlinks(); + + std::unordered_map _idsAndPatterns; + size_t _currentPatternId; + #ifdef UNIT_TESTING friend class TextBufferTests; friend class UiaTextRangeTests; diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 237963a6428..fbb1764b5ca 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -3,6 +3,8 @@ + + Native 10.0.18362.0 10.0.18362.0 - + true %(RecursiveDir)%(FileName)%(Extension) diff --git a/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp b/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp new file mode 100644 index 00000000000..64b96dd8490 --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/ColorSchemeTests.cpp @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "JsonTestClass.h" + +using namespace Microsoft::Console; +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 ColorSchemeTests : 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(ColorSchemeTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(CanLayerColorScheme); + TEST_METHOD(LayerColorSchemeProperties); + TEST_METHOD(LayerColorSchemesOnArray); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void ColorSchemeTests::CanLayerColorScheme() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505" + })" }; + const std::string scheme3String{ R"({ + // "name": "scheme3", + "foreground": "#060606", + "background": "#070707" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + const auto scheme3Json = VerifyParseSucceeded(scheme3String); + + const auto scheme0 = ColorScheme::FromJson(scheme0Json); + + VERIFY_IS_TRUE(scheme0->ShouldBeLayered(scheme0Json)); + VERIFY_IS_FALSE(scheme0->ShouldBeLayered(scheme1Json)); + VERIFY_IS_TRUE(scheme0->ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme0->ShouldBeLayered(scheme3Json)); + + const auto scheme1 = ColorScheme::FromJson(scheme1Json); + + VERIFY_IS_FALSE(scheme1->ShouldBeLayered(scheme0Json)); + VERIFY_IS_TRUE(scheme1->ShouldBeLayered(scheme1Json)); + VERIFY_IS_FALSE(scheme1->ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme1->ShouldBeLayered(scheme3Json)); + + const auto scheme3 = ColorScheme::FromJson(scheme3Json); + + VERIFY_IS_FALSE(scheme3->ShouldBeLayered(scheme0Json)); + VERIFY_IS_FALSE(scheme3->ShouldBeLayered(scheme1Json)); + VERIFY_IS_FALSE(scheme3->ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme3->ShouldBeLayered(scheme3Json)); + } + + void ColorSchemeTests::LayerColorSchemeProperties() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101", + "selectionBackground": "#010100", + "cursorColor": "#010001", + "red": "#010000", + "green": "#000100", + "blue": "#000001" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303", + "selectionBackground": "#020200", + "cursorColor": "#040004", + "red": "#020000", + + "blue": "#000002" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505", + "selectionBackground": "#030300", + "cursorColor": "#060006", + "red": "#030000", + "green": "#000300" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + + auto scheme0 = ColorScheme::FromJson(scheme0Json); + VERIFY_ARE_EQUAL(L"scheme0", scheme0->_Name); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 0), scheme0->_SelectionBackground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 0, 1), scheme0->_CursorColor); + VERIFY_ARE_EQUAL(ARGB(0, 1, 0, 0), scheme0->_table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0->_table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 1), scheme0->_table[XTERM_BLUE_ATTR]); + + Log::Comment(NoThrowString().Format( + L"Layering scheme1 on top of scheme0")); + scheme0->LayerJson(scheme1Json); + + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 0), scheme0->_SelectionBackground); + VERIFY_ARE_EQUAL(ARGB(0, 4, 0, 4), scheme0->_CursorColor); + VERIFY_ARE_EQUAL(ARGB(0, 2, 0, 0), scheme0->_table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0->_table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0->_table[XTERM_BLUE_ATTR]); + + Log::Comment(NoThrowString().Format( + L"Layering scheme2Json on top of (scheme0+scheme1)")); + scheme0->LayerJson(scheme2Json); + + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 0), scheme0->_SelectionBackground); + VERIFY_ARE_EQUAL(ARGB(0, 6, 0, 6), scheme0->_CursorColor); + VERIFY_ARE_EQUAL(ARGB(0, 3, 0, 0), scheme0->_table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 3, 0), scheme0->_table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0->_table[XTERM_BLUE_ATTR]); + } + + void ColorSchemeTests::LayerColorSchemesOnArray() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505" + })" }; + const std::string scheme3String{ R"({ + // by not providing a name, the scheme will have the name "" + "foreground": "#060606", + "background": "#070707" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + const auto scheme3Json = VerifyParseSucceeded(scheme3String); + + auto settings = winrt::make_self(); + + VERIFY_ARE_EQUAL(0u, settings->_globals->ColorSchemes().Size()); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme3Json)); + + settings->_LayerOrCreateColorScheme(scheme0Json); + { + for (auto kv : settings->_globals->ColorSchemes()) + { + Log::Comment(NoThrowString().Format( + L"kv:%s->%s", kv.Key().data(), kv.Value().Name().data())); + } + VERIFY_ARE_EQUAL(1u, settings->_globals->ColorSchemes().Size()); + + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme0")); + auto scheme0Proj = settings->_globals->ColorSchemes().Lookup(L"scheme0"); + auto scheme0 = winrt::get_self(scheme0Proj); + + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0->_Background); + } + + settings->_LayerOrCreateColorScheme(scheme1Json); + + { + VERIFY_ARE_EQUAL(2u, settings->_globals->ColorSchemes().Size()); + + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme0")); + auto scheme0Proj = settings->_globals->ColorSchemes().Lookup(L"scheme0"); + auto scheme0 = winrt::get_self(scheme0Proj); + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme1")); + auto scheme1Proj = settings->_globals->ColorSchemes().Lookup(L"scheme1"); + auto scheme1 = winrt::get_self(scheme1Proj); + + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1->_Background); + } + settings->_LayerOrCreateColorScheme(scheme2Json); + + { + VERIFY_ARE_EQUAL(2u, settings->_globals->ColorSchemes().Size()); + + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme0")); + auto scheme0Proj = settings->_globals->ColorSchemes().Lookup(L"scheme0"); + auto scheme0 = winrt::get_self(scheme0Proj); + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme1")); + auto scheme1Proj = settings->_globals->ColorSchemes().Lookup(L"scheme1"); + auto scheme1 = winrt::get_self(scheme1Proj); + + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1->_Background); + } + settings->_LayerOrCreateColorScheme(scheme3Json); + + { + VERIFY_ARE_EQUAL(3u, settings->_globals->ColorSchemes().Size()); + + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme0")); + auto scheme0Proj = settings->_globals->ColorSchemes().Lookup(L"scheme0"); + auto scheme0 = winrt::get_self(scheme0Proj); + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"scheme1")); + auto scheme1Proj = settings->_globals->ColorSchemes().Lookup(L"scheme1"); + auto scheme1 = winrt::get_self(scheme1Proj); + VERIFY_IS_TRUE(settings->_globals->ColorSchemes().HasKey(L"")); + auto scheme2Proj = settings->_globals->ColorSchemes().Lookup(L""); + auto scheme2 = winrt::get_self(scheme2Proj); + + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings->_FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1->_Background); + VERIFY_ARE_EQUAL(ARGB(0, 6, 6, 6), scheme2->_Foreground); + VERIFY_ARE_EQUAL(ARGB(0, 7, 7, 7), scheme2->_Background); + } + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp similarity index 75% rename from src/cascadia/LocalTests_TerminalApp/CommandTests.cpp rename to src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index 1babcf6a2c8..b3a0951f78d 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -3,19 +3,19 @@ #include "pch.h" -#include "../TerminalApp/CascadiaSettings.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" #include "JsonTestClass.h" #include "TestUtils.h" using namespace Microsoft::Console; -using namespace TerminalApp; -using namespace winrt::TerminalApp; -using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::Windows::Foundation::Collections; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; -namespace TerminalAppLocalTests +namespace SettingsModelLocalTests { // TODO:microsoft/terminal#3838: // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for @@ -61,25 +61,25 @@ namespace TerminalAppLocalTests const auto commands1Json = VerifyParseSucceeded(commands1String); const auto commands2Json = VerifyParseSucceeded(commands2String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); { auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); } - VERIFY_ARE_EQUAL(1u, commands.size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); { auto warnings = implementation::Command::LayerJson(commands, commands1Json); VERIFY_ARE_EQUAL(0u, warnings.size()); } - VERIFY_ARE_EQUAL(2u, commands.size()); + VERIFY_ARE_EQUAL(2u, commands.Size()); { auto warnings = implementation::Command::LayerJson(commands, commands2Json); VERIFY_ARE_EQUAL(0u, warnings.size()); } - VERIFY_ARE_EQUAL(4u, commands.size()); + VERIFY_ARE_EQUAL(4u, commands.Size()); } void CommandTests::LayerCommand() @@ -95,13 +95,13 @@ namespace TerminalAppLocalTests const auto commands2Json = VerifyParseSucceeded(commands2String); const auto commands3Json = VerifyParseSucceeded(commands3String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); { auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(1u, commands.size()); - auto command = commands.at(L"action0"); + VERIFY_ARE_EQUAL(1u, commands.Size()); + auto command = commands.Lookup(L"action0"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action()); @@ -111,8 +111,8 @@ namespace TerminalAppLocalTests { auto warnings = implementation::Command::LayerJson(commands, commands1Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(1u, commands.size()); - auto command = commands.at(L"action0"); + VERIFY_ARE_EQUAL(1u, commands.Size()); + auto command = commands.Lookup(L"action0"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::PasteText, command.Action().Action()); @@ -121,8 +121,8 @@ namespace TerminalAppLocalTests { auto warnings = implementation::Command::LayerJson(commands, commands2Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(1u, commands.size()); - auto command = commands.at(L"action0"); + VERIFY_ARE_EQUAL(1u, commands.Size()); + auto command = commands.Lookup(L"action0"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, command.Action().Action()); @@ -133,7 +133,7 @@ namespace TerminalAppLocalTests // This last command should "unbind" the action. auto warnings = implementation::Command::LayerJson(commands, commands3Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(0u, commands.size()); + VERIFY_ARE_EQUAL(0u, commands.Size()); } } @@ -144,7 +144,6 @@ namespace TerminalAppLocalTests // of from keybindings. const std::string commands0String{ R"([ - { "name": "command0", "command": { "action": "splitPane", "split": null } }, { "name": "command1", "command": { "action": "splitPane", "split": "vertical" } }, { "name": "command2", "command": { "action": "splitPane", "split": "horizontal" } }, { "name": "command4", "command": { "action": "splitPane" } }, @@ -153,61 +152,51 @@ namespace TerminalAppLocalTests const auto commands0Json = VerifyParseSucceeded(commands0String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(5u, commands.size()); + VERIFY_ARE_EQUAL(4u, commands.Size()); { - auto command = commands.at(L"command0"); + auto command = commands.Lookup(L"command1"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } { - auto command = commands.at(L"command1"); + auto command = commands.Lookup(L"command2"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); } { - auto command = commands.at(L"command2"); + auto command = commands.Lookup(L"command4"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } { - auto command = commands.at(L"command4"); + auto command = commands.Lookup(L"command5"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); - } - { - auto command = commands.at(L"command5"); - VERIFY_IS_NOT_NULL(command); - VERIFY_IS_NOT_NULL(command.Action()); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); - const auto& realArgs = command.Action().Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } } void CommandTests::TestResourceKeyName() @@ -217,17 +206,17 @@ namespace TerminalAppLocalTests const std::string commands0String{ R"([ { "name": { "key": "DuplicateTabCommandKey"}, "command": "copy" } ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); { auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(1u, commands.size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); // NOTE: We're relying on DuplicateTabCommandKey being defined as // "Duplicate Tab" here. If that string changes in our resources, // this test will break. - auto command = commands.at(L"Duplicate tab"); + auto command = commands.Lookup(L"Duplicate tab"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action()); @@ -238,6 +227,14 @@ namespace TerminalAppLocalTests void CommandTests::TestAutogeneratedName() { + // Tests run in Helix can't report Skipped until GH#7286 is resolved. + // Set ignore flag to make Helix run completely overlook it. + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Ignore", L"True") + END_TEST_METHOD_PROPERTIES() + + // This test to be corrected as a part of GH#7281 + // This test ensures that we'll correctly create commands for actions // that don't have given names, pursuant to the spec in GH#6532. @@ -257,45 +254,45 @@ namespace TerminalAppLocalTests const auto commands0Json = VerifyParseSucceeded(commands0String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); // There are only 3 commands here: all of the `"none"`, `"auto"`, // `"foo"`, `null`, and bindings all generate the same action, // which will generate just a single name for all of them. - VERIFY_ARE_EQUAL(3u, commands.size()); + VERIFY_ARE_EQUAL(3u, commands.Size()); { - auto command = commands.at(L"Split pane"); + auto command = commands.Lookup(L"Split pane"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } { - auto command = commands.at(L"Split pane, direction: vertical"); + auto command = commands.Lookup(L"Split pane, split: vertical"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } { - auto command = commands.at(L"Split pane, direction: horizontal"); + auto command = commands.Lookup(L"Split pane, split: horizontal"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); } } void CommandTests::TestLayerOnAutogeneratedName() @@ -307,21 +304,21 @@ namespace TerminalAppLocalTests const auto commands0Json = VerifyParseSucceeded(commands0String); - std::unordered_map commands; - VERIFY_ARE_EQUAL(0u, commands.size()); + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(1u, commands.size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); { - auto command = commands.at(L"Split pane"); + auto command = commands.Lookup(L"Split pane"); VERIFY_IS_NOT_NULL(command); VERIFY_IS_NOT_NULL(command.Action()); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action()); const auto& realArgs = command.Action().Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } } } diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp new file mode 100644 index 00000000000..663c09837c1 --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -0,0 +1,2597 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "JsonTestClass.h" +#include "TestUtils.h" +#include +#include "../ut_app/TestDynamicProfileGenerator.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; + +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 DeserializationTests : 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(DeserializationTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(ValidateProfilesExist); + TEST_METHOD(ValidateDefaultProfileExists); + TEST_METHOD(ValidateDuplicateProfiles); + TEST_METHOD(ValidateManyWarnings); + TEST_METHOD(LayerGlobalProperties); + TEST_METHOD(ValidateProfileOrdering); + TEST_METHOD(ValidateHideProfiles); + TEST_METHOD(ValidateProfilesGenerateGuids); + TEST_METHOD(GeneratedGuidRoundtrips); + TEST_METHOD(TestAllValidationsWithNullGuids); + TEST_METHOD(TestReorderWithNullGuids); + TEST_METHOD(TestReorderingWithoutGuid); + TEST_METHOD(TestLayeringNameOnlyProfiles); + TEST_METHOD(TestExplodingNameOnlyProfiles); + TEST_METHOD(TestHideAllProfiles); + TEST_METHOD(TestInvalidColorSchemeName); + TEST_METHOD(TestHelperFunctions); + + TEST_METHOD(TestProfileBackgroundImageWithEnvVar); + TEST_METHOD(TestProfileBackgroundImageWithDesktopWallpaper); + + TEST_METHOD(TestCloseOnExitParsing); + TEST_METHOD(TestCloseOnExitCompatibilityShim); + + TEST_METHOD(TestLayerUserDefaultsBeforeProfiles); + TEST_METHOD(TestDontLayerGuidFromUserDefaults); + TEST_METHOD(TestLayerUserDefaultsOnDynamics); + + TEST_METHOD(FindMissingProfile); + + TEST_METHOD(ValidateKeybindingsWarnings); + + TEST_METHOD(ValidateExecuteCommandlineWarning); + + TEST_METHOD(ValidateLegacyGlobalsWarning); + + TEST_METHOD(TestTrailingCommas); + + TEST_METHOD(TestCommandsAndKeybindings); + + TEST_METHOD(TestNestedCommandWithoutName); + TEST_METHOD(TestUnbindNestedCommand); + TEST_METHOD(TestRebindNestedCommand); + + TEST_METHOD(TestCopy); + TEST_METHOD(TestCloneInheritanceTree); + + TEST_METHOD(TestValidDefaults); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + + private: + void _logCommandNames(winrt::Windows::Foundation::Collections::IMapView commands, const int indentation = 1) + { + if (indentation == 1) + { + Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); + } + for (const auto& nameAndCommand : commands) + { + Log::Comment(fmt::format(L"{0:>{1}}* {2}->{3}", + L"", + indentation, + nameAndCommand.Key(), + nameAndCommand.Value().Name()) + .c_str()); + + winrt::com_ptr cmdImpl; + cmdImpl.copy_from(winrt::get_self(nameAndCommand.Value())); + if (cmdImpl->HasNestedCommands()) + { + _logCommandNames(cmdImpl->_subcommands.GetView(), indentation + 2); + } + } + } + }; + + void DeserializationTests::ValidateProfilesExist() + { + const std::string settingsWithProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0" + } + ] + })" }; + + const std::string settingsWithoutProfiles{ R"( + { + "defaultProfile": "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" + })" }; + + const std::string settingsWithEmptyProfiles{ R"( + { + "profiles": [] + })" }; + + { + // Case 1: Good settings + const auto settingsObject = VerifyParseSucceeded(settingsWithProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + settings->_ValidateProfilesExist(); + } + { + // Case 2: Bad settings + const auto settingsObject = VerifyParseSucceeded(settingsWithoutProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + bool caughtExpectedException = false; + try + { + settings->_ValidateProfilesExist(); + } + catch (const implementation::SettingsException& ex) + { + VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles); + caughtExpectedException = true; + } + VERIFY_IS_TRUE(caughtExpectedException); + } + { + // Case 3: Bad settings + const auto settingsObject = VerifyParseSucceeded(settingsWithEmptyProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + bool caughtExpectedException = false; + try + { + settings->_ValidateProfilesExist(); + } + catch (const implementation::SettingsException& ex) + { + VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles); + caughtExpectedException = true; + } + VERIFY_IS_TRUE(caughtExpectedException); + } + } + + void DeserializationTests::ValidateDefaultProfileExists() + { + const std::string goodProfiles{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string badProfiles{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string noDefaultAtAll{ R"( + { + "alwaysShowTabs": true, + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string goodProfilesSpecifiedByName{ R"( + { + "defaultProfile": "profile1", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + { + // Case 1: Good settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with unique guids, and the defaultProfile is one of those guids")); + const auto settingsObject = VerifyParseSucceeded(goodProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + settings->_ResolveDefaultProfile(); + settings->_ValidateDefaultProfileExists(); + VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(static_cast(2), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), settings->_allProfiles.GetAt(0).Guid()); + } + { + // Case 2: Bad settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with unique guids, but the defaultProfile is NOT one of those guids")); + const auto settingsObject = VerifyParseSucceeded(badProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + settings->_ResolveDefaultProfile(); + settings->_ValidateDefaultProfileExists(); + VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.GetAt(0)); + + VERIFY_ARE_EQUAL(static_cast(2), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), settings->_allProfiles.GetAt(0).Guid()); + } + { + // Case 2: Bad settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with unique guids, and no defaultProfile at all")); + const auto settingsObject = VerifyParseSucceeded(badProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + settings->_ResolveDefaultProfile(); + settings->_ValidateDefaultProfileExists(); + VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.GetAt(0)); + + VERIFY_ARE_EQUAL(static_cast(2), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), settings->_allProfiles.GetAt(0).Guid()); + } + { + // Case 4: Good settings, default profile is a string + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with unique guids, and the defaultProfile is one of the profile names")); + const auto settingsObject = VerifyParseSucceeded(goodProfilesSpecifiedByName); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + settings->_ResolveDefaultProfile(); + settings->_ValidateDefaultProfileExists(); + VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(static_cast(2), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), settings->_allProfiles.GetAt(1).Guid()); + } + } + + void DeserializationTests::ValidateDuplicateProfiles() + { + const std::string goodProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string badProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string veryBadProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile2", + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile3", + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile4", + "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile5", + "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile6", + "guid": "{6239a42c-7777-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + Profile profile0 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}")); + profile0.Name(L"profile0"); + Profile profile1 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}")); + profile1.Name(L"profile1"); + Profile profile2 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}")); + profile2.Name(L"profile2"); + Profile profile3 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}")); + profile3.Name(L"profile3"); + Profile profile4 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}")); + profile4.Name(L"profile4"); + Profile profile5 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}")); + profile5.Name(L"profile5"); + Profile profile6 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-7777-49a3-80bd-e8fdd045185c}")); + profile6.Name(L"profile6"); + + { + // Case 1: Good settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with unique guids")); + + auto settings = winrt::make_self(); + settings->_allProfiles.Append(profile0); + settings->_allProfiles.Append(profile1); + + settings->_ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(static_cast(2), settings->_allProfiles.Size()); + } + { + // Case 2: Bad settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with the same guid")); + + auto settings = winrt::make_self(); + settings->_allProfiles.Append(profile2); + settings->_allProfiles.Append(profile3); + + settings->_ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->_warnings.GetAt(0)); + + VERIFY_ARE_EQUAL(static_cast(1), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + } + { + // Case 3: Very bad settings + Log::Comment(NoThrowString().Format( + L"Testing a set of profiles, many of which with duplicated guids")); + + auto settings = winrt::make_self(); + settings->_allProfiles.Append(profile0); + settings->_allProfiles.Append(profile1); + settings->_allProfiles.Append(profile2); + settings->_allProfiles.Append(profile3); + settings->_allProfiles.Append(profile4); + settings->_allProfiles.Append(profile5); + settings->_allProfiles.Append(profile6); + + settings->_ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->_warnings.GetAt(0)); + + VERIFY_ARE_EQUAL(static_cast(4), settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"profile6", settings->_allProfiles.GetAt(3).Name()); + } + } + + void DeserializationTests::ValidateManyWarnings() + { + const std::string badProfiles{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile2", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + Profile profile4 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}")); + profile4.Name(L"profile4"); + Profile profile5 = winrt::make(::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}")); + profile5.Name(L"profile5"); + + // Case 2: Bad settings + Log::Comment(NoThrowString().Format( + L"Testing a pair of profiles with the same guid")); + const auto settingsObject = VerifyParseSucceeded(badProfiles); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + + settings->_allProfiles.Append(profile4); + settings->_allProfiles.Append(profile5); + + settings->_ValidateSettings(); + + VERIFY_ARE_EQUAL(3u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->_warnings.GetAt(0)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.GetAt(1)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::UnknownColorScheme, settings->_warnings.GetAt(2)); + + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), settings->_allProfiles.GetAt(0).Guid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + } + + void DeserializationTests::LayerGlobalProperties() + { + const std::string settings0String{ R"( + { + "alwaysShowTabs": true, + "initialCols" : 120, + "initialRows" : 30 + })" }; + const std::string settings1String{ R"( + { + "showTabsInTitlebar": false, + "initialCols" : 240, + "initialRows" : 60 + })" }; + const auto settings0Json = VerifyParseSucceeded(settings0String); + const auto settings1Json = VerifyParseSucceeded(settings1String); + + auto settings = winrt::make_self(); + + settings->LayerJson(settings0Json); + VERIFY_ARE_EQUAL(true, settings->_globals->AlwaysShowTabs()); + VERIFY_ARE_EQUAL(120, settings->_globals->InitialCols()); + VERIFY_ARE_EQUAL(30, settings->_globals->InitialRows()); + VERIFY_ARE_EQUAL(true, settings->_globals->ShowTabsInTitlebar()); + + settings->LayerJson(settings1Json); + VERIFY_ARE_EQUAL(true, settings->_globals->AlwaysShowTabs()); + VERIFY_ARE_EQUAL(240, settings->_globals->InitialCols()); + VERIFY_ARE_EQUAL(60, settings->_globals->InitialRows()); + VERIFY_ARE_EQUAL(false, settings->_globals->ShowTabsInTitlebar()); + } + + void DeserializationTests::ValidateProfileOrdering() + { + const std::string userProfiles0String{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string defaultProfilesString{ R"( + { + "profiles": [ + { + "name" : "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile3", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles1String{ R"( + { + "profiles": [ + { + "name" : "profile4", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile5", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); + const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); + const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); + + { + Log::Comment(NoThrowString().Format( + L"Case 1: Simple swapping of the ordering. The user has the " + L"default profiles in the opposite order of the default ordering.")); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(defaultProfilesString, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile3", settings->_allProfiles.GetAt(1).Name()); + + settings->_ParseJsonString(userProfiles0String, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(1).Name()); + + settings->_ReorderProfilesToMatchUserSettingsOrder(); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(1).Name()); + } + + { + Log::Comment(NoThrowString().Format( + L"Case 2: Make sure all the user's profiles appear before the defaults.")); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(defaultProfilesString, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile3", settings->_allProfiles.GetAt(1).Name()); + + settings->_ParseJsonString(userProfiles1String, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile5", settings->_allProfiles.GetAt(2).Name()); + + settings->_ReorderProfilesToMatchUserSettingsOrder(); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile5", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(2).Name()); + } + } + + void DeserializationTests::ValidateHideProfiles() + { + const std::string defaultProfilesString{ R"( + { + "profiles": [ + { + "name" : "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile3", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles0String{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "hidden": true + }, + { + "name" : "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles1String{ R"( + { + "profiles": [ + { + "name" : "profile4", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "hidden": true + }, + { + "name" : "profile5", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile6", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "hidden": true + } + ] + })" }; + + const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); + const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); + const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); + + { + auto settings = winrt::make_self(); + settings->_ParseJsonString(defaultProfilesString, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile3", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(0).Hidden()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(1).Hidden()); + + settings->_ParseJsonString(userProfiles0String, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(0).Hidden()); + VERIFY_ARE_EQUAL(true, settings->_allProfiles.GetAt(1).Hidden()); + + settings->_ReorderProfilesToMatchUserSettingsOrder(); + settings->_UpdateActiveProfiles(); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(1u, settings->_activeProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile1", settings->_activeProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(false, settings->_activeProfiles.GetAt(0).Hidden()); + } + + { + auto settings = winrt::make_self(); + settings->_ParseJsonString(defaultProfilesString, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile3", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(0).Hidden()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(1).Hidden()); + + settings->_ParseJsonString(userProfiles1String, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile2", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile5", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"profile6", settings->_allProfiles.GetAt(3).Name()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(0).Hidden()); + VERIFY_ARE_EQUAL(true, settings->_allProfiles.GetAt(1).Hidden()); + VERIFY_ARE_EQUAL(false, settings->_allProfiles.GetAt(2).Hidden()); + VERIFY_ARE_EQUAL(true, settings->_allProfiles.GetAt(3).Hidden()); + + settings->_ReorderProfilesToMatchUserSettingsOrder(); + settings->_UpdateActiveProfiles(); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(2u, settings->_activeProfiles.Size()); + VERIFY_ARE_EQUAL(L"profile5", settings->_activeProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile2", settings->_activeProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(false, settings->_activeProfiles.GetAt(0).Hidden()); + VERIFY_ARE_EQUAL(false, settings->_activeProfiles.GetAt(1).Hidden()); + } + } + + void DeserializationTests::ValidateProfilesGenerateGuids() + { + const std::string profile0String{ R"( + { + "name" : "profile0" + })" }; + const std::string profile1String{ R"( + { + "name" : "profile1" + })" }; + const std::string profile2String{ R"( + { + "name" : "profile2", + "guid" : null + })" }; + const std::string profile3String{ R"( + { + "name" : "profile3", + "guid" : "{00000000-0000-0000-0000-000000000000}" + })" }; + const std::string profile4String{ R"( + { + "name" : "profile4", + "guid" : "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile5String{ R"( + { + "name" : "profile2" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + const auto profile4Json = VerifyParseSucceeded(profile4String); + const auto profile5Json = VerifyParseSucceeded(profile5String); + + const auto profile0 = implementation::Profile::FromJson(profile0Json); + const auto profile1 = implementation::Profile::FromJson(profile1Json); + const auto profile2 = implementation::Profile::FromJson(profile2Json); + const auto profile3 = implementation::Profile::FromJson(profile3Json); + const auto profile4 = implementation::Profile::FromJson(profile4Json); + const auto profile5 = implementation::Profile::FromJson(profile5Json); + + const winrt::guid cmdGuid{ Utils::GuidFromString(L"{6239a42c-1de4-49a3-80bd-e8fdd045185c}") }; + const winrt::guid nullGuid{}; + + VERIFY_IS_FALSE(profile0->HasGuid()); + VERIFY_IS_FALSE(profile1->HasGuid()); + VERIFY_IS_FALSE(profile2->HasGuid()); + VERIFY_IS_TRUE(profile3->HasGuid()); + VERIFY_IS_TRUE(profile4->HasGuid()); + VERIFY_IS_FALSE(profile5->HasGuid()); + + VERIFY_ARE_EQUAL(profile3->Guid(), nullGuid); + VERIFY_ARE_EQUAL(profile4->Guid(), cmdGuid); + + auto settings = winrt::make_self(); + settings->_allProfiles.Append(*profile0); + settings->_allProfiles.Append(*profile1); + settings->_allProfiles.Append(*profile2); + settings->_allProfiles.Append(*profile3); + settings->_allProfiles.Append(*profile4); + settings->_allProfiles.Append(*profile5); + + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(4).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(5).HasGuid()); + + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(0).Guid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(1).Guid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(2).Guid(), nullGuid); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(3).Guid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(4).Guid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(5).Guid(), nullGuid); + + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(0).Guid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(1).Guid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(2).Guid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(3).Guid(), cmdGuid); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(4).Guid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(5).Guid(), cmdGuid); + + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(0).Guid(), settings->_allProfiles.GetAt(2).Guid()); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(1).Guid(), settings->_allProfiles.GetAt(2).Guid()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(2).Guid(), settings->_allProfiles.GetAt(2).Guid()); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(3).Guid(), settings->_allProfiles.GetAt(2).Guid()); + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(4).Guid(), settings->_allProfiles.GetAt(2).Guid()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(5).Guid(), settings->_allProfiles.GetAt(2).Guid()); + } + + void DeserializationTests::GeneratedGuidRoundtrips() + { + // Parse a profile without a guid. + // We should automatically generate a GUID for that profile. + // When that profile is serialized and deserialized again, the GUID we + // generated for it should persist. + const std::string profileWithoutGuid{ R"({ + "name" : "profile0" + })" }; + const auto profile0Json = VerifyParseSucceeded(profileWithoutGuid); + + const auto profile0 = implementation::Profile::FromJson(profile0Json); + const GUID nullGuid{ 0 }; + + VERIFY_IS_FALSE(profile0->HasGuid()); + + const auto serialized0Profile = profile0->GenerateStub(); + const auto profile1 = implementation::Profile::FromJson(serialized0Profile); + VERIFY_IS_FALSE(profile0->HasGuid()); + VERIFY_IS_TRUE(profile1->HasGuid()); + + auto settings = winrt::make_self(); + settings->_allProfiles.Append(*profile1); + + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + + const auto profileImpl = winrt::get_self(settings->_allProfiles.GetAt(0)); + const auto serialized1Profile = profileImpl->GenerateStub(); + + const auto profile2 = implementation::Profile::FromJson(serialized1Profile); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(profile2->HasGuid()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(0).Guid(), profile2->Guid()); + } + + void DeserializationTests::TestAllValidationsWithNullGuids() + { + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1" + } + ], + "schemes": [ + { "name": "Campbell" } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + + settings->_ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + } + + void DeserializationTests::TestReorderWithNullGuids() + { + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1" + }, + { + "name" : "cmdFromUserSettings", + "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}" // from defaults.json + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(3).Name()); + + settings->_ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile1", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(3).Name()); + } + + void DeserializationTests::TestReorderingWithoutGuid() + { + Log::Comment(NoThrowString().Format( + L"During the GH#2515 PR, this set of settings was found to cause an" + L" exception, crashing the terminal. This test ensures that it doesn't.")); + + Log::Comment(NoThrowString().Format( + L"While similar to TestReorderWithNullGuids, there's something else" + L" about this scenario specifically that causes a crash, when " + L" TestReorderWithNullGuids did _not_.")); + + const std::string settings0String{ R"( + { + "defaultProfile" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "profiles": [ + { + "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "acrylicOpacity" : 0.5, + "closeOnExit" : true, + "background" : "#8A00FF", + "foreground" : "#F2F2F2", + "commandline" : "cmd.exe", + "cursorColor" : "#FFFFFF", + "fontFace" : "Cascadia Code", + "fontSize" : 10, + "historySize" : 9001, + "padding" : "20", + "snapOnInput" : true, + "startingDirectory" : "%USERPROFILE%", + "useAcrylic" : true + }, + { + "name" : "ThisProfileShouldNotCrash", + "tabTitle" : "Ubuntu", + "acrylicOpacity" : 0.5, + "background" : "#2C001E", + "closeOnExit" : true, + "colorScheme" : "Campbell", + "commandline" : "wsl.exe", + "cursorColor" : "#FFFFFF", + "cursorShape" : "bar", + "fontSize" : 10, + "historySize" : 9001, + "padding" : "0, 0, 0, 0", + "snapOnInput" : true, + "useAcrylic" : true + }, + { + // This is the same profile that would be generated by the WSL profile generator. + "name" : "Ubuntu", + "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", + "acrylicOpacity" : 0.5, + "background" : "#2C001E", + "closeOnExit" : false, + "cursorColor" : "#FFFFFF", + "cursorShape" : "bar", + "fontSize" : 10, + "historySize" : 9001, + "snapOnInput" : true, + "useAcrylic" : true + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"Ubuntu", settings->_allProfiles.GetAt(3).Name()); + + settings->_ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"Ubuntu", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(3).Name()); + } + + void DeserializationTests::TestLayeringNameOnlyProfiles() + { + // This is a test discovered during GH#2782. When we add a name-only + // profile, it should only layer with other name-only profiles with the + // _same name_ + + const std::string settings0String{ R"( + { + "defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", + "profiles": [ + { + "guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", + "name" : "ThisProfileIsGood" + + }, + { + "name" : "ThisProfileShouldNotLayer" + }, + { + "name" : "NeitherShouldThisOne" + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + + Log::Comment(NoThrowString().Format( + L"Parse the user settings")); + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(5u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(4).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotLayer", settings->_allProfiles.GetAt(3).Name()); + VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings->_allProfiles.GetAt(4).Name()); + } + + void DeserializationTests::TestExplodingNameOnlyProfiles() + { + // This is a test for GH#2782. When we add a name-only profile, we'll + // generate a GUID for it. We should make sure that we don't re-append + // that profile to the list of profiles. + + const std::string settings0String{ R"( + { + "defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", + "profiles": [ + { + "guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", + "name" : "ThisProfileIsGood" + + }, + { + "name" : "ThisProfileShouldNotDuplicate" + }, + { + "name" : "NeitherShouldThisOne" + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + + Log::Comment(NoThrowString().Format( + L"Parse the user settings")); + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(5u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(4).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings->_allProfiles.GetAt(3).Name()); + VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings->_allProfiles.GetAt(4).Name()); + + Log::Comment(NoThrowString().Format( + L"Pretend like we're checking to append dynamic profiles to the " + L"user's settings file. We absolutely _shouldn't_ be adding anything here.")); + bool const needToWriteFile = settings->_AppendDynamicProfilesToUserSettings(); + VERIFY_IS_FALSE(needToWriteFile); + VERIFY_ARE_EQUAL(settings0String.size(), settings->_userSettingsString.size()); + + Log::Comment(NoThrowString().Format( + L"Re-parse the settings file. We should have the _same_ settings as before.")); + Log::Comment(NoThrowString().Format( + L"Do this to a _new_ settings object, to make sure it turns out the same.")); + { + auto settings2 = winrt::make_self(); + settings2->_ParseJsonString(DefaultJson, true); + settings2->LayerJson(settings2->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings2->_allProfiles.Size()); + // Initialize the second settings object from the first settings + // object's settings string, the one that we synthesized. + const auto firstSettingsString = settings->_userSettingsString; + settings2->_ParseJsonString(firstSettingsString, false); + settings2->LayerJson(settings2->_userSettings); + VERIFY_ARE_EQUAL(5u, settings2->_allProfiles.Size()); + VERIFY_IS_TRUE(settings2->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_TRUE(settings2->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_TRUE(settings2->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_FALSE(settings2->_allProfiles.GetAt(3).HasGuid()); + VERIFY_IS_FALSE(settings2->_allProfiles.GetAt(4).HasGuid()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings2->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings2->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings2->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings2->_allProfiles.GetAt(3).Name()); + VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings2->_allProfiles.GetAt(4).Name()); + } + + Log::Comment(NoThrowString().Format( + L"Validate the settings. All the profiles we have should be valid.")); + settings->_ValidateSettings(); + + VERIFY_ARE_EQUAL(5u, settings->_allProfiles.Size()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(4).HasGuid()); + VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->_allProfiles.GetAt(3).Name()); + VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(4).Name()); + } + + void DeserializationTests::TestHideAllProfiles() + { + const std::string settingsWithProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "hidden": false + }, + { + "name" : "profile1", + "hidden": true + } + ] + })" }; + + const std::string settingsWithoutProfiles{ R"( + { + "profiles": [ + { + "name" : "profile0", + "hidden": true + }, + { + "name" : "profile1", + "hidden": true + } + ] + })" }; + + VerifyParseSucceeded(settingsWithProfiles); + VerifyParseSucceeded(settingsWithoutProfiles); + + { + // Case 1: Good settings + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsWithProfiles, false); + settings->LayerJson(settings->_userSettings); + + settings->_UpdateActiveProfiles(); + Log::Comment(NoThrowString().Format( + L"settingsWithProfiles successfully parsed and validated")); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(1u, settings->_activeProfiles.Size()); + } + { + // Case 2: Bad settings + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsWithoutProfiles, false); + settings->LayerJson(settings->_userSettings); + + bool caughtExpectedException = false; + try + { + settings->_UpdateActiveProfiles(); + } + catch (const implementation::SettingsException& ex) + { + VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::AllProfilesHidden); + caughtExpectedException = true; + } + VERIFY_IS_TRUE(caughtExpectedException); + } + } + + void DeserializationTests::TestInvalidColorSchemeName() + { + Log::Comment(NoThrowString().Format( + L"Ensure that setting a profile's scheme to a non-existent scheme causes a warning.")); + + const std::string settings0String{ R"( + { + "profiles": [ + { + "name" : "profile0", + "colorScheme": "schemeOne" + }, + { + "name" : "profile1", + "colorScheme": "InvalidSchemeName" + }, + { + "name" : "profile2" + // Will use the Profile default value, "Campbell" + } + ], + "schemes": [ + { + "name": "schemeOne", + "foreground": "#111111" + }, + { + "name": "schemeTwo", + "foreground": "#222222" + } + ] + })" }; + + VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(2u, settings->_globals->ColorSchemes().Size()); + + VERIFY_ARE_EQUAL(L"schemeOne", settings->_allProfiles.GetAt(0).ColorSchemeName()); + VERIFY_ARE_EQUAL(L"InvalidSchemeName", settings->_allProfiles.GetAt(1).ColorSchemeName()); + VERIFY_ARE_EQUAL(L"Campbell", settings->_allProfiles.GetAt(2).ColorSchemeName()); + + settings->_ValidateAllSchemesExist(); + + VERIFY_ARE_EQUAL(1u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::UnknownColorScheme, settings->_warnings.GetAt(0)); + + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(2u, settings->_globals->ColorSchemes().Size()); + + VERIFY_ARE_EQUAL(L"schemeOne", settings->_allProfiles.GetAt(0).ColorSchemeName()); + VERIFY_ARE_EQUAL(L"Campbell", settings->_allProfiles.GetAt(1).ColorSchemeName()); + VERIFY_ARE_EQUAL(L"Campbell", settings->_allProfiles.GetAt(2).ColorSchemeName()); + } + + void DeserializationTests::TestHelperFunctions() + { + const std::string settings0String{ R"( + { + "defaultProfile" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "ThisProfileShouldNotThrow" + }, + { + "name" : "Ubuntu", + "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}" + } + ] + })" }; + + auto name0{ L"profile0" }; + auto name1{ L"profile1" }; + auto name2{ L"Ubuntu" }; + auto name3{ L"ThisProfileShouldNotThrow" }; + auto badName{ L"DoesNotExist" }; + + const winrt::guid guid0{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid1{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid2{ ::Microsoft::Console::Utils::GuidFromString(L"{2C4DE342-38B7-51CF-B940-2309A097F518}") }; + const winrt::guid fakeGuid{ ::Microsoft::Console::Utils::GuidFromString(L"{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}") }; + const winrt::guid autogeneratedGuid{ implementation::Profile::_GenerateGuidForProfile(name3, L"") }; + const std::optional badGuid{}; + + VerifyParseSucceeded(settings0String); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settings0String, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(guid0, settings->_GetProfileGuidByName(name0)); + VERIFY_ARE_EQUAL(guid1, settings->_GetProfileGuidByName(name1)); + VERIFY_ARE_EQUAL(guid2, settings->_GetProfileGuidByName(name2)); + VERIFY_ARE_EQUAL(autogeneratedGuid, settings->_GetProfileGuidByName(name3)); + VERIFY_ARE_EQUAL(badGuid, settings->_GetProfileGuidByName(badName)); + + auto prof0{ settings->FindProfile(guid0) }; + auto prof1{ settings->FindProfile(guid1) }; + auto prof2{ settings->FindProfile(guid2) }; + + auto badProf{ settings->FindProfile(fakeGuid) }; + VERIFY_ARE_EQUAL(badProf, nullptr); + + VERIFY_ARE_EQUAL(name0, prof0.Name()); + VERIFY_ARE_EQUAL(name1, prof1.Name()); + VERIFY_ARE_EQUAL(name2, prof2.Name()); + } + + void DeserializationTests::TestProfileBackgroundImageWithEnvVar() + { + const auto expectedPath = wil::ExpandEnvironmentStringsW(L"%WINDIR%\\System32\\x_80.png"); + + const std::string settingsJson{ R"( + { + "profiles": [ + { + "name": "profile0", + "backgroundImage": "%WINDIR%\\System32\\x_80.png" + } + ] + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_NOT_EQUAL(0u, settings->_allProfiles.Size()); + VERIFY_ARE_EQUAL(expectedPath, settings->_allProfiles.GetAt(0).ExpandedBackgroundImagePath()); + } + void DeserializationTests::TestProfileBackgroundImageWithDesktopWallpaper() + { + const winrt::hstring expectedBackgroundImagePath{ L"desktopWallpaper" }; + + const std::string settingsJson{ R"( + { + "profiles": [ + { + "name": "profile0", + "backgroundImage": "desktopWallpaper" + } + ] + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(expectedBackgroundImagePath, settings->_allProfiles.GetAt(0).BackgroundImagePath()); + VERIFY_ARE_NOT_EQUAL(expectedBackgroundImagePath, settings->_allProfiles.GetAt(0).ExpandedBackgroundImagePath()); + } + void DeserializationTests::TestCloseOnExitParsing() + { + const std::string settingsJson{ R"( + { + "profiles": [ + { + "name": "profile0", + "closeOnExit": "graceful" + }, + { + "name": "profile1", + "closeOnExit": "always" + }, + { + "name": "profile2", + "closeOnExit": "never" + }, + { + "name": "profile3", + "closeOnExit": null + } + ] + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + 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()); + + // Unknown modes parse as "Graceful" + VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->_allProfiles.GetAt(3).CloseOnExit()); + } + void DeserializationTests::TestCloseOnExitCompatibilityShim() + { + const std::string settingsJson{ R"( + { + "profiles": [ + { + "name": "profile0", + "closeOnExit": true + }, + { + "name": "profile1", + "closeOnExit": false + } + ] + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->_allProfiles.GetAt(0).CloseOnExit()); + VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->_allProfiles.GetAt(1).CloseOnExit()); + } + + void DeserializationTests::TestLayerUserDefaultsBeforeProfiles() + { + // Test for microsoft/terminal#2325. For this test, we'll be setting the + // "historySize" in the "defaultSettings", so it should apply to all + // profiles, unless they override it. In one of the user's profiles, + // we'll override that value, and in the other, we'll leave it + // untouched. + + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "historySize": 1234 + }, + "list": [ + { + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "name": "profile0", + "historySize": 2345 + }, + { + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "name": "profile1" + } + ] + } + })" }; + VerifyParseSucceeded(settings0String); + + const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"; + + { + auto settings = winrt::make_self(false); + settings->_ParseJsonString(settings0String, false); + VERIFY_IS_NULL(settings->_userDefaultProfileSettings); + settings->_ApplyDefaultsFromUserSettings(); + VERIFY_IS_NOT_NULL(settings->_userDefaultProfileSettings); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(guid1String, settings->_globals->UnparsedDefaultProfile()); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + + VERIFY_ARE_EQUAL(2345, settings->_allProfiles.GetAt(0).HistorySize()); + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(1).HistorySize()); + } + } + + void DeserializationTests::TestDontLayerGuidFromUserDefaults() + { + // Test for microsoft/terminal#2325. We don't want the user to put a + // "guid" in the "defaultSettings", and have that apply to all the other + // profiles + + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + "list": [ + { + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "name": "profile0", + "historySize": 2345 + }, + { + // Doesn't have a GUID, we'll auto-generate one + "name": "profile1" + } + ] + } + })" }; + VerifyParseSucceeded(settings0String); + + const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"; + const winrt::guid guid1{ ::Microsoft::Console::Utils::GuidFromString(guid1String) }; + const winrt::guid guid2{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}") }; + + { + auto settings = winrt::make_self(false); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + + settings->_ParseJsonString(settings0String, false); + VERIFY_IS_NULL(settings->_userDefaultProfileSettings); + settings->_ApplyDefaultsFromUserSettings(); + VERIFY_IS_NOT_NULL(settings->_userDefaultProfileSettings); + + Log::Comment(NoThrowString().Format( + L"Ensure that cmd and powershell don't get their GUIDs overwritten")); + VERIFY_ARE_NOT_EQUAL(guid2, settings->_allProfiles.GetAt(0).Guid()); + VERIFY_ARE_NOT_EQUAL(guid2, settings->_allProfiles.GetAt(1).Guid()); + + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(guid1String, settings->_globals->UnparsedDefaultProfile()); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + + VERIFY_ARE_EQUAL(guid1, settings->_allProfiles.GetAt(2).Guid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(3).HasGuid()); + } + } + + void DeserializationTests::TestLayerUserDefaultsOnDynamics() + { + // Test for microsoft/terminal#2325. For this test, we'll be setting the + // "historySize" in the "defaultSettings", so it should apply to all + // profiles, unless they override it. The dynamic profiles will _also_ + // set this value, but from discussion in GH#2325, we decided that + // settings in defaultSettings should apply _on top_ of settings from + // dynamic profiles. + + const winrt::guid guid1{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid2{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid3{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}") }; + + const std::string userProfiles{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "historySize": 1234 + }, + "list": [ + { + "name" : "profile0FromUserSettings", // this is _allProfiles.GetAt(0) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.0" + }, + { + "name" : "profile1FromUserSettings", // this is _allProfiles.GetAt(2) + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.1", + "historySize": 4444 + }, + { + "name" : "profile2FromUserSettings", // this is _allProfiles.GetAt(3) + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "historySize": 5555 + } + ] + } + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid1, guid2]() { + std::vector profiles; + Profile p0 = winrt::make(guid1); + p0.Name(L"profile0"); // this is _allProfiles.GetAt(0) + p0.HistorySize(1111); + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid1, guid2]() { + std::vector profiles; + Profile p0 = winrt::make(guid1); + Profile p1 = winrt::make(guid2); + p0.Name(L"profile0"); // this is _allProfiles.GetAt(1) + p1.Name(L"profile1"); // this is _allProfiles.GetAt(2) + p0.HistorySize(2222); + profiles.push_back(p0); + p1.HistorySize(3333); + profiles.push_back(p1); + return profiles; + }; + + auto settings = winrt::make_self(false); + settings->_profileGenerators.emplace_back(std::move(gen0)); + settings->_profileGenerators.emplace_back(std::move(gen1)); + + Log::Comment(NoThrowString().Format( + L"All profiles with the same name have the same GUID. However, they" + L" will not be layered, because they have different source's")); + + // parse userProfiles as the user settings + settings->_ParseJsonString(userProfiles, false); + VERIFY_ARE_EQUAL(0u, settings->_allProfiles.Size(), L"Just parsing the user settings doesn't actually layer them"); + settings->_LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + + VERIFY_ARE_EQUAL(1111, settings->_allProfiles.GetAt(0).HistorySize()); + VERIFY_ARE_EQUAL(2222, settings->_allProfiles.GetAt(1).HistorySize()); + VERIFY_ARE_EQUAL(3333, settings->_allProfiles.GetAt(2).HistorySize()); + + settings->_ApplyDefaultsFromUserSettings(); + + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(0).HistorySize()); + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(1).HistorySize()); + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(2).HistorySize()); + + settings->LayerJson(settings->_userSettings); + VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); + + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(0).Source().empty()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).Source().empty()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(2).Source().empty()); + VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).Source().empty()); + + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings->_allProfiles.GetAt(0).Source()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings->_allProfiles.GetAt(1).Source()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings->_allProfiles.GetAt(2).Source()); + + VERIFY_ARE_EQUAL(guid1, settings->_allProfiles.GetAt(0).Guid()); + VERIFY_ARE_EQUAL(guid1, settings->_allProfiles.GetAt(1).Guid()); + VERIFY_ARE_EQUAL(guid2, settings->_allProfiles.GetAt(2).Guid()); + + VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"profile2FromUserSettings", settings->_allProfiles.GetAt(3).Name()); + + Log::Comment(NoThrowString().Format( + L"This is the real meat of the test: The two dynamic profiles that " + L"_didn't_ have historySize set in the userSettings should have " + L"1234 as their historySize(from the defaultSettings).The other two" + L" profiles should have their custom historySize value.")); + + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(0).HistorySize()); + VERIFY_ARE_EQUAL(1234, settings->_allProfiles.GetAt(1).HistorySize()); + VERIFY_ARE_EQUAL(4444, settings->_allProfiles.GetAt(2).HistorySize()); + VERIFY_ARE_EQUAL(5555, settings->_allProfiles.GetAt(3).HistorySize()); + } + + void DeserializationTests::FindMissingProfile() + { + // Test that CascadiaSettings::FindProfile returns null for a GUID that + // doesn't exist + const std::string settingsString{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + const auto settingsJsonObj = VerifyParseSucceeded(settingsString); + auto settings = implementation::CascadiaSettings::FromJson(settingsJsonObj); + + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + const auto guid3 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + + const auto profile1 = settings->FindProfile(guid1); + const auto profile2 = settings->FindProfile(guid2); + const auto profile3 = settings->FindProfile(guid3); + + VERIFY_IS_NOT_NULL(profile1); + VERIFY_IS_NOT_NULL(profile2); + VERIFY_IS_NULL(profile3); + + VERIFY_ARE_EQUAL(L"profile0", profile1.Name()); + VERIFY_ARE_EQUAL(L"profile1", profile2.Name()); + } + + void DeserializationTests::ValidateKeybindingsWarnings() + { + const std::string badSettings{ R"( + { + "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + } + ], + "keybindings": [ + { "command": { "action": "splitPane", "split":"auto" }, "keys": [ "ctrl+alt+t", "ctrl+a" ] }, + { "command": { "action": "moveFocus" }, "keys": [ "ctrl+a" ] }, + { "command": { "action": "resizePane" }, "keys": [ "ctrl+b" ] } + ] + })" }; + + const auto settingsObject = VerifyParseSucceeded(badSettings); + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + + VERIFY_ARE_EQUAL(0u, settings->_globals->_keymap->_keyShortcuts.size()); + + VERIFY_ARE_EQUAL(3u, settings->_globals->_keybindingsWarnings.size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, settings->_globals->_keybindingsWarnings.at(0)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_globals->_keybindingsWarnings.at(1)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_globals->_keybindingsWarnings.at(2)); + + settings->_ValidateKeybindings(); + + VERIFY_ARE_EQUAL(4u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->_warnings.GetAt(0)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, settings->_warnings.GetAt(1)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.GetAt(2)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.GetAt(3)); + } + + void DeserializationTests::ValidateExecuteCommandlineWarning() + { + const std::string badSettings{ R"( + { + "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + } + ], + "keybindings": [ + { "name":null, "command": { "action": "wt" }, "keys": [ "ctrl+a" ] }, + { "name":null, "command": { "action": "wt", "commandline":"" }, "keys": [ "ctrl+b" ] }, + { "name":null, "command": { "action": "wt", "commandline":null }, "keys": [ "ctrl+c" ] } + ] + })" }; + + const auto settingsObject = VerifyParseSucceeded(badSettings); + + auto settings = implementation::CascadiaSettings::FromJson(settingsObject); + + VERIFY_ARE_EQUAL(0u, settings->_globals->_keymap->_keyShortcuts.size()); + + for (const auto& warning : settings->_globals->_keybindingsWarnings) + { + Log::Comment(NoThrowString().Format( + L"warning:%d", warning)); + } + VERIFY_ARE_EQUAL(3u, settings->_globals->_keybindingsWarnings.size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_globals->_keybindingsWarnings.at(0)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_globals->_keybindingsWarnings.at(1)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_globals->_keybindingsWarnings.at(2)); + + settings->_ValidateKeybindings(); + + VERIFY_ARE_EQUAL(4u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->_warnings.GetAt(0)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.GetAt(1)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.GetAt(2)); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.GetAt(3)); + } + + void DeserializationTests::ValidateLegacyGlobalsWarning() + { + const std::string badSettings{ R"( + { + "globals": {}, + "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + } + ], + "keybindings": [] + })" }; + + // Create the default settings + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + + settings->_ValidateNoGlobalsKey(); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + + // Now layer on the user's settings + settings->_ParseJsonString(badSettings, false); + settings->LayerJson(settings->_userSettings); + + settings->_ValidateNoGlobalsKey(); + VERIFY_ARE_EQUAL(1u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(SettingsLoadWarnings::LegacyGlobalsProperty, settings->_warnings.GetAt(0)); + } + + void DeserializationTests::TestTrailingCommas() + { + const std::string badSettings{ R"( + { + "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + }, + ], + "keybindings": [], + })" }; + + // Create the default settings + auto settings = winrt::make_self(); + settings->_ParseJsonString(DefaultJson, true); + settings->LayerJson(settings->_defaultSettings); + + // Now layer on the user's settings + try + { + settings->_ParseJsonString(badSettings, false); + settings->LayerJson(settings->_userSettings); + } + catch (...) + { + VERIFY_IS_TRUE(false, L"This call to LayerJson should succeed, even with the trailing comma"); + } + } + + void DeserializationTests::TestCommandsAndKeybindings() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { "keys": "ctrl+a", "command": { "action": "splitPane", "split": "vertical" } }, + { "name": "ctrl+b", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+c", "name": "ctrl+c", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+d", "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": "ctrl+e", "command": { "action": "splitPane", "split": "horizontal" } }, + { "keys": "ctrl+f", "name":null, "command": { "action": "splitPane", "split": "horizontal" } } + ] + })" }; + + const auto guid0 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + + const auto profile2Guid = settings->_allProfiles.GetAt(2).Guid(); + VERIFY_ARE_NOT_EQUAL(winrt::guid{}, profile2Guid); + + auto keymap = winrt::get_self(settings->_globals->KeyMap()); + VERIFY_ARE_EQUAL(5u, keymap->_keyShortcuts.size()); + + // A/D, B, C, E will be in the list of commands, for 4 total. + // * A and D share the same name, so they'll only generate a single action. + // * F's name is set manually to `null` + auto commands = settings->_globals->Commands(); + VERIFY_ARE_EQUAL(4u, commands.Size()); + + { + KeyChord kc{ true, false, false, static_cast('A') }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + + Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); + + { + KeyChord kc{ true, false, false, static_cast('C') }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('D') }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('E') }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + KeyChord kc{ true, false, false, static_cast('F') }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + + Log::Comment(L"Now verify the commands"); + _logCommandNames(commands); + { + auto command = commands.Lookup(L"Split pane, split: vertical"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.Lookup(L"ctrl+b"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.Lookup(L"ctrl+c"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + { + auto command = commands.Lookup(L"Split pane, split: horizontal"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + } + } + + void DeserializationTests::TestNestedCommandWithoutName() + { + // This test tests a nested command without a name specified. This type + // of command should just be ignored, since we can't auto-generate names + // for nested commands, they _must_ have names specified. + + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "commands": [ + { + "name": "child1", + "command": { "action": "newTab", "commandline": "ssh me@first.com" } + }, + { + "name": "child2", + "command": { "action": "newTab", "commandline": "ssh me@second.com" } + } + ] + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + + auto commands = settings->_globals->Commands(); + settings->_ValidateSettings(); + _logCommandNames(commands); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.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 + // ignored. + VERIFY_ARE_EQUAL(0u, commands.Size()); + } + + void DeserializationTests::TestUnbindNestedCommand() + { + // Test that layering a command with `"commands": null` set will unbind a command that already exists. + + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "name": "parent", + "commands": [ + { + "name": "child1", + "command": { "action": "newTab", "commandline": "ssh me@first.com" } + }, + { + "name": "child2", + "command": { "action": "newTab", "commandline": "ssh me@second.com" } + } + ] + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. + })" }; + + const std::string settings1Json{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "actions": [ + { + "name": "parent", + "commands": null + }, + ], + })" }; + + VerifyParseSucceeded(settingsJson); + VerifyParseSucceeded(settings1Json); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + + auto commands = settings->_globals->Commands(); + settings->_ValidateSettings(); + _logCommandNames(commands); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + Log::Comment(L"Layer second bit of json, to unbind the original command."); + + settings->_ParseJsonString(settings1Json, false); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + commands = settings->_globals->Commands(); + _logCommandNames(commands); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(0u, commands.Size()); + } + + void DeserializationTests::TestRebindNestedCommand() + { + // Test that layering a command with an action set on top of a command + // with nested commands replaces the nested commands with an action. + + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "name": "parent", + "commands": [ + { + "name": "child1", + "command": { "action": "newTab", "commandline": "ssh me@first.com" } + }, + { + "name": "child2", + "command": { "action": "newTab", "commandline": "ssh me@second.com" } + } + ] + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. + })" }; + + const std::string settings1Json{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "actions": [ + { + "name": "parent", + "command": "newTab" + }, + ], + })" }; + + VerifyParseSucceeded(settingsJson); + VerifyParseSucceeded(settings1Json); + + auto settings = winrt::make_self(); + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + + auto commands = settings->_globals->Commands(); + settings->_ValidateSettings(); + _logCommandNames(commands); + + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + { + winrt::hstring commandName{ L"parent" }; + auto commandProj = commands.Lookup(commandName); + VERIFY_IS_NOT_NULL(commandProj); + + winrt::com_ptr commandImpl; + commandImpl.copy_from(winrt::get_self(commandProj)); + + VERIFY_IS_TRUE(commandImpl->HasNestedCommands()); + VERIFY_ARE_EQUAL(2u, commandImpl->_subcommands.Size()); + } + + Log::Comment(L"Layer second bit of json, to unbind the original command."); + settings->_ParseJsonString(settings1Json, false); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + commands = settings->_globals->Commands(); + _logCommandNames(commands); + VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + { + winrt::hstring commandName{ L"parent" }; + auto commandProj = commands.Lookup(commandName); + + VERIFY_IS_NOT_NULL(commandProj); + auto actionAndArgs = commandProj.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + + winrt::com_ptr commandImpl; + commandImpl.copy_from(winrt::get_self(commandProj)); + + VERIFY_IS_FALSE(commandImpl->HasNestedCommands()); + } + } + + void DeserializationTests::TestCopy() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "initialCols": 50, + "profiles": + [ + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Custom Profile", + "fontFace": "Cascadia Code" + } + ], + "schemes": + [ + { + "name": "Campbell, but for a test", + "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": + [ + { "command": "openSettings", "keys": "ctrl+," }, + { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," }, + + { + "name": { "key": "SetColorSchemeParentCommandName" }, + "commands": [ + { + "iterateOn": "schemes", + "name": "${scheme.name}", + "command": { "action": "setColorScheme", "colorScheme": "${scheme.name}" } + } + ] + } + ] + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings{ winrt::make_self() }; + settings->_ParseJsonString(settingsJson, false); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + const auto copy{ settings->Copy() }; + const auto copyImpl{ winrt::get_self(copy) }; + + // test globals + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), copyImpl->_globals->DefaultProfile()); + + // test profiles + VERIFY_ARE_EQUAL(settings->_allProfiles.Size(), copyImpl->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(0).Name(), copyImpl->_allProfiles.GetAt(0).Name()); + + // test schemes + const auto schemeName{ L"Campbell, but for a test" }; + VERIFY_ARE_EQUAL(settings->_globals->_colorSchemes.Size(), copyImpl->_globals->_colorSchemes.Size()); + VERIFY_ARE_EQUAL(settings->_globals->_colorSchemes.HasKey(schemeName), copyImpl->_globals->_colorSchemes.HasKey(schemeName)); + + // test actions + VERIFY_ARE_EQUAL(settings->_globals->_keymap->_keyShortcuts.size(), copyImpl->_globals->_keymap->_keyShortcuts.size()); + VERIFY_ARE_EQUAL(settings->_globals->_commands.Size(), copyImpl->_globals->_commands.Size()); + + // Test that changing the copy should not change the original + VERIFY_ARE_EQUAL(settings->_globals->WordDelimiters(), copyImpl->_globals->WordDelimiters()); + copyImpl->_globals->WordDelimiters(L"changed value"); + VERIFY_ARE_NOT_EQUAL(settings->_globals->WordDelimiters(), copyImpl->_globals->WordDelimiters()); + } + + void DeserializationTests::TestCloneInheritanceTree() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "profiles": + { + "defaults": { + "name": "PROFILE DEFAULTS" + }, + "list": [ + { + "guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "name": "CMD" + }, + { + "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", + "name": "PowerShell" + }, + { + "guid": "{61c54bbd-3333-5271-96e7-009a87ff44bf}" + } + ] + } + })" }; + + VerifyParseSucceeded(settingsJson); + + auto settings{ winrt::make_self() }; + settings->_ParseJsonString(settingsJson, false); + settings->_ApplyDefaultsFromUserSettings(); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + const auto copy{ settings->Copy() }; + const auto copyImpl{ winrt::get_self(copy) }; + + // test globals + VERIFY_ARE_EQUAL(settings->_globals->DefaultProfile(), copyImpl->_globals->DefaultProfile()); + + // test profiles + VERIFY_ARE_EQUAL(settings->_allProfiles.Size(), copyImpl->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(0).Name(), copyImpl->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(1).Name(), copyImpl->_allProfiles.GetAt(1).Name()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(2).Name(), copyImpl->_allProfiles.GetAt(2).Name()); + VERIFY_ARE_EQUAL(settings->_userDefaultProfileSettings->Name(), copyImpl->_userDefaultProfileSettings->Name()); + + // Modifying profile.defaults should... + VERIFY_ARE_EQUAL(settings->_userDefaultProfileSettings->HasName(), copyImpl->_userDefaultProfileSettings->HasName()); + copyImpl->_userDefaultProfileSettings->Name(L"changed value"); + + // ...keep the same name for the first two profiles + VERIFY_ARE_EQUAL(settings->_allProfiles.Size(), copyImpl->_allProfiles.Size()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(0).Name(), copyImpl->_allProfiles.GetAt(0).Name()); + VERIFY_ARE_EQUAL(settings->_allProfiles.GetAt(1).Name(), copyImpl->_allProfiles.GetAt(1).Name()); + + // ...but change the name for the one that inherited it from profile.defaults + VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(2).Name(), copyImpl->_allProfiles.GetAt(2).Name()); + + // profile.defaults should be different between the two graphs + VERIFY_ARE_EQUAL(settings->_userDefaultProfileSettings->HasName(), copyImpl->_userDefaultProfileSettings->HasName()); + VERIFY_ARE_NOT_EQUAL(settings->_userDefaultProfileSettings->Name(), copyImpl->_userDefaultProfileSettings->Name()); + + Log::Comment(L"Test empty profiles.defaults"); + const std::string emptyPDJson{ R"( + { + "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "profiles": + { + "defaults": { + }, + "list": [ + { + "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", + "name": "PowerShell" + } + ] + } + })" }; + + const std::string missingPDJson{ R"( + { + "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "profiles": + [ + { + "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", + "name": "PowerShell" + } + ] + })" }; + + auto verifyEmptyPD = [this](const std::string json) { + VerifyParseSucceeded(json); + + auto settings{ winrt::make_self() }; + settings->_ParseJsonString(json, false); + settings->_ApplyDefaultsFromUserSettings(); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + const auto copy{ settings->Copy() }; + const auto copyImpl{ winrt::get_self(copy) }; + + // test optimization: if we don't have profiles.defaults, don't add it to the tree + VERIFY_IS_NULL(settings->_userDefaultProfileSettings); + VERIFY_ARE_EQUAL(settings->_userDefaultProfileSettings, copyImpl->_userDefaultProfileSettings); + + VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), 1u); + VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), copyImpl->ActiveProfiles().Size()); + + // so we should only have one parent, instead of two + auto srcProfile{ winrt::get_self(settings->ActiveProfiles().GetAt(0)) }; + auto copyProfile{ winrt::get_self(copyImpl->ActiveProfiles().GetAt(0)) }; + VERIFY_ARE_EQUAL(srcProfile->Parents().size(), 0u); + VERIFY_ARE_EQUAL(srcProfile->Parents().size(), copyProfile->Parents().size()); + }; + + verifyEmptyPD(emptyPDJson); + verifyEmptyPD(missingPDJson); + } + + void DeserializationTests::TestValidDefaults() + { + // GH#8146: A LoadDefaults call should populate the list of active profiles + + const auto settings{ CascadiaSettings::LoadDefaults() }; + VERIFY_ARE_EQUAL(settings.ActiveProfiles().Size(), settings.AllProfiles().Size()); + VERIFY_ARE_EQUAL(settings.AllProfiles().Size(), 2u); + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/JsonTestClass.h b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h similarity index 70% rename from src/cascadia/LocalTests_TerminalApp/JsonTestClass.h rename to src/cascadia/LocalTests_SettingsModel/JsonTestClass.h index a66b9f2af58..5cb0bf3f56b 100644 --- a/src/cascadia/LocalTests_TerminalApp/JsonTestClass.h +++ b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h @@ -22,6 +22,12 @@ class JsonTestClass { _reader = std::unique_ptr(Json::CharReaderBuilder::CharReaderBuilder().newCharReader()); }; + + void InitializeJsonWriter() + { + _writer = std::unique_ptr(Json::StreamWriterBuilder::StreamWriterBuilder().newStreamWriter()); + } + Json::Value VerifyParseSucceeded(std::string content) { Json::Value root; @@ -31,6 +37,14 @@ class JsonTestClass return root; }; + std::string toString(const Json::Value& json) + { + std::stringstream s; + _writer->write(json, &s); + return s.str(); + } + protected: std::unique_ptr _reader; + std::unique_ptr _writer; }; diff --git a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp similarity index 61% rename from src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp rename to src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp index ad51cf8269b..e70e69f1b2c 100644 --- a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/KeyBindingsTests.cpp @@ -3,20 +3,20 @@ #include "pch.h" -#include "../TerminalApp/ColorScheme.h" -#include "../TerminalApp/CascadiaSettings.h" +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "../TerminalSettingsModel/KeyMapping.h" #include "JsonTestClass.h" #include "TestUtils.h" using namespace Microsoft::Console; -using namespace TerminalApp; -using namespace winrt::TerminalApp; -using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; -namespace TerminalAppLocalTests +namespace SettingsModelLocalTests { // TODO:microsoft/terminal#3838: // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for @@ -46,6 +46,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestSetTabColorArgs); + TEST_METHOD(TestScrollArgs); + TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -66,18 +68,18 @@ namespace TerminalAppLocalTests const auto bindings1Json = VerifyParseSucceeded(bindings1String); const auto bindings2Json = VerifyParseSucceeded(bindings2String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings1Json); - VERIFY_ARE_EQUAL(2u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(2u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings2Json); - VERIFY_ARE_EQUAL(4u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(4u, keymap->_keyShortcuts.size()); } void KeyBindingsTests::LayerKeybindings() @@ -90,18 +92,18 @@ namespace TerminalAppLocalTests const auto bindings1Json = VerifyParseSucceeded(bindings1String); const auto bindings2Json = VerifyParseSucceeded(bindings2String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings1Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings2Json); - VERIFY_ARE_EQUAL(2u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(2u, keymap->_keyShortcuts.size()); } void KeyBindingsTests::UnbindKeybindings() @@ -120,52 +122,52 @@ namespace TerminalAppLocalTests const auto bindings4Json = VerifyParseSucceeded(bindings4String); const auto bindings5Json = VerifyParseSucceeded(bindings5String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings1Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); Log::Comment(NoThrowString().Format( L"Try unbinding a key using `\"unbound\"` to unbind the key")); - appKeyBindings->LayerJson(bindings2Json); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); Log::Comment(NoThrowString().Format( L"Try unbinding a key using `null` to unbind the key")); // First add back a good binding - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); // Then try layering in the bad setting - appKeyBindings->LayerJson(bindings3Json); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings3Json); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); Log::Comment(NoThrowString().Format( L"Try unbinding a key using an unrecognized command to unbind the key")); // First add back a good binding - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); // Then try layering in the bad setting - appKeyBindings->LayerJson(bindings4Json); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings4Json); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); Log::Comment(NoThrowString().Format( L"Try unbinding a key using a straight up invalid value to unbind the key")); // First add back a good binding - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); // Then try layering in the bad setting - appKeyBindings->LayerJson(bindings5Json); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings5Json); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); Log::Comment(NoThrowString().Format( L"Try unbinding a key that wasn't bound at all")); - appKeyBindings->LayerJson(bindings2Json); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + keymap->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); } void KeyBindingsTests::TestArbitraryArgs() @@ -189,17 +191,17 @@ namespace TerminalAppLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(10u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(10u, keymap->_keyShortcuts.size()); { Log::Comment(NoThrowString().Format( L"Verify that `copy` without args parses as Copy(SingleLine=false)")); KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -210,7 +212,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); KeyChord kc{ true, false, true, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -221,7 +223,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); KeyChord kc{ false, true, true, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -232,7 +234,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `newTab` without args parses as NewTab(Index=null)")); KeyChord kc{ true, false, false, static_cast('T') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -244,7 +246,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `newTab` parses args correctly")); KeyChord kc{ true, false, true, static_cast('T') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -258,7 +260,7 @@ namespace TerminalAppLocalTests L"Verify that `newTab` with an index greater than the legacy " L"args afforded parses correctly")); KeyChord kc{ true, false, true, static_cast('Y') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -272,7 +274,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` ignores args it doesn't understand")); KeyChord kc{ true, false, true, static_cast('B') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -284,7 +286,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` null as it's `args` parses as the default option")); KeyChord kc{ true, false, true, static_cast('B') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -296,7 +298,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a positive delta parses args correctly")); KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -308,7 +310,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `adjustFontSize` with a negative delta parses args correctly")); KeyChord kc{ true, false, false, static_cast('G') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -320,7 +322,6 @@ namespace TerminalAppLocalTests void KeyBindingsTests::TestSplitPaneArgs() { const std::string bindings0String{ R"([ - { "keys": ["ctrl+c"], "command": { "action": "splitPane", "split": null } }, { "keys": ["ctrl+d"], "command": { "action": "splitPane", "split": "vertical" } }, { "keys": ["ctrl+e"], "command": { "action": "splitPane", "split": "horizontal" } }, { "keys": ["ctrl+g"], "command": { "action": "splitPane" } }, @@ -329,56 +330,47 @@ namespace TerminalAppLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(5u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(4u, keymap->_keyShortcuts.size()); - { - KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); - } { KeyChord kc{ true, false, false, static_cast('D') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } { KeyChord kc{ true, false, false, static_cast('E') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); } { KeyChord kc{ true, false, false, static_cast('G') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } { KeyChord kc{ true, false, false, static_cast('H') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); } } @@ -392,15 +384,15 @@ namespace TerminalAppLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(3u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(3u, keymap->_keyShortcuts.size()); { KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -409,18 +401,18 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('D') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_IS_NOT_NULL(realArgs.TabColor()); // Remember that COLORREFs are actually BBGGRR order, while the string is in #RRGGBB order - VERIFY_ARE_EQUAL(static_cast(til::color(0x563412)), realArgs.TabColor().Value()); + VERIFY_ARE_EQUAL(til::color(0x563412), til::color(realArgs.TabColor().Value())); } { KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -437,19 +429,104 @@ namespace TerminalAppLocalTests const auto bindings0Json = VerifyParseSucceeded(bindings0String); - auto appKeyBindings = winrt::make_self(); - VERIFY_IS_NOT_NULL(appKeyBindings); - VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); - appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, keymap->_keyShortcuts.size()); { KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_IS_FALSE(realArgs.SingleLine()); } } + + void KeyBindingsTests::TestScrollArgs() + { + const std::string bindings0String{ R"([ + { "keys": ["up"], "command": "scrollUp" }, + { "keys": ["down"], "command": "scrollDown" }, + { "keys": ["ctrl+up"], "command": { "action": "scrollUp" } }, + { "keys": ["ctrl+down"], "command": { "action": "scrollDown" } }, + { "keys": ["ctrl+shift+up"], "command": { "action": "scrollUp", "rowsToScroll": 10 } }, + { "keys": ["ctrl+shift+down"], "command": { "action": "scrollDown", "rowsToScroll": 10 } } + ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + + auto keymap = winrt::make_self(); + VERIFY_IS_NOT_NULL(keymap); + VERIFY_ARE_EQUAL(0u, keymap->_keyShortcuts.size()); + keymap->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(6u, keymap->_keyShortcuts.size()); + + { + KeyChord kc{ false, false, false, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ false, false, false, static_cast(VK_DOWN) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, false, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, false, static_cast(VK_DOWN) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NULL(realArgs.RowsToScroll()); + } + { + KeyChord kc{ true, false, true, static_cast(VK_UP) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); + VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); + } + { + KeyChord kc{ true, false, true, static_cast(VK_DOWN) }; + auto actionAndArgs = ::TestUtils::GetActionAndArgs(*keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.RowsToScroll()); + VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value()); + } + { + const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "scrollDown", "rowsToScroll": -1 } }])" }; + const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString); + auto invalidKeyMap = winrt::make_self(); + VERIFY_IS_NOT_NULL(invalidKeyMap); + VERIFY_ARE_EQUAL(0u, invalidKeyMap->_keyShortcuts.size()); + VERIFY_THROWS(invalidKeyMap->LayerJson(bindingsInvalidJson);, std::exception); + } + } } diff --git a/src/cascadia/TerminalApp/TerminalApp.def b/src/cascadia/LocalTests_SettingsModel/LocalTests_SettingsModel.def similarity index 100% rename from src/cascadia/TerminalApp/TerminalApp.def rename to src/cascadia/LocalTests_SettingsModel/LocalTests_SettingsModel.def diff --git a/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp new file mode 100644 index 00000000000..d157ad9cd8b --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "JsonTestClass.h" + +using namespace Microsoft::Console; +using namespace winrt::Microsoft::Terminal::Settings::Model; +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 ProfileTests : 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(ProfileTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(CanLayerProfile); + TEST_METHOD(LayerProfileProperties); + TEST_METHOD(LayerProfileIcon); + TEST_METHOD(LayerProfilesOnArray); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void ProfileTests::CanLayerProfile() + { + const std::string profile0String{ R"({ + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile1String{ R"({ + "name" : "profile1", + "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile2String{ R"({ + "name" : "profile2", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name" : "profile3" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + + const auto profile0 = implementation::Profile::FromJson(profile0Json); + + VERIFY_IS_FALSE(profile0->ShouldBeLayered(profile1Json)); + VERIFY_IS_TRUE(profile0->ShouldBeLayered(profile2Json)); + VERIFY_IS_FALSE(profile0->ShouldBeLayered(profile3Json)); + + const auto profile1 = implementation::Profile::FromJson(profile1Json); + + VERIFY_IS_FALSE(profile1->ShouldBeLayered(profile0Json)); + // A profile _can_ be layered with itself, though what's the point? + VERIFY_IS_TRUE(profile1->ShouldBeLayered(profile1Json)); + VERIFY_IS_FALSE(profile1->ShouldBeLayered(profile2Json)); + VERIFY_IS_FALSE(profile1->ShouldBeLayered(profile3Json)); + + const auto profile3 = implementation::Profile::FromJson(profile3Json); + + VERIFY_IS_FALSE(profile3->ShouldBeLayered(profile0Json)); + // A profile _can_ be layered with itself, though what's the point? + VERIFY_IS_FALSE(profile3->ShouldBeLayered(profile1Json)); + VERIFY_IS_FALSE(profile3->ShouldBeLayered(profile2Json)); + VERIFY_IS_TRUE(profile3->ShouldBeLayered(profile3Json)); + } + + void ProfileTests::LayerProfileProperties() + { + const std::string profile0String{ R"({ + "name": "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#000000", + "background": "#010101", + "selectionBackground": "#010101" + })" }; + const std::string profile1String{ R"({ + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#020202", + "startingDirectory": "C:/" + })" }; + const std::string profile2String{ R"({ + "name": "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#030303", + "selectionBackground": "#020202" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + + auto profile0 = implementation::Profile::FromJson(profile0Json); + VERIFY_IS_NOT_NULL(profile0->Foreground()); + VERIFY_ARE_EQUAL(til::color(0, 0, 0), til::color{ profile0->Foreground().Value() }); + + VERIFY_IS_NOT_NULL(profile0->Background()); + VERIFY_ARE_EQUAL(til::color(1, 1, 1), til::color{ profile0->Background().Value() }); + + VERIFY_IS_NOT_NULL(profile0->SelectionBackground()); + VERIFY_ARE_EQUAL(til::color(1, 1, 1), til::color{ profile0->SelectionBackground().Value() }); + + VERIFY_ARE_EQUAL(L"profile0", profile0->Name()); + + VERIFY_IS_TRUE(profile0->StartingDirectory().empty()); + + Log::Comment(NoThrowString().Format( + L"Layering profile1 on top of profile0")); + auto profile1{ profile0->CreateChild() }; + profile1->LayerJson(profile1Json); + + VERIFY_IS_NOT_NULL(profile1->Foreground()); + VERIFY_ARE_EQUAL(til::color(2, 2, 2), til::color{ profile1->Foreground().Value() }); + + VERIFY_IS_NOT_NULL(profile1->Background()); + VERIFY_ARE_EQUAL(til::color(1, 1, 1), til::color{ profile1->Background().Value() }); + + VERIFY_IS_NOT_NULL(profile1->Background()); + VERIFY_ARE_EQUAL(til::color(1, 1, 1), til::color{ profile1->Background().Value() }); + + VERIFY_ARE_EQUAL(L"profile1", profile1->Name()); + + VERIFY_IS_FALSE(profile1->StartingDirectory().empty()); + VERIFY_ARE_EQUAL(L"C:/", profile1->StartingDirectory()); + + Log::Comment(NoThrowString().Format( + L"Layering profile2 on top of (profile0+profile1)")); + auto profile2{ profile1->CreateChild() }; + profile2->LayerJson(profile2Json); + + VERIFY_IS_NOT_NULL(profile2->Foreground()); + VERIFY_ARE_EQUAL(til::color(3, 3, 3), til::color{ profile2->Foreground().Value() }); + + VERIFY_IS_NOT_NULL(profile2->Background()); + VERIFY_ARE_EQUAL(til::color(1, 1, 1), til::color{ profile2->Background().Value() }); + + VERIFY_IS_NOT_NULL(profile2->SelectionBackground()); + VERIFY_ARE_EQUAL(til::color(2, 2, 2), til::color{ profile2->SelectionBackground().Value() }); + + VERIFY_ARE_EQUAL(L"profile2", profile2->Name()); + + VERIFY_IS_FALSE(profile2->StartingDirectory().empty()); + VERIFY_ARE_EQUAL(L"C:/", profile2->StartingDirectory()); + } + + void ProfileTests::LayerProfileIcon() + { + const std::string profile0String{ R"({ + "name": "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": "not-null.png" + })" }; + const std::string profile1String{ R"({ + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": null + })" }; + const std::string profile2String{ R"({ + "name": "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name": "profile3", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": "another-real.png" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + + auto profile0 = implementation::Profile::FromJson(profile0Json); + VERIFY_IS_FALSE(profile0->Icon().empty()); + VERIFY_ARE_EQUAL(L"not-null.png", profile0->Icon()); + + Log::Comment(NoThrowString().Format( + L"Verify that layering an object the key set to null will clear the key")); + profile0->LayerJson(profile1Json); + VERIFY_IS_TRUE(profile0->Icon().empty()); + + profile0->LayerJson(profile2Json); + VERIFY_IS_TRUE(profile0->Icon().empty()); + + profile0->LayerJson(profile3Json); + VERIFY_IS_FALSE(profile0->Icon().empty()); + VERIFY_ARE_EQUAL(L"another-real.png", profile0->Icon()); + + Log::Comment(NoThrowString().Format( + L"Verify that layering an object _without_ the key will not clear the key")); + profile0->LayerJson(profile2Json); + VERIFY_IS_FALSE(profile0->Icon().empty()); + VERIFY_ARE_EQUAL(L"another-real.png", profile0->Icon()); + + auto profile1 = implementation::Profile::FromJson(profile1Json); + VERIFY_IS_TRUE(profile1->Icon().empty()); + profile1->LayerJson(profile3Json); + VERIFY_IS_FALSE(profile1->Icon().empty()); + VERIFY_ARE_EQUAL(L"another-real.png", profile1->Icon()); + } + + void ProfileTests::LayerProfilesOnArray() + { + const std::string profile0String{ R"({ + "name" : "profile0", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile1String{ R"({ + "name" : "profile1", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile2String{ R"({ + "name" : "profile2", + "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name" : "profile3", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile4String{ R"({ + "name" : "profile4", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + const auto profile4Json = VerifyParseSucceeded(profile4String); + + auto settings = winrt::make_self(); + + VERIFY_ARE_EQUAL(0u, settings->_allProfiles.Size()); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile4Json)); + + settings->_LayerOrCreateProfile(profile0Json); + VERIFY_ARE_EQUAL(1u, settings->_allProfiles.Size()); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile4Json)); + + settings->_LayerOrCreateProfile(profile1Json); + VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile4Json)); + + settings->_LayerOrCreateProfile(profile2Json); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(0).Name()); + + settings->_LayerOrCreateProfile(profile3Json); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile3", settings->_allProfiles.GetAt(0).Name()); + + settings->_LayerOrCreateProfile(profile4Json); + VERIFY_ARE_EQUAL(3u, settings->_allProfiles.Size()); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings->_FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile4", settings->_allProfiles.GetAt(0).Name()); + } + +} diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp new file mode 100644 index 00000000000..60491de9cbf --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "JsonTestClass.h" +#include "TestUtils.h" +#include +#include "../ut_app/TestDynamicProfileGenerator.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; + +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 SerializationTests : 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(SerializationTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(GlobalSettings); + TEST_METHOD(Profile); + TEST_METHOD(ColorScheme); + TEST_METHOD(CascadiaSettings); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + InitializeJsonWriter(); + return true; + } + + private: + // Method Description: + // - deserializes and reserializes a json string representing a settings object model of type T + // - verifies that the generated json string matches the provided one + // Template Types: + // - : The type of Settings Model object to generate (must be impl type) + // Arguments: + // - jsonString - JSON string we're performing the test on + // Return Value: + // - the JsonObject representing this instance + template + void RoundtripTest(const std::string& jsonString) + { + const auto json{ VerifyParseSucceeded(jsonString) }; + const auto settings{ T::FromJson(json) }; + const auto result{ settings->ToJson() }; + + // Compare toString(json) instead of jsonString here. + // The toString writes the json out alphabetically. + // This trick allows jsonString to _not_ have to be + // written alphabetically. + VERIFY_ARE_EQUAL(toString(json), toString(result)); + } + }; + + void SerializationTests::GlobalSettings() + { + const std::string globalsString{ R"( + { + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + + "initialRows": 30, + "initialCols": 120, + "initialPosition": ",", + "launchMode": "default", + "alwaysOnTop": false, + + "copyOnSelect": false, + "copyFormatting": "all", + "wordDelimiters": " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502", + + "alwaysShowTabs": true, + "showTabsInTitlebar": true, + "showTerminalTitleInTitlebar": true, + "tabWidthMode": "equal", + "tabSwitcherMode": "mru", + + "startOnUserLogin": false, + "theme": "system", + "snapToGridOnResize": true, + "disableAnimations": false, + + "confirmCloseAllTabs": true, + "largePasteWarning": true, + "multiLinePasteWarning": true, + + "experimental.input.forceVT": false, + "experimental.rendering.forceFullRepaint": false, + "experimental.rendering.software": false + })" }; + + const std::string smallGlobalsString{ R"( + { + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}" + })" }; + + RoundtripTest(globalsString); + RoundtripTest(smallGlobalsString); + } + + void SerializationTests::Profile() + { + const std::string profileString{ R"( + { + "name": "Windows PowerShell", + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + + "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "startingDirectory": "%USERPROFILE%", + + "icon": "ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png", + "hidden": false, + + "tabTitle": "Cool Tab", + "suppressApplicationTitle": false, + + "fontFace": "Cascadia Mono", + "fontSize": 12, + "fontWeight": "normal", + "padding": "8, 8, 8, 8", + "antialiasingMode": "grayscale", + + "cursorShape": "bar", + "cursorColor": "#CCBBAA", + "cursorHeight": 10, + + "altGrAliasing": true, + + "colorScheme": "Campbell", + "tabColor": "#0C0C0C", + "foreground": "#AABBCC", + "background": "#BBCCAA", + "selectionBackground": "#CCAABB", + + "useAcrylic": false, + "acrylicOpacity": 0.5, + + "backgroundImage": "made_you_look.jpeg", + "backgroundImageStretchMode": "uniformToFill", + "backgroundImageAlignment": "center", + "backgroundImageOpacity": 1.0, + + "scrollbarState": "visible", + "snapOnInput": true, + "historySize": 9001, + + "closeOnExit": "graceful", + "experimental.retroTerminalEffect": false + })" }; + + const std::string smallProfileString{ R"( + { + "name": "Custom Profile" + })" }; + + // Setting "tabColor" to null tests two things: + // - null should count as an explicit user-set value, not falling back to the parent's value + // - null should be acceptable even though we're working with colors + const std::string weirdProfileString{ R"( + { + "name": "Weird Profile", + "tabColor": null, + "foreground": null, + "source": "local" + })" }; + + RoundtripTest(profileString); + RoundtripTest(smallProfileString); + RoundtripTest(weirdProfileString); + } + + void SerializationTests::ColorScheme() + { + const std::string schemeString{ R"({ + "name": "Campbell", + + "cursorColor": "#FFFFFF", + "selectionBackground": "#131313", + + "background": "#0C0C0C", + "foreground": "#F2F2F2", + + "black": "#0C0C0C", + "blue": "#0037DA", + "cyan": "#3A96DD", + "green": "#13A10E", + "purple": "#881798", + "red": "#C50F1F", + "white": "#CCCCCC", + "yellow": "#C19C00", + "brightBlack": "#767676", + "brightBlue": "#3B78FF", + "brightCyan": "#61D6D6", + "brightGreen": "#16C60C", + "brightPurple": "#B4009E", + "brightRed": "#E74856", + "brightWhite": "#F2F2F2", + "brightYellow": "#F9F1A5" + })" }; + + RoundtripTest(schemeString); + } + + void SerializationTests::CascadiaSettings() + { + const std::string settingsString{ R"({ + "$schema": "https://aka.ms/terminal-profiles-schema", + "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + + "profiles": { + "defaults": { + "fontFace": "Zamora Code" + }, + "list": [ + { + "fontFace": "Cascadia Code", + "guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "name": "HowettShell" + }, + { + "hidden": true, + "name": "BhojwaniShell" + }, + { + "antialiasingMode": "aliased", + "name": "NiksaShell" + } + ] + }, + "schemes": [ + { + "name": "Cinnamon Roll", + + "cursorColor": "#FFFFFD", + "selectionBackground": "#FFFFFF", + + "background": "#3C0315", + "foreground": "#FFFFFD", + + "black": "#282A2E", + "blue": "#0170C5", + "cyan": "#3F8D83", + "green": "#76AB23", + "purple": "#7D498F", + "red": "#BD0940", + "white": "#FFFFFD", + "yellow": "#E0DE48", + "brightBlack": "#676E7A", + "brightBlue": "#5C98C5", + "brightCyan": "#8ABEB7", + "brightGreen": "#B5D680", + "brightPurple": "#AC79BB", + "brightRed": "#BD6D85", + "brightWhite": "#FFFFFD", + "brightYellow": "#FFFD76" + } + ], + "actions": [ + {"command": { "action": "renameTab","input": "Liang Tab" },"keys": "ctrl+t" } + ], + "keybindings": [ + { "command": { "action": "sendInput","input": "VT Griese Mode" },"keys": "ctrl+k" } + ] + })" }; + + auto settings{ winrt::make_self(false) }; + settings->_ParseJsonString(settingsString, false); + settings->_ApplyDefaultsFromUserSettings(); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + const auto result{ settings->ToJson() }; + VERIFY_ARE_EQUAL(toString(settings->_userSettings), toString(result)); + } +} diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj new file mode 100644 index 00000000000..547ea04dadb --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -0,0 +1,100 @@ + + + + + + + {CA5CAD1A-9B68-456A-B13E-C8218070DC42} + Win32Proj + SettingsModelLocalTests + LocalTests_SettingsModel + SettingsModel.LocalTests + DynamicLibrary + 10.0.18362.0 + 10.0.18362.0 + true + + + + + + + + + + + + + + + + + + + + + Create + + + + NotUsing + + + + + + + + + + + + + + + + + + + + ..;$(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 + + + 4702;%(DisableSpecificWarnings) + + + onecoreuap.lib;%(AdditionalDependencies) + + + + + true + true + + + + + + + + + x86 + $(Platform) + <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.2.5.0-prerelease.200609001\runtimes\win10-$(Native-Platform)\native\" + + + + + + diff --git a/src/cascadia/LocalTests_TerminalApp/TestUtils.h b/src/cascadia/LocalTests_SettingsModel/TestUtils.h similarity index 61% rename from src/cascadia/LocalTests_TerminalApp/TestUtils.h rename to src/cascadia/LocalTests_SettingsModel/TestUtils.h index ea1df508e1f..e211d50c43c 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestUtils.h +++ b/src/cascadia/LocalTests_SettingsModel/TestUtils.h @@ -12,42 +12,38 @@ Author(s): Mike Griese (migrie) December-2019 --*/ -class TerminalAppLocalTests::TestUtils +class TestUtils { public: // Function Description: // - This is a helper to retrieve the ActionAndArgs from the keybindings // for a given chord. // Arguments: - // - bindings: The AppKeyBindings to lookup the ActionAndArgs from. + // - keymap: The AppKeyBindings to lookup the ActionAndArgs from. // - kc: The key chord to look up the bound ActionAndArgs for. // Return Value: // - The ActionAndArgs bound to the given key, or nullptr if nothing is bound to it. - static const winrt::TerminalApp::ActionAndArgs GetActionAndArgs(const winrt::TerminalApp::implementation::AppKeyBindings& bindings, - const winrt::Microsoft::Terminal::Settings::KeyChord& kc) + static const winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs GetActionAndArgs(const winrt::Microsoft::Terminal::Settings::Model::KeyMapping& keymap, + const winrt::Microsoft::Terminal::TerminalControl::KeyChord& kc) { std::wstring buffer{ L"" }; - if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Ctrl)) + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::TerminalControl::KeyModifiers::Ctrl)) { buffer += L"Ctrl+"; } - if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Shift)) + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::TerminalControl::KeyModifiers::Shift)) { buffer += L"Shift+"; } - if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Alt)) + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::TerminalControl::KeyModifiers::Alt)) { buffer += L"Alt+"; } buffer += static_cast(MapVirtualKeyW(kc.Vkey(), MAPVK_VK_TO_CHAR)); WEX::Logging::Log::Comment(WEX::Common::NoThrowString().Format(L"Looking for key:%s", buffer.c_str())); - const auto keyIter = bindings._keyShortcuts.find(kc); - VERIFY_IS_TRUE(keyIter != bindings._keyShortcuts.end(), L"Expected to find an action bound to the given KeyChord"); - if (keyIter != bindings._keyShortcuts.end()) - { - return keyIter->second; - } - return nullptr; + const auto action = keymap.TryLookup(kc); + VERIFY_IS_NOT_NULL(action, L"Expected to find an action bound to the given KeyChord"); + return action; }; }; diff --git a/src/cascadia/TerminalApp/lib/pch.cpp b/src/cascadia/LocalTests_SettingsModel/pch.cpp similarity index 100% rename from src/cascadia/TerminalApp/lib/pch.cpp rename to src/cascadia/LocalTests_SettingsModel/pch.cpp diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h new file mode 100644 index 00000000000..9a06e029cc6 --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/pch.h @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). + +Author(s): +- Carlos Zamora (cazamor) April 2019 +--*/ + +#pragma once + +// 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" +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include +#include +#include + +#include +#include +#include "consoletaeftemplates.hpp" + +#include +#include "winrt/Windows.UI.Xaml.Markup.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#include "til.h" + +// Common includes for most tests: +#include "../../inc/argb.h" +#include "../../inc/conattrs.hpp" +#include "../../types/inc/utils.hpp" +#include "../../inc/DefaultSettings.h" diff --git a/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp b/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp deleted file mode 100644 index 55c2026bada..00000000000 --- a/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" - -#include "../TerminalApp/ColorScheme.h" -#include "../TerminalApp/CascadiaSettings.h" -#include "JsonTestClass.h" - -using namespace Microsoft::Console; -using namespace TerminalApp; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using namespace WEX::Common; - -namespace TerminalAppLocalTests -{ - // 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 ColorSchemeTests : 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(ColorSchemeTests) - TEST_CLASS_PROPERTY(L"RunAs", L"UAP") - TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") - END_TEST_CLASS() - - TEST_METHOD(CanLayerColorScheme); - TEST_METHOD(LayerColorSchemeProperties); - TEST_METHOD(LayerColorSchemesOnArray); - - TEST_CLASS_SETUP(ClassSetup) - { - InitializeJsonReader(); - return true; - } - }; - - void ColorSchemeTests::CanLayerColorScheme() - { - const std::string scheme0String{ R"({ - "name": "scheme0", - "foreground": "#000000", - "background": "#010101" - })" }; - const std::string scheme1String{ R"({ - "name": "scheme1", - "foreground": "#020202", - "background": "#030303" - })" }; - const std::string scheme2String{ R"({ - "name": "scheme0", - "foreground": "#040404", - "background": "#050505" - })" }; - const std::string scheme3String{ R"({ - // "name": "scheme3", - "foreground": "#060606", - "background": "#070707" - })" }; - - const auto scheme0Json = VerifyParseSucceeded(scheme0String); - const auto scheme1Json = VerifyParseSucceeded(scheme1String); - const auto scheme2Json = VerifyParseSucceeded(scheme2String); - const auto scheme3Json = VerifyParseSucceeded(scheme3String); - - const auto scheme0 = ColorScheme::FromJson(scheme0Json); - - VERIFY_IS_TRUE(scheme0.ShouldBeLayered(scheme0Json)); - VERIFY_IS_FALSE(scheme0.ShouldBeLayered(scheme1Json)); - VERIFY_IS_TRUE(scheme0.ShouldBeLayered(scheme2Json)); - VERIFY_IS_FALSE(scheme0.ShouldBeLayered(scheme3Json)); - - const auto scheme1 = ColorScheme::FromJson(scheme1Json); - - VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme0Json)); - VERIFY_IS_TRUE(scheme1.ShouldBeLayered(scheme1Json)); - VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme2Json)); - VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme3Json)); - - const auto scheme3 = ColorScheme::FromJson(scheme3Json); - - VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme0Json)); - VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme1Json)); - VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme2Json)); - VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme3Json)); - } - - void ColorSchemeTests::LayerColorSchemeProperties() - { - const std::string scheme0String{ R"({ - "name": "scheme0", - "foreground": "#000000", - "background": "#010101", - "selectionBackground": "#010100", - "cursorColor": "#010001", - "red": "#010000", - "green": "#000100", - "blue": "#000001" - })" }; - const std::string scheme1String{ R"({ - "name": "scheme1", - "foreground": "#020202", - "background": "#030303", - "selectionBackground": "#020200", - "cursorColor": "#040004", - "red": "#020000", - - "blue": "#000002" - })" }; - const std::string scheme2String{ R"({ - "name": "scheme0", - "foreground": "#040404", - "background": "#050505", - "selectionBackground": "#030300", - "cursorColor": "#060006", - "red": "#030000", - "green": "#000300" - })" }; - - const auto scheme0Json = VerifyParseSucceeded(scheme0String); - const auto scheme1Json = VerifyParseSucceeded(scheme1String); - const auto scheme2Json = VerifyParseSucceeded(scheme2String); - - auto scheme0 = ColorScheme::FromJson(scheme0Json); - VERIFY_ARE_EQUAL(L"scheme0", scheme0._schemeName); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 0), scheme0._selectionBackground); - VERIFY_ARE_EQUAL(ARGB(0, 1, 0, 1), scheme0._cursorColor); - VERIFY_ARE_EQUAL(ARGB(0, 1, 0, 0), scheme0._table[XTERM_RED_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0._table[XTERM_GREEN_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 1), scheme0._table[XTERM_BLUE_ATTR]); - - Log::Comment(NoThrowString().Format( - L"Layering scheme1 on top of scheme0")); - scheme0.LayerJson(scheme1Json); - - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 0), scheme0._selectionBackground); - VERIFY_ARE_EQUAL(ARGB(0, 4, 0, 4), scheme0._cursorColor); - VERIFY_ARE_EQUAL(ARGB(0, 2, 0, 0), scheme0._table[XTERM_RED_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0._table[XTERM_GREEN_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0._table[XTERM_BLUE_ATTR]); - - Log::Comment(NoThrowString().Format( - L"Layering scheme2Json on top of (scheme0+scheme1)")); - scheme0.LayerJson(scheme2Json); - - VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 0), scheme0._selectionBackground); - VERIFY_ARE_EQUAL(ARGB(0, 6, 0, 6), scheme0._cursorColor); - VERIFY_ARE_EQUAL(ARGB(0, 3, 0, 0), scheme0._table[XTERM_RED_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 3, 0), scheme0._table[XTERM_GREEN_ATTR]); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0._table[XTERM_BLUE_ATTR]); - } - - void ColorSchemeTests::LayerColorSchemesOnArray() - { - const std::string scheme0String{ R"({ - "name": "scheme0", - "foreground": "#000000", - "background": "#010101" - })" }; - const std::string scheme1String{ R"({ - "name": "scheme1", - "foreground": "#020202", - "background": "#030303" - })" }; - const std::string scheme2String{ R"({ - "name": "scheme0", - "foreground": "#040404", - "background": "#050505" - })" }; - const std::string scheme3String{ R"({ - // by not providing a name, the scheme will have the name "" - "foreground": "#060606", - "background": "#070707" - })" }; - - const auto scheme0Json = VerifyParseSucceeded(scheme0String); - const auto scheme1Json = VerifyParseSucceeded(scheme1String); - const auto scheme2Json = VerifyParseSucceeded(scheme2String); - const auto scheme3Json = VerifyParseSucceeded(scheme3String); - - CascadiaSettings settings; - - VERIFY_ARE_EQUAL(0u, settings._globals.GetColorSchemes().size()); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme0Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme1Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme2Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); - - settings._LayerOrCreateColorScheme(scheme0Json); - { - for (auto& kv : settings._globals._colorSchemes) - { - Log::Comment(NoThrowString().Format( - L"kv:%s->%s", kv.first.data(), kv.second.GetName().data())); - } - VERIFY_ARE_EQUAL(1u, settings._globals.GetColorSchemes().size()); - - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme0") != settings._globals._colorSchemes.end()); - auto scheme0 = settings._globals._colorSchemes.find(L"scheme0")->second; - - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0._defaultBackground); - } - - settings._LayerOrCreateColorScheme(scheme1Json); - - { - VERIFY_ARE_EQUAL(2u, settings._globals.GetColorSchemes().size()); - - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme0") != settings._globals._colorSchemes.end()); - auto scheme0 = settings._globals._colorSchemes.find(L"scheme0")->second; - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme1") != settings._globals._colorSchemes.end()); - auto scheme1 = settings._globals._colorSchemes.find(L"scheme1")->second; - - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1._defaultBackground); - } - settings._LayerOrCreateColorScheme(scheme2Json); - - { - VERIFY_ARE_EQUAL(2u, settings._globals.GetColorSchemes().size()); - - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme0") != settings._globals._colorSchemes.end()); - auto scheme0 = settings._globals._colorSchemes.find(L"scheme0")->second; - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme1") != settings._globals._colorSchemes.end()); - auto scheme1 = settings._globals._colorSchemes.find(L"scheme1")->second; - - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); - VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1._defaultBackground); - } - settings._LayerOrCreateColorScheme(scheme3Json); - - { - VERIFY_ARE_EQUAL(3u, settings._globals.GetColorSchemes().size()); - - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme0") != settings._globals._colorSchemes.end()); - auto scheme0 = settings._globals._colorSchemes.find(L"scheme0")->second; - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"scheme1") != settings._globals._colorSchemes.end()); - auto scheme1 = settings._globals._colorSchemes.find(L"scheme1")->second; - VERIFY_IS_TRUE(settings._globals._colorSchemes.find(L"") != settings._globals._colorSchemes.end()); - auto scheme2 = settings._globals._colorSchemes.find(L"")->second; - - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); - VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); - VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme1._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme1._defaultBackground); - VERIFY_ARE_EQUAL(ARGB(0, 6, 6, 6), scheme2._defaultForeground); - VERIFY_ARE_EQUAL(ARGB(0, 7, 7, 7), scheme2._defaultBackground); - } - } -} diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index d9735e2ccd4..b9261c1c683 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -4,14 +4,15 @@ #include "pch.h" #include +#include "../types/inc/utils.hpp" #include "../TerminalApp/TerminalPage.h" #include "../TerminalApp/AppCommandlineArgs.h" -#include "../TerminalApp/ActionArgs.h" using namespace WEX::Logging; using namespace WEX::Common; using namespace WEX::TestExecution; +using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::TerminalApp; using namespace ::TerminalApp; @@ -57,6 +58,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestSimpleExecuteCommandlineAction); TEST_METHOD(TestMultipleCommandExecuteCommandlineAction); TEST_METHOD(TestInvalidExecuteCommandlineAction); + TEST_METHOD(TestLaunchMode); + TEST_METHOD(TestLaunchModeWithNoCommand); private: void _buildCommandlinesHelper(AppCommandlineArgs& appArgs, @@ -333,7 +336,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); auto myCommand = myArgs.TerminalArgs().Commandline(); VERIFY_ARE_EQUAL(L"powershell.exe \"This is an arg \"", myCommand); @@ -348,7 +352,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); auto myCommand = myArgs.TerminalArgs().Commandline(); VERIFY_ARE_EQUAL(L"\" with spaces\"", myCommand); @@ -371,7 +376,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); auto myCommand = myArgs.TerminalArgs().Commandline(); VERIFY_ARE_EQUAL(L"powershell.exe \"This is an arg ; with spaces\"", myCommand); @@ -417,7 +423,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); } { @@ -436,7 +443,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"cmd", myArgs.TerminalArgs().Profile()); } @@ -456,7 +464,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_FALSE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"c:\\Foo", myArgs.TerminalArgs().StartingDirectory()); } @@ -476,7 +485,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"powershell.exe", myArgs.TerminalArgs().Commandline()); } @@ -496,7 +506,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); auto myCommand = myArgs.TerminalArgs().Commandline(); VERIFY_ARE_EQUAL(L"powershell.exe \"This is an arg with spaces\"", myCommand); @@ -517,7 +528,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); auto myCommand = myArgs.TerminalArgs().Commandline(); VERIFY_ARE_EQUAL(L"powershell.exe \"This is an arg with spaces\" another-arg \"more spaces in this one\"", myCommand); @@ -538,7 +550,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"Windows PowerShell", myArgs.TerminalArgs().Profile()); } @@ -558,7 +571,7 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"wsl -d Alpine", myArgs.TerminalArgs().Commandline()); } @@ -578,11 +591,35 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"wsl -d Alpine", myArgs.TerminalArgs().Commandline()); VERIFY_ARE_EQUAL(L"1", myArgs.TerminalArgs().Profile()); } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", subcommand, L"--tabColor", L"#009999" }; + const auto expectedColor = Microsoft::Console::Utils::ColorFromHexString("#009999"); + + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_ARE_EQUAL(1u, appArgs._startupActions.size()); + + auto actionAndArgs = appArgs._startupActions.at(0); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_IS_NOT_NULL(myArgs.TerminalArgs()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_NOT_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_ARE_EQUAL(til::color(myArgs.TerminalArgs().TabColor().Value()), expectedColor); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); + } } void CommandlineTest::ParseSplitPaneIntoArgs() @@ -675,7 +712,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"wsl -d Alpine", myArgs.TerminalArgs().Commandline()); VERIFY_ARE_EQUAL(L"1", myArgs.TerminalArgs().Profile()); @@ -702,7 +740,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"wsl -d Alpine", myArgs.TerminalArgs().Commandline()); VERIFY_ARE_EQUAL(L"1", myArgs.TerminalArgs().Profile()); @@ -729,7 +768,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"wsl -d Alpine -H", myArgs.TerminalArgs().Commandline()); VERIFY_ARE_EQUAL(L"1", myArgs.TerminalArgs().Profile()); @@ -777,7 +817,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); } { @@ -796,7 +837,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"cmd", myArgs.TerminalArgs().Profile()); } @@ -816,7 +858,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_FALSE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"c:\\Foo", myArgs.TerminalArgs().StartingDirectory()); } @@ -836,7 +879,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"powershell.exe", myArgs.TerminalArgs().Commandline()); } @@ -856,7 +900,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"powershell.exe \"This is an arg with spaces\"", myArgs.TerminalArgs().Commandline()); } @@ -986,7 +1031,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); actionAndArgs = appArgs._startupActions.at(1); @@ -998,7 +1044,8 @@ namespace TerminalAppLocalTests VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"slpit-pane", myArgs.TerminalArgs().Commandline()); } @@ -1076,9 +1123,8 @@ namespace TerminalAppLocalTests void CommandlineTest::TestSimpleExecuteCommandlineAction() { - auto args = winrt::make_self(); - args->Commandline(L"new-tab"); - auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(*args); + ExecuteCommandlineArgs args{ L"new-tab" }; + auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(args); VERIFY_ARE_EQUAL(1u, actions.size()); auto actionAndArgs = actions.at(0); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); @@ -1089,15 +1135,15 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); } void CommandlineTest::TestMultipleCommandExecuteCommandlineAction() { - auto args = winrt::make_self(); - args->Commandline(L"new-tab ; split-pane"); - auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(*args); + ExecuteCommandlineArgs args{ L"new-tab ; split-pane" }; + auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(args); VERIFY_ARE_EQUAL(2u, actions.size()); { auto actionAndArgs = actions.at(0); @@ -1109,7 +1155,8 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); } { @@ -1122,17 +1169,160 @@ namespace TerminalAppLocalTests VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_NULL(myArgs.TerminalArgs().TabColor()); + VERIFY_IS_NULL(myArgs.TerminalArgs().ProfileIndex()); VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); } } void CommandlineTest::TestInvalidExecuteCommandlineAction() { - auto args = winrt::make_self(); // -H and -V cannot be combined. - args->Commandline(L"split-pane -H -V"); - auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(*args); + ExecuteCommandlineArgs args{ L"split-pane -H -V" }; + auto actions = implementation::TerminalPage::ConvertExecuteCommandlineToActions(args); VERIFY_ARE_EQUAL(0u, actions.size()); } + + void CommandlineTest::TestLaunchMode() + { + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_FALSE(appArgs.GetLaunchMode().has_value()); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-F" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::FullscreenMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--fullscreen" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::FullscreenMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-M" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--maximized" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-f" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::FocusMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--focus" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::FocusMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-fM" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedFocusMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--maximized", L"--focus" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedFocusMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--maximized", L"--focus", L"--focus" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedFocusMode); + } + { + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"--maximized", L"--focus", L"--maximized" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedFocusMode); + } + } + + void CommandlineTest::TestLaunchModeWithNoCommand() + { + { + Log::Comment(NoThrowString().Format(L"Pass a launch mode and profile")); + + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-M", L"--profile", L"cmd" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedMode); + VERIFY_ARE_EQUAL(1u, appArgs._startupActions.size()); + + auto actionAndArgs = appArgs._startupActions.at(0); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_IS_NOT_NULL(myArgs.TerminalArgs()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_FALSE(myArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"cmd", myArgs.TerminalArgs().Profile()); + } + { + Log::Comment(NoThrowString().Format(L"Pass a launch mode and command line")); + + AppCommandlineArgs appArgs{}; + std::vector rawCommands{ L"wt.exe", L"-M", L"powershell.exe" }; + _buildCommandlinesHelper(appArgs, 1u, rawCommands); + + VERIFY_IS_TRUE(appArgs.GetLaunchMode().has_value()); + VERIFY_ARE_EQUAL(appArgs.GetLaunchMode().value(), LaunchMode::MaximizedMode); + VERIFY_ARE_EQUAL(1u, appArgs._startupActions.size()); + + auto actionAndArgs = appArgs._startupActions.at(0); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + VERIFY_IS_NOT_NULL(actionAndArgs.Args()); + auto myArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(myArgs); + VERIFY_IS_NOT_NULL(myArgs.TerminalArgs()); + VERIFY_IS_FALSE(myArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(myArgs.TerminalArgs().ProfileIndex() == nullptr); + VERIFY_IS_TRUE(myArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"powershell.exe", myArgs.TerminalArgs().Commandline()); + } + } } diff --git a/src/cascadia/LocalTests_TerminalApp/FilteredCommandTests.cpp b/src/cascadia/LocalTests_TerminalApp/FilteredCommandTests.cpp new file mode 100644 index 00000000000..f84fcd9c125 --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/FilteredCommandTests.cpp @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "../TerminalApp/TerminalSettings.h" +#include "../TerminalApp/CommandPalette.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; + +namespace TerminalAppLocalTests +{ + class FilteredCommandTests + { + BEGIN_TEST_CLASS(FilteredCommandTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(VerifyHighlighting); + TEST_METHOD(VerifyWeight); + TEST_METHOD(VerifyCompare); + }; + + void FilteredCommandTests::VerifyHighlighting() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" } + ] + })" }; + + CascadiaSettings settings{ til::u8u16(settingsJson) }; + const auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + const auto command = commands.Lookup(L"AAAAAABBBBBBCCC"); + VERIFY_IS_NOT_NULL(command); + VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC"); + { + Log::Comment(L"Testing command name segmentation with no filter"); + const auto filteredCommand = winrt::make_self(command); + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 1u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with empty filter"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L""; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 1u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with filter equals to the string"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"AAAAAABBBBBBCCC"; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 1u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC"); + VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with filter with first character matching"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"A"; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 2u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A"); + VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted()); + VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with filter with other case"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"a"; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 2u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A"); + VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted()); + VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAABBBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with filter matching several characters"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"ab"; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 4u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"A"); + VERIFY_IS_TRUE(segments.GetAt(0).IsHighlighted()); + VERIFY_ARE_EQUAL(segments.GetAt(1).TextSegment(), L"AAAAA"); + VERIFY_IS_FALSE(segments.GetAt(1).IsHighlighted()); + VERIFY_ARE_EQUAL(segments.GetAt(2).TextSegment(), L"B"); + VERIFY_IS_TRUE(segments.GetAt(2).IsHighlighted()); + VERIFY_ARE_EQUAL(segments.GetAt(3).TextSegment(), L"BBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(3).IsHighlighted()); + } + { + Log::Comment(L"Testing command name segmentation with non matching filter"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"abcd"; + auto segments = filteredCommand->_computeHighlightedName().Segments(); + VERIFY_ARE_EQUAL(segments.Size(), 1u); + VERIFY_ARE_EQUAL(segments.GetAt(0).TextSegment(), L"AAAAAABBBBBBCCC"); + VERIFY_IS_FALSE(segments.GetAt(0).IsHighlighted()); + } + } + + void FilteredCommandTests::VerifyWeight() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" } + ] + })" }; + + CascadiaSettings settings{ til::u8u16(settingsJson) }; + const auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + const auto command = commands.Lookup(L"AAAAAABBBBBBCCC"); + VERIFY_IS_NOT_NULL(command); + VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC"); + { + Log::Comment(L"Testing weight of command with no filter"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 0); + } + { + Log::Comment(L"Testing weight of command with empty filter"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L""; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 0); + } + { + Log::Comment(L"Testing weight of command with filter equals to the string"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"AAAAAABBBBBBCCC"; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 30); // 1 point for the first char and 2 points for the 14 consequent ones + 1 point for the beginning of the word + } + { + Log::Comment(L"Testing weight of command with filter with first character matching"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"A"; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word + } + { + Log::Comment(L"Testing weight of command with filter with other case"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"a"; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 2); // 1 point for the first char match + 1 point for the beginning of the word + } + { + Log::Comment(L"Testing weight of command with filter matching several characters"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"ab"; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + auto weight = filteredCommand->_computeWeight(); + VERIFY_ARE_EQUAL(weight, 3); // 1 point for the first char match + 1 point for the beginning of the word + 1 point for the match of "b" + } + } + + void FilteredCommandTests::VerifyCompare() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" }, "name": "AAAAAABBBBBBCCC" }, + { "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "horizontal" }, "name": "BBBBBCCC" } + ] + })" }; + + CascadiaSettings settings{ til::u8u16(settingsJson) }; + const auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(2u, commands.Size()); + + const auto command = commands.Lookup(L"AAAAAABBBBBBCCC"); + VERIFY_IS_NOT_NULL(command); + VERIFY_ARE_EQUAL(command.Name(), L"AAAAAABBBBBBCCC"); + + const auto command2 = commands.Lookup(L"BBBBBCCC"); + VERIFY_IS_NOT_NULL(command2); + VERIFY_ARE_EQUAL(command2.Name(), L"BBBBBCCC"); + { + Log::Comment(L"Testing comparison of commands with no filter"); + const auto filteredCommand = winrt::make_self(command); + const auto filteredCommand2 = winrt::make_self(command2); + + VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight()); + VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2)); + } + { + Log::Comment(L"Testing comparison of commands with empty filter"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L""; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + filteredCommand->_Weight = filteredCommand->_computeWeight(); + + const auto filteredCommand2 = winrt::make_self(command2); + filteredCommand2->_Filter = L""; + filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName(); + filteredCommand2->_Weight = filteredCommand2->_computeWeight(); + + VERIFY_ARE_EQUAL(filteredCommand->Weight(), filteredCommand2->Weight()); + VERIFY_IS_TRUE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2)); + } + { + Log::Comment(L"Testing comparison of commands with different weights"); + const auto filteredCommand = winrt::make_self(command); + filteredCommand->_Filter = L"B"; + filteredCommand->_HighlightedName = filteredCommand->_computeHighlightedName(); + filteredCommand->_Weight = filteredCommand->_computeWeight(); + + const auto filteredCommand2 = winrt::make_self(command2); + filteredCommand2->_Filter = L"B"; + filteredCommand2->_HighlightedName = filteredCommand2->_computeHighlightedName(); + filteredCommand2->_Weight = filteredCommand2->_computeWeight(); + + VERIFY_IS_TRUE(filteredCommand->Weight() < filteredCommand2->Weight()); // Second command gets more points due to the beginning of the word + VERIFY_IS_FALSE(winrt::TerminalApp::implementation::FilteredCommand::Compare(*filteredCommand, *filteredCommand2)); + } + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp b/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp deleted file mode 100644 index c0064c6a3af..00000000000 --- a/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" - -#include "../TerminalApp/ColorScheme.h" -#include "../TerminalApp/CascadiaSettings.h" -#include "JsonTestClass.h" - -using namespace Microsoft::Console; -using namespace TerminalApp; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using namespace WEX::Common; - -namespace TerminalAppLocalTests -{ - // 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 ProfileTests : 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(ProfileTests) - TEST_CLASS_PROPERTY(L"RunAs", L"UAP") - TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") - END_TEST_CLASS() - - TEST_METHOD(CanLayerProfile); - TEST_METHOD(LayerProfileProperties); - TEST_METHOD(LayerProfileIcon); - TEST_METHOD(LayerProfilesOnArray); - - TEST_CLASS_SETUP(ClassSetup) - { - InitializeJsonReader(); - return true; - } - }; - - void ProfileTests::CanLayerProfile() - { - const std::string profile0String{ R"({ - "name" : "profile0", - "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile1String{ R"({ - "name" : "profile1", - "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile2String{ R"({ - "name" : "profile2", - "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile3String{ R"({ - "name" : "profile3" - })" }; - - const auto profile0Json = VerifyParseSucceeded(profile0String); - const auto profile1Json = VerifyParseSucceeded(profile1String); - const auto profile2Json = VerifyParseSucceeded(profile2String); - const auto profile3Json = VerifyParseSucceeded(profile3String); - - const auto profile0 = Profile::FromJson(profile0Json); - - VERIFY_IS_FALSE(profile0.ShouldBeLayered(profile1Json)); - VERIFY_IS_TRUE(profile0.ShouldBeLayered(profile2Json)); - VERIFY_IS_FALSE(profile0.ShouldBeLayered(profile3Json)); - - const auto profile1 = Profile::FromJson(profile1Json); - - VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile0Json)); - // A profile _can_ be layered with itself, though what's the point? - VERIFY_IS_TRUE(profile1.ShouldBeLayered(profile1Json)); - VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile2Json)); - VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile3Json)); - - const auto profile3 = Profile::FromJson(profile3Json); - - VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile0Json)); - // A profile _can_ be layered with itself, though what's the point? - VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile1Json)); - VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile2Json)); - VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile3Json)); - } - - void ProfileTests::LayerProfileProperties() - { - const std::string profile0String{ R"({ - "name": "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "foreground": "#000000", - "background": "#010101", - "selectionBackground": "#010101" - })" }; - const std::string profile1String{ R"({ - "name": "profile1", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "foreground": "#020202", - "startingDirectory": "C:/" - })" }; - const std::string profile2String{ R"({ - "name": "profile2", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "foreground": "#030303", - "selectionBackground": "#020202" - })" }; - - const auto profile0Json = VerifyParseSucceeded(profile0String); - const auto profile1Json = VerifyParseSucceeded(profile1String); - const auto profile2Json = VerifyParseSucceeded(profile2String); - - auto profile0 = Profile::FromJson(profile0Json); - VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), profile0._defaultForeground.value()); - - VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); - - VERIFY_IS_TRUE(profile0._selectionBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._selectionBackground.value()); - - VERIFY_ARE_EQUAL(L"profile0", profile0._name); - - VERIFY_IS_FALSE(profile0._startingDirectory.has_value()); - - Log::Comment(NoThrowString().Format( - L"Layering profile1 on top of profile0")); - profile0.LayerJson(profile1Json); - - VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), profile0._defaultForeground.value()); - - VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); - - VERIFY_IS_TRUE(profile0._selectionBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._selectionBackground.value()); - - VERIFY_ARE_EQUAL(L"profile1", profile0._name); - - VERIFY_IS_TRUE(profile0._startingDirectory.has_value()); - VERIFY_ARE_EQUAL(L"C:/", profile0._startingDirectory.value()); - - Log::Comment(NoThrowString().Format( - L"Layering profile2 on top of (profile0+profile1)")); - profile0.LayerJson(profile2Json); - - VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), profile0._defaultForeground.value()); - - VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); - - VERIFY_IS_TRUE(profile0._selectionBackground.has_value()); - VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), profile0._selectionBackground.value()); - - VERIFY_ARE_EQUAL(L"profile2", profile0._name); - - VERIFY_IS_TRUE(profile0._startingDirectory.has_value()); - VERIFY_ARE_EQUAL(L"C:/", profile0._startingDirectory.value()); - } - - void ProfileTests::LayerProfileIcon() - { - const std::string profile0String{ R"({ - "name": "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "icon": "not-null.png" - })" }; - const std::string profile1String{ R"({ - "name": "profile1", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "icon": null - })" }; - const std::string profile2String{ R"({ - "name": "profile2", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile3String{ R"({ - "name": "profile3", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "icon": "another-real.png" - })" }; - - const auto profile0Json = VerifyParseSucceeded(profile0String); - const auto profile1Json = VerifyParseSucceeded(profile1String); - const auto profile2Json = VerifyParseSucceeded(profile2String); - const auto profile3Json = VerifyParseSucceeded(profile3String); - - auto profile0 = Profile::FromJson(profile0Json); - VERIFY_IS_TRUE(profile0._icon.has_value()); - VERIFY_ARE_EQUAL(L"not-null.png", profile0._icon.value()); - - Log::Comment(NoThrowString().Format( - L"Verify that layering an object the key set to null will clear the key")); - profile0.LayerJson(profile1Json); - VERIFY_IS_FALSE(profile0._icon.has_value()); - - profile0.LayerJson(profile2Json); - VERIFY_IS_FALSE(profile0._icon.has_value()); - - profile0.LayerJson(profile3Json); - VERIFY_IS_TRUE(profile0._icon.has_value()); - VERIFY_ARE_EQUAL(L"another-real.png", profile0._icon.value()); - - Log::Comment(NoThrowString().Format( - L"Verify that layering an object _without_ the key will not clear the key")); - profile0.LayerJson(profile2Json); - VERIFY_IS_TRUE(profile0._icon.has_value()); - VERIFY_ARE_EQUAL(L"another-real.png", profile0._icon.value()); - - auto profile1 = Profile::FromJson(profile1Json); - VERIFY_IS_FALSE(profile1._icon.has_value()); - profile1.LayerJson(profile3Json); - VERIFY_IS_TRUE(profile1._icon.has_value()); - VERIFY_ARE_EQUAL(L"another-real.png", profile1._icon.value()); - } - - void ProfileTests::LayerProfilesOnArray() - { - const std::string profile0String{ R"({ - "name" : "profile0", - "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile1String{ R"({ - "name" : "profile1", - "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile2String{ R"({ - "name" : "profile2", - "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile3String{ R"({ - "name" : "profile3", - "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile4String{ R"({ - "name" : "profile4", - "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - })" }; - - const auto profile0Json = VerifyParseSucceeded(profile0String); - const auto profile1Json = VerifyParseSucceeded(profile1String); - const auto profile2Json = VerifyParseSucceeded(profile2String); - const auto profile3Json = VerifyParseSucceeded(profile3String); - const auto profile4Json = VerifyParseSucceeded(profile4String); - - CascadiaSettings settings; - - VERIFY_ARE_EQUAL(0u, settings._profiles.size()); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile4Json)); - - settings._LayerOrCreateProfile(profile0Json); - VERIFY_ARE_EQUAL(1u, settings._profiles.size()); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); - - settings._LayerOrCreateProfile(profile1Json); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); - - settings._LayerOrCreateProfile(profile2Json); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); - - settings._LayerOrCreateProfile(profile3Json); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); - VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(0)._name); - - settings._LayerOrCreateProfile(profile4Json); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); - VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); - VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(0)._name); - } - -} diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index cdfb08415f5..abd1c219ef8 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -3,20 +3,17 @@ #include "pch.h" -#include "../TerminalApp/ColorScheme.h" -#include "../TerminalApp/CascadiaSettings.h" -#include "JsonTestClass.h" -#include "TestUtils.h" -#include -#include "../ut_app/TestDynamicProfileGenerator.h" +#include "../TerminalApp/TerminalPage.h" +#include "../TerminalApp/TerminalSettings.h" +#include "../LocalTests_SettingsModel/TestUtils.h" using namespace Microsoft::Console; -using namespace TerminalApp; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; using namespace winrt::TerminalApp; -using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; namespace TerminalAppLocalTests { @@ -25,7 +22,7 @@ namespace TerminalAppLocalTests // 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 SettingsTests : public JsonTestClass + class SettingsTests { // 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 @@ -38,62 +35,57 @@ namespace TerminalAppLocalTests END_TEST_CLASS() TEST_METHOD(TryCreateWinRTType); - TEST_METHOD(ValidateProfilesExist); - TEST_METHOD(ValidateDefaultProfileExists); - TEST_METHOD(ValidateDuplicateProfiles); - TEST_METHOD(ValidateManyWarnings); - TEST_METHOD(LayerGlobalProperties); - TEST_METHOD(ValidateProfileOrdering); - TEST_METHOD(ValidateHideProfiles); - TEST_METHOD(ValidateProfilesGenerateGuids); - TEST_METHOD(GeneratedGuidRoundtrips); - TEST_METHOD(TestAllValidationsWithNullGuids); - TEST_METHOD(TestReorderWithNullGuids); - TEST_METHOD(TestReorderingWithoutGuid); - TEST_METHOD(TestLayeringNameOnlyProfiles); - TEST_METHOD(TestExplodingNameOnlyProfiles); - TEST_METHOD(TestHideAllProfiles); - TEST_METHOD(TestInvalidColorSchemeName); - TEST_METHOD(TestHelperFunctions); - - TEST_METHOD(TestProfileIconWithEnvVar); - TEST_METHOD(TestProfileBackgroundImageWithEnvVar); - - TEST_METHOD(TestCloseOnExitParsing); - TEST_METHOD(TestCloseOnExitCompatibilityShim); - - TEST_METHOD(TestLayerUserDefaultsBeforeProfiles); - TEST_METHOD(TestDontLayerGuidFromUserDefaults); - TEST_METHOD(TestLayerUserDefaultsOnDynamics); TEST_METHOD(TestTerminalArgsForBinding); - TEST_METHOD(FindMissingProfile); TEST_METHOD(MakeSettingsForProfileThatDoesntExist); TEST_METHOD(MakeSettingsForDefaultProfileThatDoesntExist); TEST_METHOD(TestLayerProfileOnColorScheme); - TEST_METHOD(ValidateKeybindingsWarnings); + TEST_METHOD(TestIterateCommands); + TEST_METHOD(TestIterateOnGeneratedNamedCommands); + TEST_METHOD(TestIterateOnBadJson); + TEST_METHOD(TestNestedCommands); + TEST_METHOD(TestNestedInNestedCommand); + TEST_METHOD(TestNestedInIterableCommand); + TEST_METHOD(TestIterableInNestedCommand); + TEST_METHOD(TestMixedNestedAndIterableCommand); - TEST_METHOD(ValidateExecuteCommandlineWarning); - - TEST_METHOD(ValidateLegacyGlobalsWarning); - - TEST_METHOD(TestTrailingCommas); - - TEST_METHOD(TestCommandsAndKeybindings); + TEST_METHOD(TestIterableColorSchemeCommands); TEST_CLASS_SETUP(ClassSetup) { - InitializeJsonReader(); return true; } + + private: + void _logCommandNames(winrt::Windows::Foundation::Collections::IMapView commands, const int indentation = 1) + { + if (indentation == 1) + { + Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); + } + for (const auto& nameAndCommand : commands) + { + Log::Comment(fmt::format(L"{0:>{1}}* {2}->{3}", + L"", + indentation, + nameAndCommand.Key(), + nameAndCommand.Value().Name()) + .c_str()); + + if (nameAndCommand.Value().HasNestedCommands()) + { + _logCommandNames(nameAndCommand.Value().NestedCommands(), indentation + 2); + } + } + } }; void SettingsTests::TryCreateWinRTType() { - winrt::Microsoft::Terminal::Settings::TerminalSettings settings; + TerminalSettings settings; VERIFY_IS_NOT_NULL(settings); auto oldFontSize = settings.FontSize(); settings.FontSize(oldFontSize + 5); @@ -101,1609 +93,483 @@ namespace TerminalAppLocalTests VERIFY_ARE_NOT_EQUAL(oldFontSize, newFontSize); } - void SettingsTests::ValidateProfilesExist() + void SettingsTests::TestTerminalArgsForBinding() { - const std::string settingsWithProfiles{ R"( + const std::string settingsJson{ R"( { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0" + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "vertical", "profile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } }, + { "keys": ["ctrl+c"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile1" } }, + { "keys": ["ctrl+d"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile2" } }, + { "keys": ["ctrl+e"], "command": { "action": "splitPane", "split": "horizontal", "commandline": "foo.exe" } }, + { "keys": ["ctrl+f"], "command": { "action": "splitPane", "split": "horizontal", "profile": "profile1", "commandline": "foo.exe" } }, + { "keys": ["ctrl+g"], "command": { "action": "newTab" } }, + { "keys": ["ctrl+h"], "command": { "action": "newTab", "startingDirectory": "c:\\foo" } }, + { "keys": ["ctrl+i"], "command": { "action": "newTab", "profile": "profile2", "startingDirectory": "c:\\foo" } }, + { "keys": ["ctrl+j"], "command": { "action": "newTab", "tabTitle": "bar" } }, + { "keys": ["ctrl+k"], "command": { "action": "newTab", "profile": "profile2", "tabTitle": "bar" } }, + { "keys": ["ctrl+l"], "command": { "action": "newTab", "profile": "profile1", "tabTitle": "bar", "startingDirectory": "c:\\foo", "commandline":"foo.exe" } } ] })" }; - const std::string settingsWithoutProfiles{ R"( - { - "defaultProfile": "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" - })" }; + const winrt::guid guid0{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}") }; + const winrt::guid guid1{ ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}") }; - const std::string settingsWithEmptyProfiles{ R"( - { - "profiles": [] - })" }; + CascadiaSettings settings{ til::u8u16(settingsJson) }; + + auto keymap = settings.GlobalSettings().KeyMap(); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); + + const auto profile2Guid = settings.ActiveProfiles().GetAt(2).Guid(); + VERIFY_ARE_NOT_EQUAL(winrt::guid{}, profile2Guid); + + VERIFY_ARE_EQUAL(12u, keymap.Size()); { - // Case 1: Good settings - const auto settingsObject = VerifyParseSucceeded(settingsWithProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ValidateProfilesExist(); + KeyChord kc{ true, false, false, static_cast('A') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - // Case 2: Bad settings - const auto settingsObject = VerifyParseSucceeded(settingsWithoutProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - bool caughtExpectedException = false; - try - { - settings->_ValidateProfilesExist(); - } - catch (const ::TerminalApp::SettingsException& ex) - { - VERIFY_IS_TRUE(ex.Error() == ::TerminalApp::SettingsLoadErrors::NoProfiles); - caughtExpectedException = true; - } - VERIFY_IS_TRUE(caughtExpectedException); + KeyChord kc{ true, false, false, static_cast('B') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } { - // Case 3: Bad settings - const auto settingsObject = VerifyParseSucceeded(settingsWithEmptyProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - bool caughtExpectedException = false; - try - { - settings->_ValidateProfilesExist(); - } - catch (const ::TerminalApp::SettingsException& ex) - { - VERIFY_IS_TRUE(ex.Error() == ::TerminalApp::SettingsLoadErrors::NoProfiles); - caughtExpectedException = true; - } - VERIFY_IS_TRUE(caughtExpectedException); - } - } + KeyChord kc{ true, false, false, static_cast('C') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - void SettingsTests::ValidateDefaultProfileExists() - { - const std::string goodProfiles{ R"( + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - } - ] - })" }; + KeyChord kc{ true, false, false, static_cast('D') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - const std::string badProfiles{ R"( + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + } { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" - } - ] - })" }; + KeyChord kc{ true, false, false, static_cast('E') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - const std::string noDefaultAtAll{ R"( + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } { - "alwaysShowTabs": true, - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" - } - ] - })" }; + KeyChord kc{ true, false, false, static_cast('F') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - const std::string goodProfilesSpecifiedByName{ R"( + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } { - "defaultProfile": "profile1", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - } - ] - })" }; + KeyChord kc{ true, false, false, static_cast('G') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - { - // Case 1: Good settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with unique guids, and the defaultProfile is one of those guids")); - const auto settingsObject = VerifyParseSucceeded(goodProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ResolveDefaultProfile(); - settings->_ValidateDefaultProfileExists(); - VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.size()); - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(0).GetGuid()); + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - // Case 2: Bad settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with unique guids, but the defaultProfile is NOT one of those guids")); - const auto settingsObject = VerifyParseSucceeded(badProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ResolveDefaultProfile(); - settings->_ValidateDefaultProfileExists(); - VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.at(0)); - - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(0).GetGuid()); + KeyChord kc{ true, false, false, static_cast('H') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } { - // Case 2: Bad settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with unique guids, and no defaultProfile at all")); - const auto settingsObject = VerifyParseSucceeded(badProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ResolveDefaultProfile(); - settings->_ValidateDefaultProfileExists(); - VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.at(0)); - - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(0).GetGuid()); + KeyChord kc{ true, false, false, static_cast('I') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } { - // Case 4: Good settings, default profile is a string - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with unique guids, and the defaultProfile is one of the profile names")); - const auto settingsObject = VerifyParseSucceeded(goodProfilesSpecifiedByName); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ResolveDefaultProfile(); - settings->_ValidateDefaultProfileExists(); - VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.size()); - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(1).GetGuid()); - } - } + KeyChord kc{ true, false, false, static_cast('J') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); - void SettingsTests::ValidateDuplicateProfiles() - { - const std::string goodProfiles{ R"( - { - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string badProfiles{ R"( - { - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string veryBadProfiles{ R"( - { - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile2", - "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile3", - "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile4", - "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile5", - "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile6", - "guid": "{6239a42c-7777-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - Profile profile0{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; - profile0._name = L"profile0"; - Profile profile1{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; - profile1._name = L"profile1"; - Profile profile2{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; - profile2._name = L"profile2"; - Profile profile3{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; - profile3._name = L"profile3"; - Profile profile4{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") }; - profile4._name = L"profile4"; - Profile profile5{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; - profile5._name = L"profile5"; - Profile profile6{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-7777-49a3-80bd-e8fdd045185c}") }; - profile6._name = L"profile6"; - - { - // Case 1: Good settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with unique guids")); - - CascadiaSettings settings; - settings._profiles.push_back(profile0); - settings._profiles.push_back(profile1); - - settings._ValidateNoDuplicateProfiles(); - - VERIFY_ARE_EQUAL(static_cast(0), settings._warnings.size()); - VERIFY_ARE_EQUAL(static_cast(2), settings._profiles.size()); - } - { - // Case 2: Bad settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with the same guid")); - - CascadiaSettings settings; - settings._profiles.push_back(profile2); - settings._profiles.push_back(profile3); - - settings._ValidateNoDuplicateProfiles(); - - VERIFY_ARE_EQUAL(static_cast(1), settings._warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0)); - - VERIFY_ARE_EQUAL(static_cast(1), settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0).GetName()); - } - { - // Case 3: Very bad settings - Log::Comment(NoThrowString().Format( - L"Testing a set of profiles, many of which with duplicated guids")); - - CascadiaSettings settings; - settings._profiles.push_back(profile0); - settings._profiles.push_back(profile1); - settings._profiles.push_back(profile2); - settings._profiles.push_back(profile3); - settings._profiles.push_back(profile4); - settings._profiles.push_back(profile5); - settings._profiles.push_back(profile6); - - settings._ValidateNoDuplicateProfiles(); - - VERIFY_ARE_EQUAL(static_cast(1), settings._warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0)); - - VERIFY_ARE_EQUAL(static_cast(4), settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName()); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName()); - VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(2).GetName()); - VERIFY_ARE_EQUAL(L"profile6", settings._profiles.at(3).GetName()); + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); } - } - - void SettingsTests::ValidateManyWarnings() - { - const std::string badProfiles{ R"( - { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile2", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - Profile profile4{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; - profile4._name = L"profile4"; - Profile profile5{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; - profile5._name = L"profile5"; - - // Case 2: Bad settings - Log::Comment(NoThrowString().Format( - L"Testing a pair of profiles with the same guid")); - const auto settingsObject = VerifyParseSucceeded(badProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - - settings->_profiles.push_back(profile4); - settings->_profiles.push_back(profile5); - - settings->_ValidateSettings(); - - VERIFY_ARE_EQUAL(3u, settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.at(1)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::UnknownColorScheme, settings->_warnings.at(2)); - - VERIFY_ARE_EQUAL(3u, settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(0).GetGuid()); - VERIFY_IS_TRUE(settings->_profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings->_profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings->_profiles.at(2)._guid.has_value()); - } - - void SettingsTests::LayerGlobalProperties() - { - const std::string settings0String{ R"( - { - "alwaysShowTabs": true, - "initialCols" : 120, - "initialRows" : 30 - })" }; - const std::string settings1String{ R"( - { - "showTabsInTitlebar": false, - "initialCols" : 240, - "initialRows" : 60 - })" }; - const auto settings0Json = VerifyParseSucceeded(settings0String); - const auto settings1Json = VerifyParseSucceeded(settings1String); - - CascadiaSettings settings; - - settings.LayerJson(settings0Json); - VERIFY_ARE_EQUAL(true, settings._globals._AlwaysShowTabs); - VERIFY_ARE_EQUAL(120, settings._globals._InitialCols); - VERIFY_ARE_EQUAL(30, settings._globals._InitialRows); - VERIFY_ARE_EQUAL(true, settings._globals._ShowTabsInTitlebar); - - settings.LayerJson(settings1Json); - VERIFY_ARE_EQUAL(true, settings._globals._AlwaysShowTabs); - VERIFY_ARE_EQUAL(240, settings._globals._InitialCols); - VERIFY_ARE_EQUAL(60, settings._globals._InitialRows); - VERIFY_ARE_EQUAL(false, settings._globals._ShowTabsInTitlebar); - } - - void SettingsTests::ValidateProfileOrdering() - { - const std::string userProfiles0String{ R"( - { - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string defaultProfilesString{ R"( - { - "profiles": [ - { - "name" : "profile2", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile3", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string userProfiles1String{ R"( { - "profiles": [ - { - "name" : "profile4", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile5", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); - const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); - const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); - - { - Log::Comment(NoThrowString().Format( - L"Case 1: Simple swapping of the ordering. The user has the " - L"default profiles in the opposite order of the default ordering.")); - - CascadiaSettings settings; - settings._ParseJsonString(defaultProfilesString, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - - settings._ParseJsonString(userProfiles0String, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); - - settings._ReorderProfilesToMatchUserSettingsOrder(); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1)._name); - } + KeyChord kc{ true, false, false, static_cast('K') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - { - Log::Comment(NoThrowString().Format( - L"Case 2: Make sure all the user's profiles appear before the defaults.")); - - CascadiaSettings settings; - settings._ParseJsonString(defaultProfilesString, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - - settings._ParseJsonString(userProfiles1String, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(2)._name); - - settings._ReorderProfilesToMatchUserSettingsOrder(); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(2)._name); + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } - } - - void SettingsTests::ValidateHideProfiles() - { - const std::string defaultProfilesString{ R"( - { - "profiles": [ - { - "name" : "profile2", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile3", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string userProfiles0String{ R"( - { - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", - "hidden": true - }, - { - "name" : "profile1", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - } - ] - })" }; - - const std::string userProfiles1String{ R"( { - "profiles": [ - { - "name" : "profile4", - "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", - "hidden": true - }, - { - "name" : "profile5", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile6", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", - "hidden": true - } - ] - })" }; - - const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); - const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); - const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); - - { - CascadiaSettings settings; - settings._ParseJsonString(defaultProfilesString, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); - - settings._ParseJsonString(userProfiles0String, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - VERIFY_ARE_EQUAL(true, settings._profiles.at(1)._hidden); - - settings._ReorderProfilesToMatchUserSettingsOrder(); - settings._RemoveHiddenProfiles(); - VERIFY_ARE_EQUAL(1u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - } + KeyChord kc{ true, false, false, static_cast('L') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(keymap, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - { - CascadiaSettings settings; - settings._ParseJsonString(defaultProfilesString, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); - - settings._ParseJsonString(userProfiles1String, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"profile6", settings._profiles.at(3)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - VERIFY_ARE_EQUAL(true, settings._profiles.at(1)._hidden); - VERIFY_ARE_EQUAL(false, settings._profiles.at(2)._hidden); - VERIFY_ARE_EQUAL(true, settings._profiles.at(3)._hidden); - - settings._ReorderProfilesToMatchUserSettingsOrder(); - settings._RemoveHiddenProfiles(); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); - VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, realArgs.TerminalArgs(), nullptr); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } } - void SettingsTests::ValidateProfilesGenerateGuids() - { - const std::string profile0String{ R"( - { - "name" : "profile0" - })" }; - const std::string profile1String{ R"( - { - "name" : "profile1" - })" }; - const std::string profile2String{ R"( - { - "name" : "profile2", - "guid" : null - })" }; - const std::string profile3String{ R"( - { - "name" : "profile3", - "guid" : "{00000000-0000-0000-0000-000000000000}" - })" }; - const std::string profile4String{ R"( - { - "name" : "profile4", - "guid" : "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" - })" }; - const std::string profile5String{ R"( - { - "name" : "profile2" - })" }; - - const auto profile0Json = VerifyParseSucceeded(profile0String); - const auto profile1Json = VerifyParseSucceeded(profile1String); - const auto profile2Json = VerifyParseSucceeded(profile2String); - const auto profile3Json = VerifyParseSucceeded(profile3String); - const auto profile4Json = VerifyParseSucceeded(profile4String); - const auto profile5Json = VerifyParseSucceeded(profile5String); - - const auto profile0 = Profile::FromJson(profile0Json); - const auto profile1 = Profile::FromJson(profile1Json); - const auto profile2 = Profile::FromJson(profile2Json); - const auto profile3 = Profile::FromJson(profile3Json); - const auto profile4 = Profile::FromJson(profile4Json); - const auto profile5 = Profile::FromJson(profile5Json); - - const GUID cmdGuid = Utils::GuidFromString(L"{6239a42c-1de4-49a3-80bd-e8fdd045185c}"); - const GUID nullGuid{ 0 }; - - VERIFY_IS_FALSE(profile0._guid.has_value()); - VERIFY_IS_FALSE(profile1._guid.has_value()); - VERIFY_IS_FALSE(profile2._guid.has_value()); - VERIFY_IS_TRUE(profile3._guid.has_value()); - VERIFY_IS_TRUE(profile4._guid.has_value()); - VERIFY_IS_FALSE(profile5._guid.has_value()); - - VERIFY_ARE_EQUAL(profile3.GetGuid(), nullGuid); - VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); - - CascadiaSettings settings; - settings._profiles.emplace_back(profile0); - settings._profiles.emplace_back(profile1); - settings._profiles.emplace_back(profile2); - settings._profiles.emplace_back(profile3); - settings._profiles.emplace_back(profile4); - settings._profiles.emplace_back(profile5); - - settings._ValidateProfilesHaveGuid(); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(5)._guid.has_value()); - - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(2).GetGuid(), nullGuid); - VERIFY_ARE_EQUAL(settings._profiles.at(3).GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(4).GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(5).GetGuid(), nullGuid); - - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(2).GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3).GetGuid(), cmdGuid); - VERIFY_ARE_EQUAL(settings._profiles.at(4).GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(5).GetGuid(), cmdGuid); - - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), settings._profiles.at(2).GetGuid()); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), settings._profiles.at(2).GetGuid()); - VERIFY_ARE_EQUAL(settings._profiles.at(2).GetGuid(), settings._profiles.at(2).GetGuid()); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3).GetGuid(), settings._profiles.at(2).GetGuid()); - VERIFY_ARE_NOT_EQUAL(settings._profiles.at(4).GetGuid(), settings._profiles.at(2).GetGuid()); - VERIFY_ARE_EQUAL(settings._profiles.at(5).GetGuid(), settings._profiles.at(2).GetGuid()); - } - - void SettingsTests::GeneratedGuidRoundtrips() - { - // Parse a profile without a guid. - // We should automatically generate a GUID for that profile. - // When that profile is serialized and deserialized again, the GUID we - // generated for it should persist. - const std::string profileWithoutGuid{ R"({ - "name" : "profile0" - })" }; - const auto profile0Json = VerifyParseSucceeded(profileWithoutGuid); - - const auto profile0 = Profile::FromJson(profile0Json); - const GUID nullGuid{ 0 }; - - VERIFY_IS_FALSE(profile0._guid.has_value()); - - const auto serialized0Profile = profile0.GenerateStub(); - const auto profile1 = Profile::FromJson(serialized0Profile); - VERIFY_IS_FALSE(profile0._guid.has_value()); - VERIFY_ARE_EQUAL(profile1._guid.has_value(), profile0._guid.has_value()); - - CascadiaSettings settings; - settings._profiles.emplace_back(profile1); - settings._ValidateProfilesHaveGuid(); - - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - - const auto serialized1Profile = settings._profiles.at(0).GenerateStub(); - - const auto profile2 = Profile::FromJson(serialized1Profile); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_ARE_EQUAL(settings._profiles.at(0)._guid.has_value(), profile2._guid.has_value()); - VERIFY_ARE_EQUAL(settings._profiles.at(0).GetGuid(), profile2.GetGuid()); - } - - void SettingsTests::TestAllValidationsWithNullGuids() - { - const std::string settings0String{ R"( - { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1" - } - ], - "schemes": [ - { "name": "Campbell" } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value()); - - settings._ValidateSettings(); - VERIFY_ARE_EQUAL(0u, settings._warnings.size()); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - } - - void SettingsTests::TestReorderWithNullGuids() + void SettingsTests::MakeSettingsForProfileThatDoesntExist() { - const std::string settings0String{ R"( + // Test that MakeSettings throws when the GUID doesn't exist + const std::string settingsString{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", - "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1" - }, - { - "name" : "cmdFromUserSettings", - "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}" // from defaults.json - } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(3)._name); - - settings._ValidateSettings(); - VERIFY_ARE_EQUAL(0u, settings._warnings.size()); - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(3)._name); - } - - void SettingsTests::TestReorderingWithoutGuid() - { - Log::Comment(NoThrowString().Format( - L"During the GH#2515 PR, this set of settings was found to cause an" - L" exception, crashing the terminal. This test ensures that it doesn't.")); - - Log::Comment(NoThrowString().Format( - L"While similar to TestReorderWithNullGuids, there's something else" - L" about this scenario specifically that causes a crash, when " - L" TestReorderWithNullGuids did _not_.")); - - const std::string settings0String{ R"( - { - "defaultProfile" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "profiles": [ - { - "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "acrylicOpacity" : 0.5, - "closeOnExit" : true, - "background" : "#8A00FF", - "foreground" : "#F2F2F2", - "commandline" : "cmd.exe", - "cursorColor" : "#FFFFFF", - "fontFace" : "Cascadia Code", - "fontSize" : 10, - "historySize" : 9001, - "padding" : "20", - "snapOnInput" : true, - "startingDirectory" : "%USERPROFILE%", - "useAcrylic" : true - }, - { - "name" : "ThisProfileShouldNotCrash", - "tabTitle" : "Ubuntu", - "acrylicOpacity" : 0.5, - "background" : "#2C001E", - "closeOnExit" : true, - "colorScheme" : "Campbell", - "commandline" : "wsl.exe", - "cursorColor" : "#FFFFFF", - "cursorShape" : "bar", - "fontSize" : 10, - "historySize" : 9001, - "padding" : "0, 0, 0, 0", - "snapOnInput" : true, - "useAcrylic" : true - }, - { - // This is the same profile that would be generated by the WSL profile generator. - "name" : "Ubuntu", - "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", - "acrylicOpacity" : 0.5, - "background" : "#2C001E", - "closeOnExit" : false, - "cursorColor" : "#FFFFFF", - "cursorShape" : "bar", - "fontSize" : 10, - "historySize" : 9001, - "snapOnInput" : true, - "useAcrylic" : true - } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"Ubuntu", settings._profiles.at(3)._name); - - settings._ValidateSettings(); - VERIFY_ARE_EQUAL(0u, settings._warnings.size()); - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"Ubuntu", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(3)._name); - } - - void SettingsTests::TestLayeringNameOnlyProfiles() - { - // This is a test discovered during GH#2782. When we add a name-only - // profile, it should only layer with other name-only profiles with the - // _same name_ - - const std::string settings0String{ R"( - { - "defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", - "profiles": [ - { - "guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", - "name" : "ThisProfileIsGood" - - }, - { - "name" : "ThisProfileShouldNotLayer" - }, - { - "name" : "NeitherShouldThisOne" - } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - - Log::Comment(NoThrowString().Format( - L"Parse the user settings")); - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(5u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(4)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotLayer", settings._profiles.at(3)._name); - VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings._profiles.at(4)._name); - } - - void SettingsTests::TestExplodingNameOnlyProfiles() - { - // This is a test for GH#2782. When we add a name-only profile, we'll - // generate a GUID for it. We should make sure that we don't re-append - // that profile to the list of profiles. - - const std::string settings0String{ R"( - { - "defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", - "profiles": [ - { - "guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", - "name" : "ThisProfileIsGood" - - }, - { - "name" : "ThisProfileShouldNotDuplicate" - }, - { - "name" : "NeitherShouldThisOne" - } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - - Log::Comment(NoThrowString().Format( - L"Parse the user settings")); - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(5u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(4)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings._profiles.at(3)._name); - VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings._profiles.at(4)._name); - - Log::Comment(NoThrowString().Format( - L"Pretend like we're checking to append dynamic profiles to the " - L"user's settings file. We absolutely _shouldn't_ be adding anything here.")); - bool const needToWriteFile = settings._AppendDynamicProfilesToUserSettings(); - VERIFY_IS_FALSE(needToWriteFile); - VERIFY_ARE_EQUAL(settings0String.size(), settings._userSettingsString.size()); - - Log::Comment(NoThrowString().Format( - L"Re-parse the settings file. We should have the _same_ settings as before.")); - Log::Comment(NoThrowString().Format( - L"Do this to a _new_ settings object, to make sure it turns out the same.")); - { - CascadiaSettings settings2; - settings2._ParseJsonString(DefaultJson, true); - settings2.LayerJson(settings2._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings2._profiles.size()); - // Initialize the second settings object from the first settings - // object's settings string, the one that we synthesized. - const auto firstSettingsString = settings._userSettingsString; - settings2._ParseJsonString(firstSettingsString, false); - settings2.LayerJson(settings2._userSettings); - VERIFY_ARE_EQUAL(5u, settings2._profiles.size()); - VERIFY_IS_TRUE(settings2._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings2._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings2._profiles.at(2)._guid.has_value()); - VERIFY_IS_FALSE(settings2._profiles.at(3)._guid.has_value()); - VERIFY_IS_FALSE(settings2._profiles.at(4)._guid.has_value()); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings2._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings2._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings2._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings2._profiles.at(3)._name); - VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings2._profiles.at(4)._name); - } - - Log::Comment(NoThrowString().Format( - L"Validate the settings. All the profiles we have should be valid.")); - settings._ValidateSettings(); - - VERIFY_ARE_EQUAL(5u, settings._profiles.size()); - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value()); - VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"ThisProfileShouldNotDuplicate", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(3)._name); - VERIFY_ARE_EQUAL(L"Command Prompt", settings._profiles.at(4)._name); - } - - void SettingsTests::TestHideAllProfiles() - { - const std::string settingsWithProfiles{ R"( - { - "profiles": [ - { - "name" : "profile0", - "hidden": false - }, - { - "name" : "profile1", - "hidden": true - } - ] - })" }; - - const std::string settingsWithoutProfiles{ R"( - { - "profiles": [ - { - "name" : "profile0", - "hidden": true - }, - { - "name" : "profile1", - "hidden": true - } - ] - })" }; - - VerifyParseSucceeded(settingsWithProfiles); - VerifyParseSucceeded(settingsWithoutProfiles); - - { - // Case 1: Good settings - CascadiaSettings settings; - settings._ParseJsonString(settingsWithProfiles, false); - settings.LayerJson(settings._userSettings); - - settings._RemoveHiddenProfiles(); - Log::Comment(NoThrowString().Format( - L"settingsWithProfiles successfully parsed and validated")); - VERIFY_ARE_EQUAL(1u, settings._profiles.size()); - } - { - // Case 2: Bad settings - - CascadiaSettings settings; - settings._ParseJsonString(settingsWithoutProfiles, false); - settings.LayerJson(settings._userSettings); - - bool caughtExpectedException = false; - try - { - settings._RemoveHiddenProfiles(); - } - catch (const ::TerminalApp::SettingsException& ex) - { - VERIFY_IS_TRUE(ex.Error() == ::TerminalApp::SettingsLoadErrors::AllProfilesHidden); - caughtExpectedException = true; - } - VERIFY_IS_TRUE(caughtExpectedException); - } - } - - void SettingsTests::TestInvalidColorSchemeName() - { - Log::Comment(NoThrowString().Format( - L"Ensure that setting a profile's scheme to a non-existent scheme causes a warning.")); - - const std::string settings0String{ R"( - { - "profiles": [ - { - "name" : "profile0", - "colorScheme": "schemeOne" - }, - { - "name" : "profile1", - "colorScheme": "InvalidSchemeName" - }, - { - "name" : "profile2" - // Will use the Profile default value, "Campbell" - } - ], - "schemes": [ - { - "name": "schemeOne", - "foreground": "#111111" - }, - { - "name": "schemeTwo", - "foreground": "#222222" - } - ] - })" }; - - VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_ARE_EQUAL(2u, settings._globals._colorSchemes.size()); - - VERIFY_ARE_EQUAL(L"schemeOne", settings._profiles.at(0)._schemeName.value()); - VERIFY_ARE_EQUAL(L"InvalidSchemeName", settings._profiles.at(1)._schemeName.value()); - VERIFY_ARE_EQUAL(L"Campbell", settings._profiles.at(2)._schemeName.value()); - - settings._ValidateAllSchemesExist(); - - VERIFY_ARE_EQUAL(1u, settings._warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::UnknownColorScheme, settings._warnings.at(0)); - - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); - VERIFY_ARE_EQUAL(2u, settings._globals._colorSchemes.size()); - - VERIFY_ARE_EQUAL(L"schemeOne", settings._profiles.at(0)._schemeName.value()); - VERIFY_ARE_EQUAL(L"Campbell", settings._profiles.at(1)._schemeName.value()); - VERIFY_ARE_EQUAL(L"Campbell", settings._profiles.at(2)._schemeName.value()); - } - - void SettingsTests::TestHelperFunctions() - { - const std::string settings0String{ R"( - { - "defaultProfile" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "ThisProfileShouldNotThrow" - }, - { - "name" : "Ubuntu", - "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}" - } - ] - })" }; - - auto name0{ L"profile0" }; - auto name1{ L"profile1" }; - auto name2{ L"Ubuntu" }; - auto name3{ L"ThisProfileShouldNotThrow" }; - auto badName{ L"DoesNotExist" }; - - auto guid0{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; - auto guid1{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") }; - auto guid2{ Microsoft::Console::Utils::GuidFromString(L"{2C4DE342-38B7-51CF-B940-2309A097F518}") }; - auto fakeGuid{ Microsoft::Console::Utils::GuidFromString(L"{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}") }; - std::optional badGuid{}; - - VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(guid0, settings._GetProfileGuidByName(name0)); - VERIFY_ARE_EQUAL(guid1, settings._GetProfileGuidByName(name1)); - VERIFY_ARE_EQUAL(guid2, settings._GetProfileGuidByName(name2)); - VERIFY_ARE_EQUAL(badGuid, settings._GetProfileGuidByName(name3)); - VERIFY_ARE_EQUAL(badGuid, settings._GetProfileGuidByName(badName)); - - auto prof0{ settings.FindProfile(guid0) }; - auto prof1{ settings.FindProfile(guid1) }; - auto prof2{ settings.FindProfile(guid2) }; - - auto badProf{ settings.FindProfile(fakeGuid) }; - VERIFY_ARE_EQUAL(badProf, nullptr); - - VERIFY_ARE_EQUAL(name0, prof0->GetName()); - VERIFY_ARE_EQUAL(name1, prof1->GetName()); - VERIFY_ARE_EQUAL(name2, prof2->GetName()); - } - - void SettingsTests::TestProfileIconWithEnvVar() - { - const auto expectedPath = wil::ExpandEnvironmentStringsW(L"%WINDIR%\\System32\\x_80.png"); - - const std::string settingsJson{ R"( - { - "profiles": [ - { - "name": "profile0", - "icon": "%WINDIR%\\System32\\x_80.png" - } - ] - })" }; - - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - VERIFY_IS_FALSE(settings._profiles.empty()); - VERIFY_ARE_EQUAL(expectedPath, settings._profiles[0].GetExpandedIconPath()); - } - void SettingsTests::TestProfileBackgroundImageWithEnvVar() - { - const auto expectedPath = wil::ExpandEnvironmentStringsW(L"%WINDIR%\\System32\\x_80.png"); - - const std::string settingsJson{ R"( - { - "profiles": [ - { - "name": "profile0", - "backgroundImage": "%WINDIR%\\System32\\x_80.png" - } - ] - })" }; - - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - VERIFY_IS_FALSE(settings._profiles.empty()); - - GlobalAppSettings globalSettings{}; - auto terminalSettings = settings._profiles[0].CreateTerminalSettings(globalSettings.GetColorSchemes()); - VERIFY_ARE_EQUAL(expectedPath, terminalSettings.BackgroundImage()); - } - void SettingsTests::TestCloseOnExitParsing() - { - const std::string settingsJson{ R"( - { - "profiles": [ - { - "name": "profile0", - "closeOnExit": "graceful" - }, - { - "name": "profile1", - "closeOnExit": "always" - }, - { - "name": "profile2", - "closeOnExit": "never" - }, - { - "name": "profile3", - "closeOnExit": null - } - ] - })" }; - - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[0].GetCloseOnExitMode()); - VERIFY_ARE_EQUAL(CloseOnExitMode::Always, settings._profiles[1].GetCloseOnExitMode()); - VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings._profiles[2].GetCloseOnExitMode()); - - // Unknown modes parse as "Graceful" - VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[3].GetCloseOnExitMode()); - } - void SettingsTests::TestCloseOnExitCompatibilityShim() - { - const std::string settingsJson{ R"( - { - "profiles": [ - { - "name": "profile0", - "closeOnExit": true + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 1 }, { - "name": "profile1", - "closeOnExit": false + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 } ] })" }; + CascadiaSettings settings{ til::u8u16(settingsString) }; - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[0].GetCloseOnExitMode()); - VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings._profiles[1].GetCloseOnExitMode()); - } + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + const auto guid3 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); - void SettingsTests::TestLayerUserDefaultsBeforeProfiles() - { - // Test for microsoft/terminal#2325. For this test, we'll be setting the - // "historySize" in the "defaultSettings", so it should apply to all - // profiles, unless they override it. In one of the user's profiles, - // we'll override that value, and in the other, we'll leave it - // untouched. + try + { + auto terminalSettings = winrt::make(settings, guid1, nullptr); + VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); + VERIFY_ARE_EQUAL(1, terminalSettings.HistorySize()); + } + catch (...) + { + VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); + } - const std::string settings0String{ R"( + try { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": { - "defaults": { - "historySize": 1234 - }, - "list": [ - { - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "name": "profile0", - "historySize": 2345 - }, - { - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - "name": "profile1" - } - ] - } - })" }; - VerifyParseSucceeded(settings0String); + auto terminalSettings = winrt::make(settings, guid2, nullptr); + VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); + VERIFY_ARE_EQUAL(2, terminalSettings.HistorySize()); + } + catch (...) + { + VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); + } - const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"; + VERIFY_THROWS(auto terminalSettings = winrt::make(settings, guid3, nullptr), wil::ResultException, L"This call to BuildSettings should fail"); + try + { + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, nullptr, nullptr); + VERIFY_ARE_NOT_EQUAL(nullptr, termSettings); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + catch (...) { - CascadiaSettings settings{ false }; - settings._ParseJsonString(settings0String, false); - VERIFY_IS_TRUE(settings._userDefaultProfileSettings == Json::Value::null); - settings._ApplyDefaultsFromUserSettings(); - VERIFY_IS_FALSE(settings._userDefaultProfileSettings == Json::Value::null); - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(guid1String, settings._globals._unparsedDefaultProfile); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - - VERIFY_ARE_EQUAL(2345, settings._profiles.at(0)._historySize); - VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); + VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); } } - void SettingsTests::TestDontLayerGuidFromUserDefaults() + void SettingsTests::MakeSettingsForDefaultProfileThatDoesntExist() { - // Test for microsoft/terminal#2325. We don't want the user to put a - // "guid" in the "defaultSettings", and have that apply to all the other - // profiles - - const std::string settings0String{ R"( + // Test that MakeSettings _doesnt_ throw when we load settings with a + // defaultProfile that's not in the list, we validate the settings, and + // then call MakeSettings(nullopt). The validation should ensure that + // the default profile is something reasonable + const std::string settingsString{ R"( { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": { - "defaults": { - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + "defaultProfile": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 1 }, - "list": [ - { - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "name": "profile0", - "historySize": 2345 - }, - { - // Doesn't have a GUID, we'll auto-generate one - "name": "profile1" - } - ] - } + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] })" }; - VerifyParseSucceeded(settings0String); - - const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"; - const auto guid1 = Microsoft::Console::Utils::GuidFromString(guid1String); - const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + CascadiaSettings settings{ til::u8u16(settingsString) }; + VERIFY_ARE_EQUAL(2u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(2u, settings.ActiveProfiles().Size()); + VERIFY_ARE_EQUAL(settings.GlobalSettings().DefaultProfile(), settings.ActiveProfiles().GetAt(0).Guid()); + try { - CascadiaSettings settings{ false }; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); - VERIFY_ARE_EQUAL(2u, settings._profiles.size()); - - settings._ParseJsonString(settings0String, false); - VERIFY_IS_TRUE(settings._userDefaultProfileSettings == Json::Value::null); - settings._ApplyDefaultsFromUserSettings(); - VERIFY_IS_FALSE(settings._userDefaultProfileSettings == Json::Value::null); - - Log::Comment(NoThrowString().Format( - L"Ensure that cmd and powershell don't get their GUIDs overwritten")); - VERIFY_ARE_NOT_EQUAL(guid2, settings._profiles.at(0)._guid); - VERIFY_ARE_NOT_EQUAL(guid2, settings._profiles.at(1)._guid); - - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(guid1String, settings._globals._unparsedDefaultProfile); - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - - VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid); - VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); + const auto [guid, termSettings] = winrt::TerminalApp::implementation::TerminalSettings::BuildSettings(settings, nullptr, nullptr); + VERIFY_ARE_NOT_EQUAL(nullptr, termSettings); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + catch (...) + { + VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); } } - void SettingsTests::TestLayerUserDefaultsOnDynamics() + void SettingsTests::TestLayerProfileOnColorScheme() { - // Test for microsoft/terminal#2325. For this test, we'll be setting the - // "historySize" in the "defaultSettings", so it should apply to all - // profiles, unless they override it. The dynamic profiles will _also_ - // set this value, but from discussion in GH#2325, we decided that - // settings in defaultSettings should apply _on top_ of settings from - // dynamic profiles. - - GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - GUID guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - GUID guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); - - const std::string userProfiles{ R"( + Log::Comment(NoThrowString().Format( + L"Ensure that setting (or not) a property in the profile that should override a property of the color scheme works correctly.")); + + const std::string settings0String{ R"( { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": { - "defaults": { - "historySize": 1234 + "defaultProfile": "profile5", + "profiles": [ + { + "name" : "profile0", + "colorScheme": "schemeWithCursorColor" }, - "list": [ - { - "name" : "profile0FromUserSettings", // this is _profiles.at(0) - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "source": "Terminal.App.UnitTest.0" - }, - { - "name" : "profile1FromUserSettings", // this is _profiles.at(2) - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - "source": "Terminal.App.UnitTest.1", - "historySize": 4444 - }, - { - "name" : "profile2FromUserSettings", // this is _profiles.at(3) - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", - "historySize": 5555 - } - ] - } + { + "name" : "profile1", + "colorScheme": "schemeWithoutCursorColor" + }, + { + "name" : "profile2", + "colorScheme": "schemeWithCursorColor", + "cursorColor": "#234567" + }, + { + "name" : "profile3", + "colorScheme": "schemeWithoutCursorColor", + "cursorColor": "#345678" + }, + { + "name" : "profile4", + "cursorColor": "#456789" + }, + { + "name" : "profile5" + } + ], + "schemes": [ + { + "name": "schemeWithCursorColor", + "cursorColor": "#123456" + }, + { + "name": "schemeWithoutCursorColor" + } + ] })" }; - auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); - gen0->pfnGenerate = [guid1, guid2]() { - std::vector profiles; - Profile p0{ guid1 }; - p0.SetName(L"profile0"); // this is _profiles.at(0) - p0._historySize = 1111; - profiles.push_back(p0); - return profiles; - }; - auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); - gen1->pfnGenerate = [guid1, guid2]() { - std::vector profiles; - Profile p0{ guid1 }, p1{ guid2 }; - p0.SetName(L"profile0"); // this is _profiles.at(1) - p1.SetName(L"profile1"); // this is _profiles.at(2) - p0._historySize = 2222; - profiles.push_back(p0); - p1._historySize = 3333; - profiles.push_back(p1); - return profiles; - }; - - CascadiaSettings settings{ false }; - settings._profileGenerators.emplace_back(std::move(gen0)); - settings._profileGenerators.emplace_back(std::move(gen1)); - - Log::Comment(NoThrowString().Format( - L"All profiles with the same name have the same GUID. However, they" - L" will not be layered, because they have different source's")); - - // parse userProfiles as the user settings - settings._ParseJsonString(userProfiles, false); - VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them"); - settings._LoadDynamicProfiles(); - VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + CascadiaSettings settings{ til::u8u16(settings0String) }; - VERIFY_ARE_EQUAL(1111, settings._profiles.at(0)._historySize); - VERIFY_ARE_EQUAL(2222, settings._profiles.at(1)._historySize); - VERIFY_ARE_EQUAL(3333, settings._profiles.at(2)._historySize); + VERIFY_ARE_EQUAL(6u, settings.ActiveProfiles().Size()); + VERIFY_ARE_EQUAL(2u, settings.GlobalSettings().ColorSchemes().Size()); - settings._ApplyDefaultsFromUserSettings(); - - VERIFY_ARE_EQUAL(1234, settings._profiles.at(0)._historySize); - VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); - VERIFY_ARE_EQUAL(1234, settings._profiles.at(2)._historySize); - - settings.LayerJson(settings._userSettings); - VERIFY_ARE_EQUAL(4u, settings._profiles.size()); - - VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); - VERIFY_IS_FALSE(settings._profiles.at(3)._source.has_value()); - - VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value()); - VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value()); - VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value()); - - VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); - VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); - - VERIFY_ARE_EQUAL(guid1, settings._profiles.at(0)._guid.value()); - VERIFY_ARE_EQUAL(guid1, settings._profiles.at(1)._guid.value()); - VERIFY_ARE_EQUAL(guid2, settings._profiles.at(2)._guid.value()); - - VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name); - VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); - VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(2)._name); - VERIFY_ARE_EQUAL(L"profile2FromUserSettings", settings._profiles.at(3)._name); + auto createTerminalSettings = [&](const auto& profile, const auto& schemes) { + auto terminalSettings{ winrt::make_self() }; + terminalSettings->_ApplyProfileSettings(profile, schemes); + return terminalSettings; + }; - Log::Comment(NoThrowString().Format( - L"This is the real meat of the test: The two dynamic profiles that " - L"_didn't_ have historySize set in the userSettings should have " - L"1234 as their historySize(from the defaultSettings).The other two" - L" profiles should have their custom historySize value.")); - - VERIFY_ARE_EQUAL(1234, settings._profiles.at(0)._historySize); - VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); - VERIFY_ARE_EQUAL(4444, settings._profiles.at(2)._historySize); - VERIFY_ARE_EQUAL(5555, settings._profiles.at(3)._historySize); + auto terminalSettings0 = createTerminalSettings(settings.ActiveProfiles().GetAt(0), settings.GlobalSettings().ColorSchemes()); + auto terminalSettings1 = createTerminalSettings(settings.ActiveProfiles().GetAt(1), settings.GlobalSettings().ColorSchemes()); + auto terminalSettings2 = createTerminalSettings(settings.ActiveProfiles().GetAt(2), settings.GlobalSettings().ColorSchemes()); + auto terminalSettings3 = createTerminalSettings(settings.ActiveProfiles().GetAt(3), settings.GlobalSettings().ColorSchemes()); + auto terminalSettings4 = createTerminalSettings(settings.ActiveProfiles().GetAt(4), settings.GlobalSettings().ColorSchemes()); + auto terminalSettings5 = createTerminalSettings(settings.ActiveProfiles().GetAt(5), settings.GlobalSettings().ColorSchemes()); + + VERIFY_ARE_EQUAL(ARGB(0, 0x12, 0x34, 0x56), terminalSettings0->CursorColor()); // from color scheme + VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings1->CursorColor()); // default + VERIFY_ARE_EQUAL(ARGB(0, 0x23, 0x45, 0x67), terminalSettings2->CursorColor()); // from profile (trumps color scheme) + VERIFY_ARE_EQUAL(ARGB(0, 0x34, 0x56, 0x78), terminalSettings3->CursorColor()); // from profile (not set in color scheme) + VERIFY_ARE_EQUAL(ARGB(0, 0x45, 0x67, 0x89), terminalSettings4->CursorColor()); // from profile (no color scheme) + VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings5->CursorColor()); // default } - void SettingsTests::TestTerminalArgsForBinding() + void SettingsTests::TestIterateCommands() { + // For this test, put an iterable command with a given `name`, + // containing a ${profile.name} to replace. When we expand it, it should + // have created one command for each profile. + const std::string settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", @@ -1726,660 +592,974 @@ namespace TerminalAppLocalTests "commandline": "wsl.exe" } ], - "keybindings": [ - { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" } }, - { "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "vertical", "profile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } }, - { "keys": ["ctrl+c"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile1" } }, - { "keys": ["ctrl+d"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile2" } }, - { "keys": ["ctrl+e"], "command": { "action": "splitPane", "split": "horizontal", "commandline": "foo.exe" } }, - { "keys": ["ctrl+f"], "command": { "action": "splitPane", "split": "horizontal", "profile": "profile1", "commandline": "foo.exe" } }, - { "keys": ["ctrl+g"], "command": { "action": "newTab" } }, - { "keys": ["ctrl+h"], "command": { "action": "newTab", "startingDirectory": "c:\\foo" } }, - { "keys": ["ctrl+i"], "command": { "action": "newTab", "profile": "profile2", "startingDirectory": "c:\\foo" } }, - { "keys": ["ctrl+j"], "command": { "action": "newTab", "tabTitle": "bar" } }, - { "keys": ["ctrl+k"], "command": { "action": "newTab", "profile": "profile2", "tabTitle": "bar" } }, - { "keys": ["ctrl+l"], "command": { "action": "newTab", "profile": "profile1", "tabTitle": "bar", "startingDirectory": "c:\\foo", "commandline":"foo.exe" } } - ] + "actions": [ + { + "name": "iterable command ${profile.name}", + "iterateOn": "profiles", + "command": { "action": "splitPane", "profile": "${profile.name}" } + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - const auto guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); - const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid0 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - settings._ValidateSettings(); + CascadiaSettings settings{ til::u8u16(settingsJson) }; - auto appKeyBindings = settings._globals._keybindings; - VERIFY_ARE_EQUAL(3u, settings.GetProfiles().size()); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); - const auto profile2Guid = settings._profiles.at(2).GetGuid(); - VERIFY_ARE_NOT_EQUAL(GUID{ 0 }, profile2Guid); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - VERIFY_ARE_EQUAL(12u, appKeyBindings->_keyShortcuts.size()); + auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); { - KeyChord kc{ true, false, false, static_cast('A') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = commands.Lookup(L"iterable command ${profile.name}"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid0, guid); - VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } + + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); + { - KeyChord kc{ true, false, false, static_cast('B') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = expandedCommands.Lookup(L"iterable command profile0"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid1, guid); - VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); } + { - KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = expandedCommands.Lookup(L"iterable command profile1"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid1, guid); - VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); } + { - KeyChord kc{ true, false, false, static_cast('D') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = expandedCommands.Lookup(L"iterable command profile2"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(profile2Guid, guid); - VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); } + } + + void SettingsTests::TestIterateOnGeneratedNamedCommands() + { + // For this test, put an iterable command without a given `name` to + // replace. When we expand it, it should still work. + + const std::string settingsJson{ R"( { - KeyChord kc{ true, false, false, static_cast('E') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "iterateOn": "profiles", + "command": { "action": "splitPane", "profile": "${profile.name}" } + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. + })" }; + + const auto guid0 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + + CascadiaSettings settings{ til::u8u16(settingsJson) }; + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); + + auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + { + auto command = commands.Lookup(L"Split pane, profile: ${profile.name}"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid0, guid); - VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } + + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); + { - KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = expandedCommands.Lookup(L"Split pane, profile: profile0"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid1, guid); - VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); } + { - KeyChord kc{ true, false, false, static_cast('G') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + auto command = expandedCommands.Lookup(L"Split pane, profile: profile1"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid0, guid); - VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); } + { - KeyChord kc{ true, false, false, static_cast('H') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + auto command = expandedCommands.Lookup(L"Split pane, profile: profile2"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid0, guid); - VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); } + } + + void SettingsTests::TestIterateOnBadJson() + { + // For this test, put an iterable command with a profile name that would + // cause bad json to be filled in. Something like a profile with a name + // of "Foo\"", so the trailing '"' might break the json parsing. + + const std::string settingsJson{ R"( { - KeyChord kc{ true, false, false, static_cast('I') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1\"", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "name": "iterable command ${profile.name}", + "iterateOn": "profiles", + "command": { "action": "splitPane", "profile": "${profile.name}" } + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. + })" }; + + const auto guid0 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = ::Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + + CascadiaSettings settings{ til::u8u16(settingsJson) }; + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); + + auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); + + { + auto command = commands.Lookup(L"iterable command ${profile.name}"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); - VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(profile2Guid, guid); - VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); - VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } + + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); + { - KeyChord kc{ true, false, false, static_cast('J') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + auto command = expandedCommands.Lookup(L"iterable command profile0"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid0, guid); - VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile0", realArgs.TerminalArgs().Profile()); } + { - KeyChord kc{ true, false, false, static_cast('K') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + auto command = expandedCommands.Lookup(L"iterable command profile1\""); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); - VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(profile2Guid, guid); - VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); - VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + VERIFY_ARE_EQUAL(L"profile1\"", realArgs.TerminalArgs().Profile()); } + { - KeyChord kc{ true, false, false, static_cast('L') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); + auto command = expandedCommands.Lookup(L"iterable command profile2"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); - VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); - VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); - VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); - VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); - - const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); - VERIFY_ARE_EQUAL(guid1, guid); - VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); - VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); - VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); - VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); } } - void SettingsTests::FindMissingProfile() + void SettingsTests::TestNestedCommands() { - // Test that CascadiaSettings::FindProfile returns null for a GUID that - // doesn't exist - const std::string settingsString{ R"( + // This test checks a nested command. + // The commands should look like: + // + // + // └─ Connect to ssh... + // ├─ first.com + // └─ second.com + + const std::string settingsJson{ R"( { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" }, { - "name" : "profile1", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" } - ] + ], + "actions": [ + { + "name": "Connect to ssh...", + "commands": [ + { + "name": "first.com", + "command": { "action": "newTab", "commandline": "ssh me@first.com" } + }, + { + "name": "second.com", + "command": { "action": "newTab", "commandline": "ssh me@second.com" } + } + ] + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - const auto settingsJsonObj = VerifyParseSucceeded(settingsString); - auto settings = CascadiaSettings::FromJson(settingsJsonObj); - const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + CascadiaSettings settings{ til::u8u16(settingsJson) }; - const Profile* const profile1 = settings->FindProfile(guid1); - const Profile* const profile2 = settings->FindProfile(guid2); - const Profile* const profile3 = settings->FindProfile(guid3); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - VERIFY_IS_NOT_NULL(profile1); - VERIFY_IS_NOT_NULL(profile2); - VERIFY_IS_NULL(profile3); + auto commands = settings.GlobalSettings().Commands(); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); - VERIFY_ARE_EQUAL(L"profile0", profile1->GetName()); - VERIFY_ARE_EQUAL(L"profile1", profile2->GetName()); - } + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - void SettingsTests::MakeSettingsForProfileThatDoesntExist() - { - // Test that MakeSettings throws when the GUID doesn't exist - const std::string settingsString{ R"( - { - "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "historySize": 1 - }, - { - "name" : "profile1", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - "historySize": 2 - } - ] - })" }; - const auto settingsJsonObj = VerifyParseSucceeded(settingsString); - auto settings = CascadiaSettings::FromJson(settingsJsonObj); - settings->_ResolveDefaultProfile(); + auto rootCommand = expandedCommands.Lookup(L"Connect to ssh..."); + VERIFY_IS_NOT_NULL(rootCommand); + auto rootActionAndArgs = rootCommand.Action(); + VERIFY_IS_NULL(rootActionAndArgs); - const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + VERIFY_ARE_EQUAL(2u, rootCommand.NestedCommands().Size()); - try - { - auto terminalSettings = settings->BuildSettings(guid1); - VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); - VERIFY_ARE_EQUAL(1, terminalSettings.HistorySize()); - } - catch (...) { - VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); - } + winrt::hstring commandName{ L"first.com" }; + auto command = rootCommand.NestedCommands().Lookup(commandName); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); - try - { - auto terminalSettings = settings->BuildSettings(guid2); - VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings); - VERIFY_ARE_EQUAL(2, terminalSettings.HistorySize()); + VERIFY_IS_FALSE(command.HasNestedCommands()); } - catch (...) { - VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); - } - - VERIFY_THROWS(auto terminalSettings = settings->BuildSettings(guid3), wil::ResultException, L"This call to BuildSettings should fail"); + winrt::hstring commandName{ L"second.com" }; + auto command = rootCommand.NestedCommands().Lookup(commandName); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); - try - { - const auto [guid, termSettings] = settings->BuildSettings(nullptr); - VERIFY_ARE_NOT_EQUAL(nullptr, termSettings); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); - } - catch (...) - { - VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); + VERIFY_IS_FALSE(command.HasNestedCommands()); } } - void SettingsTests::MakeSettingsForDefaultProfileThatDoesntExist() + void SettingsTests::TestNestedInNestedCommand() { - // Test that MakeSettings _doesnt_ throw when we load settings with a - // defaultProfile that's not in the list, we validate the settings, and - // then call MakeSettings(nullopt). The validation should ensure that - // the default profile is something reasonable - const std::string settingsString{ R"( + // This test checks a nested command that includes nested commands. + // The commands should look like: + // + // + // └─ grandparent + // └─ parent + // ├─ child1 + // └─ child2 + + const std::string settingsJson{ R"( { - "defaultProfile": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0", + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "historySize": 1 + "historySize": 2, + "commandline": "pwsh.exe" }, { - "name" : "profile1", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - "historySize": 2 + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" } - ] + ], + "actions": [ + { + "name": "grandparent", + "commands": [ + { + "name": "parent", + "commands": [ + { + "name": "child1", + "command": { "action": "newTab", "commandline": "ssh me@first.com" } + }, + { + "name": "child2", + "command": { "action": "newTab", "commandline": "ssh me@second.com" } + } + ] + }, + ] + }, + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - const auto settingsJsonObj = VerifyParseSucceeded(settingsString); - auto settings = CascadiaSettings::FromJson(settingsJsonObj); - settings->_ValidateSettings(); - VERIFY_ARE_EQUAL(2u, settings->_warnings.size()); - VERIFY_ARE_EQUAL(2u, settings->_profiles.size()); - VERIFY_ARE_EQUAL(settings->_globals.DefaultProfile(), settings->_profiles.at(0).GetGuid()); - try + CascadiaSettings settings{ til::u8u16(settingsJson) }; + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); + + auto commands = settings.GlobalSettings().Commands(); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); + + auto grandparentCommand = expandedCommands.Lookup(L"grandparent"); + VERIFY_IS_NOT_NULL(grandparentCommand); + auto grandparentActionAndArgs = grandparentCommand.Action(); + VERIFY_IS_NULL(grandparentActionAndArgs); + + VERIFY_ARE_EQUAL(1u, grandparentCommand.NestedCommands().Size()); + + winrt::hstring parentName{ L"parent" }; + auto parent = grandparentCommand.NestedCommands().Lookup(parentName); + VERIFY_IS_NOT_NULL(parent); + auto parentActionAndArgs = parent.Action(); + VERIFY_IS_NULL(parentActionAndArgs); + + VERIFY_ARE_EQUAL(2u, parent.NestedCommands().Size()); { - const auto [guid, termSettings] = settings->BuildSettings(nullptr); - VERIFY_ARE_NOT_EQUAL(nullptr, termSettings); - VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + winrt::hstring childName{ L"child1" }; + auto child = parent.NestedCommands().Lookup(childName); + VERIFY_IS_NOT_NULL(child); + auto childActionAndArgs = child.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"ssh me@first.com", realArgs.TerminalArgs().Commandline()); + + VERIFY_IS_FALSE(child.HasNestedCommands()); } - catch (...) { - VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed"); + winrt::hstring childName{ L"child2" }; + auto child = parent.NestedCommands().Lookup(childName); + VERIFY_IS_NOT_NULL(child); + auto childActionAndArgs = child.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"ssh me@second.com", realArgs.TerminalArgs().Commandline()); + + VERIFY_IS_FALSE(child.HasNestedCommands()); } } - void SettingsTests::TestLayerProfileOnColorScheme() + void SettingsTests::TestNestedInIterableCommand() { - Log::Comment(NoThrowString().Format( - L"Ensure that setting (or not) a property in the profile that should override a property of the color scheme works correctly.")); + // This test checks a iterable command that includes a nested command. + // The commands should look like: + // + // + // ├─ profile0... + // | ├─ Split pane, profile: profile0 + // | ├─ Split pane, direction: vertical, profile: profile0 + // | └─ Split pane, direction: horizontal, profile: profile0 + // ├─ profile1... + // | ├─Split pane, profile: profile1 + // | ├─Split pane, direction: vertical, profile: profile1 + // | └─Split pane, direction: horizontal, profile: profile1 + // └─ profile2... + // ├─ Split pane, profile: profile2 + // ├─ Split pane, direction: vertical, profile: profile2 + // └─ Split pane, direction: horizontal, profile: profile2 - const std::string settings0String{ R"( + const std::string settingsJson{ R"( { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0", - "colorScheme": "schemeWithCursorColor" - }, - { - "name" : "profile1", - "colorScheme": "schemeWithoutCursorColor" - }, - { - "name" : "profile2", - "colorScheme": "schemeWithCursorColor", - "cursorColor": "#234567" - }, - { - "name" : "profile3", - "colorScheme": "schemeWithoutCursorColor", - "cursorColor": "#345678" + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" }, { - "name" : "profile4", - "cursorColor": "#456789" + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" }, { - "name" : "profile5" + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" } ], - "schemes": [ - { - "name": "schemeWithCursorColor", - "cursorColor": "#123456" - }, - { - "name": "schemeWithoutCursorColor" + "actions": [ + { + "iterateOn": "profiles", + "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" } } + ] } - ] + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - VerifyParseSucceeded(settings0String); + CascadiaSettings settings{ til::u8u16(settingsJson) }; + + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - CascadiaSettings settings; - settings._ParseJsonString(settings0String, false); - settings.LayerJson(settings._userSettings); + auto commands = settings.GlobalSettings().Commands(); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); - VERIFY_ARE_EQUAL(6u, settings._profiles.size()); - VERIFY_ARE_EQUAL(2u, settings._globals._colorSchemes.size()); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); - auto terminalSettings0 = settings._profiles[0].CreateTerminalSettings(settings._globals._colorSchemes); - auto terminalSettings1 = settings._profiles[1].CreateTerminalSettings(settings._globals._colorSchemes); - auto terminalSettings2 = settings._profiles[2].CreateTerminalSettings(settings._globals._colorSchemes); - auto terminalSettings3 = settings._profiles[3].CreateTerminalSettings(settings._globals._colorSchemes); - auto terminalSettings4 = settings._profiles[4].CreateTerminalSettings(settings._globals._colorSchemes); - auto terminalSettings5 = settings._profiles[5].CreateTerminalSettings(settings._globals._colorSchemes); + VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); + + for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) + { + winrt::hstring commandName{ name + L"..." }; + auto command = expandedCommands.Lookup(commandName); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NULL(actionAndArgs); - VERIFY_ARE_EQUAL(ARGB(0, 0x12, 0x34, 0x56), terminalSettings0.CursorColor()); // from color scheme - VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings1.CursorColor()); // default - VERIFY_ARE_EQUAL(ARGB(0, 0x23, 0x45, 0x67), terminalSettings2.CursorColor()); // from profile (trumps color scheme) - VERIFY_ARE_EQUAL(ARGB(0, 0x34, 0x56, 0x78), terminalSettings3.CursorColor()); // from profile (not set in color scheme) - VERIFY_ARE_EQUAL(ARGB(0, 0x45, 0x67, 0x89), terminalSettings4.CursorColor()); // from profile (no color scheme) - VERIFY_ARE_EQUAL(DEFAULT_CURSOR_COLOR, terminalSettings5.CursorColor()); // default + VERIFY_IS_TRUE(command.HasNestedCommands()); + VERIFY_ARE_EQUAL(3u, command.NestedCommands().Size()); + _logCommandNames(command.NestedCommands()); + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, split: horizontal, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, split: vertical, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } + } } - void SettingsTests::ValidateKeybindingsWarnings() + void SettingsTests::TestIterableInNestedCommand() { - const std::string badSettings{ R"( + // This test checks a nested command that includes an iterable command. + // The commands should look like: + // + // + // └─ New Tab With Profile... + // ├─ Profile 1 + // ├─ Profile 2 + // └─ Profile 3 + + const std::string settingsJson{ R"( { - "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" }, { - "name" : "profile1", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" } ], - "keybindings": [ - { "command": { "action": "splitPane", "split":"auto" }, "keys": [ "ctrl+alt+t", "ctrl+a" ] }, - { "command": { "action": "moveFocus" }, "keys": [ "ctrl+a" ] }, - { "command": { "action": "resizePane" }, "keys": [ "ctrl+b" ] } - ] + "actions": [ + { + "name": "New Tab With Profile...", + "commands": [ + { + "iterateOn": "profiles", + "command": { "action": "newTab", "profile": "${profile.name}" } + } + ] + } + ], + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - const auto settingsObject = VerifyParseSucceeded(badSettings); - auto settings = CascadiaSettings::FromJson(settingsObject); + CascadiaSettings settings{ til::u8u16(settingsJson) }; - VERIFY_ARE_EQUAL(0u, settings->_globals._keybindings->_keyShortcuts.size()); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - VERIFY_ARE_EQUAL(3u, settings->_globals._keybindingsWarnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::TooManyKeysForChord, settings->_globals._keybindingsWarnings.at(0)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_globals._keybindingsWarnings.at(1)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_globals._keybindingsWarnings.at(2)); + auto commands = settings.GlobalSettings().Commands(); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); - settings->_ValidateKeybindings(); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - VERIFY_ARE_EQUAL(4u, settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->_warnings.at(0)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::TooManyKeysForChord, settings->_warnings.at(1)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(2)); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(3)); - } + auto rootCommand = expandedCommands.Lookup(L"New Tab With Profile..."); + VERIFY_IS_NOT_NULL(rootCommand); + auto rootActionAndArgs = rootCommand.Action(); + VERIFY_IS_NULL(rootActionAndArgs); - void SettingsTests::ValidateExecuteCommandlineWarning() - { - Log::Comment(L"This test is affected by GH#6949, so we're just skipping it for now."); - Log::Result(WEX::Logging::TestResults::Skipped); - return; - - // const std::string badSettings{ R"( - // { - // "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - // "profiles": [ - // { - // "name" : "profile0", - // "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - // }, - // { - // "name" : "profile1", - // "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - // } - // ], - // "keybindings": [ - // { "name":null, "command": { "action": "wt" }, "keys": [ "ctrl+a" ] }, - // { "name":null, "command": { "action": "wt", "commandline":"" }, "keys": [ "ctrl+b" ] }, - // { "name":null, "command": { "action": "wt", "commandline":null }, "keys": [ "ctrl+c" ] } - // ] - // })" }; - - // const auto settingsObject = VerifyParseSucceeded(badSettings); - - // auto settings = CascadiaSettings::FromJson(settingsObject); - - // VERIFY_ARE_EQUAL(0u, settings->_globals._keybindings->_keyShortcuts.size()); - - // for (const auto& warning : settings->_globals._keybindingsWarnings) - // { - // Log::Comment(NoThrowString().Format( - // L"warning:%d", warning)); - // } - // VERIFY_ARE_EQUAL(3u, settings->_globals._keybindingsWarnings.size()); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_globals._keybindingsWarnings.at(0)); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_globals._keybindingsWarnings.at(1)); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_globals._keybindingsWarnings.at(2)); - - // settings->_ValidateKeybindings(); - - // VERIFY_ARE_EQUAL(4u, settings->_warnings.size()); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->_warnings.at(0)); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(1)); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(2)); - // VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(3)); - } + VERIFY_ARE_EQUAL(3u, rootCommand.NestedCommands().Size()); + + for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) + { + winrt::hstring commandName{ fmt::format(L"New tab, profile: {}", name) }; + auto command = rootCommand.NestedCommands().Lookup(commandName); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); - void SettingsTests::ValidateLegacyGlobalsWarning() + VERIFY_IS_FALSE(command.HasNestedCommands()); + } + } + void SettingsTests::TestMixedNestedAndIterableCommand() { - const std::string badSettings{ R"( + // This test checks a nested commands that includes an iterable command + // that includes a nested command. + // The commands should look like: + // + // + // └─ New Pane... + // ├─ profile0... + // | ├─ Split automatically + // | ├─ Split vertically + // | └─ Split horizontally + // ├─ profile1... + // | ├─ Split automatically + // | ├─ Split vertically + // | └─ Split horizontally + // └─ profile2... + // ├─ Split automatically + // ├─ Split vertically + // └─ Split horizontally + + const std::string settingsJson{ R"( { - "globals": {}, - "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" }, { - "name" : "profile1", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "actions": [ + { + "name": "New Pane...", + "commands": [ + { + "iterateOn": "profiles", + "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" } } + ] + } + ] } ], - "keybindings": [] + "schemes": [ { "name": "Campbell" } ] // This is included here to prevent settings validation errors. })" }; - // Create the default settings - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); + CascadiaSettings settings{ til::u8u16(settingsJson) }; - settings._ValidateNoGlobalsKey(); - VERIFY_ARE_EQUAL(0u, settings._warnings.size()); + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - // Now layer on the user's settings - settings._ParseJsonString(badSettings, false); - settings.LayerJson(settings._userSettings); + auto commands = settings.GlobalSettings().Commands(); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); - settings._ValidateNoGlobalsKey(); - VERIFY_ARE_EQUAL(1u, settings._warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::LegacyGlobalsProperty, settings._warnings.at(0)); - } + VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - void SettingsTests::TestTrailingCommas() - { - const std::string badSettings{ R"( - { - "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - "profiles": [ - { - "name" : "profile0", - "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" - }, - { - "name" : "profile1", - "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" - }, - ], - "keybindings": [], - })" }; + auto rootCommand = expandedCommands.Lookup(L"New Pane..."); + VERIFY_IS_NOT_NULL(rootCommand); + auto rootActionAndArgs = rootCommand.Action(); + VERIFY_IS_NULL(rootActionAndArgs); - // Create the default settings - CascadiaSettings settings; - settings._ParseJsonString(DefaultJson, true); - settings.LayerJson(settings._defaultSettings); + VERIFY_ARE_EQUAL(3u, rootCommand.NestedCommands().Size()); - // Now layer on the user's settings - try + for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) { - settings._ParseJsonString(badSettings, false); - settings.LayerJson(settings._userSettings); - } - catch (...) - { - VERIFY_IS_TRUE(false, L"This call to LayerJson should succeed, even with the trailing comma"); + winrt::hstring commandName{ name + L"..." }; + auto command = rootCommand.NestedCommands().Lookup(commandName); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NULL(actionAndArgs); + + VERIFY_IS_TRUE(command.HasNestedCommands()); + VERIFY_ARE_EQUAL(3u, command.NestedCommands().Size()); + + _logCommandNames(command.NestedCommands()); + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, split: horizontal, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } + { + winrt::hstring childCommandName{ fmt::format(L"Split pane, split: vertical, profile: {}", name) }; + auto childCommand = command.NestedCommands().Lookup(childCommandName); + VERIFY_IS_NOT_NULL(childCommand); + auto childActionAndArgs = childCommand.Action(); + VERIFY_IS_NOT_NULL(childActionAndArgs); + + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, childActionAndArgs.Action()); + const auto& realArgs = childActionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(name, realArgs.TerminalArgs().Profile()); + + VERIFY_IS_FALSE(childCommand.HasNestedCommands()); + } } } - void SettingsTests::TestCommandsAndKeybindings() + void SettingsTests::TestIterableColorSchemeCommands() { + // For this test, put an iterable command with a given `name`, + // containing a ${profile.name} to replace. When we expand it, it should + // have created one command for each profile. + const std::string settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", @@ -2402,133 +1582,62 @@ namespace TerminalAppLocalTests "commandline": "wsl.exe" } ], - "bindings": [ - { "keys": "ctrl+a", "command": { "action": "splitPane", "split": "vertical" } }, - { "name": "ctrl+b", "command": { "action": "splitPane", "split": "vertical" } }, - { "keys": "ctrl+c", "name": "ctrl+c", "command": { "action": "splitPane", "split": "vertical" } }, - { "keys": "ctrl+d", "command": { "action": "splitPane", "split": "vertical" } }, - { "keys": "ctrl+e", "command": { "action": "splitPane", "split": "horizontal" } }, - { "keys": "ctrl+f", "name":null, "command": { "action": "splitPane", "split": "horizontal" } } + "schemes": [ + { "name": "scheme_0" }, + { "name": "scheme_1" }, + { "name": "scheme_2" }, + ], + "actions": [ + { + "name": "iterable command ${scheme.name}", + "iterateOn": "schemes", + "command": { "action": "splitPane", "profile": "${scheme.name}" } + }, ] })" }; - const auto guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); - const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - - VerifyParseSucceeded(settingsJson); - CascadiaSettings settings{}; - settings._ParseJsonString(settingsJson, false); - settings.LayerJson(settings._userSettings); - settings._ValidateSettings(); - - VERIFY_ARE_EQUAL(3u, settings.GetProfiles().size()); + CascadiaSettings settings{ til::u8u16(settingsJson) }; - const auto profile2Guid = settings._profiles.at(2).GetGuid(); - VERIFY_ARE_NOT_EQUAL(GUID{ 0 }, profile2Guid); + // Since at least one profile does not reference a color scheme, + // we add a warning saying "the color scheme is unknown" + VERIFY_ARE_EQUAL(1u, settings.Warnings().Size()); - auto appKeyBindings = settings._globals._keybindings; - VERIFY_ARE_EQUAL(5u, appKeyBindings->_keyShortcuts.size()); + VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - // A/D, B, C, E will be in the list of commands, for 4 total. - // * A and D share the same name, so they'll only generate a single action. - // * F's name is set manually to `null` - auto commands = settings._globals.GetCommands(); - VERIFY_ARE_EQUAL(4u, commands.size()); + auto commands = settings.GlobalSettings().Commands(); + VERIFY_ARE_EQUAL(1u, commands.Size()); { - KeyChord kc{ true, false, false, static_cast('A') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + auto command = commands.Lookup(L"iterable command ${scheme.name}"); + VERIFY_IS_NOT_NULL(command); + auto actionAndArgs = command.Action(); + VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"${scheme.name}", realArgs.TerminalArgs().Profile()); } - Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); + auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(commands, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); + _logCommandNames(expandedCommands.GetView()); - { - KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); - VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - } - { - KeyChord kc{ true, false, false, static_cast('D') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); - VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - } - { - KeyChord kc{ true, false, false, static_cast('E') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); - VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - } - { - KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); - VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - } + // This is the same warning as above + VERIFY_ARE_EQUAL(1u, settings.Warnings().Size()); + VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); - Log::Comment(L"Now verify the commands"); + // Yes, this test is testing splitPane with profiles named after each + // color scheme. These would obviously not work in real life, they're + // just easy tests to write. { - auto command = commands.at(L"Split pane, direction: Vertical"); - VERIFY_IS_NOT_NULL(command); - auto actionAndArgs = command.Action(); - VERIFY_IS_NOT_NULL(actionAndArgs); - VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); - const auto& realArgs = actionAndArgs.Args().try_as(); - VERIFY_IS_NOT_NULL(realArgs); - // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); - VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); - } - { - auto command = commands.at(L"ctrl+b"); + auto command = expandedCommands.Lookup(L"iterable command scheme_0"); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.Action(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -2536,15 +1645,17 @@ namespace TerminalAppLocalTests const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"scheme_0", realArgs.TerminalArgs().Profile()); } + { - auto command = commands.at(L"ctrl+c"); + auto command = expandedCommands.Lookup(L"iterable command scheme_1"); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.Action(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -2552,15 +1663,17 @@ namespace TerminalAppLocalTests const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"scheme_1", realArgs.TerminalArgs().Profile()); } + { - auto command = commands.at(L"Split pane, direction: Horizontal"); + auto command = expandedCommands.Lookup(L"iterable command scheme_2"); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.Action(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -2568,12 +1681,13 @@ namespace TerminalAppLocalTests const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); - VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"scheme_2", realArgs.TerminalArgs().Profile()); } } diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index b515eacd93e..bb5003f1757 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -7,17 +7,33 @@ #include "../TerminalApp/MinMaxCloseControl.h" #include "../TerminalApp/TabRowControl.h" #include "../TerminalApp/ShortcutActionDispatch.h" -#include "../TerminalApp/Tab.h" +#include "../TerminalApp/TerminalTab.h" +#include "../TerminalApp/CommandPalette.h" #include "../CppWinrtTailored.h" -#include "JsonTestClass.h" using namespace Microsoft::Console; using namespace TerminalApp; using namespace winrt::TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings::Model; + using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; + using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Text; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} namespace TerminalAppLocalTests { @@ -26,7 +42,7 @@ namespace TerminalAppLocalTests // 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 TabTests : public JsonTestClass + class TabTests { // For this set of tests, we need to activate some XAML content. For // release builds, the application runs as a centennial application, @@ -51,7 +67,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TryCreateSettingsType); TEST_METHOD(TryCreateConnectionType); TEST_METHOD(TryCreateXamlObjects); - TEST_METHOD(TryCreateTab); + + TEST_METHOD(TryInitializePage); TEST_METHOD(CreateSimpleTerminalXamlType); TEST_METHOD(CreateTerminalMuxXamlType); @@ -61,17 +78,36 @@ namespace TerminalAppLocalTests TEST_METHOD(TryDuplicateBadTab); TEST_METHOD(TryDuplicateBadPane); + TEST_METHOD(TryZoomPane); + TEST_METHOD(MoveFocusFromZoomedPane); + TEST_METHOD(CloseZoomedPane); + + TEST_METHOD(NextMRUTab); + TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder); + TEST_CLASS_SETUP(ClassSetup) { - InitializeJsonReader(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { return true; } private: void _initializeTerminalPage(winrt::com_ptr& page, - std::shared_ptr initialSettings); + CascadiaSettings initialSettings); + winrt::com_ptr _commonSetup(); }; + template + void TestOnUIThread(const TFunction& function) + { + const auto result = RunOnUIThread(function); + VERIFY_SUCCEEDED(result); + } + void TabTests::EnsureTestsActivate() { // This test was originally used to ensure that XAML Islands was @@ -84,7 +120,7 @@ namespace TerminalAppLocalTests { // Verify we can create a WinRT type we authored // Just creating it is enough to know that everything is working. - winrt::Microsoft::Terminal::Settings::TerminalSettings settings; + TerminalSettings settings; VERIFY_IS_NOT_NULL(settings); auto oldFontSize = settings.FontSize(); settings.FontSize(oldFontSize + 5); @@ -125,35 +161,6 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } - void TabTests::TryCreateTab() - { - // If you leave the Tab ptr owned by the RunOnUIThread lambda, it - // will crash when the test tears down. Not totally clear why, but make - // sure it's owned outside the lambda - winrt::com_ptr newTab{ nullptr }; - - auto result = RunOnUIThread([&newTab]() { - // Try creating all of: - // 1. one of our pure c++ types (Profile) - // 2. one of our c++winrt types (TerminalSettings, EchoConnection) - // 3. one of our types that uses MUX/Xaml (TermControl). - // 4. one of our types that uses MUX/Xaml in this dll (Tab). - // Just creating all of them is enough to know that everything is working. - const auto profileGuid{ Utils::CreateGuid() }; - winrt::Microsoft::Terminal::Settings::TerminalSettings settings{}; - VERIFY_IS_NOT_NULL(settings); - winrt::Microsoft::Terminal::TerminalConnection::EchoConnection conn{}; - VERIFY_IS_NOT_NULL(conn); - winrt::Microsoft::Terminal::TerminalControl::TermControl term{ settings, conn }; - VERIFY_IS_NOT_NULL(term); - - newTab = winrt::make_self(profileGuid, term); - VERIFY_IS_NOT_NULL(newTab); - }); - - VERIFY_SUCCEEDED(result); - } - void TabTests::CreateSimpleTerminalXamlType() { winrt::com_ptr mmcc{ nullptr }; @@ -213,7 +220,7 @@ namespace TerminalAppLocalTests // Return Value: // - void TabTests::_initializeTerminalPage(winrt::com_ptr& page, - std::shared_ptr initialSettings) + CascadiaSettings initialSettings) { // This is super wacky, but we can't just initialize the // com_ptr in the lambda and assign it back out of @@ -269,236 +276,639 @@ namespace TerminalAppLocalTests // In the real app, this isn't a problem, but doesn't happen // reliably in the unit tests. Log::Comment(L"Ensure we set the first tab as the selected one."); - auto tab{ page->_GetStrongTabImpl(0) }; - page->_tabView.SelectedItem(tab->GetTabViewItem()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + page->_tabView.SelectedItem(tab->TabViewItem()); page->_UpdatedSelectedTab(0); }); VERIFY_SUCCEEDED(result); } + void TabTests::TryInitializePage() + { + // This is a very simple test to prove we can create settings and a + // TerminalPage and not only create them successfully, but also create a + // tab using those settings successfully. + + const std::string settingsJson0{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 1 + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] + })" }; + + CascadiaSettings settings0{ til::u8u16(settingsJson0) }; + VERIFY_IS_NOT_NULL(settings0); + + // This is super wacky, but we can't just initialize the + // com_ptr in the lambda and assign it back out of + // the lambda. We'll crash trying to get a weak_ref to the TerminalPage + // during TerminalPage::Create() below. + // + // Instead, create the winrt object, then get a com_ptr to the + // implementation _from_ the winrt object. This seems to work, even if + // it's weird. + winrt::com_ptr page{ nullptr }; + _initializeTerminalPage(page, settings0); + + auto result = RunOnUIThread([&page]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + }); + VERIFY_SUCCEEDED(result); + } + void TabTests::TryDuplicateBadTab() { - Log::Comment(L"This test regressed recently - it is temporarily disabled while GH#5169 is investigated"); - Log::Result(WEX::Logging::TestResults::Skipped); - return; - - // // * Create a tab with a profile with GUID 1 - // // * Reload the settings so that GUID 1 is no longer in the list of profiles - // // * Try calling _DuplicateTabViewItem on tab 1 - // // * No new tab should be created (and more importantly, the app should not crash) - // // - // // Created to test GH#2455 - - // const std::string settingsJson0{ R"( - // { - // "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "profiles": [ - // { - // "name" : "profile0", - // "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "historySize": 1 - // }, - // { - // "name" : "profile1", - // "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - // "historySize": 2 - // } - // ] - // })" }; - - // const std::string settingsJson1{ R"( - // { - // "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "profiles": [ - // { - // "name" : "profile1", - // "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - // "historySize": 2 - // } - // ] - // })" }; - - // VerifyParseSucceeded(settingsJson0); - // auto settings0 = std::make_shared(false); - // VERIFY_IS_NOT_NULL(settings0); - // settings0->_ParseJsonString(settingsJson0, false); - // settings0->LayerJson(settings0->_userSettings); - // settings0->_ValidateSettings(); - - // VerifyParseSucceeded(settingsJson1); - // auto settings1 = std::make_shared(false); - // VERIFY_IS_NOT_NULL(settings1); - // settings1->_ParseJsonString(settingsJson1, false); - // settings1->LayerJson(settings1->_userSettings); - // settings1->_ValidateSettings(); - - // const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - // const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - // const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); - - // // This is super wacky, but we can't just initialize the - // // com_ptr in the lambda and assign it back out of - // // the lambda. We'll crash trying to get a weak_ref to the TerminalPage - // // during TerminalPage::Create() below. - // // - // // Instead, create the winrt object, then get a com_ptr to the - // // implementation _from_ the winrt object. This seems to work, even if - // // it's weird. - // winrt::com_ptr page{ nullptr }; - // _initializeTerminalPage(page, settings0); - - // auto result = RunOnUIThread([&page]() { - // VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(L"Duplicate the first tab"); - // result = RunOnUIThread([&page]() { - // page->_DuplicateTabViewItem(); - // VERIFY_ARE_EQUAL(2u, page->_tabs.Size()); - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(NoThrowString().Format( - // L"Change the settings of the TerminalPage so the first profile is " - // L"no longer in the list of profiles")); - // result = RunOnUIThread([&page, settings1]() { - // page->_settings = settings1; - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(L"Duplicate the tab, and don't crash"); - // result = RunOnUIThread([&page]() { - // page->_DuplicateTabViewItem(); - // VERIFY_ARE_EQUAL(2u, page->_tabs.Size(), L"We should gracefully do nothing here - the profile no longer exists."); - // }); - // VERIFY_SUCCEEDED(result); + // * Create a tab with a profile with GUID 1 + // * Reload the settings so that GUID 1 is no longer in the list of profiles + // * Try calling _DuplicateTabViewItem on tab 1 + // * No new tab should be created (and more importantly, the app should not crash) + // + // Created to test GH#2455 + + const std::string settingsJson0{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 1 + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] + })" }; + + const std::string settingsJson1{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] + })" }; + + CascadiaSettings settings0{ til::u8u16(settingsJson0) }; + VERIFY_IS_NOT_NULL(settings0); + + CascadiaSettings settings1{ til::u8u16(settingsJson1) }; + VERIFY_IS_NOT_NULL(settings1); + + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + + // This is super wacky, but we can't just initialize the + // com_ptr in the lambda and assign it back out of + // the lambda. We'll crash trying to get a weak_ref to the TerminalPage + // during TerminalPage::Create() below. + // + // Instead, create the winrt object, then get a com_ptr to the + // implementation _from_ the winrt object. This seems to work, even if + // it's weird. + winrt::com_ptr page{ nullptr }; + _initializeTerminalPage(page, settings0); + + auto result = RunOnUIThread([&page]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Duplicate the first tab"); + result = RunOnUIThread([&page]() { + page->_DuplicateTabViewItem(); + VERIFY_ARE_EQUAL(2u, page->_tabs.Size()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(NoThrowString().Format( + L"Change the settings of the TerminalPage so the first profile is " + L"no longer in the list of profiles")); + result = RunOnUIThread([&page, settings1]() { + page->_settings = settings1; + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Duplicate the tab, and don't crash"); + result = RunOnUIThread([&page]() { + page->_DuplicateTabViewItem(); + VERIFY_ARE_EQUAL(2u, page->_tabs.Size(), L"We should gracefully do nothing here - the profile no longer exists."); + }); + VERIFY_SUCCEEDED(result); } void TabTests::TryDuplicateBadPane() { - Log::Comment(L"This test regressed recently - it is temporarily disabled while GH#5169 is investigated"); - Log::Result(WEX::Logging::TestResults::Skipped); - return; - - // // * Create a tab with a profile with GUID 1 - // // * Reload the settings so that GUID 1 is no longer in the list of profiles - // // * Try calling _SplitPane(Duplicate) on tab 1 - // // * No new pane should be created (and more importantly, the app should not crash) - // // - // // Created to test GH#2455 - - // const std::string settingsJson0{ R"( - // { - // "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "profiles": [ - // { - // "name" : "profile0", - // "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "historySize": 1 - // }, - // { - // "name" : "profile1", - // "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - // "historySize": 2 - // } - // ] - // })" }; - - // const std::string settingsJson1{ R"( - // { - // "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - // "profiles": [ - // { - // "name" : "profile1", - // "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", - // "historySize": 2 - // } - // ] - // })" }; - - // VerifyParseSucceeded(settingsJson0); - // auto settings0 = std::make_shared(false); - // VERIFY_IS_NOT_NULL(settings0); - // settings0->_ParseJsonString(settingsJson0, false); - // settings0->LayerJson(settings0->_userSettings); - // settings0->_ValidateSettings(); - - // VerifyParseSucceeded(settingsJson1); - // auto settings1 = std::make_shared(false); - // VERIFY_IS_NOT_NULL(settings1); - // settings1->_ParseJsonString(settingsJson1, false); - // settings1->LayerJson(settings1->_userSettings); - // settings1->_ValidateSettings(); - - // const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); - // const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); - // const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); - - // // This is super wacky, but we can't just initialize the - // // com_ptr in the lambda and assign it back out of - // // the lambda. We'll crash trying to get a weak_ref to the TerminalPage - // // during TerminalPage::Create() below. - // // - // // Instead, create the winrt object, then get a com_ptr to the - // // implementation _from_ the winrt object. This seems to work, even if - // // it's weird. - // winrt::com_ptr page{ nullptr }; - // _initializeTerminalPage(page, settings0); - - // auto result = RunOnUIThread([&page]() { - // VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); - // }); - // VERIFY_SUCCEEDED(result); - - // result = RunOnUIThread([&page]() { - // VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); - // auto tab = page->_GetStrongTabImpl(0); - // VERIFY_ARE_EQUAL(1, tab->_GetLeafPaneCount()); - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(NoThrowString().Format(L"Duplicate the first pane")); - // result = RunOnUIThread([&page]() { - // page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr); - - // VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); - // auto tab = page->_GetStrongTabImpl(0); - // VERIFY_ARE_EQUAL(2, tab->_GetLeafPaneCount()); - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(NoThrowString().Format( - // L"Change the settings of the TerminalPage so the first profile is " - // L"no longer in the list of profiles")); - // result = RunOnUIThread([&page, settings1]() { - // page->_settings = settings1; - // }); - // VERIFY_SUCCEEDED(result); - - // Log::Comment(NoThrowString().Format(L"Duplicate the pane, and don't crash")); - // result = RunOnUIThread([&page]() { - // page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr); - - // VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); - // auto tab = page->_GetStrongTabImpl(0); - // VERIFY_ARE_EQUAL(2, - // tab->_GetLeafPaneCount(), - // L"We should gracefully do nothing here - the profile no longer exists."); - // }); - // VERIFY_SUCCEEDED(result); - - // auto cleanup = wil::scope_exit([] { - // auto result = RunOnUIThread([]() { - // // There's something causing us to crash north of - // // TSFInputControl::NotifyEnter, or LayoutRequested. It's very - // // unclear what that issue is. Since these tests don't run in - // // CI, simply log a message so that the dev running these tests - // // knows it's expected. - // Log::Comment(L"This test often crashes on cleanup, even when it succeeds. If it succeeded, then crashes, that's okay."); - // }); - // VERIFY_SUCCEEDED(result); - // }); + // * Create a tab with a profile with GUID 1 + // * Reload the settings so that GUID 1 is no longer in the list of profiles + // * Try calling _SplitPane(Duplicate) on tab 1 + // * No new pane should be created (and more importantly, the app should not crash) + // + // Created to test GH#2455 + + const std::string settingsJson0{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 1 + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] + })" }; + + const std::string settingsJson1{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "historySize": 2 + } + ] + })" }; + + CascadiaSettings settings0{ til::u8u16(settingsJson0) }; + VERIFY_IS_NOT_NULL(settings0); + + CascadiaSettings settings1{ til::u8u16(settingsJson1) }; + VERIFY_IS_NOT_NULL(settings1); + + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + + // This is super wacky, but we can't just initialize the + // com_ptr in the lambda and assign it back out of + // the lambda. We'll crash trying to get a weak_ref to the TerminalPage + // during TerminalPage::Create() below. + // + // Instead, create the winrt object, then get a com_ptr to the + // implementation _from_ the winrt object. This seems to work, even if + // it's weird. + winrt::com_ptr page{ nullptr }; + _initializeTerminalPage(page, settings0); + + auto result = RunOnUIThread([&page]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + }); + VERIFY_SUCCEEDED(result); + + result = RunOnUIThread([&page]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(1, tab->GetLeafPaneCount()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(NoThrowString().Format(L"Duplicate the first pane")); + result = RunOnUIThread([&page]() { + page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr); + + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, tab->GetLeafPaneCount()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(NoThrowString().Format( + L"Change the settings of the TerminalPage so the first profile is " + L"no longer in the list of profiles")); + result = RunOnUIThread([&page, settings1]() { + page->_settings = settings1; + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(NoThrowString().Format(L"Duplicate the pane, and don't crash")); + result = RunOnUIThread([&page]() { + page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr); + + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + auto tab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, + tab->GetLeafPaneCount(), + L"We should gracefully do nothing here - the profile no longer exists."); + }); + VERIFY_SUCCEEDED(result); + + auto cleanup = wil::scope_exit([] { + auto result = RunOnUIThread([]() { + // There's something causing us to crash north of + // TSFInputControl::NotifyEnter, or LayoutRequested. It's very + // unclear what that issue is. Since these tests don't run in + // CI, simply log a message so that the dev running these tests + // knows it's expected. + Log::Comment(L"This test often crashes on cleanup, even when it succeeds. If it succeeded, then crashes, that's okay."); + }); + VERIFY_SUCCEEDED(result); + }); } + // Method Description: + // - This is a helper method for setting up a TerminalPage with some common + // settings, and creating the first tab. + // Arguments: + // - + // Return Value: + // - The initialized TerminalPage, ready to use. + winrt::com_ptr TabTests::_commonSetup() + { + const std::string settingsJson0{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "showTabsInTitlebar": false, + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "tabTitle" : "Profile 0", + "historySize": 1 + }, + { + "name" : "profile1", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "tabTitle" : "Profile 1", + "historySize": 2 + }, + { + "name" : "profile2", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "tabTitle" : "Profile 2", + "historySize": 3 + }, + { + "name" : "profile3", + "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}", + "tabTitle" : "Profile 3", + "historySize": 4 + } + ] + })" }; + + CascadiaSettings settings0{ til::u8u16(settingsJson0) }; + VERIFY_IS_NOT_NULL(settings0); + + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + // This is super wacky, but we can't just initialize the + // com_ptr in the lambda and assign it back out of + // the lambda. We'll crash trying to get a weak_ref to the TerminalPage + // during TerminalPage::Create() below. + // + // Instead, create the winrt object, then get a com_ptr to the + // implementation _from_ the winrt object. This seems to work, even if + // it's weird. + winrt::com_ptr page{ nullptr }; + _initializeTerminalPage(page, settings0); + + auto result = RunOnUIThread([&page]() { + VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + }); + VERIFY_SUCCEEDED(result); + + return page; + } + + void TabTests::TryZoomPane() + { + auto page = _commonSetup(); + + Log::Comment(L"Create a second pane"); + auto result = RunOnUIThread([&page]() { + SplitPaneArgs args{ SplitType::Duplicate }; + ActionEventArgs eventArgs{ args }; + // eventArgs.Args(args); + page->_HandleSplitPane(nullptr, eventArgs); + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Zoom in on the pane"); + result = RunOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleTogglePaneZoom(nullptr, eventArgs); + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_TRUE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Zoom out of the pane"); + result = RunOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleTogglePaneZoom(nullptr, eventArgs); + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + } + + void TabTests::MoveFocusFromZoomedPane() + { + auto page = _commonSetup(); + + Log::Comment(L"Create a second pane"); + auto result = RunOnUIThread([&page]() { + // Set up action + SplitPaneArgs args{ SplitType::Duplicate }; + ActionEventArgs eventArgs{ args }; + page->_HandleSplitPane(nullptr, eventArgs); + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Zoom in on the pane"); + result = RunOnUIThread([&page]() { + // Set up action + ActionEventArgs eventArgs{}; + + page->_HandleTogglePaneZoom(nullptr, eventArgs); + + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_TRUE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Move focus. This will cause us to un-zoom."); + result = RunOnUIThread([&page]() { + // Set up action + MoveFocusArgs args{ Direction::Left }; + ActionEventArgs eventArgs{ args }; + + page->_HandleMoveFocus(nullptr, eventArgs); + + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + } + + void TabTests::CloseZoomedPane() + { + auto page = _commonSetup(); + + Log::Comment(L"Create a second pane"); + auto result = RunOnUIThread([&page]() { + // Set up action + SplitPaneArgs args{ SplitType::Duplicate }; + ActionEventArgs eventArgs{ args }; + page->_HandleSplitPane(nullptr, eventArgs); + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Zoom in on the pane"); + result = RunOnUIThread([&page]() { + // Set up action + ActionEventArgs eventArgs{}; + + page->_HandleTogglePaneZoom(nullptr, eventArgs); + + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); + VERIFY_IS_TRUE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + Log::Comment(L"Close Pane. This should cause us to un-zoom, and remove the second pane from the tree"); + result = RunOnUIThread([&page]() { + // Set up action + ActionEventArgs eventArgs{}; + + page->_HandleClosePane(nullptr, eventArgs); + + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + + // Introduce a slight delay to let the events finish propagating + Sleep(250); + + Log::Comment(L"Check to ensure there's only one pane left."); + + result = RunOnUIThread([&page]() { + auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); + VERIFY_ARE_EQUAL(1, firstTab->GetLeafPaneCount()); + VERIFY_IS_FALSE(firstTab->IsZoomed()); + }); + VERIFY_SUCCEEDED(result); + } + + void TabTests::NextMRUTab() + { + // This is a test for GH#8025 - we want to make sure that we can do both + // in-order and MRU tab traversal, using the tab switcher and with the + // tab switcher disabled. + + auto page = _commonSetup(); + + Log::Comment(L"Create a second tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 1 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(2u, page->_tabs.Size()); + + Log::Comment(L"Create a third tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 2 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(3u, page->_tabs.Size()); + + Log::Comment(L"Create a fourth tab"); + TestOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 3 }; + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(4u, page->_tabs.Size()); + + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one"); + }); + + Log::Comment(L"Select the second tab"); + TestOnUIThread([&page]() { + page->_SelectTab(1); + }); + + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify the second tab is the focused one"); + }); + + Log::Comment(L"Change the tab switch order to MRU switching"); + TestOnUIThread([&page]() { + page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::MostRecentlyUsed); + }); + + Log::Comment(L"Switch to the next MRU tab, which is the fourth tab"); + TestOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleNextTab(nullptr, eventArgs); + }); + + Log::Comment(L"Sleep to let events propagate"); + Sleep(250); + + TestOnUIThread([&page]() { + Log::Comment(L"Hide the command palette, to confirm the selection"); + // 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); + }); + + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one"); + }); + + Log::Comment(L"Switch to the next MRU tab, which is the second tab"); + TestOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleNextTab(nullptr, eventArgs); + }); + + Log::Comment(L"Sleep to let events propagate"); + Sleep(250); + + TestOnUIThread([&page]() { + Log::Comment(L"Hide the command palette, to confirm the selection"); + // 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); + }); + + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify the second tab is the focused one"); + }); + + Log::Comment(L"Change the tab switch order to in-order switching"); + page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::InOrder); + + Log::Comment(L"Switch to the next in-order tab, which is the third tab"); + TestOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleNextTab(nullptr, eventArgs); + }); + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(2u, focusedIndex, L"Verify the third tab is the focused one"); + }); + + Log::Comment(L"Change the tab switch order to not use the tab switcher (which is in-order always)"); + page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::Disabled); + + Log::Comment(L"Switch to the next in-order tab, which is the fourth tab"); + TestOnUIThread([&page]() { + ActionEventArgs eventArgs{}; + page->_HandleNextTab(nullptr, eventArgs); + }); + TestOnUIThread([&page]() { + uint32_t focusedIndex = page->_GetFocusedTabIndex().value_or(-1); + VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one"); + }); + } + + void TabTests::VerifyCommandPaletteTabSwitcherOrder() + { + // This is a test for GH#8188 - we want to make sure that the order of tabs + // is preserved in the CommandPalette's TabSwitcher + + auto page = _commonSetup(); + + Log::Comment(L"Create 3 additional tabs"); + RunOnUIThread([&page]() { + NewTerminalArgs newTerminalArgs{ 1 }; + page->_OpenNewTab(newTerminalArgs); + page->_OpenNewTab(newTerminalArgs); + page->_OpenNewTab(newTerminalArgs); + }); + VERIFY_ARE_EQUAL(4u, page->_mruTabActions.Size()); + + Log::Comment(L"give alphabetical names to all switch tab actions"); + RunOnUIThread([&page]() { + page->_tabs.GetAt(0).SwitchToTabCommand().Name(L"a"); + page->_tabs.GetAt(1).SwitchToTabCommand().Name(L"b"); + page->_tabs.GetAt(2).SwitchToTabCommand().Name(L"c"); + page->_tabs.GetAt(3).SwitchToTabCommand().Name(L"d"); + }); + + Log::Comment(L"Change the tab switch order to MRU switching"); + TestOnUIThread([&page]() { + page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::MostRecentlyUsed); + }); + + Log::Comment(L"Select the tabs from 0 to 3"); + RunOnUIThread([&page]() { + page->_UpdatedSelectedTab(0); + page->_UpdatedSelectedTab(1); + page->_UpdatedSelectedTab(2); + page->_UpdatedSelectedTab(3); + }); + + VERIFY_ARE_EQUAL(4u, page->_mruTabActions.Size()); + VERIFY_ARE_EQUAL(L"d", page->_mruTabActions.GetAt(0).Name()); + VERIFY_ARE_EQUAL(L"c", page->_mruTabActions.GetAt(1).Name()); + VERIFY_ARE_EQUAL(L"b", page->_mruTabActions.GetAt(2).Name()); + VERIFY_ARE_EQUAL(L"a", page->_mruTabActions.GetAt(3).Name()); + + Log::Comment(L"Switch to the next MRU tab, which is the third tab"); + RunOnUIThread([&page]() { + page->_SelectNextTab(true); + }); + + const auto palette = winrt::get_self(page->CommandPalette()); + + VERIFY_ARE_EQUAL(1u, palette->_switcherStartIdx, L"Verify the index is 1 as we went right"); + VERIFY_ARE_EQUAL(implementation::CommandPaletteMode::TabSwitchMode, palette->_currentMode, L"Verify we are in the tab switcher mode"); + + Log::Comment(L"Verify command palette preserves MRU order of tabs"); + VERIFY_ARE_EQUAL(4u, palette->_filteredActions.Size()); + VERIFY_ARE_EQUAL(L"d", palette->_filteredActions.GetAt(0).Command().Name()); + VERIFY_ARE_EQUAL(L"c", palette->_filteredActions.GetAt(1).Command().Name()); + VERIFY_ARE_EQUAL(L"b", palette->_filteredActions.GetAt(2).Command().Name()); + VERIFY_ARE_EQUAL(L"a", palette->_filteredActions.GetAt(3).Command().Name()); + } } diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index 8faeef38f1d..0d89af423d4 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -20,19 +20,7 @@ DynamicLibrary 10.0.18362.0 10.0.18362.0 - - true - - - - - $(SolutionDir)bin\$(Platform)\$(Configuration)\$(ProjectName)\ - $(SolutionDir)obj\$(Platform)\$(Configuration)\$(ProjectName)\ + true @@ -49,7 +37,6 @@ - @@ -57,32 +44,24 @@ - - - - + Create - - - NotUsing - - + - - + + @@ -90,7 +69,7 @@ - ..;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\jsoncpp\json;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalApp\lib\Generated Files";%(AdditionalIncludeDirectories) + ..;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\jsoncpp\json;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalApp\Generated Files";%(AdditionalIncludeDirectories) pch.h @@ -111,7 +90,6 @@ - <_CppWinrtBinRoot>"$(OpenConsoleDir)$(Platform)\$(Configuration)\" x86 $(Platform) diff --git a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj index ad2e4cf7675..7825f6606e7 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj @@ -2,6 +2,7 @@ {A021EDFF-45C8-4DC2-BEF7-36E1B3B8CFE8} + TestHostApp TestHostApp en-US 14.0 @@ -14,7 +15,7 @@ Tests\Data true Application - true + true - + false - + 4453;%(DisableSpecificWarnings) @@ -94,27 +95,27 @@ {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} - - + {ca5cad1a-44bd-4ac7-ac72-f16e576fdd12} + + {CA5CAD1A-082C-4476-9F33-94B339494076} + + - <_TestBinRoot>$(OpenConsoleDir)\bin\$(Platform)\$(Configuration) - <_CppWinrtBinRoot>$(OpenConsoleDir)\$(Platform)\$(Configuration) - <_CppWinrtBinRoot Condition="'$(Platform)'=='Win32'">$(OpenConsoleDir)\$(Configuration) <_TAEFPlatformName>$(Platform) <_TAEFPlatformName Condition="'$(Platform)'=='Win32'">x86 - $(OpenConsoleDir)\packages\Taef.Redist.Wlk.10.51.200127004\lib\Microsoft.VisualStudio.TestPlatform.TestExecutor.WinRTCore.winmd + $(OpenConsoleDir)\packages\Taef.Redist.Wlk.10.57.200731005-develop\lib\Microsoft.VisualStudio.TestPlatform.TestExecutor.WinRTCore.winmd true @@ -126,7 +127,12 @@ - + + + + + + - + - - + + + DestinationFolder="$(TargetDir)" + UseHardLinksIfPossible="true" + SkipUnchangedFiles="true" /> diff --git a/src/cascadia/LocalTests_TerminalApp/pch.h b/src/cascadia/LocalTests_TerminalApp/pch.h index 6f134f00e4d..eaf9c0448e2 100644 --- a/src/cascadia/LocalTests_TerminalApp/pch.h +++ b/src/cascadia/LocalTests_TerminalApp/pch.h @@ -15,6 +15,8 @@ Author(s): #pragma once +// 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" // This is inexplicable, but for whatever reason, cppwinrt conflicts with the @@ -33,12 +35,6 @@ Author(s): #include #include "consoletaeftemplates.hpp" -// Common includes for most tests: -#include "../../inc/argb.h" -#include "../../inc/conattrs.hpp" -#include "../../types/inc/utils.hpp" -#include "../../inc/DefaultSettings.h" - #include #include "winrt/Windows.UI.Xaml.Markup.h" #include @@ -58,8 +54,18 @@ Author(s): #include #include +#include #include #include #include + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#include "til.h" + +// Common includes for most tests: +#include "../../inc/argb.h" +#include "../../inc/conattrs.hpp" +#include "../../types/inc/utils.hpp" +#include "../../inc/DefaultSettings.h" diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 16e41fef05f..e25ebf98549 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -16,12 +16,16 @@ using namespace ::Microsoft::Terminal::Core; static LPCWSTR term_window_class = L"HwndTerminalClass"; +// 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) }; + static constexpr bool _IsMouseMessage(UINT uMsg) { return uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK || uMsg == WM_MBUTTONDOWN || uMsg == WM_MBUTTONUP || uMsg == WM_MBUTTONDBLCLK || uMsg == WM_RBUTTONDOWN || uMsg == WM_RBUTTONUP || uMsg == WM_RBUTTONDBLCLK || - uMsg == WM_MOUSEMOVE || uMsg == WM_MOUSEWHEEL; + uMsg == WM_MOUSEMOVE || uMsg == WM_MOUSEWHEEL || uMsg == WM_MOUSEHWHEEL; } // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. @@ -55,10 +59,31 @@ try if (terminal) { - if (_IsMouseMessage(uMsg) && terminal->_CanSendVTMouseInput()) + if (_IsMouseMessage(uMsg)) { - if (terminal->_SendMouseEvent(uMsg, wParam, lParam)) + if (terminal->_CanSendVTMouseInput() && terminal->_SendMouseEvent(uMsg, wParam, lParam)) { + // GH#6401: Capturing the mouse ensures that we get drag/release events + // even if the user moves outside the window. + // _SendMouseEvent returns false if the terminal's not in VT mode, so we'll + // fall through to release the capture. + switch (uMsg) + { + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + SetCapture(hwnd); + break; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + ReleaseCapture(); + break; + default: + break; + } + + // Suppress all mouse events that made it into the terminal. return 0; } } @@ -76,6 +101,10 @@ try return 0; case WM_LBUTTONUP: terminal->_singleClickTouchdownPos = std::nullopt; + [[fallthrough]]; + case WM_MBUTTONUP: + case WM_RBUTTONUP: + ReleaseCapture(); break; case WM_MOUSEMOVE: if (WI_IsFlagSet(wParam, MK_LBUTTON)) @@ -105,6 +134,8 @@ try terminal->_hwnd.release(); terminal->Teardown(); return 0; + default: + break; } } return DefWindowProc(hwnd, uMsg, wParam, lParam); @@ -211,7 +242,7 @@ HRESULT HwndTerminal::Initialize() _terminal->Create(COORD{ 80, 25 }, 1000, *_renderer); _terminal->SetDefaultBackground(RGB(12, 12, 12)); _terminal->SetDefaultForeground(RGB(204, 204, 204)); - _terminal->SetWriteInputCallback([=](std::wstring & input) noexcept { _WriteTextToConnection(input); }); + _terminal->SetWriteInputCallback([=](std::wstring& input) noexcept { _WriteTextToConnection(input); }); localPointerToThread->EnablePainting(); _multiClickTime = std::chrono::milliseconds{ GetDoubleClickTime() }; @@ -402,7 +433,15 @@ void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data) publicTerminal->SendOutput(data); } -HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions) +/// +/// Triggers a terminal resize using the new width and height in pixel. +/// +/// Terminal pointer. +/// New width of the terminal in pixels. +/// New height of the terminal in pixels +/// Out parameter containing the columns and rows that fit the new size. +/// HRESULT of the attempted resize. +HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions) { const auto publicTerminal = static_cast(terminal); @@ -415,10 +454,55 @@ HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double heig static_cast(height), 0)); - const SIZE windowSize{ static_cast(width), static_cast(height) }; + const SIZE windowSize{ width, height }; return publicTerminal->Refresh(windowSize, dimensions); } +/// +/// Helper method for resizing the terminal using character column and row counts +/// +/// Pointer to the terminal object. +/// New terminal size in row and column count. +/// Out parameter with the new size of the renderer. +/// HRESULT of the attempted resize. +HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensionsInCharacters, _Out_ SIZE* dimensionsInPixels) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, dimensionsInPixels); + + const auto publicTerminal = static_cast(terminal); + + const auto viewInCharacters = Viewport::FromDimensions({ 0, 0 }, { (dimensionsInCharacters.X), (dimensionsInCharacters.Y) }); + const auto viewInPixels = publicTerminal->_renderEngine->GetViewportInPixels(viewInCharacters); + + dimensionsInPixels->cx = viewInPixels.Width(); + dimensionsInPixels->cy = viewInPixels.Height(); + + COORD unused{ 0, 0 }; + + return TerminalTriggerResize(terminal, viewInPixels.Width(), viewInPixels.Height(), &unused); +} + +/// +/// Calculates the amount of rows and columns that fit in the provided width and height. +/// +/// Terminal pointer +/// Width of the terminal area to calculate. +/// Height of the terminal area to calculate. +/// Out parameter containing the columns and rows that fit the new size. +/// HRESULT of the calculation. +HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions) +{ + const auto publicTerminal = static_cast(terminal); + + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, { width, height }); + const auto viewInCharacters = publicTerminal->_renderEngine->GetViewportInCharacters(viewInPixels); + + dimensions->X = viewInCharacters.Width(); + dimensions->Y = viewInCharacters.Height(); + + return S_OK; +} + void _stdcall TerminalDpiChanged(void* terminal, int newDpi) { const auto publicTerminal = static_cast(terminal); @@ -605,19 +689,30 @@ bool HwndTerminal::_CanSendVTMouseInput() const noexcept bool HwndTerminal::_SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept try { - const til::point cursorPosition{ + til::point cursorPosition{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), }; const til::size fontSize{ this->_actualFont.GetSize() }; short wheelDelta{ 0 }; - if (uMsg == WM_MOUSEWHEEL) + if (uMsg == WM_MOUSEWHEEL || uMsg == WM_MOUSEHWHEEL) { wheelDelta = HIWORD(wParam); + + // If it's a *WHEEL event, it's in screen coordinates, not window (?!) + POINT coordsToTransform = cursorPosition; + ScreenToClient(_hwnd.get(), &coordsToTransform); + cursorPosition = coordsToTransform; } - return _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta); + const TerminalInput::MouseButtonState state{ + WI_IsFlagSet(GetKeyState(VK_LBUTTON), KeyPressed), + WI_IsFlagSet(GetKeyState(VK_MBUTTON), KeyPressed), + WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed) + }; + + return _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta, state); } catch (...) { @@ -625,15 +720,19 @@ catch (...) return false; } -void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept +void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode, WORD flags, bool keyDown) noexcept try { - const auto flags = getControlKeyState(); - _terminal->SendKeyEvent(vkey, scanCode, flags, keyDown); + auto modifiers = getControlKeyState(); + if (WI_IsFlagSet(flags, ENHANCED_KEY)) + { + modifiers |= ControlKeyStates::EnhancedKey; + } + _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown); } CATCH_LOG(); -void HwndTerminal::_SendCharEvent(wchar_t ch, WORD scanCode) noexcept +void HwndTerminal::_SendCharEvent(wchar_t ch, WORD scanCode, WORD flags) noexcept try { if (_terminal->IsSelectionActive()) @@ -653,21 +752,25 @@ try return; } - const auto flags = getControlKeyState(); - _terminal->SendCharEvent(ch, scanCode, flags); + auto modifiers = getControlKeyState(); + if (WI_IsFlagSet(flags, ENHANCED_KEY)) + { + modifiers |= ControlKeyStates::EnhancedKey; + } + _terminal->SendCharEvent(ch, scanCode, modifiers); } CATCH_LOG(); -void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown) +void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown) { const auto publicTerminal = static_cast(terminal); - publicTerminal->_SendKeyEvent(vkey, scanCode, keyDown); + publicTerminal->_SendKeyEvent(vkey, scanCode, flags, keyDown); } -void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode) +void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode, WORD flags) { const auto publicTerminal = static_cast(terminal); - publicTerminal->_SendCharEvent(ch, scanCode); + publicTerminal->_SendCharEvent(ch, scanCode, flags); } void _stdcall DestroyTerminal(void* terminal) @@ -685,6 +788,7 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font publicTerminal->_terminal->SetDefaultForeground(theme.DefaultForeground); publicTerminal->_terminal->SetDefaultBackground(theme.DefaultBackground); + publicTerminal->_renderEngine->SetSelectionBackground(theme.DefaultSelectionBackground, theme.SelectionBackgroundAlpha); // Set the font colors for (size_t tableIndex = 0; tableIndex < 16; tableIndex++) @@ -709,18 +813,6 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font publicTerminal->Refresh(windowSize, &dimensions); } -// Resizes the terminal to the specified rows and columns. -HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions) -{ - const auto publicTerminal = static_cast(terminal); - - auto lock = publicTerminal->_terminal->LockForWriting(); - publicTerminal->_terminal->ClearSelection(); - publicTerminal->_renderer->TriggerRedrawAll(); - - return publicTerminal->_terminal->UserResize(dimensions); -} - void _stdcall TerminalBlinkCursor(void* terminal) { const auto publicTerminal = static_cast(terminal); diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index f904f526cad..f26f69ccac5 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -12,10 +12,13 @@ using namespace Microsoft::Console::VirtualTerminal; +// Keep in sync with TerminalTheme.cs typedef struct _TerminalTheme { COLORREF DefaultBackground; COLORREF DefaultForeground; + COLORREF DefaultSelectionBackground; + float SelectionBackgroundAlpha; DispatchTypes::CursorStyle CursorStyle; COLORREF ColorTable[16]; } TerminalTheme, *LPTerminalTheme; @@ -24,8 +27,9 @@ extern "C" { __declspec(dllexport) HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal); __declspec(dllexport) void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data); __declspec(dllexport) void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int)); -__declspec(dllexport) HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions); -__declspec(dllexport) HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions); +__declspec(dllexport) HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); +__declspec(dllexport) HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensions, _Out_ SIZE* dimensionsInPixels); +__declspec(dllexport) HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); __declspec(dllexport) void _stdcall TerminalDpiChanged(void* terminal, int newDpi); __declspec(dllexport) void _stdcall TerminalUserScroll(void* terminal, int viewTop); __declspec(dllexport) void _stdcall TerminalClearSelection(void* terminal); @@ -34,8 +38,8 @@ __declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal); __declspec(dllexport) void _stdcall DestroyTerminal(void* terminal); __declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); __declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*)); -__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown); -__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); +__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown); +__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD flags, WORD scanCode); __declspec(dllexport) void _stdcall TerminalBlinkCursor(void* terminal); __declspec(dllexport) void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); __declspec(dllexport) void _stdcall TerminalSetFocus(void* terminal); @@ -87,14 +91,16 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo std::optional _singleClickTouchdownPos; friend HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal); - friend HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions); + friend HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); + friend HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensions, _Out_ SIZE* dimensionsInPixels); + friend HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); friend void _stdcall TerminalDpiChanged(void* terminal, int newDpi); friend void _stdcall TerminalUserScroll(void* terminal, int viewTop); friend void _stdcall TerminalClearSelection(void* terminal); friend const wchar_t* _stdcall TerminalGetSelection(void* terminal); friend bool _stdcall TerminalIsSelectionActive(void* terminal); - friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown); - friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); + friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown); + friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode, WORD flags); friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); friend void _stdcall TerminalBlinkCursor(void* terminal); friend void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); @@ -118,8 +124,8 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo bool _CanSendVTMouseInput() const noexcept; bool _SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept; - void _SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept; - void _SendCharEvent(wchar_t ch, WORD scanCode) noexcept; + void _SendKeyEvent(WORD vkey, WORD scanCode, WORD flags, bool keyDown) noexcept; + void _SendCharEvent(wchar_t ch, WORD scanCode, WORD flags) noexcept; // Inherited via IControlAccessibilityInfo COORD GetFontSize() const override; diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index 8091754832c..d5669e21f63 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -185,12 +185,17 @@ HRESULT OpenTerminalHere::GetState(IShellItemArray* /*psiItemArray*/, HRESULT OpenTerminalHere::GetIcon(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszIcon) +try { - // the icon ref ("dll,-") is provided here, in this case none is provided - *ppszIcon = nullptr; - // TODO GH#6111: Return the Terminal icon here - return E_NOTIMPL; + std::filesystem::path modulePath{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + modulePath.replace_filename(WindowsTerminalExe); + // WindowsTerminal.exe,-101 will be the first icon group in WT + // We're using WindowsTerminal here explicitly, and not wt (from _getExePath), because + // WindowsTerminal is the only one built with the right icons. + const auto resource{ modulePath.wstring() + L",-101" }; + return SHStrDupW(resource.c_str(), ppszIcon); } +CATCH_RETURN(); HRESULT OpenTerminalHere::GetFlags(EXPCMDFLAGS* pFlags) { diff --git a/src/cascadia/ShellExtension/WindowsTerminalShellExt.vcxproj b/src/cascadia/ShellExtension/WindowsTerminalShellExt.vcxproj index 12565176959..681bcad5cf7 100644 --- a/src/cascadia/ShellExtension/WindowsTerminalShellExt.vcxproj +++ b/src/cascadia/ShellExtension/WindowsTerminalShellExt.vcxproj @@ -10,12 +10,6 @@ Console true - - true diff --git a/src/cascadia/ShellExtension/packages.config b/src/cascadia/ShellExtension/packages.config index 8db4233e6a9..8a013cf32b2 100644 --- a/src/cascadia/ShellExtension/packages.config +++ b/src/cascadia/ShellExtension/packages.config @@ -1,4 +1,4 @@  - + diff --git a/src/cascadia/TerminalApp/ActionArgs.h b/src/cascadia/TerminalApp/ActionArgs.h deleted file mode 100644 index 091774230d1..00000000000 --- a/src/cascadia/TerminalApp/ActionArgs.h +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -// HEY YOU: When adding ActionArgs types, make sure to add the corresponding -// *.g.cpp to ActionArgs.cpp! -#include "ActionEventArgs.g.h" -#include "NewTerminalArgs.g.h" -#include "CopyTextArgs.g.h" -#include "NewTabArgs.g.h" -#include "SwitchToTabArgs.g.h" -#include "ResizePaneArgs.g.h" -#include "MoveFocusArgs.g.h" -#include "AdjustFontSizeArgs.g.h" -#include "SplitPaneArgs.g.h" -#include "OpenSettingsArgs.g.h" -#include "SetTabColorArgs.g.h" -#include "RenameTabArgs.g.h" -#include "ExecuteCommandlineArgs.g.h" - -#include "../../cascadia/inc/cppwinrt_utils.h" -#include "Utils.h" -#include "JsonUtils.h" -#include "TerminalWarnings.h" - -#include "TerminalSettingsSerializationHelpers.h" - -// Notes on defining ActionArgs and ActionEventArgs: -// * All properties specific to an action should be defined as an ActionArgs -// class that implements IActionArgs -// * ActionEventArgs holds a single IActionArgs. For events that don't need -// additional args, this can be nullptr. - -namespace winrt::TerminalApp::implementation -{ - using namespace ::TerminalApp; - using FromJsonResult = std::tuple>; - - struct ActionEventArgs : public ActionEventArgsT - { - ActionEventArgs() = default; - - explicit ActionEventArgs(const TerminalApp::IActionArgs& args) : - _ActionArgs{ args } {}; - GETSET_PROPERTY(IActionArgs, ActionArgs, nullptr); - GETSET_PROPERTY(bool, Handled, false); - }; - - struct NewTerminalArgs : public NewTerminalArgsT - { - NewTerminalArgs() = default; - GETSET_PROPERTY(winrt::hstring, Commandline, L""); - GETSET_PROPERTY(winrt::hstring, StartingDirectory, L""); - GETSET_PROPERTY(winrt::hstring, TabTitle, L""); - GETSET_PROPERTY(Windows::Foundation::IReference, ProfileIndex, nullptr); - GETSET_PROPERTY(winrt::hstring, Profile, L""); - - static constexpr std::string_view CommandlineKey{ "commandline" }; - static constexpr std::string_view StartingDirectoryKey{ "startingDirectory" }; - static constexpr std::string_view TabTitleKey{ "tabTitle" }; - static constexpr std::string_view ProfileIndexKey{ "index" }; - static constexpr std::string_view ProfileKey{ "profile" }; - - public: - hstring GenerateName() const; - - bool Equals(const winrt::TerminalApp::NewTerminalArgs& other) - { - return other.Commandline() == _Commandline && - other.StartingDirectory() == _StartingDirectory && - other.TabTitle() == _TabTitle && - other.ProfileIndex() == _ProfileIndex && - other.Profile() == _Profile; - }; - static winrt::TerminalApp::NewTerminalArgs FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, CommandlineKey, args->_Commandline); - JsonUtils::GetValueForKey(json, StartingDirectoryKey, args->_StartingDirectory); - JsonUtils::GetValueForKey(json, TabTitleKey, args->_TabTitle); - JsonUtils::GetValueForKey(json, ProfileIndexKey, args->_ProfileIndex); - JsonUtils::GetValueForKey(json, ProfileKey, args->_Profile); - return *args; - } - }; - - struct CopyTextArgs : public CopyTextArgsT - { - CopyTextArgs() = default; - GETSET_PROPERTY(bool, SingleLine, false); - - static constexpr std::string_view SingleLineKey{ "singleLine" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_SingleLine == _SingleLine; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, SingleLineKey, args->_SingleLine); - return { *args, {} }; - } - }; - - struct NewTabArgs : public NewTabArgsT - { - NewTabArgs() = default; - GETSET_PROPERTY(winrt::TerminalApp::NewTerminalArgs, TerminalArgs, nullptr); - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_TerminalArgs.Equals(_TerminalArgs); - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - args->_TerminalArgs = NewTerminalArgs::FromJson(json); - return { *args, {} }; - } - }; - - struct SwitchToTabArgs : public SwitchToTabArgsT - { - SwitchToTabArgs() = default; - GETSET_PROPERTY(uint32_t, TabIndex, 0); - - static constexpr std::string_view TabIndexKey{ "index" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_TabIndex == _TabIndex; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, TabIndexKey, args->_TabIndex); - return { *args, {} }; - } - }; - - struct ResizePaneArgs : public ResizePaneArgsT - { - ResizePaneArgs() = default; - GETSET_PROPERTY(TerminalApp::Direction, Direction, TerminalApp::Direction::None); - - static constexpr std::string_view DirectionKey{ "direction" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Direction == _Direction; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); - if (args->_Direction == TerminalApp::Direction::None) - { - return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; - } - else - { - return { *args, {} }; - } - } - }; - - struct MoveFocusArgs : public MoveFocusArgsT - { - MoveFocusArgs() = default; - GETSET_PROPERTY(TerminalApp::Direction, Direction, TerminalApp::Direction::None); - - static constexpr std::string_view DirectionKey{ "direction" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Direction == _Direction; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); - if (args->_Direction == TerminalApp::Direction::None) - { - return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; - } - else - { - return { *args, {} }; - } - } - }; - - struct AdjustFontSizeArgs : public AdjustFontSizeArgsT - { - AdjustFontSizeArgs() = default; - GETSET_PROPERTY(int32_t, Delta, 0); - - static constexpr std::string_view AdjustFontSizeDelta{ "delta" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Delta == _Delta; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, AdjustFontSizeDelta, args->_Delta); - return { *args, {} }; - } - }; - - struct SplitPaneArgs : public SplitPaneArgsT - { - SplitPaneArgs() = default; - GETSET_PROPERTY(winrt::TerminalApp::SplitState, SplitStyle, winrt::TerminalApp::SplitState::Automatic); - GETSET_PROPERTY(winrt::TerminalApp::NewTerminalArgs, TerminalArgs, nullptr); - GETSET_PROPERTY(winrt::TerminalApp::SplitType, SplitMode, winrt::TerminalApp::SplitType::Manual); - - static constexpr std::string_view SplitKey{ "split" }; - static constexpr std::string_view SplitModeKey{ "splitMode" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_SplitStyle == _SplitStyle && - (otherAsUs->_TerminalArgs ? otherAsUs->_TerminalArgs.Equals(_TerminalArgs) : - otherAsUs->_TerminalArgs == _TerminalArgs) && - otherAsUs->_SplitMode == _SplitMode; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - args->_TerminalArgs = NewTerminalArgs::FromJson(json); - JsonUtils::GetValueForKey(json, SplitKey, args->_SplitStyle); - JsonUtils::GetValueForKey(json, SplitModeKey, args->_SplitMode); - return { *args, {} }; - } - }; - - struct OpenSettingsArgs : public OpenSettingsArgsT - { - OpenSettingsArgs() = default; - GETSET_PROPERTY(TerminalApp::SettingsTarget, Target, TerminalApp::SettingsTarget::SettingsFile); - - static constexpr std::string_view TargetKey{ "target" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Target == _Target; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, TargetKey, args->_Target); - return { *args, {} }; - } - }; - - struct SetTabColorArgs : public SetTabColorArgsT - { - SetTabColorArgs() = default; - GETSET_PROPERTY(Windows::Foundation::IReference, TabColor, nullptr); - - static constexpr std::string_view ColorKey{ "color" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_TabColor == _TabColor; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - if (const auto temp{ JsonUtils::GetValueForKey>(json, ColorKey) }) - { - args->_TabColor = static_cast(*temp); - } - return { *args, {} }; - } - }; - - struct RenameTabArgs : public RenameTabArgsT - { - RenameTabArgs() = default; - GETSET_PROPERTY(winrt::hstring, Title, L""); - - static constexpr std::string_view TitleKey{ "title" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Title == _Title; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, TitleKey, args->_Title); - return { *args, {} }; - } - }; - - struct ExecuteCommandlineArgs : public ExecuteCommandlineArgsT - { - ExecuteCommandlineArgs() = default; - GETSET_PROPERTY(winrt::hstring, Commandline, L""); - - static constexpr std::string_view CommandlineKey{ "commandline" }; - - public: - hstring GenerateName() const; - - bool Equals(const IActionArgs& other) - { - auto otherAsUs = other.try_as(); - if (otherAsUs) - { - return otherAsUs->_Commandline == _Commandline; - } - return false; - }; - static FromJsonResult FromJson(const Json::Value& json) - { - // LOAD BEARING: Not using make_self here _will_ break you in the future! - auto args = winrt::make_self(); - JsonUtils::GetValueForKey(json, CommandlineKey, args->_Commandline); - if (args->_Commandline.empty()) - { - return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; - } - return { *args, {} }; - } - }; - -} - -namespace winrt::TerminalApp::factory_implementation -{ - BASIC_FACTORY(ActionEventArgs); - BASIC_FACTORY(NewTerminalArgs); -} diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index 018c495b805..5e000c5a182 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "../AppLogic.idl"; +import "AppLogic.idl"; namespace TerminalApp { diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 63a2743d80b..5ac7c419340 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -14,7 +14,7 @@ using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::System; using namespace winrt::Microsoft::Terminal; -using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::Microsoft::Terminal::TerminalConnection; using namespace ::TerminalApp; @@ -28,100 +28,149 @@ namespace winrt namespace winrt::TerminalApp::implementation { void TerminalPage::_HandleOpenNewTabDropdown(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _OpenNewTabDropdown(); args.Handled(true); } void TerminalPage::_HandleDuplicateTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _DuplicateTabViewItem(); args.Handled(true); } void TerminalPage::_HandleCloseTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _CloseFocusedTab(); args.Handled(true); } void TerminalPage::_HandleClosePane(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _CloseFocusedPane(); args.Handled(true); } void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { CloseWindow(); args.Handled(true); } void TerminalPage::_HandleScrollUp(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - _Scroll(-1); - args.Handled(true); + const auto& realArgs = args.ActionArgs().try_as(); + if (realArgs) + { + _Scroll(ScrollUp, realArgs.RowsToScroll()); + args.Handled(true); + } } void TerminalPage::_HandleScrollDown(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - _Scroll(1); - args.Handled(true); + const auto& realArgs = args.ActionArgs().try_as(); + if (realArgs) + { + _Scroll(ScrollDown, realArgs.RowsToScroll()); + args.Handled(true); + } } void TerminalPage::_HandleNextTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _SelectNextTab(true); args.Handled(true); } void TerminalPage::_HandlePrevTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _SelectNextTab(false); args.Handled(true); } + void TerminalPage::_HandleSendInput(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args == nullptr) + { + args.Handled(false); + } + else if (const auto& realArgs = args.ActionArgs().try_as()) + { + const auto termControl = _GetActiveControl(); + termControl.SendInput(realArgs.Input()); + args.Handled(true); + } + } + void TerminalPage::_HandleSplitPane(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { if (args == nullptr) { args.Handled(false); } - else if (const auto& realArgs = args.ActionArgs().try_as()) + else if (const auto& realArgs = args.ActionArgs().try_as()) { _SplitPane(realArgs.SplitStyle(), realArgs.SplitMode(), realArgs.TerminalArgs()); args.Handled(true); } } + void TerminalPage::_HandleTogglePaneZoom(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (auto focusedTab = _GetFocusedTab()) + { + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) + { + // Don't do anything if there's only one pane. It's already zoomed. + if (activeTab && activeTab->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(); + } + } + } + + args.Handled(true); + } + void TerminalPage::_HandleScrollUpPage(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - _ScrollPage(-1); + _ScrollPage(ScrollUp); args.Handled(true); } void TerminalPage::_HandleScrollDownPage(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - _ScrollPage(1); + _ScrollPage(ScrollDown); args.Handled(true); } void TerminalPage::_HandleOpenSettings(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { _LaunchSettings(realArgs.Target()); args.Handled(true); @@ -129,21 +178,21 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandlePasteText(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _PasteText(); args.Handled(true); } void TerminalPage::_HandleNewTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { if (args == nullptr) { _OpenNewTab(nullptr); args.Handled(true); } - else if (const auto& realArgs = args.ActionArgs().try_as()) + else if (const auto& realArgs = args.ActionArgs().try_as()) { _OpenNewTab(realArgs.TerminalArgs()); args.Handled(true); @@ -151,9 +200,9 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleSwitchToTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { const auto handled = _SelectTab({ realArgs.TabIndex() }); args.Handled(handled); @@ -161,11 +210,11 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleResizePane(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { - if (realArgs.Direction() == TerminalApp::Direction::None) + if (realArgs.Direction() == Direction::None) { // Do nothing args.Handled(false); @@ -179,11 +228,11 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleMoveFocus(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { - if (realArgs.Direction() == TerminalApp::Direction::None) + if (realArgs.Direction() == Direction::None) { // Do nothing args.Handled(false); @@ -197,19 +246,19 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleCopyText(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { - const auto handled = _CopyText(realArgs.SingleLine()); + const auto handled = _CopyText(realArgs.SingleLine(), realArgs.CopyFormatting()); args.Handled(handled); } } void TerminalPage::_HandleAdjustFontSize(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { const auto termControl = _GetActiveControl(); termControl.AdjustFontSize(realArgs.Delta()); @@ -218,14 +267,14 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleFind(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { _Find(); args.Handled(true); } void TerminalPage::_HandleResetFontSize(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { const auto termControl = _GetActiveControl(); termControl.ResetFontSize(); @@ -233,7 +282,7 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleToggleRetroEffect(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { const auto termControl = _GetActiveControl(); termControl.ToggleRetroEffect(); @@ -241,107 +290,149 @@ namespace winrt::TerminalApp::implementation } void TerminalPage::_HandleToggleFocusMode(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { ToggleFocusMode(); args.Handled(true); } void TerminalPage::_HandleToggleFullscreen(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { ToggleFullscreen(); args.Handled(true); } void TerminalPage::_HandleToggleAlwaysOnTop(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { ToggleAlwaysOnTop(); args.Handled(true); } void TerminalPage::_HandleToggleCommandPalette(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { // TODO GH#6677: When we add support for commandline mode, first set the // mode that the command palette should be in, before making it visible. + CommandPalette().EnableCommandPaletteMode(); CommandPalette().Visibility(CommandPalette().Visibility() == Visibility::Visible ? Visibility::Collapsed : Visibility::Visible); args.Handled(true); } - void TerminalPage::_HandleSetTabColor(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + void TerminalPage::_HandleSetColorScheme(const IInspectable& /*sender*/, + const ActionEventArgs& args) { - std::optional tabColor; - - if (const auto& realArgs = args.ActionArgs().try_as()) + args.Handled(false); + if (const auto& realArgs = args.ActionArgs().try_as()) { - if (realArgs.TabColor() != nullptr) + if (auto focusedTab = _GetFocusedTab()) { - tabColor = realArgs.TabColor().Value(); + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) + { + if (auto activeControl = activeTab->GetActiveTerminalControl()) + { + if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) + { + auto controlSettings = activeControl.Settings().as(); + controlSettings->ApplyColorScheme(scheme); + activeControl.UpdateSettings(*controlSettings); + args.Handled(true); + } + } + } } } + } + + void TerminalPage::_HandleSetTabColor(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + Windows::Foundation::IReference tabColor; - auto activeTab = _GetFocusedTab(); - if (activeTab) + if (const auto& realArgs = args.ActionArgs().try_as()) { - if (tabColor.has_value()) - { - activeTab->SetTabColor(tabColor.value()); - } - else + tabColor = realArgs.TabColor(); + } + + if (auto focusedTab = _GetFocusedTab()) + { + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) { - activeTab->ResetTabColor(); + if (tabColor) + { + activeTab->SetRuntimeTabColor(tabColor.Value()); + } + else + { + activeTab->ResetRuntimeTabColor(); + } } } args.Handled(true); } void TerminalPage::_HandleOpenTabColorPicker(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { - auto activeTab = _GetFocusedTab(); - if (activeTab) + if (auto focusedTab = _GetFocusedTab()) { - activeTab->ActivateColorPicker(); + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) + { + activeTab->ActivateColorPicker(); + } } args.Handled(true); } void TerminalPage::_HandleRenameTab(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& args) + const ActionEventArgs& args) { std::optional title; - if (const auto& realArgs = args.ActionArgs().try_as()) + if (const auto& realArgs = args.ActionArgs().try_as()) { title = realArgs.Title(); } - auto activeTab = _GetFocusedTab(); - if (activeTab) + if (auto focusedTab = _GetFocusedTab()) { - if (title.has_value()) + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) { - activeTab->SetTabText(title.value()); + if (title.has_value()) + { + activeTab->SetTabText(title.value()); + } + else + { + activeTab->ResetTabText(); + } } - else + } + args.Handled(true); + } + + void TerminalPage::_HandleOpenTabRenamer(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (auto focusedTab = _GetFocusedTab()) + { + if (auto activeTab = _GetTerminalTabImpl(focusedTab)) { - activeTab->ResetTabText(); + activeTab->ActivateTabRenamer(); } } args.Handled(true); } void TerminalPage::_HandleExecuteCommandline(const IInspectable& /*sender*/, - const TerminalApp::ActionEventArgs& actionArgs) + const ActionEventArgs& actionArgs) { - if (const auto& realArgs = actionArgs.ActionArgs().try_as()) + if (const auto& realArgs = actionArgs.ActionArgs().try_as()) { - auto actions = winrt::single_threaded_vector(std::move( + auto actions = winrt::single_threaded_vector(std::move( TerminalPage::ConvertExecuteCommandlineToActions(realArgs))); if (_startupActions.Size() != 0) @@ -351,4 +442,93 @@ namespace winrt::TerminalApp::implementation } } } + + void TerminalPage::_HandleCloseOtherTabs(const IInspectable& /*sender*/, + const ActionEventArgs& actionArgs) + { + if (const auto& realArgs = actionArgs.ActionArgs().try_as()) + { + uint32_t index; + if (realArgs.Index()) + { + index = realArgs.Index().Value(); + } + else if (auto focusedTabIndex = _GetFocusedTabIndex()) + { + index = *focusedTabIndex; + } + else + { + // Do nothing + actionArgs.Handled(false); + return; + } + + // Remove tabs after the current one + while (_tabs.Size() > index + 1) + { + _RemoveTabViewItemByIndex(_tabs.Size() - 1); + } + + // Remove all of them leading up to the selected tab + while (_tabs.Size() > 1) + { + _RemoveTabViewItemByIndex(0); + } + + actionArgs.Handled(true); + } + } + + void TerminalPage::_HandleCloseTabsAfter(const IInspectable& /*sender*/, + const ActionEventArgs& actionArgs) + { + if (const auto& realArgs = actionArgs.ActionArgs().try_as()) + { + uint32_t index; + if (realArgs.Index()) + { + index = realArgs.Index().Value(); + } + else if (auto focusedTabIndex = _GetFocusedTabIndex()) + { + index = *focusedTabIndex; + } + else + { + // Do nothing + actionArgs.Handled(false); + return; + } + + // Remove tabs after the current one + while (_tabs.Size() > index + 1) + { + _RemoveTabViewItemByIndex(_tabs.Size() - 1); + } + + // TODO:GH#7182 For whatever reason, if you run this action + // when the tab that's currently focused is _before_ the `index` + // param, then the tabs will expand to fill the entire width of the + // tab row, until you mouse over them. Probably has something to do + // with tabs not resizing down until there's a mouse exit event. + + actionArgs.Handled(true); + } + } + + void TerminalPage::_HandleOpenTabSearch(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + // Tab search is always in-order. + _UpdatePaletteWithInOrderTabs(); + + auto opt = _GetFocusedTabIndex(); + uint32_t startIdx = opt.value_or(0); + + CommandPalette().EnableTabSwitcherMode(true, startIdx); + CommandPalette().Visibility(Visibility::Visible); + + args.Handled(true); + } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 70e6df6940b..7b7e2138265 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -4,10 +4,11 @@ #include "pch.h" #include "AppLogic.h" #include "AppCommandlineArgs.h" -#include "ActionArgs.h" +#include "../types/inc/utils.hpp" #include using namespace winrt::TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace TerminalApp; // Either a ; at the start of a line, or a ; preceded by any non-\ char. @@ -69,50 +70,29 @@ int AppCommandlineArgs::ParseCommand(const Commandline& command) { throw CLI::CallForHelp(); } - // Clear the parser's internal state - _app.clear(); - // attempt to parse the commandline + // attempt to parse the commandline prefix of the form [options][subcommand] _app.parse(args); + auto remainingParams = _app.remaining_size(); // If we parsed the commandline, and _no_ subcommands were provided, try - // parsing again as a "new-tab" command. - + // parse the remaining suffix as a "new-tab" command. if (_noCommandsProvided()) { - _newTabCommand.subcommand->clear(); _newTabCommand.subcommand->parse(args); + remainingParams = _newTabCommand.subcommand->remaining_size(); + } + + // if after parsing the prefix and (optionally) the implicit tab subcommand + // we still have unparsed parameters we need to fail + if (remainingParams > 0) + { + throw CLI::ExtrasError(args); } - } - catch (const CLI::CallForHelp& e) - { - return _handleExit(_app, e); } catch (const CLI::ParseError& e) { - // If we parsed the commandline, and _no_ subcommands were provided, try - // parsing again as a "new-tab" command. - if (_noCommandsProvided()) - { - try - { - // CLI11 mutated the original vector the first time it tried to - // parse the args. Reconstruct it the way CLI11 wants here. - // "See above for why it's begin() + 1" - std::vector args{ command.Args().begin() + 1, command.Args().end() }; - std::reverse(args.begin(), args.end()); - _newTabCommand.subcommand->clear(); - _newTabCommand.subcommand->parse(args); - } - catch (const CLI::ParseError& e) - { - return _handleExit(*_newTabCommand.subcommand, e); - } - } - else - { - return _handleExit(_app, e); - } + return _handleExit(_app, e); } return 0; } @@ -157,35 +137,53 @@ int AppCommandlineArgs::_handleExit(const CLI::App& command, const CLI::Error& e // - void AppCommandlineArgs::_buildParser() { + // We define or parser as a prefix command, to support "implicit new tab subcommand" scenario. + // In this scenario we will try to parse the prefix that contains parameters like launch mode, + // but will not encounter an explicit command. + // Instead we will encounter an argument that doesn't belong to the prefix indicating the prefix is over. + // Then we will try to parse the remaining arguments as a new tab subcommand. + // E.g., for "wt.exe -M -d c:/", we will use -M for the launch mode, but once we will encounter -d + // we will know that the prefix is over and try to handle the suffix as a new tab subcommand + _app.prefix_command(); + // -v,--version: Displays version info auto versionCallback = [this](int64_t /*count*/) { - if (const auto appLogic{ winrt::TerminalApp::implementation::AppLogic::Current() }) - { - // Set our message to display the application name and the current version. - _exitMessage = fmt::format("{0}\n{1}", - til::u16u8(appLogic->ApplicationDisplayName()), - til::u16u8(appLogic->ApplicationVersion())); - // Theoretically, we don't need to exit now, since this isn't really - // an error case. However, in practice, it feels weird to have `wt - // -v` open a new tab, and makes enough sense that `wt -v ; - // split-pane` (or whatever) just displays the version and exits. - _shouldExitEarly = true; - } + // Set our message to display the application name and the current version. + _exitMessage = fmt::format("{0}\n{1}", + til::u16u8(CascadiaSettings::ApplicationDisplayName()), + til::u16u8(CascadiaSettings::ApplicationVersion())); + // Theoretically, we don't need to exit now, since this isn't really + // an error case. However, in practice, it feels weird to have `wt + // -v` open a new tab, and makes enough sense that `wt -v ; + // split-pane` (or whatever) just displays the version and exits. + _shouldExitEarly = true; }; _app.add_flag_function("-v,--version", versionCallback, RS_A(L"CmdVersionDesc")); - // Maximized and Fullscreen flags + // Launch mode related flags // -M,--maximized: Maximizes the window on launch // -F,--fullscreen: Fullscreens the window on launch + // -f,--focus: Sets the terminal into the Focus mode + // While fullscreen excludes both maximized and focus mode, the user can combine between the maximized and focused (-fM) auto maximizedCallback = [this](int64_t /*count*/) { - _launchMode = winrt::TerminalApp::LaunchMode::MaximizedMode; + _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::FocusMode) ? + LaunchMode::MaximizedFocusMode : + LaunchMode::MaximizedMode; }; auto fullscreenCallback = [this](int64_t /*count*/) { - _launchMode = winrt::TerminalApp::LaunchMode::FullscreenMode; + _launchMode = LaunchMode::FullscreenMode; + }; + auto focusCallback = [this](int64_t /*count*/) { + _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::MaximizedMode) ? + LaunchMode::MaximizedFocusMode : + LaunchMode::FocusMode; }; + auto maximized = _app.add_flag_function("-M,--maximized", maximizedCallback, RS_A(L"CmdMaximizedDesc")); auto fullscreen = _app.add_flag_function("-F,--fullscreen", fullscreenCallback, RS_A(L"CmdFullscreenDesc")); + auto focus = _app.add_flag_function("-f,--focus", focusCallback, RS_A(L"CmdFocusDesc")); maximized->excludes(fullscreen); + focus->excludes(fullscreen); // Subcommands _buildNewTabParser(); @@ -214,14 +212,13 @@ void AppCommandlineArgs::_buildNewTabParser() // command was parsed. subcommand.subcommand->callback([&, this]() { // Build the NewTab action from the values we've parsed on the commandline. - auto newTabAction = winrt::make_self(); - newTabAction->Action(ShortcutAction::NewTab); - auto args = winrt::make_self(); + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); // _getNewTerminalArgs MUST be called before parsing any other options, // as it might clear those options while finding the commandline - args->TerminalArgs(_getNewTerminalArgs(subcommand)); - newTabAction->Args(*args); - _startupActions.push_back(*newTabAction); + NewTabArgs args{ _getNewTerminalArgs(subcommand) }; + newTabAction.Args(args); + _startupActions.push_back(newTabAction); }); }; @@ -257,29 +254,29 @@ void AppCommandlineArgs::_buildSplitPaneParser() // command was parsed. subcommand.subcommand->callback([&, this]() { // Build the SplitPane action from the values we've parsed on the commandline. - auto splitPaneActionAndArgs = winrt::make_self(); - splitPaneActionAndArgs->Action(ShortcutAction::SplitPane); - auto args = winrt::make_self(); + ActionAndArgs splitPaneActionAndArgs{}; + splitPaneActionAndArgs.Action(ShortcutAction::SplitPane); + // _getNewTerminalArgs MUST be called before parsing any other options, // as it might clear those options while finding the commandline - args->TerminalArgs(_getNewTerminalArgs(subcommand)); - args->SplitStyle(SplitState::Automatic); + auto terminalArgs{ _getNewTerminalArgs(subcommand) }; + auto style{ SplitState::Automatic }; // Make sure to use the `Option`s here to check if they were set - // _getNewTerminalArgs might reset them while parsing a commandline if ((*subcommand._horizontalOption || *subcommand._verticalOption)) { if (_splitHorizontal) { - args->SplitStyle(SplitState::Horizontal); + style = SplitState::Horizontal; } else if (_splitVertical) { - args->SplitStyle(SplitState::Vertical); + style = SplitState::Vertical; } } - - splitPaneActionAndArgs->Args(*args); - _startupActions.push_back(*splitPaneActionAndArgs); + SplitPaneArgs args{ style, terminalArgs }; + splitPaneActionAndArgs.Args(args); + _startupActions.push_back(splitPaneActionAndArgs); }); }; @@ -319,20 +316,19 @@ void AppCommandlineArgs::_buildFocusTabParser() // command was parsed. subcommand->callback([&, this]() { // Build the action from the values we've parsed on the commandline. - auto focusTabAction = winrt::make_self(); + ActionAndArgs focusTabAction{}; if (_focusTabIndex >= 0) { - focusTabAction->Action(ShortcutAction::SwitchToTab); - auto args = winrt::make_self(); - args->TabIndex(_focusTabIndex); - focusTabAction->Args(*args); - _startupActions.push_back(*focusTabAction); + focusTabAction.Action(ShortcutAction::SwitchToTab); + SwitchToTabArgs args{ static_cast(_focusTabIndex) }; + focusTabAction.Args(args); + _startupActions.push_back(focusTabAction); } else if (_focusNextTab || _focusPrevTab) { - focusTabAction->Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab); - _startupActions.push_back(*focusTabAction); + focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab); + _startupActions.push_back(std::move(focusTabAction)); } }); }; @@ -360,6 +356,10 @@ void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubc _startingTitle, RS_A(L"CmdTitleArgDesc")); + subcommand.tabColorOption = subcommand.subcommand->add_option("--tabColor", + _startingTabColor, + RS_A(L"CmdTabColorArgDesc")); + // 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", @@ -379,7 +379,7 @@ void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubc // - A fully initialized NewTerminalArgs corresponding to values we've currently parsed. NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand) { - auto args = winrt::make_self(); + NewTerminalArgs args{}; if (!_commandline.empty()) { @@ -403,25 +403,31 @@ NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewT } } - args->Commandline(winrt::to_hstring(cmdlineBuffer.str())); + args.Commandline(winrt::to_hstring(cmdlineBuffer.str())); } if (*subcommand.profileNameOption) { - args->Profile(winrt::to_hstring(_profileName)); + args.Profile(winrt::to_hstring(_profileName)); } if (*subcommand.startingDirectoryOption) { - args->StartingDirectory(winrt::to_hstring(_startingDirectory)); + args.StartingDirectory(winrt::to_hstring(_startingDirectory)); } if (*subcommand.titleOption) { - args->TabTitle(winrt::to_hstring(_startingTitle)); + args.TabTitle(winrt::to_hstring(_startingTitle)); + } + + if (*subcommand.tabColorOption) + { + const auto tabColor = Microsoft::Console::Utils::ColorFromHexString(_startingTabColor); + args.TabColor(static_cast(tabColor)); } - return *args; + return args; } // Method Description: @@ -455,6 +461,7 @@ void AppCommandlineArgs::_resetStateToDefault() _profileName.clear(); _startingDirectory.clear(); _startingTitle.clear(); + _startingTabColor.clear(); _commandline.clear(); _splitVertical = false; @@ -599,7 +606,7 @@ void AppCommandlineArgs::_addCommandsForArg(std::vector& commands, // - // Return Value: // - the deque of actions we've buffered as a result of parsing commands. -std::vector& AppCommandlineArgs::GetStartupActions() +std::vector& AppCommandlineArgs::GetStartupActions() { return _startupActions; } @@ -652,18 +659,15 @@ void AppCommandlineArgs::ValidateStartupCommands() _startupActions.front().Action() != ShortcutAction::NewTab) { // Build the NewTab action from the values we've parsed on the commandline. - auto newTabAction = winrt::make_self(); - newTabAction->Action(ShortcutAction::NewTab); - auto args = winrt::make_self(); - auto newTerminalArgs = winrt::make_self(); - args->TerminalArgs(*newTerminalArgs); - newTabAction->Args(*args); + NewTerminalArgs newTerminalArgs{}; + NewTabArgs args{ newTerminalArgs }; + ActionAndArgs newTabAction{ ShortcutAction::NewTab, args }; // push the arg onto the front - _startupActions.insert(_startupActions.begin(), 1, *newTabAction); + _startupActions.insert(_startupActions.begin(), 1, newTabAction); } } -std::optional AppCommandlineArgs::GetLaunchMode() const noexcept +std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { return _launchMode; } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index a0d87ffc6b8..6a7d08b0e7d 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -2,8 +2,6 @@ // Licensed under the MIT license. #pragma once -#include "ActionAndArgs.h" - #include "Commandline.h" #ifdef UNIT_TESTING @@ -36,11 +34,11 @@ class TerminalApp::AppCommandlineArgs final static std::vector BuildCommands(winrt::array_view& args); void ValidateStartupCommands(); - std::vector& GetStartupActions(); + std::vector& GetStartupActions(); const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; - std::optional GetLaunchMode() const noexcept; + std::optional GetLaunchMode() const noexcept; private: static const std::wregex _commandDelimiterRegex; @@ -56,6 +54,7 @@ class TerminalApp::AppCommandlineArgs final CLI::Option* profileNameOption; CLI::Option* startingDirectoryOption; CLI::Option* titleOption; + CLI::Option* tabColorOption; }; struct NewPaneSubcommand : public NewTerminalSubcommand @@ -76,6 +75,7 @@ class TerminalApp::AppCommandlineArgs final std::string _profileName; std::string _startingDirectory; std::string _startingTitle; + std::string _startingTabColor; // _commandline will contain the command line with which we'll be spawning a new terminal std::vector _commandline; @@ -89,14 +89,14 @@ class TerminalApp::AppCommandlineArgs final bool _focusNextTab{ false }; bool _focusPrevTab{ false }; - std::optional _launchMode{ std::nullopt }; + std::optional _launchMode{ std::nullopt }; // Are you adding more args here? Make sure to reset them in _resetStateToDefault - std::vector _startupActions; + std::vector _startupActions; std::string _exitMessage; bool _shouldExitEarly{ false }; - winrt::TerminalApp::NewTerminalArgs _getNewTerminalArgs(NewTerminalSubcommand& subcommand); + winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs _getNewTerminalArgs(NewTerminalSubcommand& subcommand); void _addNewTerminalArgs(NewTerminalSubcommand& subcommand); void _buildParser(); void _buildNewTabParser(); diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 58821ac5a6a..d6940987449 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -3,76 +3,20 @@ #include "pch.h" #include "AppKeyBindings.h" -#include "KeyChordSerialization.h" #include "AppKeyBindings.g.cpp" using namespace winrt::Microsoft::Terminal; using namespace winrt::TerminalApp; -using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::TerminalControl; namespace winrt::TerminalApp::implementation { - void AppKeyBindings::SetKeyBinding(const TerminalApp::ActionAndArgs& actionAndArgs, - const Settings::KeyChord& chord) + bool AppKeyBindings::TryKeyChord(const KeyChord& kc) { - _keyShortcuts[chord] = actionAndArgs; - } - - // Method Description: - // - Remove the action that's bound to a particular KeyChord. - // Arguments: - // - chord: the keystroke to remove the action for. - // Return Value: - // - - void AppKeyBindings::ClearKeyBinding(const Settings::KeyChord& chord) - { - _keyShortcuts.erase(chord); - } - - KeyChord AppKeyBindings::GetKeyBindingForAction(TerminalApp::ShortcutAction const& action) - { - for (auto& kv : _keyShortcuts) - { - if (kv.second.Action() == action) - { - return kv.first; - } - } - return { nullptr }; - } - - // Method Description: - // - Lookup the keychord bound to a particular combination of ShortcutAction - // and IActionArgs. This enables searching no only for the binding of a - // particular ShortcutAction, but also a particular set of values for - // arguments to that action. - // Arguments: - // - actionAndArgs: The ActionAndArgs to lookup the keybinding for. - // Return Value: - // - The bound keychord, if this ActionAndArgs is bound to a key, otherwise nullptr. - KeyChord AppKeyBindings::GetKeyBindingForActionWithArgs(TerminalApp::ActionAndArgs const& actionAndArgs) - { - for (auto& kv : _keyShortcuts) - { - const auto action = kv.second.Action(); - const auto args = kv.second.Args(); - const auto actionMatched = action == actionAndArgs.Action(); - const auto argsMatched = args ? args.Equals(actionAndArgs.Args()) : args == actionAndArgs.Args(); - if (actionMatched && argsMatched) - { - return kv.first; - } - } - return { nullptr }; - } - - bool AppKeyBindings::TryKeyChord(const Settings::KeyChord& kc) - { - const auto keyIter = _keyShortcuts.find(kc); - if (keyIter != _keyShortcuts.end()) + const auto actionAndArgs = _keymap.TryLookup(kc); + if (actionAndArgs) { - const auto actionAndArgs = keyIter->second; return _dispatch.DoAction(actionAndArgs); } return false; @@ -83,28 +27,8 @@ namespace winrt::TerminalApp::implementation _dispatch = dispatch; } - // Method Description: - // - Takes the KeyModifier flags from Terminal and maps them to the WinRT types which are used by XAML - // Return Value: - // - a Windows::System::VirtualKeyModifiers object with the flags of which modifiers used. - Windows::System::VirtualKeyModifiers AppKeyBindings::ConvertVKModifiers(Settings::KeyModifiers modifiers) + void AppKeyBindings::SetKeyMapping(const winrt::Microsoft::Terminal::Settings::Model::KeyMapping& keymap) { - Windows::System::VirtualKeyModifiers keyModifiers = Windows::System::VirtualKeyModifiers::None; - - if (WI_IsFlagSet(modifiers, Settings::KeyModifiers::Ctrl)) - { - keyModifiers |= Windows::System::VirtualKeyModifiers::Control; - } - if (WI_IsFlagSet(modifiers, Settings::KeyModifiers::Shift)) - { - keyModifiers |= Windows::System::VirtualKeyModifiers::Shift; - } - if (WI_IsFlagSet(modifiers, Settings::KeyModifiers::Alt)) - { - // note: Menu is the Alt VK_MENU - keyModifiers |= Windows::System::VirtualKeyModifiers::Menu; - } - - return keyModifiers; + _keymap = keymap; } } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 63df542a75a..19891d5ed66 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -4,7 +4,6 @@ #pragma once #include "AppKeyBindings.g.h" -#include "ActionArgs.h" #include "ShortcutActionDispatch.h" #include "..\inc\cppwinrt_utils.h" @@ -12,60 +11,25 @@ namespace TerminalAppLocalTests { class SettingsTests; - class KeyBindingsTests; - class TestUtils; } namespace winrt::TerminalApp::implementation { - struct KeyChordHash - { - std::size_t operator()(const winrt::Microsoft::Terminal::Settings::KeyChord& key) const - { - std::hash keyHash; - std::hash modifiersHash; - std::size_t hashedKey = keyHash(key.Vkey()); - std::size_t hashedMods = modifiersHash(key.Modifiers()); - return hashedKey ^ hashedMods; - } - }; - - struct KeyChordEquality - { - bool operator()(const winrt::Microsoft::Terminal::Settings::KeyChord& lhs, const winrt::Microsoft::Terminal::Settings::KeyChord& rhs) const - { - return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey(); - } - }; - struct AppKeyBindings : AppKeyBindingsT { AppKeyBindings() = default; - bool TryKeyChord(winrt::Microsoft::Terminal::Settings::KeyChord const& kc); - - void SetKeyBinding(TerminalApp::ActionAndArgs const& actionAndArgs, - winrt::Microsoft::Terminal::Settings::KeyChord const& chord); - void ClearKeyBinding(winrt::Microsoft::Terminal::Settings::KeyChord const& chord); - Microsoft::Terminal::Settings::KeyChord GetKeyBindingForAction(TerminalApp::ShortcutAction const& action); - Microsoft::Terminal::Settings::KeyChord GetKeyBindingForActionWithArgs(TerminalApp::ActionAndArgs const& actionAndArgs); - - static Windows::System::VirtualKeyModifiers ConvertVKModifiers(winrt::Microsoft::Terminal::Settings::KeyModifiers modifiers); - - // Defined in AppKeyBindingsSerialization.cpp - std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(const Json::Value& json); - Json::Value ToJson(); + bool TryKeyChord(winrt::Microsoft::Terminal::TerminalControl::KeyChord const& kc); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); + void SetKeyMapping(const Microsoft::Terminal::Settings::Model::KeyMapping& keymap); private: - std::unordered_map _keyShortcuts; + winrt::Microsoft::Terminal::Settings::Model::KeyMapping _keymap{ nullptr }; winrt::TerminalApp::ShortcutActionDispatch _dispatch{ nullptr }; friend class TerminalAppLocalTests::SettingsTests; - friend class TerminalAppLocalTests::KeyBindingsTests; - friend class TerminalAppLocalTests::TestUtils; }; } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.idl b/src/cascadia/TerminalApp/AppKeyBindings.idl index db287b79267..b4b979670f9 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.idl +++ b/src/cascadia/TerminalApp/AppKeyBindings.idl @@ -1,20 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "../ActionArgs.idl"; -import "../ShortcutActionDispatch.idl"; +import "ShortcutActionDispatch.idl"; namespace TerminalApp { - [default_interface] runtimeclass AppKeyBindings : Microsoft.Terminal.Settings.IKeyBindings + [default_interface] runtimeclass AppKeyBindings : Microsoft.Terminal.TerminalControl.IKeyBindings { AppKeyBindings(); - void SetKeyBinding(ActionAndArgs actionAndArgs, Microsoft.Terminal.Settings.KeyChord chord); - void ClearKeyBinding(Microsoft.Terminal.Settings.KeyChord chord); - - Microsoft.Terminal.Settings.KeyChord GetKeyBindingForAction(ShortcutAction action); - Microsoft.Terminal.Settings.KeyChord GetKeyBindingForActionWithArgs(ActionAndArgs actionAndArgs); - void SetDispatch(ShortcutActionDispatch dispatch); + void SetKeyMapping(Microsoft.Terminal.Settings.Model.KeyMapping keymap); } } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 5a6ec188703..a6f15bea939 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -15,8 +15,8 @@ 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::Settings; using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::TerminalApp; namespace winrt @@ -39,7 +39,9 @@ static const std::array(SettingsLoadWar USES_RESOURCE(L"AtLeastOneKeybindingWarning"), USES_RESOURCE(L"TooManyKeysForChord"), USES_RESOURCE(L"MissingRequiredParameter"), - USES_RESOURCE(L"LegacyGlobalsProperty") + USES_RESOURCE(L"LegacyGlobalsProperty"), + USES_RESOURCE(L"FailedToParseCommandJson"), + USES_RESOURCE(L"FailedToWriteToSettings") }; static const std::array(SettingsLoadErrors::ERRORS_SIZE)> settingsLoadErrorsLabels { USES_RESOURCE(L"NoProfilesText"), @@ -76,7 +78,7 @@ static winrt::hstring _GetMessageText(uint32_t index, std::array(warning), settingsLoadWarningsLabels); } @@ -89,7 +91,7 @@ static winrt::hstring _GetWarningText(::TerminalApp::SettingsLoadWarnings warnin // - error: the SettingsLoadErrors value to get the localized text for. // Return Value: // - localized text for the given error -static winrt::hstring _GetErrorText(::TerminalApp::SettingsLoadErrors error) +static winrt::hstring _GetErrorText(SettingsLoadErrors error) { return _GetMessageText(static_cast(error), settingsLoadErrorsLabels); } @@ -165,6 +167,17 @@ namespace winrt::TerminalApp::implementation return nullptr; } + // Method Description: + // - Returns the settings currently in use by the entire Terminal application. + // Throws: + // - HR E_INVALIDARG if the app isn't up and running. + const CascadiaSettings AppLogic::CurrentAppSettings() + { + auto appLogic{ ::winrt::TerminalApp::implementation::AppLogic::Current() }; + THROW_HR_IF_NULL(E_INVALIDARG, appLogic); + return appLogic->GetSettings(); + } + AppLogic::AppLogic() : _dialogLock{}, _loadedInitialSettings{ false }, @@ -238,7 +251,7 @@ namespace winrt::TerminalApp::implementation // so this setting is overridden to false no matter what the preference is. if (_isUwp) { - _settings->GlobalSettings().ShowTabsInTitlebar(false); + _settings.GlobalSettings().ShowTabsInTitlebar(false); } _root->SetSettings(_settings, false); @@ -253,17 +266,21 @@ namespace winrt::TerminalApp::implementation { _root->ToggleFullscreen(); } + else if (launchMode == LaunchMode::FocusMode || launchMode == LaunchMode::MaximizedFocusMode) + { + _root->ToggleFocusMode(); + } }); _root->Create(); - _ApplyTheme(_settings->GlobalSettings().Theme()); + _ApplyTheme(_settings.GlobalSettings().Theme()); _ApplyStartupTaskStateChange(); TraceLoggingWrite( g_hTerminalAppProvider, "AppCreated", TraceLoggingDescription("Event emitted when the application is started"), - TraceLoggingBool(_settings->GlobalSettings().ShowTabsInTitlebar(), "TabsInTitlebar"), + TraceLoggingBool(_settings.GlobalSettings().ShowTabsInTitlebar(), "TabsInTitlebar"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } @@ -310,7 +327,7 @@ namespace winrt::TerminalApp::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().Theme() }; + auto theme{ _settings.GlobalSettings().Theme() }; auto element{ sender.try_as() }; while (element) { @@ -400,7 +417,7 @@ namespace winrt::TerminalApp::implementation // Make sure the lines of text wrap warningsTextBlock.TextWrapping(TextWrapping::Wrap); - const auto& warnings = _settings->GetWarnings(); + const auto warnings = _settings.Warnings(); for (const auto& warning : warnings) { // Try looking up the warning message key for each warning. @@ -453,6 +470,11 @@ namespace winrt::TerminalApp::implementation void AppLogic::_OnLoaded(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { + const auto keyboardServiceIsDisabled = !_IsKeyboardServiceEnabled(); + if (keyboardServiceIsDisabled) + { + _root->ShowKeyboardServiceWarning(); + } if (FAILED(_settingsLoadedResult)) { const winrt::hstring titleKey = USES_RESOURCE(L"InitialJsonParseErrorTitle"); @@ -465,6 +487,51 @@ namespace winrt::TerminalApp::implementation } } + // 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{ OpenSCManager(nullptr, nullptr, 0) }; + + if (LOG_LAST_ERROR_IF(!hManager.is_valid())) + { + return true; + } + + // Get a handle to the keyboard service + wil::unique_schandle hService{ OpenService(hManager.get(), TabletInputServiceKey.data(), SERVICE_QUERY_STATUS) }; + if (LOG_LAST_ERROR_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 @@ -483,7 +550,7 @@ namespace winrt::TerminalApp::implementation } // Use the default profile to determine how big of a window we need. - const auto [_, settings] = _settings->BuildSettings(nullptr); + const auto [_, settings] = TerminalSettings::BuildSettings(_settings, nullptr, nullptr); auto proposedSize = TermControl::GetProposedDimensions(settings, dpi); @@ -492,7 +559,7 @@ namespace winrt::TerminalApp::implementation // 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 (_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. @@ -506,7 +573,7 @@ namespace winrt::TerminalApp::implementation titlebar.Measure({ SHRT_MAX, SHRT_MAX }); proposedSize.Height += (titlebar.DesiredSize().Height) * scale; } - else if (_settings->GlobalSettings().AlwaysShowTabs()) + else if (_settings.GlobalSettings().AlwaysShowTabs()) { // Otherwise, let's use a TabRowControl to calculate how much extra // space we'll need. @@ -544,7 +611,7 @@ namespace winrt::TerminalApp::implementation // 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 valueFromSettings = _settings.GlobalSettings().LaunchMode(); const auto valueFromCommandlineArgs = _appArgs.GetLaunchMode(); return valueFromCommandlineArgs.has_value() ? valueFromCommandlineArgs.value() : @@ -561,7 +628,7 @@ namespace winrt::TerminalApp::implementation // - defaultInitialY: the system default y coordinate value // Return Value: // - a point containing the requested initial position in pixels. - winrt::Windows::Foundation::Point AppLogic::GetLaunchInitialPositions(int32_t defaultInitialX, int32_t defaultInitialY) + TerminalApp::InitialPosition AppLogic::GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY) { if (!_loadedInitialSettings) { @@ -569,13 +636,11 @@ namespace winrt::TerminalApp::implementation LoadSettings(); } - const auto initialPosition{ _settings->GlobalSettings().InitialPosition() }; - winrt::Windows::Foundation::Point point{ - /* X */ gsl::narrow_cast(initialPosition.x.value_or(defaultInitialX)), - /* Y */ gsl::narrow_cast(initialPosition.y.value_or(defaultInitialY)) + const auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; + return { + initialPosition.X ? initialPosition.X.Value() : defaultInitialX, + initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY }; - - return point; } winrt::Windows::UI::Xaml::ElementTheme AppLogic::GetRequestedTheme() @@ -586,7 +651,7 @@ namespace winrt::TerminalApp::implementation LoadSettings(); } - return _settings->GlobalSettings().Theme(); + return _settings.GlobalSettings().Theme(); } bool AppLogic::GetShowTabsInTitlebar() @@ -597,7 +662,18 @@ namespace winrt::TerminalApp::implementation LoadSettings(); } - return _settings->GlobalSettings().ShowTabsInTitlebar(); + return _settings.GlobalSettings().ShowTabsInTitlebar(); + } + + bool AppLogic::GetInitialAlwaysOnTop() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().AlwaysOnTop(); } // Method Description: @@ -618,9 +694,20 @@ namespace winrt::TerminalApp::implementation try { auto newSettings = _isUwp ? CascadiaSettings::LoadUniversal() : CascadiaSettings::LoadAll(); - _settings = std::move(newSettings); - const auto& warnings = _settings->GetWarnings(); - hr = warnings.size() == 0 ? S_OK : S_FALSE; + _settings = newSettings; + + if (_settings.GetLoadingError()) + { + _settingsLoadExceptionText = _GetErrorText(_settings.GetLoadingError().Value()); + return E_INVALIDARG; + } + else if (!_settings.GetSerializationErrorMessage().empty()) + { + _settingsLoadExceptionText = _settings.GetSerializationErrorMessage(); + return E_INVALIDARG; + } + + hr = _settings.Warnings().Size() == 0 ? S_OK : S_FALSE; } catch (const winrt::hresult_error& e) { @@ -628,11 +715,6 @@ namespace winrt::TerminalApp::implementation _settingsLoadExceptionText = e.message(); LOG_HR(hr); } - catch (const ::TerminalApp::SettingsException& ex) - { - hr = E_INVALIDARG; - _settingsLoadExceptionText = _GetErrorText(ex.Error()); - } catch (...) { hr = wil::ResultFromCaughtException(); @@ -689,6 +771,8 @@ namespace winrt::TerminalApp::implementation // Register for directory change notification. _RegisterSettingsChange(); + + Jumplist::UpdateJumplist(_settings); } // Method Description: @@ -701,7 +785,7 @@ namespace winrt::TerminalApp::implementation void AppLogic::_RegisterSettingsChange() { // Get the containing folder. - const auto settingsPath{ CascadiaSettings::GetSettingsPath() }; + const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } }; const auto folder = settingsPath.parent_path(); _reader.create(folder.c_str(), @@ -767,7 +851,7 @@ namespace winrt::TerminalApp::implementation co_await winrt::resume_foreground(_root->Dispatcher()); // Refresh the UI theme - _ApplyTheme(_settings->GlobalSettings().Theme()); + _ApplyTheme(_settings.GlobalSettings().Theme()); } fire_and_forget AppLogic::_ApplyStartupTaskStateChange() @@ -786,7 +870,7 @@ namespace winrt::TerminalApp::implementation if (auto page{ weakThis.get() }) { StartupTaskState state; - bool tryEnableStartupTask = _settings->GlobalSettings().StartOnUserLogin(); + bool tryEnableStartupTask = _settings.GlobalSettings().StartOnUserLogin(); StartupTask task = co_await StartupTask::GetAsync(StartupTaskName); state = task.State(); @@ -847,11 +931,13 @@ namespace winrt::TerminalApp::implementation _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); + + Jumplist::UpdateJumplist(_settings); } // Method Description: // - Returns a pointer to the global shared settings. - [[nodiscard]] std::shared_ptr<::TerminalApp::CascadiaSettings> AppLogic::GetSettings() const noexcept + [[nodiscard]] CascadiaSettings AppLogic::GetSettings() const noexcept { return _settings; } @@ -911,7 +997,7 @@ namespace winrt::TerminalApp::implementation // - Implements the Alt handler (per GH#6421) // Return value: // - whether the key was handled - bool AppLogic::OnDirectKeyEvent(const uint32_t vkey, const bool down) + bool AppLogic::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { if (_root) { @@ -922,7 +1008,7 @@ namespace winrt::TerminalApp::implementation { if (auto keyListener{ focusedObject.try_as() }) { - if (keyListener.OnDirectKeyEvent(vkey, down)) + if (keyListener.OnDirectKeyEvent(vkey, scanCode, down)) { return true; } @@ -932,6 +1018,13 @@ namespace winrt::TerminalApp::implementation 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 { @@ -958,6 +1051,32 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Gets the taskbar state value from the last active control + // Return Value: + // - The taskbar state of the last active control + size_t AppLogic::GetLastActiveControlTaskbarState() + { + if (_root) + { + return _root->GetLastActiveControlTaskbarState(); + } + return {}; + } + + // Method Description: + // - Gets the taskbar progress value from the last active control + // Return Value: + // - The taskbar progress of the last active control + size_t AppLogic::GetLastActiveControlTaskbarProgress() + { + if (_root) + { + return _root->GetLastActiveControlTaskbarProgress(); + } + return {}; + } + // 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 @@ -1015,65 +1134,6 @@ namespace winrt::TerminalApp::implementation return _appArgs.ShouldExitEarly(); } - winrt::hstring AppLogic::ApplicationDisplayName() const - { - try - { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - return package.DisplayName(); - } - CATCH_LOG(); - - return RS_(L"ApplicationDisplayNameUnpackaged"); - } - - winrt::hstring AppLogic::ApplicationVersion() const - { - try - { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - const auto version{ package.Id().Version() }; - winrt::hstring formatted{ wil::str_printf(L"%u.%u.%u.%u", version.Major, version.Minor, version.Build, version.Revision) }; - return formatted; - } - CATCH_LOG(); - - // Try to get the version the old-fashioned way - try - { - struct LocalizationInfo - { - WORD language, codepage; - }; - // Use the current module instance handle for TerminalApp.dll, nullptr for WindowsTerminal.exe - auto filename{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; - auto size{ GetFileVersionInfoSizeExW(0, filename.c_str(), nullptr) }; - THROW_LAST_ERROR_IF(size == 0); - auto versionBuffer{ std::make_unique(size) }; - THROW_IF_WIN32_BOOL_FALSE(GetFileVersionInfoExW(0, filename.c_str(), 0, size, versionBuffer.get())); - - // Get the list of Version localizations - LocalizationInfo* pVarLocalization{ nullptr }; - UINT varLen{ 0 }; - THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), L"\\VarFileInfo\\Translation", reinterpret_cast(&pVarLocalization), &varLen)); - THROW_HR_IF(E_UNEXPECTED, varLen < sizeof(*pVarLocalization)); // there must be at least one translation - - // Get the product version from the localized version compartment - // We're using String/ProductVersion here because our build pipeline puts more rich information in it (like the branch name) - // than in the unlocalized numeric version fields. - WCHAR* pProductVersion{ nullptr }; - UINT versionLen{ 0 }; - const auto localizedVersionName{ wil::str_printf(L"\\StringFileInfo\\%04x%04x\\ProductVersion", - pVarLocalization->language ? pVarLocalization->language : 0x0409, // well-known en-US LCID - pVarLocalization->codepage) }; - THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), localizedVersionName.c_str(), reinterpret_cast(&pProductVersion), &versionLen)); - return { pProductVersion }; - } - CATCH_LOG(); - - return RS_(L"ApplicationVersionUnknown"); - } - bool AppLogic::FocusMode() const { return _root ? _root->FocusMode() : false; diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index de2b225f81f..c11c1a5fc24 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -4,10 +4,8 @@ #pragma once #include "AppLogic.g.h" - -#include "Tab.h" -#include "CascadiaSettings.h" #include "TerminalPage.h" +#include "Jumplist.h" #include "../../cascadia/inc/cppwinrt_utils.h" namespace winrt::TerminalApp::implementation @@ -16,6 +14,7 @@ namespace winrt::TerminalApp::implementation { public: static AppLogic* Current() noexcept; + static const Microsoft::Terminal::Settings::Model::CascadiaSettings CurrentAppSettings(); AppLogic(); ~AppLogic() = default; @@ -25,33 +24,35 @@ namespace winrt::TerminalApp::implementation void RunAsUwp(); bool IsElevated() const noexcept; void LoadSettings(); - [[nodiscard]] std::shared_ptr<::TerminalApp::CascadiaSettings> GetSettings() const noexcept; + [[nodiscard]] Microsoft::Terminal::Settings::Model::CascadiaSettings GetSettings() const noexcept; int32_t SetStartupCommandline(array_view actions); winrt::hstring ParseCommandlineMessage(); bool ShouldExitEarly(); - winrt::hstring ApplicationDisplayName() const; - winrt::hstring ApplicationVersion() const; bool FocusMode() const; bool Fullscreen() const; bool AlwaysOnTop() const; Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi); - winrt::Windows::Foundation::Point GetLaunchInitialPositions(int32_t defaultInitialX, int32_t defaultInitialY); + TerminalApp::InitialPosition GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY); winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); - LaunchMode GetLaunchMode(); + 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; hstring Title(); void TitlebarClicked(); - bool OnDirectKeyEvent(const uint32_t vkey, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); void WindowCloseButtonClicked(); + size_t GetLastActiveControlTaskbarState(); + size_t GetLastActiveControlTaskbarProgress(); + winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); // -------------------------------- WinRT Events --------------------------------- @@ -68,7 +69,7 @@ namespace winrt::TerminalApp::implementation // updated in _ApplyTheme. The root currently is _root. winrt::com_ptr _root{ nullptr }; - std::shared_ptr<::TerminalApp::CascadiaSettings> _settings{ nullptr }; + Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; HRESULT _settingsLoadedResult; winrt::hstring _settingsLoadExceptionText{}; @@ -86,6 +87,8 @@ namespace winrt::TerminalApp::implementation void _ShowLoadErrorsDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey, HRESULT settingsLoadedResult); void _ShowLoadWarningsDialog(); + bool _IsKeyboardServiceEnabled(); + void _ShowKeyboardServiceDisabledDialog(); fire_and_forget _LoadErrorsDialogRoutine(); fire_and_forget _ShowLoadWarningsDialogRoutine(); @@ -110,6 +113,8 @@ namespace winrt::TerminalApp::implementation 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(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); }; } diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 00006608ef1..d1bc606d4b8 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -1,17 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "../TerminalPage.idl"; -import "../ShortcutActionDispatch.idl"; -import "../IDirectKeyListener.idl"; +import "TerminalPage.idl"; +import "ShortcutActionDispatch.idl"; +import "IDirectKeyListener.idl"; namespace TerminalApp { - enum LaunchMode + struct InitialPosition { - DefaultMode, - MaximizedMode, - FullscreenMode, + Int64 X; + Int64 Y; }; [default_interface] runtimeclass AppLogic : IDirectKeyListener, IDialogPresenter @@ -38,23 +37,24 @@ namespace TerminalApp String Title { get; }; - String ApplicationDisplayName { get; }; - String ApplicationVersion { get; }; - Boolean FocusMode { get; }; Boolean Fullscreen { get; }; Boolean AlwaysOnTop { get; }; Windows.Foundation.Size GetLaunchDimensions(UInt32 dpi); - Windows.Foundation.Point GetLaunchInitialPositions(Int32 defaultInitialX, Int32 defaultInitialY); + InitialPosition GetInitialPosition(Int64 defaultInitialX, Int64 defaultInitialY); Windows.UI.Xaml.ElementTheme GetRequestedTheme(); - LaunchMode GetLaunchMode(); + Microsoft.Terminal.Settings.Model.LaunchMode GetLaunchMode(); Boolean GetShowTabsInTitlebar(); + Boolean GetInitialAlwaysOnTop(); Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); void WindowCloseButtonClicked(); + UInt64 GetLastActiveControlTaskbarState(); + UInt64 GetLastActiveControlTaskbarProgress(); + // See IDialogPresenter and TerminalPage's DialogPresenter for more // information. Windows.Foundation.IAsyncOperation ShowDialog(Windows.UI.Xaml.Controls.ContentDialog dialog); @@ -66,5 +66,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler FocusModeChanged; event Windows.Foundation.TypedEventHandler FullscreenChanged; event Windows.Foundation.TypedEventHandler AlwaysOnTopChanged; + event Windows.Foundation.TypedEventHandler RaiseVisualBell; + event Windows.Foundation.TypedEventHandler SetTaskbarProgress; } } diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h deleted file mode 100644 index 004f3809423..00000000000 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ /dev/null @@ -1,133 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CascadiaSettings.h - -Abstract: -- This class acts as the container for all app settings. It's composed of two - parts: Globals, which are app-wide settings, and Profiles, which contain - a set of settings that apply to a single instance of the terminal. - Also contains the logic for serializing and deserializing this object. - -Author(s): -- Mike Griese - March 2019 - ---*/ -#pragma once -#include -#include "GlobalAppSettings.h" -#include "TerminalWarnings.h" -#include "Profile.h" -#include "IDynamicProfileGenerator.h" - -// fwdecl unittest classes -namespace TerminalAppLocalTests -{ - class SettingsTests; - class ProfileTests; - class ColorSchemeTests; - class KeyBindingsTests; - class TabTests; -}; -namespace TerminalAppUnitTests -{ - class DynamicProfileTests; - class JsonTests; -}; - -namespace TerminalApp -{ - class CascadiaSettings; -}; - -class TerminalApp::CascadiaSettings final -{ -public: - CascadiaSettings(); - explicit CascadiaSettings(const bool addDynamicProfiles); - - static std::unique_ptr LoadDefaults(); - static std::unique_ptr LoadAll(); - static std::unique_ptr LoadUniversal(); - - static const CascadiaSettings& GetCurrentAppSettings(); - - std::tuple BuildSettings(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; - winrt::Microsoft::Terminal::Settings::TerminalSettings BuildSettings(GUID profileGuid) const; - - GlobalAppSettings& GlobalSettings(); - - gsl::span GetProfiles() const noexcept; - - winrt::TerminalApp::AppKeyBindings GetKeybindings() const noexcept; - - static std::unique_ptr FromJson(const Json::Value& json); - void LayerJson(const Json::Value& json); - - static std::filesystem::path GetSettingsPath(); - static std::filesystem::path GetDefaultSettingsPath(); - - const Profile* FindProfile(GUID profileGuid) const noexcept; - const ColorScheme* GetColorSchemeForProfile(const GUID profileGuid) const; - - std::vector& GetWarnings(); - -private: - GlobalAppSettings _globals; - std::vector _profiles; - std::vector _warnings; - - std::vector> _profileGenerators; - - std::string _userSettingsString; - Json::Value _userSettings; - Json::Value _defaultSettings; - Json::Value _userDefaultProfileSettings{ Json::Value::null }; - - void _LayerOrCreateProfile(const Json::Value& profileJson); - Profile* _FindMatchingProfile(const Json::Value& profileJson); - void _LayerOrCreateColorScheme(const Json::Value& schemeJson); - ColorScheme* _FindMatchingColorScheme(const Json::Value& schemeJson); - void _ParseJsonString(std::string_view fileData, const bool isDefaultSettings); - static const Json::Value& _GetProfilesJsonObject(const Json::Value& json); - static const Json::Value& _GetDisabledProfileSourcesJsonObject(const Json::Value& json); - bool _PrependSchemaDirective(); - bool _AppendDynamicProfilesToUserSettings(); - std::string _ApplyFirstRunChangesToSettingsTemplate(std::string_view settingsTemplate) const; - - void _ApplyDefaultsFromUserSettings(); - - void _LoadDynamicProfiles(); - - static bool _IsPackaged(); - static void _WriteSettings(const std::string_view content); - static std::optional _ReadUserSettings(); - static std::optional _ReadFile(HANDLE hFile); - - std::optional _GetProfileGuidByName(const std::wstring_view) const; - std::optional _GetProfileGuidByIndex(std::optional index) const; - GUID _GetProfileForArgs(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; - - void _ValidateSettings(); - void _ValidateProfilesExist(); - void _ValidateProfilesHaveGuid(); - void _ValidateDefaultProfileExists(); - void _ValidateNoDuplicateProfiles(); - void _ResolveDefaultProfile(); - void _ReorderProfilesToMatchUserSettingsOrder(); - void _RemoveHiddenProfiles(); - void _ValidateAllSchemesExist(); - void _ValidateMediaResources(); - void _ValidateKeybindings(); - void _ValidateNoGlobalsKey(); - - friend class TerminalAppLocalTests::SettingsTests; - friend class TerminalAppLocalTests::ProfileTests; - friend class TerminalAppLocalTests::ColorSchemeTests; - friend class TerminalAppLocalTests::KeyBindingsTests; - friend class TerminalAppLocalTests::TabTests; - friend class TerminalAppUnitTests::DynamicProfileTests; - friend class TerminalAppUnitTests::JsonTests; -}; diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml index 48563ac97a9..8ea72093ae0 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.xaml +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.xaml @@ -114,11 +114,11 @@ @@ -129,7 +129,7 @@ TableColors = { - "black", - "red", - "green", - "yellow", - "blue", - "purple", - "cyan", - "white", - "brightBlack", - "brightRed", - "brightGreen", - "brightYellow", - "brightBlue", - "brightPurple", - "brightCyan", - "brightWhite" -}; - -ColorScheme::ColorScheme() : - _schemeName{ L"" }, - _table{}, - _defaultForeground{ DEFAULT_FOREGROUND_WITH_ALPHA }, - _defaultBackground{ DEFAULT_BACKGROUND_WITH_ALPHA }, - _selectionBackground{ DEFAULT_FOREGROUND }, - _cursorColor{ DEFAULT_CURSOR_COLOR } -{ -} - -ColorScheme::ColorScheme(std::wstring name, til::color defaultFg, til::color defaultBg, til::color cursorColor) : - _schemeName{ name }, - _table{}, - _defaultForeground{ defaultFg }, - _defaultBackground{ defaultBg }, - _selectionBackground{ DEFAULT_FOREGROUND }, - _cursorColor{ cursorColor } -{ -} - -ColorScheme::~ColorScheme() -{ -} - -// Method Description: -// - Apply our values to the given TerminalSettings object. Sets the foreground, -// background, and color table of the settings object. -// Arguments: -// - terminalSettings: the object to apply our settings to. -// Return Value: -// - -void ColorScheme::ApplyScheme(TerminalSettings terminalSettings) const -{ - terminalSettings.DefaultForeground(static_cast(_defaultForeground)); - terminalSettings.DefaultBackground(static_cast(_defaultBackground)); - terminalSettings.SelectionBackground(static_cast(_selectionBackground)); - terminalSettings.CursorColor(static_cast(_cursorColor)); - - auto const tableCount = gsl::narrow_cast(_table.size()); - for (int i = 0; i < tableCount; i++) - { - terminalSettings.SetColorTableEntry(i, static_cast(_table[i])); - } -} - -// Method Description: -// - Create a new instance of this class from a serialized JsonObject. -// Arguments: -// - json: an object which should be a serialization of a ColorScheme object. -// Return Value: -// - a new ColorScheme instance created from the values in `json` -ColorScheme ColorScheme::FromJson(const Json::Value& json) -{ - ColorScheme result; - result.LayerJson(json); - return result; -} - -// Method Description: -// - Returns true if we think the provided json object represents an instance of -// the same object as this object. If true, we should layer that json object -// on us, instead of creating a new object. -// Arguments: -// - json: The json object to query to see if it's the same -// Return Value: -// - true iff the json object has the same `name` as we do. -bool ColorScheme::ShouldBeLayered(const Json::Value& json) const -{ - std::wstring nameFromJson{}; - if (JsonUtils::GetValueForKey(json, NameKey, nameFromJson)) - { - return nameFromJson == _schemeName; - } - return false; -} - -// Method Description: -// - Layer values from the given json object on top of the existing properties -// of this object. For any keys we're expecting to be able to parse in the -// given object, we'll parse them and replace our settings with values from -// the new json object. Properties that _aren't_ in the json object will _not_ -// be replaced. -// Arguments: -// - json: an object which should be a partial serialization of a ColorScheme object. -// Return Value: -// -void ColorScheme::LayerJson(const Json::Value& json) -{ - JsonUtils::GetValueForKey(json, NameKey, _schemeName); - JsonUtils::GetValueForKey(json, ForegroundKey, _defaultForeground); - JsonUtils::GetValueForKey(json, BackgroundKey, _defaultBackground); - JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _selectionBackground); - JsonUtils::GetValueForKey(json, CursorColorKey, _cursorColor); - - int i = 0; - for (const auto& current : TableColors) - { - JsonUtils::GetValueForKey(json, current, _table.at(i)); - i++; - } -} - -std::wstring_view ColorScheme::GetName() const noexcept -{ - return { _schemeName }; -} - -std::array& ColorScheme::GetTable() noexcept -{ - return _table; -} - -til::color ColorScheme::GetForeground() const noexcept -{ - return _defaultForeground; -} - -til::color ColorScheme::GetBackground() const noexcept -{ - return _defaultBackground; -} - -til::color ColorScheme::GetSelectionBackground() const noexcept -{ - return _selectionBackground; -} - -til::color ColorScheme::GetCursorColor() const noexcept -{ - return _cursorColor; -} - -// Method Description: -// - Parse the name from the JSON representation of a ColorScheme. -// Arguments: -// - json: an object which should be a serialization of a ColorScheme object. -// Return Value: -// - the name of the color scheme represented by `json` as a std::wstring optional -// i.e. the value of the `name` property. -// - returns std::nullopt if `json` doesn't have the `name` property -std::optional ColorScheme::GetNameFromJson(const Json::Value& json) -{ - return JsonUtils::GetValueForKey>(json, NameKey); -} diff --git a/src/cascadia/TerminalApp/ColorScheme.h b/src/cascadia/TerminalApp/ColorScheme.h deleted file mode 100644 index 92f8f4f6d70..00000000000 --- a/src/cascadia/TerminalApp/ColorScheme.h +++ /dev/null @@ -1,66 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- ColorScheme.hpp - -Abstract: -- A color scheme is a single set of colors to use as the terminal colors. These - schemes are named, and can be used to quickly change all the colors of the - terminal to another scheme. - -Author(s): -- Mike Griese - March 2019 - ---*/ -#pragma once -#include -#include -#include "../../inc/conattrs.hpp" - -// fwdecl unittest classes -namespace TerminalAppLocalTests -{ - class SettingsTests; - class ColorSchemeTests; -}; - -namespace TerminalApp -{ - class ColorScheme; -}; - -class TerminalApp::ColorScheme -{ -public: - ColorScheme(); - ColorScheme(std::wstring name, til::color defaultFg, til::color defaultBg, til::color cursorColor); - ~ColorScheme(); - - void ApplyScheme(winrt::Microsoft::Terminal::Settings::TerminalSettings terminalSettings) const; - - static ColorScheme FromJson(const Json::Value& json); - bool ShouldBeLayered(const Json::Value& json) const; - void LayerJson(const Json::Value& json); - - std::wstring_view GetName() const noexcept; - std::array& GetTable() noexcept; - til::color GetForeground() const noexcept; - til::color GetBackground() const noexcept; - til::color GetSelectionBackground() const noexcept; - til::color GetCursorColor() const noexcept; - - static std::optional GetNameFromJson(const Json::Value& json); - -private: - std::wstring _schemeName; - std::array _table; - til::color _defaultForeground; - til::color _defaultBackground; - til::color _selectionBackground; - til::color _cursorColor; - - friend class TerminalAppLocalTests::SettingsTests; - friend class TerminalAppLocalTests::ColorSchemeTests; -}; diff --git a/src/cascadia/TerminalApp/Command.cpp b/src/cascadia/TerminalApp/Command.cpp deleted file mode 100644 index f3fe1219126..00000000000 --- a/src/cascadia/TerminalApp/Command.cpp +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "Command.h" -#include "Command.g.cpp" - -#include "Utils.h" -#include "ActionAndArgs.h" -#include "JsonUtils.h" -#include - -using namespace winrt::Microsoft::Terminal::Settings; -using namespace winrt::TerminalApp; -using namespace ::TerminalApp; - -static constexpr std::string_view NameKey{ "name" }; -static constexpr std::string_view IconPathKey{ "iconPath" }; -static constexpr std::string_view ActionKey{ "command" }; -static constexpr std::string_view ArgsKey{ "args" }; - -namespace winrt::TerminalApp::implementation -{ - // Function Description: - // - attempt to get the name of this command from the provided json object. - // * If the "name" property is a string, return that value. - // * If the "name" property is an object, attempt to lookup the string - // resource specified by the "key" property, to support localizable - // command names. - // Arguments: - // - json: The Json::Value representing the command object we should get the name for. - // Return Value: - // - the empty string if we couldn't find a name, otherwise the command's name. - static winrt::hstring _nameFromJson(const Json::Value& json) - { - if (const auto name{ json[JsonKey(NameKey)] }) - { - if (name.isObject()) - { - if (const auto resourceKey{ JsonUtils::GetValueForKey>(name, "key") }) - { - if (HasLibraryResourceWithName(*resourceKey)) - { - return GetLibraryResourceString(*resourceKey); - } - } - } - else if (name.isString()) - { - return JsonUtils::GetValue(name); - } - } - - return L""; - } - - // Method Description: - // - Get the name for the command specified in `json`. If there is no "name" - // property in the provided json object, then instead generate a name for - // the provided ActionAndArgs. - // Arguments: - // - json: json for the command to generate a name for. - // - actionAndArgs: An ActionAndArgs object to use to generate a name for, - // if the json object doesn't contain a "name". - // Return Value: - // - The "name" from the json, or the generated name from ActionAndArgs::GenerateName - static winrt::hstring _nameFromJsonOrAction(const Json::Value& json, - winrt::com_ptr actionAndArgs) - { - auto manualName = _nameFromJson(json); - if (!manualName.empty()) - { - return manualName; - } - if (!actionAndArgs) - { - return L""; - } - - return actionAndArgs->GenerateName(); - } - - // Method Description: - // - Deserialize a Command from the `json` object. The json object should - // contain a "name" and "action", and optionally an "icon". - // * "name": string|object - the name of the command to display in the - // command palette. If this is an object, look for the "key" property, - // and try to load the string from our resources instead. - // * "action": string|object - A ShortcutAction, either as a name or as an - // ActionAndArgs serialization. See ActionAndArgs::FromJson for details. - // If this is null, we'll remove this command from the list of commands. - // Arguments: - // - json: the Json::Value to deserialize into a Command - // - warnings: If there were any warnings during parsing, they'll be - // appended to this vector. - // Return Value: - // - the newly constructed Command object. - winrt::com_ptr Command::FromJson(const Json::Value& json, - std::vector<::TerminalApp::SettingsLoadWarnings>& warnings) - { - auto result = winrt::make_self(); - - // TODO GH#6644: iconPath not implemented quite yet. Can't seem to get - // the binding quite right. Additionally, do we want it to be an image, - // or a FontIcon? I've had difficulty binding either/or. - - if (const auto actionJson{ json[JsonKey(ActionKey)] }) - { - auto actionAndArgs = ActionAndArgs::FromJson(actionJson, warnings); - - if (actionAndArgs) - { - result->_setAction(*actionAndArgs); - } - else - { - // Something like - // { name: "foo", action: "unbound" } - // will _remove_ the "foo" command, by returning null here. - return nullptr; - } - - result->_setName(_nameFromJsonOrAction(json, actionAndArgs)); - } - else - { - // { name: "foo", action: null } will land in this case, which - // should also be used for unbinding. - return nullptr; - } - - if (result->_Name.empty()) - { - return nullptr; - } - - return result; - } - - // Function Description: - // - Attempt to parse all the json objects in `json` into new Command - // objects, and add them to the map of commands. - // - If any parsed command has - // the same Name as an existing command in commands, the new one will - // layer on top of the existing one. - // Arguments: - // - commands: a map of Name->Command which new commands should be layered upon. - // - json: A Json::Value containing an array of serialized commands - // Return Value: - // - A vector containing any warnings detected while parsing - std::vector<::TerminalApp::SettingsLoadWarnings> Command::LayerJson(std::unordered_map& commands, - const Json::Value& json) - { - std::vector<::TerminalApp::SettingsLoadWarnings> warnings; - - for (const auto& value : json) - { - if (value.isObject()) - { - try - { - auto result = Command::FromJson(value, warnings); - if (result) - { - // Override commands with the same name - commands.insert_or_assign(result->Name(), *result); - } - else - { - // If there wasn't a parsed command, then try to get the - // name from the json blob. If that name currently - // exists in our list of commands, we should remove it. - const auto name = _nameFromJson(value); - if (!name.empty()) - { - commands.erase(name); - } - } - } - CATCH_LOG(); - } - } - return warnings; - } -} diff --git a/src/cascadia/TerminalApp/Command.h b/src/cascadia/TerminalApp/Command.h deleted file mode 100644 index 2882fc8244a..00000000000 --- a/src/cascadia/TerminalApp/Command.h +++ /dev/null @@ -1,45 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- Command.h - -Abstract: -- A command represents a single entry in the Command Palette. This is an object - that has a user facing "name" to display to the user, and an associated action - which can be dispatched. - -- For more information, see GH#2046, #5400, #5674, and #6635 - -Author(s): -- Mike Griese - June 2020 - ---*/ -#pragma once - -#include "Command.g.h" -#include "TerminalWarnings.h" -#include "..\inc\cppwinrt_utils.h" - -namespace winrt::TerminalApp::implementation -{ - struct Command : CommandT - { - Command() = default; - - static winrt::com_ptr FromJson(const Json::Value& json, std::vector<::TerminalApp::SettingsLoadWarnings>& warnings); - static std::vector<::TerminalApp::SettingsLoadWarnings> LayerJson(std::unordered_map& commands, - const Json::Value& json); - - WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); - OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Name, _PropertyChangedHandlers); - OBSERVABLE_GETSET_PROPERTY(winrt::TerminalApp::ActionAndArgs, Action, _PropertyChangedHandlers); - OBSERVABLE_GETSET_PROPERTY(winrt::hstring, KeyChordText, _PropertyChangedHandlers); - }; -} - -namespace winrt::TerminalApp::factory_implementation -{ - BASIC_FACTORY(Command); -} diff --git a/src/cascadia/TerminalApp/Command.idl b/src/cascadia/TerminalApp/Command.idl deleted file mode 100644 index 80e1a2b476b..00000000000 --- a/src/cascadia/TerminalApp/Command.idl +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import "../ShortcutActionDispatch.idl"; - -namespace TerminalApp -{ - [default_interface] runtimeclass Command : Windows.UI.Xaml.Data.INotifyPropertyChanged - { - Command(); - - String Name; - ActionAndArgs Action; - String KeyChordText; - } -} diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 33d32defe7e..685a98838f0 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -4,8 +4,9 @@ #include "pch.h" #include "CommandPalette.h" +#include + #include "CommandPalette.g.cpp" -#include using namespace winrt; using namespace winrt::TerminalApp; @@ -13,15 +14,23 @@ using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::System; using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Microsoft::Terminal::Settings::Model; namespace winrt::TerminalApp::implementation { - CommandPalette::CommandPalette() + CommandPalette::CommandPalette() : + _switcherStartIdx{ 0 } { InitializeComponent(); - _filteredActions = winrt::single_threaded_observable_vector(); - _allActions = winrt::single_threaded_vector(); + _filteredActions = winrt::single_threaded_observable_vector(); + _nestedActionStack = winrt::single_threaded_vector(); + _currentNestedCommands = winrt::single_threaded_vector(); + _allCommands = winrt::single_threaded_vector(); + _tabActions = winrt::single_threaded_vector(); + + _switchToMode(CommandPaletteMode::ActionMode); if (CommandPaletteShadow()) { @@ -39,8 +48,23 @@ namespace winrt::TerminalApp::implementation RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { if (Visibility() == Visibility::Visible) { - _searchBox().Focus(FocusState::Programmatic); - _filteredActionsView().SelectedIndex(0); + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + _searchBox().Visibility(Visibility::Collapsed); + _filteredActionsView().SelectedIndex(_switcherStartIdx); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + _filteredActionsView().Focus(FocusState::Keyboard); + + // Do this right after becoming visible so we can quickly catch scenarios where + // modifiers aren't held down (e.g. command palette invocation). + _anchorKeyUpHandler(); + } + else + { + _filteredActionsView().SelectedIndex(0); + _searchBox().Focus(FocusState::Programmatic); + _updateFilteredActions(); + } TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider @@ -56,6 +80,21 @@ namespace winrt::TerminalApp::implementation _dismissPalette(); } }); + + // Focusing the ListView when the Command Palette control is set to Visible + // for the first time fails because the ListView hasn't finished loading by + // the time Focus is called. Luckily, We can listen to SizeChanged to know + // when the ListView has been measured out and is ready, and we'll immediately + // revoke the handler because we only needed to handle it once on initialization. + _sizeChangedRevoker = _filteredActionsView().SizeChanged(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + _filteredActionsView().Focus(FocusState::Keyboard); + } + _sizeChangedRevoker.revoke(); + }); + + _filteredActionsView().SelectionChanged({ this, &CommandPalette::_selectedCommandChanged }); } // Method Description: @@ -66,7 +105,7 @@ namespace winrt::TerminalApp::implementation // list. Otherwise, we're attempting to move to the previous. // Return Value: // - - void CommandPalette::_selectNextItem(const bool moveDown) + void CommandPalette::SelectNextItem(const bool moveDown) { const auto selected = _filteredActionsView().SelectedIndex(); const int numItems = ::base::saturated_cast(_filteredActionsView().Items().Size()); @@ -78,6 +117,141 @@ namespace winrt::TerminalApp::implementation _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); } + // Method Description: + // - Scroll the command palette to the specified index + // Arguments: + // - index within a list view of commands + // Return Value: + // - + void CommandPalette::_scrollToIndex(uint32_t index) + { + auto numItems = _filteredActionsView().Items().Size(); + + if (numItems == 0) + { + // if the list is empty no need to scroll + return; + } + + auto clampedIndex = std::clamp(index, 0, numItems - 1); + _filteredActionsView().SelectedIndex(clampedIndex); + _filteredActionsView().ScrollIntoView(_filteredActionsView().SelectedItem()); + } + + // Method Description: + // - Computes the number of visible commands + // Arguments: + // - + // Return Value: + // - the approximate number of items visible in the list (in other words the size of the page) + uint32_t CommandPalette::_getNumVisibleItems() + { + const auto container = _filteredActionsView().ContainerFromIndex(0); + const auto item = container.try_as(); + const auto itemHeight = ::base::saturated_cast(item.ActualHeight()); + const auto listHeight = ::base::saturated_cast(_filteredActionsView().ActualHeight()); + return listHeight / itemHeight; + } + + // Method Description: + // - Scrolls the focus one page up the list of commands. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::ScrollPageUp() + { + auto selected = _filteredActionsView().SelectedIndex(); + auto numVisibleItems = _getNumVisibleItems(); + _scrollToIndex(selected - numVisibleItems); + } + + // Method Description: + // - Scrolls the focus one page down the list of commands. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::ScrollPageDown() + { + auto selected = _filteredActionsView().SelectedIndex(); + auto numVisibleItems = _getNumVisibleItems(); + _scrollToIndex(selected + numVisibleItems); + } + + // Method Description: + // - Moves the focus to the top item in the list of commands. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::ScrollToTop() + { + _scrollToIndex(0); + } + + // Method Description: + // - Moves the focus to the bottom item in the list of commands. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::ScrollToBottom() + { + _scrollToIndex(_filteredActionsView().Items().Size() - 1); + } + + // Method Description: + // - Called when the command selection changes. We'll use this in the tab + // switcher to "preview" tabs as the user navigates the list of tabs. To + // do that, we'll dispatch the switch to tab command for this tab, but not + // dismiss the switcher. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_selectedCommandChanged(const IInspectable& /*sender*/, + const Windows::UI::Xaml::RoutedEventArgs& /*args*/) + { + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + const auto selectedCommand = _filteredActionsView().SelectedItem(); + if (const auto filteredCommand = selectedCommand.try_as()) + { + const auto& actionAndArgs = filteredCommand.Command().Action(); + _dispatch.DoAction(actionAndArgs); + } + } + } + + void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + auto key = e.OriginalKey(); + + // Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because + // they're not considered input key presses. While they don't raise KeyDown events, + // they do raise PreviewKeyDown events. + // + // Only give anchored tab switcher the ability to cycle through tabs with the tab button. + // For unanchored mode, accessibility becomes an issue when we try to hijack tab since it's + // a really widely used keyboard navigation key. + if (_currentMode == CommandPaletteMode::TabSwitchMode && key == VirtualKey::Tab) + { + auto const state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); + if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) + { + SelectNextItem(false); + e.Handled(true); + } + else + { + SelectNextItem(true); + e.Handled(true); + } + } + } + // Method Description: // - Process keystrokes in the input box. This is used for moving focus up // and down the list of commands in Action mode, and for executing @@ -94,29 +268,62 @@ namespace winrt::TerminalApp::implementation if (key == VirtualKey::Up) { // Action Mode: Move focus to the next item in the list. - _selectNextItem(false); + SelectNextItem(false); e.Handled(true); } else if (key == VirtualKey::Down) { // Action Mode: Move focus to the previous item in the list. - _selectNextItem(true); + SelectNextItem(true); + e.Handled(true); + } + else if (key == VirtualKey::PageUp) + { + // Action Mode: Move focus to the first visible item in the list. + ScrollPageUp(); + e.Handled(true); + } + else if (key == VirtualKey::PageDown) + { + // Action Mode: Move focus to the last visible item in the list. + ScrollPageDown(); + e.Handled(true); + } + else if (key == VirtualKey::Home) + { + // Action Mode: Move focus to the first item in the list. + ScrollToTop(); + e.Handled(true); + } + else if (key == VirtualKey::End) + { + // Action Mode: Move focus to the last item in the list. + ScrollToBottom(); e.Handled(true); } else if (key == VirtualKey::Enter) { - // Action Mode: Dispatch the action of the selected command. - - if (const auto selectedItem = _filteredActionsView().SelectedItem()) + // Action, TabSwitch or TabSearchMode Mode: Dispatch the action of the selected command. + if (_currentMode != CommandPaletteMode::CommandlineMode) + { + const auto selectedCommand = _filteredActionsView().SelectedItem(); + if (const auto filteredCommand = selectedCommand.try_as()) + { + _dispatchCommand(filteredCommand); + } + } + // Commandline Mode: Use the input to synthesize an ExecuteCommandline action + else if (_currentMode == CommandPaletteMode::CommandlineMode) { - _dispatchCommand(selectedItem.try_as()); + _dispatchCommandline(); } e.Handled(true); } else if (key == VirtualKey::Escape) { - // Action Mode: Dismiss the palette if the text is empty, otherwise clear the search string. + // Dismiss the palette if the text is empty, otherwise clear the + // search string. if (_searchBox().Text().empty()) { _dismissPalette(); @@ -128,6 +335,93 @@ namespace winrt::TerminalApp::implementation e.Handled(true); } + else if (key == VirtualKey::Back) + { + // If the last filter text was empty, and we're backspacing from + // that state, then the user "backspaced" the virtual '>' we're + // using as the action mode indicator. Switch into commandline mode. + if (_searchBox().Text().empty() && _lastFilterTextWasEmpty && _currentMode == CommandPaletteMode::ActionMode) + { + _switchToMode(CommandPaletteMode::CommandlineMode); + } + + e.Handled(true); + } + else + { + const auto vkey = ::gsl::narrow_cast(e.OriginalKey()); + + // In the interest of not telling all modes to check for keybindings, limit to TabSwitch mode for now. + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + + auto success = _bindings.TryKeyChord({ + ctrlDown, + altDown, + shiftDown, + vkey, + }); + + if (success) + { + e.Handled(true); + } + } + } + } + + // Method Description: + // - Implements the Alt handler + // Return value: + // - whether the key was handled + bool CommandPalette::OnDirectKeyEvent(const uint32_t vkey, const uint8_t /*scanCode*/, const bool down) + { + auto handled = false; + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + if (vkey == VK_MENU && !down) + { + _anchorKeyUpHandler(); + handled = true; + } + } + return handled; + } + + void CommandPalette::_keyUpHandler(IInspectable const& /*sender*/, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + if (_currentMode == CommandPaletteMode::TabSwitchMode) + { + _anchorKeyUpHandler(); + e.Handled(true); + } + } + + // Method Description: + // - Handles anchor key ups during TabSwitchMode. + // We assume that at least one modifier key should be held down in order to "anchor" + // the ATS UI in place. So this function is called to check if any modifiers are + // still held down, and if not, dispatch the selected tab action and close the ATS. + // Return value: + // - + void CommandPalette::_anchorKeyUpHandler() + { + auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down); + auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down); + auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down); + + if (!ctrlDown && !altDown && !shiftDown) + { + const auto selectedCommand = _filteredActionsView().SelectedItem(); + if (const auto filteredCommand = selectedCommand.try_as()) + { + _dispatchCommand(filteredCommand); + } + } } // Method Description: @@ -169,7 +463,85 @@ namespace winrt::TerminalApp::implementation void CommandPalette::_listItemClicked(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Controls::ItemClickEventArgs const& e) { - _dispatchCommand(e.ClickedItem().try_as()); + const auto selectedCommand = e.ClickedItem(); + if (const auto filteredCommand = selectedCommand.try_as()) + { + _dispatchCommand(filteredCommand); + } + } + + // Method Description: + // This event is called when the user clicks on an ChevronLeft button right + // next to the ParentCommandName (e.g. New Tab...) above the subcommands list. + // It'll go up a level when the users click the button. + // Arguments: + // - sender: the button that got clicked + // Return Value: + // - + void CommandPalette::_moveBackButtonClicked(Windows::Foundation::IInspectable const& /*sender*/, + Windows::UI::Xaml::RoutedEventArgs const&) + { + _nestedActionStack.Clear(); + ParentCommandName(L""); + _currentNestedCommands.Clear(); + _searchBox().Focus(FocusState::Programmatic); + _updateFilteredActions(); + _filteredActionsView().SelectedIndex(0); + } + + // Method Description: + // - This is called when the user selects a command with subcommands. It + // will update our UI to now display the list of subcommands instead, and + // clear the search text so the user can search from the new list of + // commands. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_updateUIForStackChange() + { + if (_searchBox().Text().empty()) + { + // Manually call _filterTextChanged, because setting the text to the + // empty string won't update it for us (as it won't actually change value.) + _filterTextChanged(nullptr, nullptr); + } + + // Changing the value of the search box will trigger _filterTextChanged, + // which will cause us to refresh the list of filterable commands. + _searchBox().Text(L""); + _searchBox().Focus(FocusState::Programmatic); + } + + // Method Description: + // - Retrieve the list of commands that we should currently be filtering. + // * If the user has command with subcommands, this will return that command's subcommands. + // * If we're in Tab Switcher mode, return the tab actions. + // * Otherwise, just return the list of all the top-level commands. + // Arguments: + // - + // Return Value: + // - A list of Commands to filter. + Collections::IVector CommandPalette::_commandsToFilter() + { + switch (_currentMode) + { + case CommandPaletteMode::ActionMode: + if (_nestedActionStack.Size() > 0) + { + return _currentNestedCommands; + } + + return _allCommands; + case CommandPaletteMode::TabSearchMode: + return _tabActions; + case CommandPaletteMode::TabSwitchMode: + return _tabActions; + case CommandPaletteMode::CommandlineMode: + return winrt::single_threaded_vector(); + default: + return _allCommands; + } } // Method Description: @@ -181,20 +553,106 @@ namespace winrt::TerminalApp::implementation // - command: the Command to dispatch. This might be null. // Return Value: // - - void CommandPalette::_dispatchCommand(const TerminalApp::Command& command) + void CommandPalette::_dispatchCommand(winrt::TerminalApp::FilteredCommand const& filteredCommand) { - if (command) + if (filteredCommand) { - const auto actionAndArgs = command.Action(); - _dispatch.DoAction(actionAndArgs); - _close(); + if (filteredCommand.Command().HasNestedCommands()) + { + // If this Command had subcommands, then don't dispatch the + // action. Instead, display a new list of commands for the user + // to pick from. + _nestedActionStack.Append(filteredCommand); + ParentCommandName(filteredCommand.Command().Name()); + _currentNestedCommands.Clear(); + for (const auto& nameAndCommand : filteredCommand.Command().NestedCommands()) + { + const auto action = nameAndCommand.Value(); + auto nestedFilteredCommand{ winrt::make(action) }; + _currentNestedCommands.Append(nestedFilteredCommand); + } - TraceLoggingWrite( - g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider - "CommandPaletteDispatchedAction", - TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + _updateUIForStackChange(); + } + else + { + // First stash the search text length, because _close will clear this. + const auto searchTextLength = _searchBox().Text().size(); + + // An action from the root command list has depth=0 + const auto nestedCommandDepth = _nestedActionStack.Size(); + + // Close before we dispatch so that actions that open the command + // palette like the Tab Switcher will be able to have the last laugh. + _close(); + + const auto actionAndArgs = filteredCommand.Command().Action(); + _dispatch.DoAction(actionAndArgs); + + TraceLoggingWrite( + g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider + "CommandPaletteDispatchedAction", + TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"), + TraceLoggingUInt32(searchTextLength, "SearchTextLength", "Number of characters in the search string"), + TraceLoggingUInt32(nestedCommandDepth, "NestedCommandDepth", "the depth in the tree of commands for the dispatched action"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + } + } + } + // Method Description: + // - Get all the input text in _searchBox that follows any leading spaces. + // Arguments: + // - + // Return Value: + // - the string of input following any number of leading spaces + std::wstring CommandPalette::_getTrimmedInput() + { + const std::wstring input{ _searchBox().Text() }; + if (input.empty()) + { + return input; + } + + // Trim leading whitespace + const auto firstNonSpace = input.find_first_not_of(L" "); + if (firstNonSpace == std::wstring::npos) + { + // All the following characters are whitespace. + return L""; + } + + return input.substr(firstNonSpace); + } + + // Method Description: + // - Dispatch the current search text as a ExecuteCommandline action. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_dispatchCommandline() + { + auto cmdline{ _getTrimmedInput() }; + if (cmdline.empty()) + { + return; + } + + // Build the ExecuteCommandline action from the values we've parsed on the commandline. + ExecuteCommandlineArgs args{ cmdline }; + ActionAndArgs executeActionAndArgs{ ShortcutAction::ExecuteCommandline, args }; + + TraceLoggingWrite( + g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider + "CommandPaletteDispatchedCommandline", + TraceLoggingDescription("Event emitted when the user runs a commandline in the Command Palette"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + + if (_dispatch.DoAction(executeActionAndArgs)) + { + _close(); } } @@ -228,138 +686,213 @@ namespace winrt::TerminalApp::implementation void CommandPalette::_filterTextChanged(IInspectable const& /*sender*/, Windows::UI::Xaml::RoutedEventArgs const& /*args*/) { + if (_currentMode == CommandPaletteMode::CommandlineMode) + { + _evaluatePrefix(); + } + + // We're setting _lastFilterTextWasEmpty here, because if the user tries + // to backspace the last character in the input, the Backspace KeyDown + // event will fire _before_ _filterTextChanged does. Updating the value + // here will ensure that we can check this case appropriately. + _lastFilterTextWasEmpty = _searchBox().Text().empty(); + _updateFilteredActions(); _filteredActionsView().SelectedIndex(0); - _noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible); + if (_currentMode == CommandPaletteMode::TabSearchMode || _currentMode == CommandPaletteMode::ActionMode) + { + _noMatchesText().Visibility(_filteredActions.Size() > 0 ? Visibility::Collapsed : Visibility::Visible); + } + else + { + _noMatchesText().Visibility(Visibility::Collapsed); + } } - Collections::IObservableVector CommandPalette::FilteredActions() + void CommandPalette::_evaluatePrefix() { - return _filteredActions; + // This will take you from commandline mode, into action mode. The + // backspace handler in _keyDownHandler will handle taking us from + // action mode to commandline mode. + auto newMode = CommandPaletteMode::CommandlineMode; + + auto inputText = _getTrimmedInput(); + if (inputText.size() > 0) + { + if (inputText[0] == L'>') + { + newMode = CommandPaletteMode::ActionMode; + } + } + + if (newMode != _currentMode) + { + //_switchToMode will remove the '>' character from the input. + _switchToMode(newMode); + } } - void CommandPalette::SetActions(Collections::IVector const& actions) + Collections::IObservableVector CommandPalette::FilteredActions() { - _allActions = actions; - _updateFilteredActions(); + return _filteredActions; } - // This is a helper to aid in sorting commands by their `Name`s, alphabetically. - static bool _compareCommandNames(const TerminalApp::Command& lhs, const TerminalApp::Command& rhs) + void CommandPalette::SetKeyBindings(Microsoft::Terminal::TerminalControl::IKeyBindings bindings) { - std::wstring_view leftName{ lhs.Name() }; - std::wstring_view rightName{ rhs.Name() }; - return leftName.compare(rightName) < 0; + _bindings = bindings; } - // This is a helper struct to aid in sorting Commands by a given weighting. - struct WeightedCommand + void CommandPalette::SetCommands(Collections::IVector const& actions) { - TerminalApp::Command command; - int weight; + _populateFilteredActions(_allCommands, actions); + _updateFilteredActions(); + } - bool operator<(const WeightedCommand& other) const + void CommandPalette::SetTabActions(Collections::IVector const& tabs, const bool clearList) + { + _populateFilteredActions(_tabActions, tabs); + // The smooth remove/add animations that happen during + // UpdateFilteredActions don't work very well with changing the tab + // order, because of the sheer amount of remove/adds. So, let's just + // clear & rebuild the list when we change the set of tabs. + // + // Some callers might actually want smooth updating, like when the list + // of tabs changes. + if (clearList && _currentMode == CommandPaletteMode::TabSwitchMode) { - // If two commands have the same weight, then we'll sort them alphabetically. - if (weight == other.weight) - { - return !_compareCommandNames(command, other.command); - } - return weight < other.weight; + _filteredActions.Clear(); } - }; + _updateFilteredActions(); + } // Method Description: - // - Produce a list of filtered actions to reflect the current contents of - // the input box. For more details on which commands will be displayed, - // see `_getWeight`. + // - This helper function is responsible to update a collection of filtered commands (e.g., tab switcher commands) + // with the new values // Arguments: - // - A collection that will receive the filtered actions + // - vectorToPopulate - the vector of filtered commands to populate + // - actions - the raw commands to use // Return Value: // - - std::vector CommandPalette::_collectFilteredActions() + void CommandPalette::_populateFilteredActions(Collections::IVector const& vectorToPopulate, Collections::IVector const& actions) { - std::vector actions; + vectorToPopulate.Clear(); + for (const auto& action : actions) + { + auto filteredCommand{ winrt::make(action) }; + vectorToPopulate.Append(filteredCommand); + } + } - auto searchText = _searchBox().Text(); - const bool addAll = searchText.empty(); + void CommandPalette::EnableCommandPaletteMode() + { + _switchToMode(CommandPaletteMode::ActionMode); + _updateFilteredActions(); + } - // If there's no filter text, then just add all the commands in order to the list. - // - TODO GH#6647:Possibly add the MRU commands first in order, followed - // by the rest of the commands. - if (addAll) + void CommandPalette::_switchToMode(CommandPaletteMode mode) + { + // The smooth remove/add animations that happen during + // UpdateFilteredActions don't work very well when switching between + // modes because of the sheer amount of remove/adds. So, let's just + // clear + append when switching between modes. + if (mode != _currentMode) { - // Add all the commands, but make sure they're sorted alphabetically. - std::vector sortedCommands; - sortedCommands.reserve(_allActions.Size()); + _currentMode = mode; + _filteredActions.Clear(); + auto commandsToFilter = _commandsToFilter(); - for (auto action : _allActions) + for (auto action : commandsToFilter) { - sortedCommands.push_back(action); - } - std::sort(sortedCommands.begin(), - sortedCommands.end(), - _compareCommandNames); - - for (auto action : sortedCommands) - { - actions.push_back(action); + _filteredActions.Append(action); } + } - return actions; + _searchBox().Text(L""); + _searchBox().Select(_searchBox().Text().size(), 0); + // Leaving this block of code outside the above if-statement + // guarantees that the correct text is shown for the mode + // whenever _switchToMode is called. + switch (_currentMode) + { + case CommandPaletteMode::TabSearchMode: + case CommandPaletteMode::TabSwitchMode: + { + SearchBoxPlaceholderText(RS_(L"TabSwitcher_SearchBoxText")); + NoMatchesText(RS_(L"TabSwitcher_NoMatchesText")); + ControlName(RS_(L"TabSwitcherControlName")); + PrefixCharacter(L""); + break; } + case CommandPaletteMode::CommandlineMode: + SearchBoxPlaceholderText(RS_(L"CmdPalCommandlinePrompt")); + NoMatchesText(L""); + ControlName(RS_(L"CommandPaletteControlName")); + PrefixCharacter(L""); + break; + case CommandPaletteMode::ActionMode: + default: + SearchBoxPlaceholderText(RS_(L"CommandPalette_SearchBox/PlaceholderText")); + NoMatchesText(RS_(L"CommandPalette_NoMatchesText/Text")); + ControlName(RS_(L"CommandPaletteControlName")); + PrefixCharacter(L">"); + break; + } + } - // Here, there was some filter text. - // Show these actions in a weighted order. - // - Matching the first character of a word, then the first char of a - // subsequent word seems better than just "the order they appear in - // the list". - // - TODO GH#6647:"Recently used commands" ordering also seems valuable. - // * This could be done by weighting the recently used commands - // higher the more recently they were used, then weighting all - // the unused commands as 1 + // Method Description: + // - Produce a list of filtered actions to reflect the current contents of + // the input box. + // Arguments: + // - A collection that will receive the filtered actions + // Return Value: + // - + std::vector CommandPalette::_collectFilteredActions() + { + std::vector actions; + + winrt::hstring searchText{ _getTrimmedInput() }; - // Use a priority queue to order commands so that "better" matches - // appear first in the list. The ordering will be determined by the - // match weight produced by _getWeight. - std::priority_queue heap; - for (auto action : _allActions) + auto commandsToFilter = _commandsToFilter(); + for (const auto& action : commandsToFilter) { - const auto weight = CommandPalette::_getWeight(searchText, action.Name()); - if (weight > 0) + // Update filter for all commands + // This will modify the highlighting but will also lead to re-computation of weight (and consequently sorting). + // Pay attention that it already updates the highlighting in the UI + action.UpdateFilter(searchText); + + // if there is active search we skip commands with 0 weight + if (searchText.empty() || action.Weight() > 0) { - WeightedCommand wc; - wc.command = action; - wc.weight = weight; - heap.push(wc); + actions.push_back(action); } } - // At this point, all the commands in heap are matches. We've also - // sorted commands with the same weight alphabetically. - // Remove everything in-order from the queue, and add to the list of - // filtered actions. - while (!heap.empty()) + // We want to present the commands sorted, + // unless we are in the TabSwitcherMode and TabSearchMode, + // in which we want to preserve the original order (to be aligned with the tab view) + if (_currentMode != CommandPaletteMode::TabSearchMode && _currentMode != CommandPaletteMode::TabSwitchMode) { - auto top = heap.top(); - heap.pop(); - actions.push_back(top.command); + std::sort(actions.begin(), actions.end(), FilteredCommand::Compare); } - return actions; } // Method Description: // - Update our list of filtered actions to reflect the current contents of - // the input box. For more details on which commands will be displayed, - // see `_getWeight`. + // the input box. // Arguments: // - // Return Value: // - void CommandPalette::_updateFilteredActions() { + if (_currentMode == CommandPaletteMode::CommandlineMode) + { + _filteredActions.Clear(); + return; + } + auto actions = _collectFilteredActions(); // Make _filteredActions look identical to actions, using only Insert and Remove. @@ -368,7 +901,7 @@ namespace winrt::TerminalApp::implementation { for (uint32_t j = i; j < _filteredActions.Size(); j++) { - if (_filteredActions.GetAt(j) == actions[i]) + if (_filteredActions.GetAt(j).Command() == actions[i].Command()) { for (uint32_t k = i; k < j; k++) { @@ -378,7 +911,7 @@ namespace winrt::TerminalApp::implementation } } - if (_filteredActions.GetAt(i) != actions[i]) + if (_filteredActions.GetAt(i).Command() != actions[i].Command()) { _filteredActions.InsertAt(i, actions[i]); } @@ -397,92 +930,6 @@ namespace winrt::TerminalApp::implementation } } - // Function Description: - // - Calculates a "weighting" by which should be used to order a command - // name relative to other names, given a specific search string. - // Currently, this is based off of two factors: - // * The weight is incremented once for each matched character of the - // search text. - // * If a matching character from the search text was found at the start - // of a word in the name, then we increment the weight again. - // * For example, for a search string "sp", we want "Split Pane" to - // appear in the list before "Close Pane" - // * Consecutive matches will be weighted higher than matches with - // characters in between the search characters. - // - This will return 0 if the command should not be shown. If all the - // characters of search text appear in order in `name`, then this function - // will return a positive number. There can be any number of characters - // separating consecutive characters in searchText. - // * For example: - // "name": "New Tab" - // "name": "Close Tab" - // "name": "Close Pane" - // "name": "[-] Split Horizontal" - // "name": "[ | ] Split Vertical" - // "name": "Next Tab" - // "name": "Prev Tab" - // "name": "Open Settings" - // "name": "Open Media Controls" - // * "open" should return both "**Open** Settings" and "**Open** Media Controls". - // * "Tab" would return "New **Tab**", "Close **Tab**", "Next **Tab**" and "Prev - // **Tab**". - // * "P" would return "Close **P**ane", "[-] S**p**lit Horizontal", "[ | ] - // S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en Media - // Controls". - // * "sv" would return "[ | ] Split Vertical" (by matching the **S** in - // "Split", then the **V** in "Vertical"). - // Arguments: - // - searchText: the string of text to search for in `name` - // - name: the name to check - // Return Value: - // - the relative weight of this match - int CommandPalette::_getWeight(const winrt::hstring& searchText, - const winrt::hstring& name) - { - int totalWeight = 0; - bool lastWasSpace = true; - - auto it = name.cbegin(); - - for (auto searchChar : searchText) - { - searchChar = std::towlower(searchChar); - // Advance the iterator to the next character that we're looking - // for. - - bool lastWasMatch = true; - while (true) - { - // If we are at the end of the name string, we haven't found - // it. - if (it == name.cend()) - { - return false; - } - - // found it - if (std::towlower(*it) == searchChar) - { - break; - } - - lastWasSpace = *it == L' '; - ++it; - lastWasMatch = false; - } - - // Advance the iterator by one character so that we don't - // end up on the same character in the next iteration. - ++it; - - totalWeight += 1; - totalWeight += lastWasSpace ? 1 : 0; - totalWeight += (lastWasMatch) ? 1 : 0; - } - - return totalWeight; - } - void CommandPalette::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch) { _dispatch = dispatch; @@ -501,8 +948,32 @@ namespace winrt::TerminalApp::implementation { Visibility(Visibility::Collapsed); + // Reset visibility in case anchor mode tab switcher just finished. + _searchBox().Visibility(Visibility::Visible); + // Clear the text box each time we close the dialog. This is consistent with VsCode. _searchBox().Text(L""); + + _nestedActionStack.Clear(); + + ParentCommandName(L""); + _currentNestedCommands.Clear(); + } + + void CommandPalette::EnableTabSwitcherMode(const bool searchMode, const uint32_t startIdx) + { + _switcherStartIdx = startIdx; + + if (searchMode) + { + _switchToMode(CommandPaletteMode::TabSearchMode); + } + else + { + _switchToMode(CommandPaletteMode::TabSwitchMode); + } + + _updateFilteredActions(); } } diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h index a14466f4f36..ae7b6dde0e6 100644 --- a/src/cascadia/TerminalApp/CommandPalette.h +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -3,47 +3,127 @@ #pragma once +#include "FilteredCommand.h" #include "CommandPalette.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class TabTests; +}; + namespace winrt::TerminalApp::implementation { + enum class CommandPaletteMode + { + ActionMode = 0, + TabSearchMode, + TabSwitchMode, + CommandlineMode + }; + struct CommandPalette : CommandPaletteT { CommandPalette(); - Windows::Foundation::Collections::IObservableVector FilteredActions(); - void SetActions(Windows::Foundation::Collections::IVector const& actions); + Windows::Foundation::Collections::IObservableVector FilteredActions(); + + void SetCommands(Windows::Foundation::Collections::IVector const& actions); + void SetTabActions(Windows::Foundation::Collections::IVector const& tabs, const bool clearList); + void SetKeyBindings(Microsoft::Terminal::TerminalControl::IKeyBindings bindings); + + void EnableCommandPaletteMode(); void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + + void SelectNextItem(const bool moveDown); + + void ScrollPageUp(); + void ScrollPageDown(); + void ScrollToTop(); + void ScrollToBottom(); + + // Tab Switcher + void EnableTabSwitcherMode(const bool searchMode, const uint32_t startIdx); + void SetTabSwitchOrder(const Microsoft::Terminal::Settings::Model::TabSwitcherMode order); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, NoMatchesText, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, SearchBoxPlaceholderText, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, PrefixCharacter, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ControlName, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(winrt::hstring, ParentCommandName, _PropertyChangedHandlers); + private: friend struct CommandPaletteT; // for Xaml to bind events - Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; - Windows::Foundation::Collections::IVector _allActions{ nullptr }; + Windows::Foundation::Collections::IVector _allCommands{ nullptr }; + Windows::Foundation::Collections::IVector _currentNestedCommands{ nullptr }; + Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; + Windows::Foundation::Collections::IVector _nestedActionStack{ nullptr }; + winrt::TerminalApp::ShortcutActionDispatch _dispatch; + Windows::Foundation::Collections::IVector _commandsToFilter(); + + bool _lastFilterTextWasEmpty{ true }; void _filterTextChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& args); + void _previewKeyDownHandler(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void _keyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + void _keyUpHandler(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + + void _selectedCommandChanged(Windows::Foundation::IInspectable const& sender, + Windows::UI::Xaml::RoutedEventArgs const& args); + + void _updateUIForStackChange(); void _rootPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _backdropPointerPressed(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _listItemClicked(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::ItemClickEventArgs const& e); - void _selectNextItem(const bool moveDown); + void _moveBackButtonClicked(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const&); void _updateFilteredActions(); - std::vector _collectFilteredActions(); + + void _populateFilteredActions(Windows::Foundation::Collections::IVector const& vectorToPopulate, + Windows::Foundation::Collections::IVector const& actions); + + std::vector _collectFilteredActions(); + static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name); void _close(); - void _dispatchCommand(const TerminalApp::Command& command); + CommandPaletteMode _currentMode; + void _switchToMode(CommandPaletteMode mode); + std::wstring _getTrimmedInput(); + void _evaluatePrefix(); + + Microsoft::Terminal::TerminalControl::IKeyBindings _bindings; + + // Tab Switcher + Windows::Foundation::Collections::IVector _tabActions{ nullptr }; + uint32_t _switcherStartIdx; + void _anchorKeyUpHandler(); + + winrt::Windows::UI::Xaml::Controls::ListView::SizeChanged_revoker _sizeChangedRevoker; + + void _dispatchCommand(winrt::TerminalApp::FilteredCommand const& command); + void _dispatchCommandline(); void _dismissPalette(); + + void _scrollToIndex(uint32_t index); + uint32_t _getNumVisibleItems(); + + friend class TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/TerminalApp/CommandPalette.idl b/src/cascadia/TerminalApp/CommandPalette.idl index f0e7c9bb539..02ad748a893 100644 --- a/src/cascadia/TerminalApp/CommandPalette.idl +++ b/src/cascadia/TerminalApp/CommandPalette.idl @@ -1,18 +1,34 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "../Command.idl"; +import "IDirectKeyListener.idl"; +import "ShortcutActionDispatch.idl"; +import "HighlightedTextControl.idl"; +import "FilteredCommand.idl"; namespace TerminalApp { - [default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.Grid + [default_interface] runtimeclass CommandPalette : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged, IDirectKeyListener { CommandPalette(); - Windows.Foundation.Collections.IObservableVector FilteredActions { get; }; + String NoMatchesText { get; }; + String SearchBoxPlaceholderText { get; }; + String PrefixCharacter { get; }; + String ControlName { get; }; + String ParentCommandName { get; }; - void SetActions(Windows.Foundation.Collections.IVector actions); + Windows.Foundation.Collections.IObservableVector FilteredActions { get; }; + + void SetCommands(Windows.Foundation.Collections.IVector actions); + void SetTabActions(Windows.Foundation.Collections.IVector tabs, Boolean clearList); + void SetKeyBindings(Microsoft.Terminal.TerminalControl.IKeyBindings bindings); + void EnableCommandPaletteMode(); + + void SelectNextItem(Boolean moveDown); void SetDispatch(ShortcutActionDispatch dispatch); + + void EnableTabSwitcherMode(Boolean searchMode, UInt32 startIdx); } } diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index 39b0e1a25be..680a6be37f4 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -1,6 +1,6 @@ - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Windows10version1903="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 8)" + xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model" + TabNavigation="Cycle" + IsTabStop="True" + AllowFocusOnInteraction="True" PointerPressed="_rootPointerPressed" - mc:Ignorable="d"> + PreviewKeyDown="_previewKeyDownHandler" + KeyDown="_keyDownHandler" + PreviewKeyUp="_keyUpHandler" + mc:Ignorable="d" + AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}"> - + - + + + + @@ -49,7 +60,7 @@ the MIT License. See LICENSE in the project root for license information. -->