diff --git a/.github/actions/spell-check/dictionary/apis.txt b/.github/actions/spell-check/dictionary/apis.txt index fdb6a2dcd05..1e33da10d7e 100644 --- a/.github/actions/spell-check/dictionary/apis.txt +++ b/.github/actions/spell-check/dictionary/apis.txt @@ -1,11 +1,13 @@ ACCEPTFILES ACCESSDENIED +alignof bitfield bitfields CLASSNOTAVAILABLE EXPCMDFLAGS EXPCMDSTATE fullkbd +futex href IAsync IBind @@ -19,7 +21,9 @@ IExplorer IMap IObject IStorage +llabs LCID +lround LSHIFT NCHITTEST NCLBUTTONDBLCLK @@ -28,10 +32,17 @@ NOAGGREGATION NOREDIRECTIONBITMAP oaidl ocidl +otms +OUTLINETEXTMETRICW PAGESCROLL RETURNCMD rfind roundf RSHIFT +rx SIZENS +spsc +STDCPP +syscall tmp +tx diff --git a/.github/actions/spell-check/patterns/patterns.txt b/.github/actions/spell-check/patterns/patterns.txt index 413709e1202..f8c3d65534a 100644 --- a/.github/actions/spell-check/patterns/patterns.txt +++ b/.github/actions/spell-check/patterns/patterns.txt @@ -19,3 +19,4 @@ TestUtils::VerifyExpectedString\(tb, L"[^"]+" Base64::s_(?:En|De)code\(L"[^"]+" VERIFY_ARE_EQUAL\(L"[^"]+" L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\+/" +std::memory_order_[\w]+ diff --git a/NuGet.Config b/NuGet.Config index de105e187bc..00b1de60c4f 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -8,7 +8,7 @@ - + + +[#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/res/Cascadia.ttf b/res/Cascadia.ttf index a271f0dd509..48acfd4e676 100644 Binary files a/res/Cascadia.ttf and b/res/Cascadia.ttf differ diff --git a/res/CascadiaMono.ttf b/res/CascadiaMono.ttf index 2d9145a88e8..e90a0f58b82 100644 Binary files a/res/CascadiaMono.ttf and b/res/CascadiaMono.ttf differ diff --git a/res/README.md b/res/README.md index 8ae4844d54c..f0929cfff73 100644 --- a/res/README.md +++ b/res/README.md @@ -17,5 +17,5 @@ Please consult the [license](https://github.com/raw/microsoft/cascadi ### Fonts Included -* Cascadia Code, Cascadia Mono (2007.01) - * from microsoft/cascadia-code@311cc603f30635da704b6a7d13050e245e61667b +* Cascadia Code, Cascadia Mono (2007.15) + * from microsoft/cascadia-code@2a54363b2c867f7ae811b9a034c0024cef67de96 diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 6c82b718fb9..2718aa14229 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -160,66 +160,98 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const size_t index, co // If we're given a right-side column limit, use it. Otherwise, the write limit is the final column index available in the char row. const auto finalColumnInRow = limitRight.value_or(_charRow.size() - 1); - while (it && currentIndex <= finalColumnInRow) + if (it) { - // Fill the color if the behavior isn't set to keeping the current color. - if (it->TextAttrBehavior() != TextAttributeBehavior::Current) - { - const TextAttributeRun attrRun{ 1, it->TextAttr() }; - LOG_IF_FAILED(_attrRow.InsertAttrRuns({ &attrRun, 1 }, - currentIndex, - currentIndex, - _charRow.size())); - } + // Accumulate usages of the same color so we can spend less time in InsertAttrRuns rewriting it. + auto currentColor = it->TextAttr(); + size_t colorUses = 0; + size_t colorStarts = index; - // Fill the text if the behavior isn't set to saying there's only a color stored in this iterator. - if (it->TextAttrBehavior() != TextAttributeBehavior::StoredOnly) + while (it && currentIndex <= finalColumnInRow) { - const bool fillingLastColumn = currentIndex == finalColumnInRow; - - // TODO: MSFT: 19452170 - We need to ensure when writing any trailing byte that the one to the left - // is a matching leading byte. Likewise, if we're writing a leading byte, we need to make sure we still have space in this loop - // for the trailing byte coming up before writing it. - - // If we're trying to fill the first cell with a trailing byte, pad it out instead by clearing it. - // Don't increment iterator. We'll advance the index and try again with this value on the next round through the loop. - if (currentIndex == 0 && it->DbcsAttr().IsTrailing()) + // Fill the color if the behavior isn't set to keeping the current color. + if (it->TextAttrBehavior() != TextAttributeBehavior::Current) { - _charRow.ClearCell(currentIndex); + // If the color of this cell is the same as the run we're currently on, + // just increment the counter. + if (currentColor == it->TextAttr()) + { + ++colorUses; + } + else + { + // Otherwise, commit this color into the run and save off the new one. + const TextAttributeRun run{ colorUses, currentColor }; + // Now commit the new color runs into the attr row. + LOG_IF_FAILED(_attrRow.InsertAttrRuns({ &run, 1 }, + colorStarts, + currentIndex - 1, + _charRow.size())); + currentColor = it->TextAttr(); + colorUses = 1; + colorStarts = currentIndex; + } } - // If we're trying to fill the last cell with a leading byte, pad it out instead by clearing it. - // Don't increment iterator. We'll exit because we couldn't write a lead at the end of a line. - else if (fillingLastColumn && it->DbcsAttr().IsLeading()) + + // Fill the text if the behavior isn't set to saying there's only a color stored in this iterator. + if (it->TextAttrBehavior() != TextAttributeBehavior::StoredOnly) { - _charRow.ClearCell(currentIndex); - _charRow.SetDoubleBytePadded(true); + const bool fillingLastColumn = currentIndex == finalColumnInRow; + + // TODO: MSFT: 19452170 - We need to ensure when writing any trailing byte that the one to the left + // is a matching leading byte. Likewise, if we're writing a leading byte, we need to make sure we still have space in this loop + // for the trailing byte coming up before writing it. + + // If we're trying to fill the first cell with a trailing byte, pad it out instead by clearing it. + // Don't increment iterator. We'll advance the index and try again with this value on the next round through the loop. + if (currentIndex == 0 && it->DbcsAttr().IsTrailing()) + { + _charRow.ClearCell(currentIndex); + } + // If we're trying to fill the last cell with a leading byte, pad it out instead by clearing it. + // Don't increment iterator. We'll exit because we couldn't write a lead at the end of a line. + else if (fillingLastColumn && it->DbcsAttr().IsLeading()) + { + _charRow.ClearCell(currentIndex); + _charRow.SetDoubleBytePadded(true); + } + // Otherwise, copy the data given and increment the iterator. + else + { + _charRow.DbcsAttrAt(currentIndex) = it->DbcsAttr(); + _charRow.GlyphAt(currentIndex) = it->Chars(); + ++it; + } + + // If we're asked to (un)set the wrap status and we just filled the last column with some text... + // NOTE: + // - wrap = std::nullopt --> don't change the wrap value + // - wrap = true --> we're filling cells as a steam, consider this a wrap + // - wrap = false --> we're filling cells as a block, unwrap + if (wrap.has_value() && fillingLastColumn) + { + // set wrap status on the row to parameter's value. + _charRow.SetWrapForced(wrap.value()); + } } - // Otherwise, copy the data given and increment the iterator. else { - _charRow.DbcsAttrAt(currentIndex) = it->DbcsAttr(); - _charRow.GlyphAt(currentIndex) = it->Chars(); ++it; } - // If we're asked to (un)set the wrap status and we just filled the last column with some text... - // NOTE: - // - wrap = std::nullopt --> don't change the wrap value - // - wrap = true --> we're filling cells as a steam, consider this a wrap - // - wrap = false --> we're filling cells as a block, unwrap - if (wrap.has_value() && fillingLastColumn) - { - // set wrap status on the row to parameter's value. - _charRow.SetWrapForced(wrap.value()); - } + // Move to the next cell for the next time through the loop. + ++currentIndex; } - else + + // Now commit the final color into the attr row + if (colorUses) { - ++it; + const TextAttributeRun run{ colorUses, currentColor }; + LOG_IF_FAILED(_attrRow.InsertAttrRuns({ &run, 1 }, + colorStarts, + currentIndex - 1, + _charRow.size())); } - - // Move to the next cell for the next time through the loop. - ++currentIndex; } return it; diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index b58975f9a85..ad7ee0546e7 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -246,8 +246,7 @@ 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::IsOverlined() const noexcept @@ -270,7 +269,7 @@ void TextAttribute::SetFaint(bool isFaint) noexcept WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Faint, isFaint); } -void TextAttribute::SetItalics(bool isItalic) noexcept +void TextAttribute::SetItalic(bool isItalic) noexcept { WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Italics, isItalic); } @@ -290,13 +289,12 @@ void TextAttribute::SetCrossedOut(bool isCrossedOut) noexcept WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::CrossedOut, isCrossedOut); } -void TextAttribute::SetUnderline(bool isUnderlined) 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::SetOverline(bool isOverlined) noexcept +void TextAttribute::SetOverlined(bool isOverlined) noexcept { WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL, isOverlined); } diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index cccd9aaff7c..7cec03f75e6 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -100,12 +100,12 @@ class TextAttribute final void SetBold(bool isBold) noexcept; void SetFaint(bool isFaint) noexcept; - void SetItalics(bool isItalic) noexcept; + void SetItalic(bool isItalic) noexcept; void SetBlinking(bool isBlinking) noexcept; void SetInvisible(bool isInvisible) noexcept; void SetCrossedOut(bool isCrossedOut) noexcept; - void SetUnderline(bool isUnderlined) noexcept; - void SetOverline(bool isOverlined) noexcept; + void SetUnderlined(bool isUnderlined) noexcept; + void SetOverlined(bool isOverlined) noexcept; void SetReverseVideo(bool isReversed) noexcept; ExtendedAttributes GetExtendedAttributes() const noexcept; @@ -218,11 +218,12 @@ namespace WEX static WEX::Common::NoThrowString ToString(const TextAttribute& attr) { return WEX::Common::NoThrowString().Format( - L"{FG:%s,BG:%s,bold:%d,wLegacy:(0x%04x)}", + L"{FG:%s,BG:%s,bold:%d,wLegacy:(0x%04x),ext:(0x%02x)}", VerifyOutputTraits::ToString(attr._foreground).GetBuffer(), VerifyOutputTraits::ToString(attr._background).GetBuffer(), attr.IsBold(), - attr._wAttrLegacy); + attr._wAttrLegacy, + static_cast(attr._extendedAttrs)); } }; } diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index faee76caf0e..6ecb989f2f0 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -787,7 +787,7 @@ const Cursor& TextBuffer::GetCursor() const noexcept return _currentAttributes; } -void TextBuffer::SetCurrentAttributes(const TextAttribute currentAttributes) noexcept +void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept { _currentAttributes = currentAttributes; } diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 269bb6911e6..9ed2ce8b1af 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -118,7 +118,7 @@ class TextBuffer final [[nodiscard]] TextAttribute GetCurrentAttributes() const noexcept; - void SetCurrentAttributes(const TextAttribute currentAttributes) noexcept; + void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept; void Reset(); diff --git a/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp b/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp index 129e2469182..1babcf6a2c8 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandTests.cpp @@ -147,10 +147,8 @@ namespace TerminalAppLocalTests { "name": "command0", "command": { "action": "splitPane", "split": null } }, { "name": "command1", "command": { "action": "splitPane", "split": "vertical" } }, { "name": "command2", "command": { "action": "splitPane", "split": "horizontal" } }, - { "name": "command3", "command": { "action": "splitPane", "split": "none" } }, { "name": "command4", "command": { "action": "splitPane" } }, - { "name": "command5", "command": { "action": "splitPane", "split": "auto" } }, - { "name": "command6", "command": { "action": "splitPane", "split": "foo" } } + { "name": "command5", "command": { "action": "splitPane", "split": "auto" } } ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); @@ -159,7 +157,7 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, commands.size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(7u, commands.size()); + VERIFY_ARE_EQUAL(5u, commands.size()); { auto command = commands.at(L"command0"); @@ -191,16 +189,6 @@ namespace TerminalAppLocalTests // Verify the args have the expected value VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); } - { - auto command = commands.at(L"command3"); - 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"command4"); VERIFY_IS_NOT_NULL(command); @@ -221,16 +209,6 @@ namespace TerminalAppLocalTests // Verify the args have the expected value VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); } - { - auto command = commands.at(L"command6"); - 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()); - } } void CommandTests::TestResourceKeyName() { diff --git a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp index e9191a4f2f5..d9735e2ccd4 100644 --- a/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp +++ b/src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp @@ -4,7 +4,9 @@ #include "pch.h" #include +#include "../TerminalApp/TerminalPage.h" #include "../TerminalApp/AppCommandlineArgs.h" +#include "../TerminalApp/ActionArgs.h" using namespace WEX::Logging; using namespace WEX::Common; @@ -52,6 +54,10 @@ namespace TerminalAppLocalTests TEST_METHOD(CheckTypos); + TEST_METHOD(TestSimpleExecuteCommandlineAction); + TEST_METHOD(TestMultipleCommandExecuteCommandlineAction); + TEST_METHOD(TestInvalidExecuteCommandlineAction); + private: void _buildCommandlinesHelper(AppCommandlineArgs& appArgs, const size_t expectedSubcommands, @@ -1067,4 +1073,66 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"C:\\", myArgs.TerminalArgs().StartingDirectory()); } } + + void CommandlineTest::TestSimpleExecuteCommandlineAction() + { + auto args = winrt::make_self(); + args->Commandline(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()); + 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_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); + VERIFY_ARE_EQUAL(2u, actions.size()); + { + auto actionAndArgs = actions.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_TRUE(myArgs.TerminalArgs().Profile().empty()); + } + { + auto actionAndArgs = actions.at(1); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, 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_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); + VERIFY_ARE_EQUAL(0u, actions.size()); + } } diff --git a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp index 760bfd94c8a..ad51cf8269b 100644 --- a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp @@ -323,10 +323,8 @@ namespace TerminalAppLocalTests { "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+f"], "command": { "action": "splitPane", "split": "none" } }, { "keys": ["ctrl+g"], "command": { "action": "splitPane" } }, - { "keys": ["ctrl+h"], "command": { "action": "splitPane", "split": "auto" } }, - { "keys": ["ctrl+i"], "command": { "action": "splitPane", "split": "foo" } } + { "keys": ["ctrl+h"], "command": { "action": "splitPane", "split": "auto" } } ])" }; const auto bindings0Json = VerifyParseSucceeded(bindings0String); @@ -335,7 +333,7 @@ namespace TerminalAppLocalTests VERIFY_IS_NOT_NULL(appKeyBindings); VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(7u, appKeyBindings->_keyShortcuts.size()); + VERIFY_ARE_EQUAL(5u, appKeyBindings->_keyShortcuts.size()); { KeyChord kc{ true, false, false, static_cast('C') }; @@ -364,15 +362,6 @@ namespace TerminalAppLocalTests // Verify the args have the expected value VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); } - { - 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::Automatic, realArgs.SplitStyle()); - } { KeyChord kc{ true, false, false, static_cast('G') }; auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); @@ -391,15 +380,6 @@ namespace TerminalAppLocalTests // Verify the args have the expected value VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Automatic, realArgs.SplitStyle()); } - { - KeyChord kc{ true, false, false, static_cast('I') }; - 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()); - } } void KeyBindingsTests::TestSetTabColorArgs() @@ -407,7 +387,6 @@ namespace TerminalAppLocalTests const std::string bindings0String{ R"([ { "keys": ["ctrl+c"], "command": { "action": "setTabColor", "color": null } }, { "keys": ["ctrl+d"], "command": { "action": "setTabColor", "color": "#123456" } }, - { "keys": ["ctrl+e"], "command": { "action": "setTabColor", "color": "thisStringObviouslyWontWork" } }, { "keys": ["ctrl+f"], "command": "setTabColor" }, ])" }; @@ -417,7 +396,7 @@ namespace TerminalAppLocalTests VERIFY_IS_NOT_NULL(appKeyBindings); VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); appKeyBindings->LayerJson(bindings0Json); - VERIFY_ARE_EQUAL(4u, appKeyBindings->_keyShortcuts.size()); + VERIFY_ARE_EQUAL(3u, appKeyBindings->_keyShortcuts.size()); { KeyChord kc{ true, false, false, static_cast('C') }; @@ -439,15 +418,6 @@ namespace TerminalAppLocalTests // 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()); } - { - KeyChord kc{ true, false, false, static_cast('E') }; - auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, 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_NULL(realArgs.TabColor()); - } { KeyChord kc{ true, false, false, static_cast('F') }; auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 8aed7e4525d..f1eeddc2641 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -76,6 +76,8 @@ namespace TerminalAppLocalTests TEST_METHOD(ValidateKeybindingsWarnings); + TEST_METHOD(ValidateExecuteCommandlineWarning); + TEST_METHOD(ValidateLegacyGlobalsWarning); TEST_METHOD(TestTrailingCommas); @@ -91,7 +93,7 @@ namespace TerminalAppLocalTests void SettingsTests::TryCreateWinRTType() { - winrt::Microsoft::Terminal::Settings::TerminalSettings settings; + TerminalSettings settings; VERIFY_IS_NOT_NULL(settings); auto oldFontSize = settings.FontSize(); settings.FontSize(oldFontSize + 5); @@ -1431,10 +1433,6 @@ namespace TerminalAppLocalTests { "name": "profile3", "closeOnExit": null - }, - { - "name": "profile4", - "closeOnExit": { "clearly": "not a string" } } ] })" }; @@ -1449,7 +1447,6 @@ namespace TerminalAppLocalTests // Unknown modes parse as "Graceful" VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[3].GetCloseOnExitMode()); - VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[4].GetCloseOnExitMode()); } void SettingsTests::TestCloseOnExitCompatibilityShim() { @@ -2259,6 +2256,57 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter, settings->_warnings.at(3)); } + 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)); + } + void SettingsTests::ValidateLegacyGlobalsWarning() { const std::string badSettings{ R"( diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index b515eacd93e..3c417174c59 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -84,7 +84,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); @@ -140,7 +140,7 @@ namespace TerminalAppLocalTests // 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{}; + TerminalSettings settings{}; VERIFY_IS_NOT_NULL(settings); winrt::Microsoft::Terminal::TerminalConnection::EchoConnection conn{}; VERIFY_IS_NOT_NULL(conn); diff --git a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj index ad2e4cf7675..c1745442044 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj @@ -114,7 +114,7 @@ - $(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 diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index d5a151f3bf4..bb7112efb1b 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -21,7 +21,26 @@ 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. +// See microsoft/terminal#2066 for more info. +static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */) noexcept +{ + return false; // glyph is not wide. +} + +static bool _EnsureStaticInitialization() +{ + // use C++11 magic statics to make sure we only do this once. + static bool initialized = []() { + // *** THIS IS A SINGLETON *** + SetGlyphWidthFallback(_IsGlyphWideForceNarrowFallback); + + return true; + }(); + return initialized; } LRESULT CALLBACK HwndTerminal::HwndTerminalWndProc( @@ -36,10 +55,29 @@ 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; + } + + // Suppress all mouse events that made it into the terminal. return 0; } } @@ -57,6 +95,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)) @@ -72,7 +114,7 @@ try { const auto bufferData = terminal->_terminal->RetrieveSelectedTextFromBuffer(false); LOG_IF_FAILED(terminal->_CopyTextToSystemClipboard(bufferData, true)); - terminal->_terminal->ClearSelection(); + TerminalClearSelection(terminal); } CATCH_LOG(); } @@ -81,6 +123,11 @@ try terminal->_PasteTextFromClipboard(); } return 0; + case WM_DESTROY: + // Release Terminal's hwnd so Teardown doesn't try to destroy it again + terminal->_hwnd.release(); + terminal->Teardown(); + return 0; } } return DefWindowProc(hwnd, uMsg, wParam, lParam); @@ -114,14 +161,16 @@ static bool RegisterTermClass(HINSTANCE hInstance) noexcept } HwndTerminal::HwndTerminal(HWND parentHwnd) : - _desiredFont{ L"Consolas", 0, 10, { 0, 14 }, CP_UTF8 }, - _actualFont{ L"Consolas", 0, 10, { 0, 14 }, CP_UTF8, false }, + _desiredFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8 }, + _actualFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8, false }, _uiaProvider{ nullptr }, _uiaProviderInitialized{ false }, _currentDpi{ USER_DEFAULT_SCREEN_DPI }, _pfnWriteCallback{ nullptr }, _multiClickTime{ 500 } // this will be overwritten by the windows system double-click time { + _EnsureStaticInitialization(); + HINSTANCE hInstance = wil::GetModuleInstanceHandle(); if (RegisterTermClass(hInstance)) @@ -148,6 +197,11 @@ HwndTerminal::HwndTerminal(HWND parentHwnd) : } } +HwndTerminal::~HwndTerminal() +{ + Teardown(); +} + HRESULT HwndTerminal::Initialize() { _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); @@ -162,9 +216,6 @@ HRESULT HwndTerminal::Initialize() RETURN_IF_FAILED(dxEngine->Enable()); _renderer->AddRenderEngine(dxEngine.get()); - const auto pfn = std::bind(&::Microsoft::Console::Render::Renderer::IsGlyphWideByFont, _renderer.get(), std::placeholders::_1); - SetGlyphWidthFallback(pfn); - _UpdateFont(USER_DEFAULT_SCREEN_DPI); RECT windowRect; GetWindowRect(_hwnd.get(), &windowRect); @@ -181,8 +232,8 @@ HRESULT HwndTerminal::Initialize() _terminal->SetBackgroundCallback([](auto) {}); _terminal->Create(COORD{ 80, 25 }, 1000, *_renderer); - _terminal->SetDefaultBackground(RGB(5, 27, 80)); - _terminal->SetDefaultForeground(RGB(255, 255, 255)); + _terminal->SetDefaultBackground(RGB(12, 12, 12)); + _terminal->SetDefaultForeground(RGB(204, 204, 204)); _terminal->SetWriteInputCallback([=](std::wstring & input) noexcept { _WriteTextToConnection(input); }); localPointerToThread->EnablePainting(); @@ -191,6 +242,33 @@ HRESULT HwndTerminal::Initialize() return S_OK; } +void HwndTerminal::Teardown() noexcept +try +{ + // As a rule, detach resources from the Terminal before shutting them down. + // This ensures that teardown is reentrant. + + // Shut down the renderer (and therefore the thread) before we implode + if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) + { + if (auto localRenderer{ std::exchange(_renderer, nullptr) }) + { + localRenderer->TriggerTeardown(); + // renderer is destroyed + } + // renderEngine is destroyed + } + + if (auto localHwnd{ _hwnd.release() }) + { + // If we're being called through WM_DESTROY, we won't get here (hwnd is already released) + // If we're not, we may end up in Teardown _again_... but by the time we do, all other + // resources have been released and will not be released again. + DestroyWindow(localHwnd); + } +} +CATCH_LOG(); + void HwndTerminal::RegisterScrollCallback(std::function callback) { _terminal->SetScrollPositionChangedCallback(callback); @@ -467,11 +545,21 @@ try } CATCH_RETURN(); +void HwndTerminal::_ClearSelection() noexcept +try +{ + auto lock{ _terminal->LockForWriting() }; + _terminal->ClearSelection(); + _renderer->TriggerSelection(); +} +CATCH_LOG(); + void _stdcall TerminalClearSelection(void* terminal) { - const auto publicTerminal = static_cast(terminal); - publicTerminal->_terminal->ClearSelection(); + auto publicTerminal = static_cast(terminal); + publicTerminal->_ClearSelection(); } + bool _stdcall TerminalIsSelectionActive(void* terminal) { const auto publicTerminal = static_cast(terminal); @@ -482,9 +570,10 @@ bool _stdcall TerminalIsSelectionActive(void* terminal) // Returns the selected text in the terminal. const wchar_t* _stdcall TerminalGetSelection(void* terminal) { - const auto publicTerminal = static_cast(terminal); + auto publicTerminal = static_cast(terminal); const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false); + publicTerminal->_ClearSelection(); // convert text: vector --> string std::wstring selectedText; @@ -494,8 +583,6 @@ const wchar_t* _stdcall TerminalGetSelection(void* terminal) } auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str()); - TerminalClearSelection(terminal); - return returnText.release(); } @@ -541,16 +628,21 @@ 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); @@ -561,20 +653,24 @@ 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()) { - _terminal->ClearSelection(); + _ClearSelection(); if (ch == UNICODE_ESC) { // ESC should clear any selection before it triggers input. @@ -589,21 +685,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) @@ -632,7 +732,7 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font publicTerminal->_terminal->SetCursorStyle(theme.CursorStyle); - publicTerminal->_desiredFont = { fontFamily, 0, 10, { 0, fontSize }, CP_UTF8 }; + publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, { 0, fontSize }, CP_UTF8 }; publicTerminal->_UpdateFont(newDpi); // When the font changes the terminal dimensions need to be recalculated since the available row and column diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index 90b4cc50ac8..47ee1f88dc7 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -34,8 +34,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); @@ -51,9 +51,10 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo HwndTerminal(HwndTerminal&&) = default; HwndTerminal& operator=(const HwndTerminal&) = default; HwndTerminal& operator=(HwndTerminal&&) = default; - ~HwndTerminal() = default; + ~HwndTerminal(); HRESULT Initialize(); + void Teardown() noexcept; void SendOutput(std::wstring_view data); HRESULT Refresh(const SIZE windowSize, _Out_ COORD* dimensions); void RegisterScrollCallback(std::function callback); @@ -92,8 +93,8 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo 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); @@ -112,11 +113,13 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo HRESULT _MoveSelection(LPARAM lParam) noexcept; IRawElementProviderSimple* _GetUiaProvider() noexcept; + void _ClearSelection() noexcept; + 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/TerminalApp/ActionAndArgs.cpp b/src/cascadia/TerminalApp/ActionAndArgs.cpp index 5375ea02cb0..f28bfcca8a6 100644 --- a/src/cascadia/TerminalApp/ActionAndArgs.cpp +++ b/src/cascadia/TerminalApp/ActionAndArgs.cpp @@ -2,6 +2,9 @@ #include "ActionArgs.h" #include "ActionAndArgs.h" #include "ActionAndArgs.g.cpp" + +#include "JsonUtils.h" + #include static constexpr std::string_view CopyTextKey{ "copy" }; @@ -35,6 +38,7 @@ static constexpr std::string_view ToggleAlwaysOnTopKey{ "toggleAlwaysOnTop" }; static constexpr std::string_view SetTabColorKey{ "setTabColor" }; static constexpr std::string_view OpenTabColorPickerKey{ "openTabColorPicker" }; static constexpr std::string_view RenameTabKey{ "renameTab" }; +static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" }; static constexpr std::string_view ActionKey{ "action" }; @@ -44,6 +48,8 @@ static constexpr std::string_view UnboundKey{ "unbound" }; namespace winrt::TerminalApp::implementation { + using namespace ::TerminalApp; + // Specifically use a map here over an unordered_map. We want to be able to // iterate over these entries in-order when we're serializing the keybindings. // HERE BE DRAGONS: @@ -84,6 +90,7 @@ namespace winrt::TerminalApp::implementation { UnboundKey, ShortcutAction::Invalid }, { FindKey, ShortcutAction::Find }, { RenameTabKey, ShortcutAction::RenameTab }, + { ExecuteCommandlineKey, ShortcutAction::ExecuteCommandline }, { ToggleCommandPaletteKey, ShortcutAction::ToggleCommandPalette }, }; @@ -116,6 +123,8 @@ namespace winrt::TerminalApp::implementation { ShortcutAction::RenameTab, winrt::TerminalApp::implementation::RenameTabArgs::FromJson }, + { ShortcutAction::ExecuteCommandline, winrt::TerminalApp::implementation::ExecuteCommandlineArgs::FromJson }, + { ShortcutAction::Invalid, nullptr }, }; @@ -183,11 +192,9 @@ namespace winrt::TerminalApp::implementation } else if (json.isObject()) { - const auto actionVal = json[JsonKey(ActionKey)]; - if (actionVal.isString()) + if (const auto actionString{ JsonUtils::GetValueForKey>(json, ActionKey) }) { - auto actionString = actionVal.asString(); - action = GetActionFromString(actionString); + action = GetActionFromString(*actionString); argsVal = json; } } @@ -265,6 +272,7 @@ namespace winrt::TerminalApp::implementation { ShortcutAction::SetTabColor, RS_(L"ResetTabColorCommandKey") }, { ShortcutAction::OpenTabColorPicker, RS_(L"OpenTabColorPickerCommandKey") }, { ShortcutAction::RenameTab, RS_(L"ResetTabNameCommandKey") }, + { ShortcutAction::ExecuteCommandline, RS_(L"ExecuteCommandlineCommandKey") }, { ShortcutAction::ToggleCommandPalette, RS_(L"ToggleCommandPaletteCommandKey") }, }; }(); @@ -281,5 +289,4 @@ namespace winrt::TerminalApp::implementation const auto found = GeneratedActionNames.find(_Action); return found != GeneratedActionNames.end() ? found->second : L""; } - } diff --git a/src/cascadia/TerminalApp/ActionArgs.cpp b/src/cascadia/TerminalApp/ActionArgs.cpp index 012eacd7f22..a5a83203afa 100644 --- a/src/cascadia/TerminalApp/ActionArgs.cpp +++ b/src/cascadia/TerminalApp/ActionArgs.cpp @@ -17,6 +17,7 @@ #include "OpenSettingsArgs.g.cpp" #include "SetTabColorArgs.g.cpp" #include "RenameTabArgs.g.cpp" +#include "ExecuteCommandlineArgs.g.cpp" #include @@ -258,4 +259,17 @@ namespace winrt::TerminalApp::implementation return RS_(L"ResetTabNameCommandKey"); } + winrt::hstring ExecuteCommandlineArgs::GenerateName() const + { + // "Run commandline "{_Commandline}" in this window" + if (!_Commandline.empty()) + { + return winrt::hstring{ + fmt::format(std::wstring_view(RS_(L"ExecuteCommandlineCommandKey")), + _Commandline.c_str()) + }; + } + return L""; + } + } diff --git a/src/cascadia/TerminalApp/ActionArgs.h b/src/cascadia/TerminalApp/ActionArgs.h index eb233b5967a..091774230d1 100644 --- a/src/cascadia/TerminalApp/ActionArgs.h +++ b/src/cascadia/TerminalApp/ActionArgs.h @@ -17,12 +17,15 @@ #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 @@ -31,6 +34,7 @@ namespace winrt::TerminalApp::implementation { + using namespace ::TerminalApp; using FromJsonResult = std::tuple>; struct ActionEventArgs : public ActionEventArgsT @@ -73,26 +77,11 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto commandline{ json[JsonKey(CommandlineKey)] }) - { - args->_Commandline = winrt::to_hstring(commandline.asString()); - } - if (auto startingDirectory{ json[JsonKey(StartingDirectoryKey)] }) - { - args->_StartingDirectory = winrt::to_hstring(startingDirectory.asString()); - } - if (auto tabTitle{ json[JsonKey(TabTitleKey)] }) - { - args->_TabTitle = winrt::to_hstring(tabTitle.asString()); - } - if (auto index{ json[JsonKey(ProfileIndexKey)] }) - { - args->_ProfileIndex = index.asInt(); - } - if (auto profile{ json[JsonKey(ProfileKey)] }) - { - args->_Profile = winrt::to_hstring(profile.asString()); - } + 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; } }; @@ -120,10 +109,7 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto singleLine{ json[JsonKey(SingleLineKey)] }) - { - args->_SingleLine = singleLine.asBool(); - } + JsonUtils::GetValueForKey(json, SingleLineKey, args->_SingleLine); return { *args, {} }; } }; @@ -177,49 +163,11 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto tabIndex{ json[JsonKey(TabIndexKey)] }) - { - args->_TabIndex = tabIndex.asUInt(); - } + JsonUtils::GetValueForKey(json, TabIndexKey, args->_TabIndex); return { *args, {} }; } }; - // Possible Direction values - // TODO:GH#2550/#3475 - move these to a centralized deserializing place - static constexpr std::string_view LeftString{ "left" }; - static constexpr std::string_view RightString{ "right" }; - static constexpr std::string_view UpString{ "up" }; - static constexpr std::string_view DownString{ "down" }; - - // Function Description: - // - Helper function for parsing a Direction from a string - // Arguments: - // - directionString: the string to attempt to parse - // Return Value: - // - The encoded Direction value, or Direction::None if it was an invalid string - static TerminalApp::Direction ParseDirection(const std::string& directionString) - { - if (directionString == LeftString) - { - return TerminalApp::Direction::Left; - } - else if (directionString == RightString) - { - return TerminalApp::Direction::Right; - } - else if (directionString == UpString) - { - return TerminalApp::Direction::Up; - } - else if (directionString == DownString) - { - return TerminalApp::Direction::Down; - } - // default behavior for invalid data - return TerminalApp::Direction::None; - }; - struct ResizePaneArgs : public ResizePaneArgsT { ResizePaneArgs() = default; @@ -243,10 +191,7 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto directionString{ json[JsonKey(DirectionKey)] }) - { - args->_Direction = ParseDirection(directionString.asString()); - } + JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); if (args->_Direction == TerminalApp::Direction::None) { return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; @@ -281,10 +226,7 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto directionString{ json[JsonKey(DirectionKey)] }) - { - args->_Direction = ParseDirection(directionString.asString()); - } + JsonUtils::GetValueForKey(json, DirectionKey, args->_Direction); if (args->_Direction == TerminalApp::Direction::None) { return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; @@ -319,48 +261,11 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto jsonDelta{ json[JsonKey(AdjustFontSizeDelta)] }) - { - args->_Delta = jsonDelta.asInt(); - } + JsonUtils::GetValueForKey(json, AdjustFontSizeDelta, args->_Delta); return { *args, {} }; } }; - // Possible SplitState values - // TODO:GH#2550/#3475 - move these to a centralized deserializing place - static constexpr std::string_view VerticalKey{ "vertical" }; - static constexpr std::string_view HorizontalKey{ "horizontal" }; - static constexpr std::string_view AutomaticKey{ "auto" }; - static TerminalApp::SplitState ParseSplitState(const std::string& stateString) - { - if (stateString == VerticalKey) - { - return TerminalApp::SplitState::Vertical; - } - else if (stateString == HorizontalKey) - { - return TerminalApp::SplitState::Horizontal; - } - else if (stateString == AutomaticKey) - { - return TerminalApp::SplitState::Automatic; - } - // default behavior for invalid data - return TerminalApp::SplitState::Automatic; - }; - - // Possible SplitType values - static constexpr std::string_view DuplicateKey{ "duplicate" }; - static TerminalApp::SplitType ParseSplitModeState(const std::string& stateString) - { - if (stateString == DuplicateKey) - { - return TerminalApp::SplitType::Duplicate; - } - return TerminalApp::SplitType::Manual; - } - struct SplitPaneArgs : public SplitPaneArgsT { SplitPaneArgs() = default; @@ -391,48 +296,12 @@ namespace winrt::TerminalApp::implementation // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); args->_TerminalArgs = NewTerminalArgs::FromJson(json); - if (auto jsonStyle{ json[JsonKey(SplitKey)] }) - { - args->_SplitStyle = ParseSplitState(jsonStyle.asString()); - } - if (auto jsonStyle{ json[JsonKey(SplitModeKey)] }) - { - args->_SplitMode = ParseSplitModeState(jsonStyle.asString()); - } + JsonUtils::GetValueForKey(json, SplitKey, args->_SplitStyle); + JsonUtils::GetValueForKey(json, SplitModeKey, args->_SplitMode); return { *args, {} }; } }; - // Possible SettingsTarget values - // TODO:GH#2550/#3475 - move these to a centralized deserializing place - static constexpr std::string_view SettingsFileString{ "settingsFile" }; - static constexpr std::string_view DefaultsFileString{ "defaultsFile" }; - static constexpr std::string_view AllFilesString{ "allFiles" }; - - // Function Description: - // - Helper function for parsing a SettingsTarget from a string - // Arguments: - // - targetString: the string to attempt to parse - // Return Value: - // - The encoded SettingsTarget value, or SettingsTarget::SettingsFile if it was an invalid string - static TerminalApp::SettingsTarget ParseSettingsTarget(const std::string& targetString) - { - if (targetString == SettingsFileString) - { - return TerminalApp::SettingsTarget::SettingsFile; - } - else if (targetString == DefaultsFileString) - { - return TerminalApp::SettingsTarget::DefaultsFile; - } - else if (targetString == AllFilesString) - { - return TerminalApp::SettingsTarget::AllFiles; - } - // default behavior for invalid data - return TerminalApp::SettingsTarget::SettingsFile; - }; - struct OpenSettingsArgs : public OpenSettingsArgsT { OpenSettingsArgs() = default; @@ -456,10 +325,7 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto targetString{ json[JsonKey(TargetKey)] }) - { - args->_Target = ParseSettingsTarget(targetString.asString()); - } + JsonUtils::GetValueForKey(json, TargetKey, args->_Target); return { *args, {} }; } }; @@ -487,16 +353,10 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - std::optional temp; - try + if (const auto temp{ JsonUtils::GetValueForKey>(json, ColorKey) }) { - ::TerminalApp::JsonUtils::GetOptionalColor(json, ColorKey, temp); - if (temp.has_value()) - { - args->_TabColor = static_cast(temp.value()); - } + args->_TabColor = static_cast(*temp); } - CATCH_LOG(); return { *args, {} }; } }; @@ -524,13 +384,43 @@ namespace winrt::TerminalApp::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); - if (auto title{ json[JsonKey(TitleKey)] }) + 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()) { - args->_Title = winrt::to_hstring(title.asString()); + return { nullptr, { ::TerminalApp::SettingsLoadWarnings::MissingRequiredParameter } }; } return { *args, {} }; } }; + } namespace winrt::TerminalApp::factory_implementation diff --git a/src/cascadia/TerminalApp/ActionArgs.idl b/src/cascadia/TerminalApp/ActionArgs.idl index 36d5025bb4a..84e5588bf2d 100644 --- a/src/cascadia/TerminalApp/ActionArgs.idl +++ b/src/cascadia/TerminalApp/ActionArgs.idl @@ -115,4 +115,9 @@ namespace TerminalApp { String Title { get; }; }; + + [default_interface] runtimeclass ExecuteCommandlineArgs : IActionArgs + { + String Commandline; + }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 271513d4e58..63a2743d80b 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -11,6 +11,7 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Text; 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; @@ -334,4 +335,20 @@ namespace winrt::TerminalApp::implementation } args.Handled(true); } + + void TerminalPage::_HandleExecuteCommandline(const IInspectable& /*sender*/, + const TerminalApp::ActionEventArgs& actionArgs) + { + if (const auto& realArgs = actionArgs.ActionArgs().try_as()) + { + auto actions = winrt::single_threaded_vector(std::move( + TerminalPage::ConvertExecuteCommandlineToActions(realArgs))); + + if (_startupActions.Size() != 0) + { + actionArgs.Handled(true); + _ProcessStartupActions(actions, false); + } + } + } } diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 753afb2fe6e..70e6df6940b 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -599,7 +599,7 @@ void AppCommandlineArgs::_addCommandsForArg(std::vector& commands, // - // Return Value: // - the deque of actions we've buffered as a result of parsing commands. -std::deque& AppCommandlineArgs::GetStartupActions() +std::vector& AppCommandlineArgs::GetStartupActions() { return _startupActions; } @@ -658,7 +658,8 @@ void AppCommandlineArgs::ValidateStartupCommands() auto newTerminalArgs = winrt::make_self(); args->TerminalArgs(*newTerminalArgs); newTabAction->Args(*args); - _startupActions.push_front(*newTabAction); + // push the arg onto the front + _startupActions.insert(_startupActions.begin(), 1, *newTabAction); } } @@ -666,3 +667,52 @@ std::optional AppCommandlineArgs::GetLaunchMode( { return _launchMode; } + +// Method Description: +// - Attempts to parse an array of commandline args into a list of +// commands to execute, and then parses these commands. As commands are +// successfully parsed, they will generate ShortcutActions for us to be +// able to execute. If we fail to parse any commands, we'll return the +// error code from the failure to parse that command, and stop processing +// additional commands. +// - The first arg in args should be the program name "wt" (or some variant). It +// will be ignored during parsing. +// Arguments: +// - args: an array of strings to process as a commandline. These args can contain spaces +// Return Value: +// - 0 if the commandline was successfully parsed +int AppCommandlineArgs::ParseArgs(winrt::array_view& args) +{ + auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args); + + for (auto& cmdBlob : commands) + { + // On one hand, it seems like we should be able to have one + // AppCommandlineArgs for parsing all of them, and collect the + // results one at a time. + // + // On the other hand, re-using a CLI::App seems to leave state from + // previous parsings around, so we could get mysterious behavior + // where one command affects the values of the next. + // + // From https://cliutils.github.io/CLI11/book/chapters/options.html: + // > If that option is not given, CLI11 will not touch the initial + // > value. This allows you to set up defaults by simply setting + // > your value beforehand. + // + // So we pretty much need the to either manually reset the state + // each command, or build new ones. + const auto result = ParseCommand(cmdBlob); + + // If this succeeded, result will be 0. Otherwise, the caller should + // exit(result), to exit the program. + if (result != 0) + { + return result; + } + } + + // If all the args were successfully parsed, we'll have some commands + // built in _appArgs, which we'll use when the application starts up. + return 0; +} diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 00c7f3cd904..a0d87ffc6b8 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -28,13 +28,15 @@ class TerminalApp::AppCommandlineArgs final AppCommandlineArgs(); ~AppCommandlineArgs() = default; + int ParseCommand(const Commandline& command); + int ParseArgs(winrt::array_view& args); static std::vector BuildCommands(const std::vector& args); static std::vector BuildCommands(winrt::array_view& args); void ValidateStartupCommands(); - std::deque& GetStartupActions(); + std::vector& GetStartupActions(); const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; @@ -90,7 +92,7 @@ class TerminalApp::AppCommandlineArgs final std::optional _launchMode{ std::nullopt }; // Are you adding more args here? Make sure to reset them in _resetStateToDefault - std::deque _startupActions; + std::vector _startupActions; std::string _exitMessage; bool _shouldExitEarly{ false }; diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index e036bd9ee2c..5a6ec188703 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -474,7 +474,7 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - a point containing the requested dimensions in pixels. - winrt::Windows::Foundation::Point AppLogic::GetLaunchDimensions(uint32_t dpi) + winrt::Windows::Foundation::Size AppLogic::GetLaunchDimensions(uint32_t dpi) { if (!_loadedInitialSettings) { @@ -504,7 +504,7 @@ namespace winrt::TerminalApp::implementation // of the height calculation here. auto titlebar = TitlebarControl{ static_cast(0) }; titlebar.Measure({ SHRT_MAX, SHRT_MAX }); - proposedSize.Y += (titlebar.DesiredSize().Height) * scale; + proposedSize.Height += (titlebar.DesiredSize().Height) * scale; } else if (_settings->GlobalSettings().AlwaysShowTabs()) { @@ -519,7 +519,7 @@ namespace winrt::TerminalApp::implementation // For whatever reason, there's about 6px of unaccounted-for space // in the application. I couldn't tell you where these 6px are // coming from, but they need to be included in this math. - proposedSize.Y += (tabControl.DesiredSize().Height + 6) * scale; + proposedSize.Width += (tabControl.DesiredSize().Height + 6) * scale; } return proposedSize; @@ -974,7 +974,7 @@ namespace winrt::TerminalApp::implementation // or 0. (see AppLogic::_ParseArgs) int32_t AppLogic::SetStartupCommandline(array_view args) { - const auto result = _ParseArgs(args); + const auto result = _appArgs.ParseArgs(args); if (result == 0) { _appArgs.ValidateStartupCommands(); @@ -984,53 +984,6 @@ namespace winrt::TerminalApp::implementation return result; } - // Method Description: - // - Attempts to parse an array of commandline args into a list of - // commands to execute, and then parses these commands. As commands are - // successfully parsed, they will generate ShortcutActions for us to be - // able to execute. If we fail to parse any commands, we'll return the - // error code from the failure to parse that command, and stop processing - // additional commands. - // Arguments: - // - args: an array of strings to process as a commandline. These args can contain spaces - // Return Value: - // - 0 if the commandline was successfully parsed - int AppLogic::_ParseArgs(winrt::array_view& args) - { - auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args); - - for (auto& cmdBlob : commands) - { - // On one hand, it seems like we should be able to have one - // AppCommandlineArgs for parsing all of them, and collect the - // results one at a time. - // - // On the other hand, re-using a CLI::App seems to leave state from - // previous parsings around, so we could get mysterious behavior - // where one command affects the values of the next. - // - // From https://cliutils.github.io/CLI11/book/chapters/options.html: - // > If that option is not given, CLI11 will not touch the initial - // > value. This allows you to set up defaults by simply setting - // > your value beforehand. - // - // So we pretty much need the to either manually reset the state - // each command, or build new ones. - const auto result = _appArgs.ParseCommand(cmdBlob); - - // If this succeeded, result will be 0. Otherwise, the caller should - // exit(result), to exit the program. - if (result != 0) - { - return result; - } - } - - // If all the args were successfully parsed, we'll have some commands - // built in _appArgs, which we'll use when the application starts up. - return 0; - } - // Method Description: // - If there were any errors parsing the commandline that was used to // initialize the terminal, this will return a string containing that diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index c879a6f1876..de2b225f81f 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -37,7 +37,7 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; - Windows::Foundation::Point GetLaunchDimensions(uint32_t dpi); + Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi); winrt::Windows::Foundation::Point GetLaunchInitialPositions(int32_t defaultInitialX, int32_t defaultInitialY); winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 071a5ce84cc..00006608ef1 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -45,7 +45,8 @@ namespace TerminalApp Boolean Fullscreen { get; }; Boolean AlwaysOnTop { get; }; - Windows.Foundation.Point GetLaunchDimensions(UInt32 dpi); + Windows.Foundation.Size GetLaunchDimensions(UInt32 dpi); + Windows.Foundation.Point GetLaunchInitialPositions(Int32 defaultInitialX, Int32 defaultInitialY); Windows.UI.Xaml.ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index 004f3809423..08a65fba5d7 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -54,8 +54,8 @@ class TerminalApp::CascadiaSettings final static const CascadiaSettings& GetCurrentAppSettings(); - std::tuple BuildSettings(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; - winrt::Microsoft::Terminal::Settings::TerminalSettings BuildSettings(GUID profileGuid) const; + std::tuple BuildSettings(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; + winrt::TerminalApp::TerminalSettings BuildSettings(GUID profileGuid) const; GlobalAppSettings& GlobalSettings(); diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp index 2ed23f8c770..1a8f2b6e71f 100644 --- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -249,9 +249,9 @@ void CascadiaSettings::_LoadDynamicProfiles() const auto disabledProfileSources = CascadiaSettings::_GetDisabledProfileSourcesJsonObject(_userSettings); if (disabledProfileSources.isArray()) { - for (const auto& ns : disabledProfileSources) + for (const auto& json : disabledProfileSources) { - ignoredNamespaces.emplace(GetWstringFromJson(ns)); + ignoredNamespaces.emplace(JsonUtils::GetValue(json)); } } diff --git a/src/cascadia/TerminalApp/ColorScheme.cpp b/src/cascadia/TerminalApp/ColorScheme.cpp index 45d4e7748e4..1659013809c 100644 --- a/src/cascadia/TerminalApp/ColorScheme.cpp +++ b/src/cascadia/TerminalApp/ColorScheme.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT license. #include "pch.h" +#include #include "ColorScheme.h" #include "DefaultSettings.h" #include "../../types/inc/Utils.hpp" @@ -10,8 +11,7 @@ using namespace ::Microsoft::Console; using namespace TerminalApp; -using namespace winrt::Microsoft::Terminal::Settings; -using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::TerminalApp; static constexpr std::string_view NameKey{ "name" }; static constexpr std::string_view ForegroundKey{ "foreground" }; @@ -105,9 +105,9 @@ ColorScheme ColorScheme::FromJson(const Json::Value& json) // - true iff the json object has the same `name` as we do. bool ColorScheme::ShouldBeLayered(const Json::Value& json) const { - if (const auto name{ json[JsonKey(NameKey)] }) + std::wstring nameFromJson{}; + if (JsonUtils::GetValueForKey(json, NameKey, nameFromJson)) { - const auto nameFromJson = GetWstringFromJson(name); return nameFromJson == _schemeName; } return false; @@ -125,39 +125,16 @@ bool ColorScheme::ShouldBeLayered(const Json::Value& json) const // void ColorScheme::LayerJson(const Json::Value& json) { - if (auto name{ json[JsonKey(NameKey)] }) - { - _schemeName = winrt::to_hstring(name.asString()); - } - if (auto fgString{ json[JsonKey(ForegroundKey)] }) - { - const auto color = Utils::ColorFromHexString(fgString.asString()); - _defaultForeground = color; - } - if (auto bgString{ json[JsonKey(BackgroundKey)] }) - { - const auto color = Utils::ColorFromHexString(bgString.asString()); - _defaultBackground = color; - } - if (auto sbString{ json[JsonKey(SelectionBackgroundKey)] }) - { - const auto color = Utils::ColorFromHexString(sbString.asString()); - _selectionBackground = color; - } - if (auto sbString{ json[JsonKey(CursorColorKey)] }) - { - const auto color = Utils::ColorFromHexString(sbString.asString()); - _cursorColor = color; - } + 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) { - if (auto str{ json[JsonKey(current)] }) - { - const auto color = Utils::ColorFromHexString(str.asString()); - _table.at(i) = color; - } + JsonUtils::GetValueForKey(json, current, _table.at(i)); i++; } } @@ -200,11 +177,7 @@ til::color ColorScheme::GetCursorColor() const noexcept // - 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 TerminalApp::ColorScheme::GetNameFromJson(const Json::Value& json) +std::optional ColorScheme::GetNameFromJson(const Json::Value& json) { - if (const auto name{ json[JsonKey(NameKey)] }) - { - return GetWstringFromJson(name); - } - return std::nullopt; + return JsonUtils::GetValueForKey>(json, NameKey); } diff --git a/src/cascadia/TerminalApp/ColorScheme.h b/src/cascadia/TerminalApp/ColorScheme.h index 92f8f4f6d70..dca0f53759a 100644 --- a/src/cascadia/TerminalApp/ColorScheme.h +++ b/src/cascadia/TerminalApp/ColorScheme.h @@ -15,8 +15,8 @@ Author(s): --*/ #pragma once -#include #include +#include "TerminalSettings.h" #include "../../inc/conattrs.hpp" // fwdecl unittest classes @@ -38,7 +38,7 @@ class TerminalApp::ColorScheme ColorScheme(std::wstring name, til::color defaultFg, til::color defaultBg, til::color cursorColor); ~ColorScheme(); - void ApplyScheme(winrt::Microsoft::Terminal::Settings::TerminalSettings terminalSettings) const; + void ApplyScheme(winrt::TerminalApp::TerminalSettings terminalSettings) const; static ColorScheme FromJson(const Json::Value& json); bool ShouldBeLayered(const Json::Value& json) const; diff --git a/src/cascadia/TerminalApp/Command.cpp b/src/cascadia/TerminalApp/Command.cpp index a8e72ec6a84..f3fe1219126 100644 --- a/src/cascadia/TerminalApp/Command.cpp +++ b/src/cascadia/TerminalApp/Command.cpp @@ -7,10 +7,12 @@ #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" }; @@ -35,25 +37,17 @@ namespace winrt::TerminalApp::implementation { if (name.isObject()) { - try + if (const auto resourceKey{ JsonUtils::GetValueForKey>(name, "key") }) { - if (const auto keyJson{ name[JsonKey("key")] }) + if (HasLibraryResourceWithName(*resourceKey)) { - // Make sure the key is present before we try - // loading it. Otherwise we'll crash - const auto resourceKey = GetWstringFromJson(keyJson); - if (HasLibraryResourceWithName(resourceKey)) - { - return GetLibraryResourceString(resourceKey); - } + return GetLibraryResourceString(*resourceKey); } } - CATCH_LOG(); } else if (name.isString()) { - auto nameStr = name.asString(); - return winrt::to_hstring(nameStr); + return JsonUtils::GetValue(name); } } diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 6fac2d35d62..fd0a139281b 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -187,14 +187,16 @@ namespace winrt::TerminalApp::implementation { const auto actionAndArgs = command.Action(); _dispatch.DoAction(actionAndArgs); - _close(); TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider "CommandPaletteDispatchedAction", TraceLoggingDescription("Event emitted when the user selects an action in the Command Palette"), + TraceLoggingUInt32(_searchBox().Text().size(), "SearchTextLength", "Number of characters in the search string"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + + _close(); } } @@ -271,16 +273,17 @@ namespace winrt::TerminalApp::implementation }; // Method Description: - // - Update our list of filtered actions to reflect the current contents of + // - 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`. // Arguments: - // - + // - A collection that will receive the filtered actions // Return Value: // - - void CommandPalette::_updateFilteredActions() + std::vector CommandPalette::_collectFilteredActions() { - _filteredActions.Clear(); + std::vector actions; + auto searchText = _searchBox().Text(); const bool addAll = searchText.empty(); @@ -303,10 +306,10 @@ namespace winrt::TerminalApp::implementation for (auto action : sortedCommands) { - _filteredActions.Append(action); + actions.push_back(action); } - return; + return actions; } // Here, there was some filter text. @@ -343,7 +346,56 @@ namespace winrt::TerminalApp::implementation { auto top = heap.top(); heap.pop(); - _filteredActions.Append(top.command); + actions.push_back(top.command); + } + + 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`. + // Arguments: + // - + // Return Value: + // - + void CommandPalette::_updateFilteredActions() + { + auto actions = _collectFilteredActions(); + + // Make _filteredActions look identical to actions, using only Insert and Remove. + // This allows WinUI to nicely animate the ListView as it changes. + for (uint32_t i = 0; i < _filteredActions.Size() && i < actions.size(); i++) + { + for (uint32_t j = i; j < _filteredActions.Size(); j++) + { + if (_filteredActions.GetAt(j) == actions[i]) + { + for (uint32_t k = i; k < j; k++) + { + _filteredActions.RemoveAt(i); + } + break; + } + } + + if (_filteredActions.GetAt(i) != actions[i]) + { + _filteredActions.InsertAt(i, actions[i]); + } + } + + // Remove any extra trailing items from the destination + while (_filteredActions.Size() > actions.size()) + { + _filteredActions.RemoveAtEnd(); + } + + // Add any extra trailing items from the source + while (_filteredActions.Size() < actions.size()) + { + _filteredActions.Append(actions[_filteredActions.Size()]); } } diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h index 2d396070518..a14466f4f36 100644 --- a/src/cascadia/TerminalApp/CommandPalette.h +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -37,6 +37,7 @@ namespace winrt::TerminalApp::implementation void _selectNextItem(const bool moveDown); void _updateFilteredActions(); + std::vector _collectFilteredActions(); static int _getWeight(const winrt::hstring& searchText, const winrt::hstring& name); void _close(); diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index e3d89d3e00a..39b0e1a25be 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -195,9 +195,10 @@ the MIT License. See LICENSE in the project root for license information. --> - - - + + + + diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index 1e074d86cf2..dd3b17ae723 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -7,11 +7,11 @@ #include "../../inc/DefaultSettings.h" #include "Utils.h" #include "JsonUtils.h" +#include "TerminalSettingsSerializationHelpers.h" using namespace TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::TerminalApp; -using namespace winrt::Windows::Data::Json; using namespace winrt::Windows::UI::Xaml; using namespace ::Microsoft::Console; using namespace winrt::Microsoft::UI::Xaml::Controls; @@ -44,21 +44,6 @@ static constexpr std::string_view ForceFullRepaintRenderingKey{ "experimental.re static constexpr std::string_view SoftwareRenderingKey{ "experimental.rendering.software" }; static constexpr std::string_view ForceVTInputKey{ "experimental.input.forceVT" }; -// Launch mode values -static constexpr std::wstring_view DefaultLaunchModeValue{ L"default" }; -static constexpr std::wstring_view MaximizedLaunchModeValue{ L"maximized" }; -static constexpr std::wstring_view FullscreenLaunchModeValue{ L"fullscreen" }; - -// Tab Width Mode values -static constexpr std::wstring_view EqualTabWidthModeValue{ L"equal" }; -static constexpr std::wstring_view TitleLengthTabWidthModeValue{ L"titleLength" }; -static constexpr std::wstring_view TitleLengthCompactModeValue{ L"compact" }; - -// Theme values -static constexpr std::wstring_view LightThemeValue{ L"light" }; -static constexpr std::wstring_view DarkThemeValue{ L"dark" }; -static constexpr std::wstring_view SystemThemeValue{ L"system" }; - #ifdef _DEBUG static constexpr bool debugFeaturesDefault{ true }; #else @@ -149,66 +134,51 @@ GlobalAppSettings GlobalAppSettings::FromJson(const Json::Value& json) void GlobalAppSettings::LayerJson(const Json::Value& json) { - if (auto defaultProfile{ json[JsonKey(DefaultProfileKey)] }) - { - _unparsedDefaultProfile.emplace(GetWstringFromJson(defaultProfile)); - } + JsonUtils::GetValueForKey(json, DefaultProfileKey, _unparsedDefaultProfile); - JsonUtils::GetBool(json, AlwaysShowTabsKey, _AlwaysShowTabs); + JsonUtils::GetValueForKey(json, AlwaysShowTabsKey, _AlwaysShowTabs); - JsonUtils::GetBool(json, ConfirmCloseAllKey, _ConfirmCloseAllTabs); + JsonUtils::GetValueForKey(json, ConfirmCloseAllKey, _ConfirmCloseAllTabs); - JsonUtils::GetInt(json, InitialRowsKey, _InitialRows); + JsonUtils::GetValueForKey(json, InitialRowsKey, _InitialRows); - JsonUtils::GetInt(json, InitialColsKey, _InitialCols); + JsonUtils::GetValueForKey(json, InitialColsKey, _InitialCols); - if (auto initialPosition{ json[JsonKey(InitialPositionKey)] }) - { - _ParseInitialPosition(initialPosition.asString(), _InitialPosition); - } + JsonUtils::GetValueForKey(json, InitialPositionKey, _InitialPosition); - JsonUtils::GetBool(json, ShowTitleInTitlebarKey, _ShowTitleInTitlebar); + JsonUtils::GetValueForKey(json, ShowTitleInTitlebarKey, _ShowTitleInTitlebar); - JsonUtils::GetBool(json, ShowTabsInTitlebarKey, _ShowTabsInTitlebar); + JsonUtils::GetValueForKey(json, ShowTabsInTitlebarKey, _ShowTabsInTitlebar); - JsonUtils::GetWstring(json, WordDelimitersKey, _WordDelimiters); + JsonUtils::GetValueForKey(json, WordDelimitersKey, _WordDelimiters); - JsonUtils::GetBool(json, CopyOnSelectKey, _CopyOnSelect); + JsonUtils::GetValueForKey(json, CopyOnSelectKey, _CopyOnSelect); - JsonUtils::GetBool(json, CopyFormattingKey, _CopyFormatting); + JsonUtils::GetValueForKey(json, CopyFormattingKey, _CopyFormatting); - JsonUtils::GetBool(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); + JsonUtils::GetValueForKey(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); - JsonUtils::GetBool(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); + JsonUtils::GetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); - if (auto launchMode{ json[JsonKey(LaunchModeKey)] }) - { - _LaunchMode = _ParseLaunchMode(GetWstringFromJson(launchMode)); - } + JsonUtils::GetValueForKey(json, LaunchModeKey, _LaunchMode); - if (auto theme{ json[JsonKey(ThemeKey)] }) - { - _Theme = _ParseTheme(GetWstringFromJson(theme)); - } + JsonUtils::GetValueForKey(json, ThemeKey, _Theme); - if (auto tabWidthMode{ json[JsonKey(TabWidthModeKey)] }) - { - _TabWidthMode = _ParseTabWidthMode(GetWstringFromJson(tabWidthMode)); - } + JsonUtils::GetValueForKey(json, TabWidthModeKey, _TabWidthMode); - JsonUtils::GetBool(json, SnapToGridOnResizeKey, _SnapToGridOnResize); + JsonUtils::GetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); - JsonUtils::GetBool(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); + // GetValueForKey will only override the current value if the key exists + JsonUtils::GetValueForKey(json, DebugFeaturesKey, _DebugFeaturesEnabled); - JsonUtils::GetBool(json, SoftwareRenderingKey, _SoftwareRendering); - JsonUtils::GetBool(json, ForceVTInputKey, _ForceVTInput); + JsonUtils::GetValueForKey(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); - // GetBool will only override the current value if the key exists - JsonUtils::GetBool(json, DebugFeaturesKey, _DebugFeaturesEnabled); + JsonUtils::GetValueForKey(json, SoftwareRenderingKey, _SoftwareRendering); + JsonUtils::GetValueForKey(json, ForceVTInputKey, _ForceVTInput); - JsonUtils::GetBool(json, EnableStartupTaskKey, _StartOnUserLogin); + JsonUtils::GetValueForKey(json, EnableStartupTaskKey, _StartOnUserLogin); - JsonUtils::GetBool(json, AlwaysOnTopKey, _AlwaysOnTop); + JsonUtils::GetValueForKey(json, AlwaysOnTopKey, _AlwaysOnTop); // This is a helper lambda to get the keybindings and commands out of both // and array of objects. We'll use this twice, once on the legacy @@ -229,123 +199,12 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) warnings = winrt::TerminalApp::implementation::Command::LayerJson(_commands, bindings); // It's possible that the user provided commands have some warnings // in them, similar to the keybindings. - _keybindingsWarnings.insert(_keybindingsWarnings.end(), warnings.begin(), warnings.end()); } }; parseBindings(LegacyKeybindingsKey); parseBindings(BindingsKey); } -// Method Description: -// - Helper function for converting a user-specified cursor style corresponding -// CursorStyle enum value -// Arguments: -// - themeString: The string value from the settings file to parse -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -ElementTheme GlobalAppSettings::_ParseTheme(const std::wstring& themeString) noexcept -{ - if (themeString == LightThemeValue) - { - return ElementTheme::Light; - } - else if (themeString == DarkThemeValue) - { - return ElementTheme::Dark; - } - // default behavior for invalid data or SystemThemeValue - return ElementTheme::Default; -} - -// Method Description: -// - Helper function for converting the initial position string into -// 2 coordinate values. We allow users to only provide one coordinate, -// thus, we use comma as the separator: -// (100, 100): standard input string -// (, 100), (100, ): if a value is missing, we set this value as a default -// (,): both x and y are set to default -// (abc, 100): if a value is not valid, we treat it as default -// (100, 100, 100): we only read the first two values, this is equivalent to (100, 100) -// Arguments: -// - initialPosition: the initial position string from json -// ret: reference to a struct whose optionals will be populated -// Return Value: -// - None -void GlobalAppSettings::_ParseInitialPosition(const std::string& initialPosition, - LaunchPosition& ret) noexcept -{ - static constexpr char singleCharDelim = ','; - std::stringstream tokenStream(initialPosition); - std::string token; - uint8_t initialPosIndex = 0; - - // Get initial position values till we run out of delimiter separated values in the stream - // or we hit max number of allowable values (= 2) - // Non-numeral values or empty string will be caught as exception and we do not assign them - for (; std::getline(tokenStream, token, singleCharDelim) && (initialPosIndex < 2); initialPosIndex++) - { - try - { - int32_t position = std::stoi(token); - if (initialPosIndex == 0) - { - ret.x.emplace(position); - } - - if (initialPosIndex == 1) - { - ret.y.emplace(position); - } - } - catch (...) - { - // Do nothing - } - } -} - -// Method Description: -// - Helper function for converting the user-specified launch mode -// to a LaunchMode enum value -// Arguments: -// - launchModeString: The string value from the settings file to parse -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -LaunchMode GlobalAppSettings::_ParseLaunchMode(const std::wstring& launchModeString) noexcept -{ - if (launchModeString == MaximizedLaunchModeValue) - { - return LaunchMode::MaximizedMode; - } - else if (launchModeString == FullscreenLaunchModeValue) - { - return LaunchMode::FullscreenMode; - } - - return LaunchMode::DefaultMode; -} - -// Method Description: -// - Helper function for converting the user-specified tab width -// to a TabViewWidthMode enum value -// Arguments: -// - tabWidthModeString: The string value from the settings file to parse -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -TabViewWidthMode GlobalAppSettings::_ParseTabWidthMode(const std::wstring& tabWidthModeString) noexcept -{ - if (tabWidthModeString == TitleLengthTabWidthModeValue) - { - return TabViewWidthMode::SizeToContent; - } - else if (tabWidthModeString == TitleLengthCompactModeValue) - { - return TabViewWidthMode::Compact; - } - // default behavior for invalid data or EqualTabWidthValue - return TabViewWidthMode::Equal; -} - // Method Description: // - Adds the given colorscheme to our map of schemes, using its name as the key. // Arguments: diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h index 988d7fe1c08..78222106716 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.h +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -17,6 +17,7 @@ Author(s): #include "AppKeyBindings.h" #include "ColorScheme.h" #include "Command.h" +#include "SettingsTypes.h" // fwdecl unittest classes namespace TerminalAppLocalTests @@ -28,12 +29,6 @@ namespace TerminalAppLocalTests namespace TerminalApp { class GlobalAppSettings; - - struct LaunchPosition - { - std::optional x; - std::optional y; - }; }; class TerminalApp::GlobalAppSettings final @@ -51,7 +46,7 @@ class TerminalApp::GlobalAppSettings final static GlobalAppSettings FromJson(const Json::Value& json); void LayerJson(const Json::Value& json); - void ApplyToSettings(winrt::Microsoft::Terminal::Settings::TerminalSettings& settings) const noexcept; + void ApplyToSettings(winrt::TerminalApp::TerminalSettings& settings) const noexcept; std::vector GetKeybindingsWarnings() const; @@ -96,15 +91,6 @@ class TerminalApp::GlobalAppSettings final std::unordered_map _colorSchemes; std::unordered_map _commands; - static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept; - - static winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode _ParseTabWidthMode(const std::wstring& tabWidthModeString) noexcept; - - static void _ParseInitialPosition(const std::string& initialPosition, - LaunchPosition& ret) noexcept; - - static winrt::TerminalApp::LaunchMode _ParseLaunchMode(const std::wstring& launchModeString) noexcept; - friend class TerminalAppLocalTests::SettingsTests; friend class TerminalAppLocalTests::ColorSchemeTests; }; diff --git a/src/cascadia/TerminalApp/JsonUtils.cpp b/src/cascadia/TerminalApp/JsonUtils.cpp deleted file mode 100644 index f4282097b83..00000000000 --- a/src/cascadia/TerminalApp/JsonUtils.cpp +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "Utils.h" -#include "JsonUtils.h" -#include "../../types/inc/Utils.hpp" - -void TerminalApp::JsonUtils::GetOptionalColor(const Json::Value& json, - std::string_view key, - std::optional& target) -{ - const auto conversionFn = [](const Json::Value& value) -> til::color { - return ::Microsoft::Console::Utils::ColorFromHexString(value.asString()); - }; - GetOptionalValue(json, - key, - target, - conversionFn); -} - -void TerminalApp::JsonUtils::GetOptionalString(const Json::Value& json, - std::string_view key, - std::optional& target) -{ - const auto conversionFn = [](const Json::Value& value) -> std::wstring { - return GetWstringFromJson(value); - }; - GetOptionalValue(json, - key, - target, - conversionFn); -} - -void TerminalApp::JsonUtils::GetOptionalGuid(const Json::Value& json, - std::string_view key, - std::optional& target) -{ - const auto conversionFn = [](const Json::Value& value) -> GUID { - return ::Microsoft::Console::Utils::GuidFromString(GetWstringFromJson(value)); - }; - GetOptionalValue(json, - key, - target, - conversionFn); -} - -void TerminalApp::JsonUtils::GetOptionalDouble(const Json::Value& json, - std::string_view key, - std::optional& target) -{ - const auto conversionFn = [](const Json::Value& value) -> double { - return value.asFloat(); - }; - const auto validationFn = [](const Json::Value& value) -> bool { - return value.isNumeric(); - }; - GetOptionalValue(json, - key, - target, - conversionFn, - validationFn); -} - -void TerminalApp::JsonUtils::GetInt(const Json::Value& json, - std::string_view key, - int& target) -{ - const auto conversionFn = [](const Json::Value& value) -> int { - return value.asInt(); - }; - const auto validationFn = [](const Json::Value& value) -> bool { - return value.isInt(); - }; - GetValue(json, key, target, conversionFn, validationFn); -} - -void TerminalApp::JsonUtils::GetUInt(const Json::Value& json, - std::string_view key, - uint32_t& target) -{ - const auto conversionFn = [](const Json::Value& value) -> uint32_t { - return value.asUInt(); - }; - const auto validationFn = [](const Json::Value& value) -> bool { - return value.isUInt(); - }; - GetValue(json, key, target, conversionFn, validationFn); -} - -void TerminalApp::JsonUtils::GetDouble(const Json::Value& json, - std::string_view key, - double& target) -{ - const auto conversionFn = [](const Json::Value& value) -> double { - return value.asFloat(); - }; - const auto validationFn = [](const Json::Value& value) -> bool { - return value.isNumeric(); - }; - GetValue(json, key, target, conversionFn, validationFn); -} - -void TerminalApp::JsonUtils::GetBool(const Json::Value& json, - std::string_view key, - bool& target) -{ - const auto conversionFn = [](const Json::Value& value) -> bool { - return value.asBool(); - }; - const auto validationFn = [](const Json::Value& value) -> bool { - return value.isBool(); - }; - GetValue(json, key, target, conversionFn, validationFn); -} - -void TerminalApp::JsonUtils::GetWstring(const Json::Value& json, - std::string_view key, - std::wstring& target) -{ - const auto conversionFn = [](const Json::Value& value) -> std::wstring { - return GetWstringFromJson(value); - }; - GetValue(json, key, target, conversionFn, nullptr); -} diff --git a/src/cascadia/TerminalApp/JsonUtils.h b/src/cascadia/TerminalApp/JsonUtils.h index 1845a9d6524..b98ade1da9d 100644 --- a/src/cascadia/TerminalApp/JsonUtils.h +++ b/src/cascadia/TerminalApp/JsonUtils.h @@ -9,136 +9,483 @@ Module Name: - Helpers for the TerminalApp project Author(s): - Mike Griese - August 2019 - +- Dustin Howett - January 2020 --*/ + #pragma once +#include + +#include "../types/inc/utils.hpp" + +namespace winrt +{ + // If we don't use winrt, nobody will include the ConversionTraits for winrt stuff. + // If nobody includes it, these forward declarations will suffice. + struct guid; + struct hstring; + namespace Windows::Foundation + { + template + struct IReference; + } +} + namespace TerminalApp::JsonUtils { - void GetOptionalColor(const Json::Value& json, - std::string_view key, - std::optional& target); + namespace Detail + { + // Function Description: + // - Returns a string_view to a Json::Value's internal string storage, + // hopefully without copying it. + __declspec(noinline) inline const std::string_view GetStringView(const Json::Value& json) + { + const char* begin{ nullptr }; + const char* end{ nullptr }; + json.getString(&begin, &end); + const std::string_view zeroCopyString{ begin, gsl::narrow_cast(end - begin) }; + return zeroCopyString; + } - void GetOptionalString(const Json::Value& json, - std::string_view key, - std::optional& target); + template + struct DeduceOptional + { + using Type = typename std::decay::type; + static constexpr bool IsOptional = false; + }; - void GetOptionalGuid(const Json::Value& json, - std::string_view key, - std::optional& target); + template + struct DeduceOptional> + { + using Type = typename std::decay::type; + static constexpr bool IsOptional = true; + }; - void GetOptionalDouble(const Json::Value& json, - std::string_view key, - std::optional& target); + template + struct DeduceOptional<::winrt::Windows::Foundation::IReference> + { + using Type = typename std::decay::type; + static constexpr bool IsOptional = true; + }; + } - // Method Description: - // - Helper that can be used for retrieving an optional value from a json - // object, and parsing it's value to layer on a given target object. - // - If the key we're looking for _doesn't_ exist in the json object, - // we'll leave the target object unmodified. - // - If the key exists in the json object, but is set to `null`, then - // we'll instead set the target back to nullopt. - // - Each caller should provide a conversion function that takes a - // Json::Value and returns an object of the same type as target. - // Arguments: - // - json: The json object to search for the given key - // - key: The key to look for in the json object - // - target: the optional object to receive the value from json - // - conversion: a std::function which can be used to - // convert the Json::Value to the appropriate type. - // - validation: optional, if provided, will be called first to ensure that - // the json::value is of the correct type before attempting to call - // `conversion`. - // Return Value: - // - - template - void GetOptionalValue(const Json::Value& json, - std::string_view key, - std::optional& target, - F&& conversion, - const std::function& validation = nullptr) + // These exceptions cannot use localized messages, as we do not have + // guaranteed access to the resource loader. + class TypeMismatchException : public std::runtime_error + { + public: + TypeMismatchException() : + runtime_error("unexpected data type") {} + }; + + class KeyedException : public std::runtime_error + { + public: + KeyedException(const std::string_view key, std::exception_ptr exception) : + runtime_error(fmt::format("error parsing \"{0}\"", key).c_str()), + _key{ key }, + _innerException{ std::move(exception) } {} + + std::string GetKey() const + { + return _key; + } + + [[noreturn]] void RethrowInner() const + { + std::rethrow_exception(_innerException); + } + + private: + std::string _key; + std::exception_ptr _innerException; + }; + + class UnexpectedValueException : public std::runtime_error + { + public: + UnexpectedValueException(const std::string_view value) : + runtime_error(fmt::format("unexpected value \"{0}\"", value).c_str()), + _value{ value } {} + + std::string GetValue() const + { + return _value; + } + + private: + std::string _value; + }; + + template + struct ConversionTrait + { + // Forward-declare these so the linker can pick up specializations from elsewhere! + T FromJson(const Json::Value&); + bool CanConvert(const Json::Value& json); + }; + + template<> + struct ConversionTrait + { + std::string FromJson(const Json::Value& json) + { + return json.asString(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isString(); + } + }; + + template<> + struct ConversionTrait + { + std::wstring FromJson(const Json::Value& json) + { + return til::u8u16(Detail::GetStringView(json)); + } + + bool CanConvert(const Json::Value& json) + { + return json.isString(); + } + }; + +#ifdef WINRT_BASE_H + template<> + struct ConversionTrait : public ConversionTrait + { + // Leverage the wstring converter's validation + winrt::hstring FromJson(const Json::Value& json) + { + return winrt::hstring{ til::u8u16(Detail::GetStringView(json)) }; + } + }; +#endif + + template<> + struct ConversionTrait + { + bool FromJson(const Json::Value& json) + { + return json.asBool(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isBool(); + } + }; + + template<> + struct ConversionTrait + { + int FromJson(const Json::Value& json) + { + return json.asInt(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isInt(); + } + }; + + template<> + struct ConversionTrait + { + unsigned int FromJson(const Json::Value& json) + { + return json.asUInt(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isUInt(); + } + }; + + template<> + struct ConversionTrait + { + float FromJson(const Json::Value& json) + { + return json.asFloat(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isNumeric(); + } + }; + + template<> + struct ConversionTrait + { + double FromJson(const Json::Value& json) + { + return json.asDouble(); + } + + bool CanConvert(const Json::Value& json) + { + return json.isNumeric(); + } + }; + + template<> + struct ConversionTrait + { + GUID FromJson(const Json::Value& json) + { + return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json))); + } + + bool CanConvert(const Json::Value& json) + { + if (!json.isString()) + { + return false; + } + + const auto string{ Detail::GetStringView(json) }; + return string.length() == 38 && string.front() == '{' && string.back() == '}'; + } + }; + + // (GUID and winrt::guid are mutually convertible!) + template<> + struct ConversionTrait : public ConversionTrait + { + }; + + template<> + struct ConversionTrait + { + til::color FromJson(const Json::Value& json) + { + return ::Microsoft::Console::Utils::ColorFromHexString(Detail::GetStringView(json)); + } + + bool CanConvert(const Json::Value& json) + { + if (!json.isString()) + { + return false; + } + + const auto string{ Detail::GetStringView(json) }; + return (string.length() == 7 || string.length() == 4) && string.front() == '#'; + } + }; + + template + struct EnumMapper { - if (json.isMember(JsonKey(key))) + using BaseEnumMapper = EnumMapper; + using ValueType = T; + using pair_type = std::pair; + T FromJson(const Json::Value& json) { - if (auto jsonVal{ json[JsonKey(key)] }) + const auto name{ Detail::GetStringView(json) }; + for (const auto& pair : TBase::mappings) { - if (validation == nullptr || validation(jsonVal)) + if (pair.first == name) { - target = conversion(jsonVal); + return pair.second; } } - else + + throw UnexpectedValueException{ name }; + } + + bool CanConvert(const Json::Value& json) + { + return json.isString(); + } + }; + + // FlagMapper is EnumMapper, but it works for bitfields. + // It supports a string (single flag) or an array of strings. + // Does an O(n*m) search; meant for small search spaces! + // + // Cleverly leverage EnumMapper to do the heavy lifting. + template + struct FlagMapper : public EnumMapper + { + private: + // Hide BaseEnumMapper so FlagMapper's consumers cannot see + // it. + using BaseEnumMapper = EnumMapper::BaseEnumMapper; + + public: + using BaseFlagMapper = FlagMapper; + static constexpr T AllSet{ static_cast(~0u) }; + static constexpr T AllClear{ static_cast(0u) }; + + T FromJson(const Json::Value& json) + { + if (json.isString()) + { + return BaseEnumMapper::FromJson(json); + } + else if (json.isArray()) { - // This branch is hit when the json object contained the key, - // but the key was set to `null`. In this case, explicitly clear - // the target. - target = std::nullopt; + unsigned int seen{ 0 }; + T value{}; + for (const auto& element : json) + { + const auto newFlag{ BaseEnumMapper::FromJson(element) }; + if (++seen > 1 && + ((newFlag == AllClear && value != AllClear) || + (value == AllClear && newFlag != AllClear))) + { + // attempt to combine AllClear (explicitly) with anything else + throw UnexpectedValueException{ element.asString() }; + } + value |= newFlag; + } + return value; } + + // We'll only get here if CanConvert has failed us. + return AllClear; } - } + + bool CanConvert(const Json::Value& json) + { + return BaseEnumMapper::CanConvert(json) || json.isArray(); + } + }; // Method Description: - // - Helper that can be used for retrieving a value from a json - // object, and parsing it's value to set on a given target object. - // - If the key we're looking for _doesn't_ exist in the json object, - // we'll leave the target object unmodified. - // - If the key exists in the json object, we'll use the provided - // `validation` function to ensure that the json value is of the - // correct type. - // - If we successfully validate the json value type (or no validation - // function was provided), then we'll use `conversion` to parse the - // value and place the result into `target` - // - Each caller should provide a conversion function that takes a - // Json::Value and returns an object of the same type as target. - // - Unlike GetOptionalValue, if the key exists but is set to `null`, we'll - // just ignore it. + // - Helper that will populate a reference with a value converted from a json object. // Arguments: - // - json: The json object to search for the given key - // - key: The key to look for in the json object - // - target: the optional object to receive the value from json - // - conversion: a std::function which can be used to - // convert the Json::Value to the appropriate type. - // - validation: optional, if provided, will be called first to ensure that - // the json::value is of the correct type before attempting to call - // `conversion`. + // - json: the json object to convert + // - target: the value to populate with the converted result // Return Value: - // - - template - void GetValue(const Json::Value& json, - std::string_view key, - T& target, - F&& conversion, - const std::function& validation = nullptr) + // - a boolean indicating whether the value existed (in this case, was non-null) + // + // GetValue, type-deduced, manual converter + template + bool GetValue(const Json::Value& json, T& target, Converter&& conv) { - if (json.isMember(JsonKey(key))) + if constexpr (Detail::DeduceOptional::IsOptional) { - if (auto jsonVal{ json[JsonKey(key)] }) + // FOR OPTION TYPES + // - If the json object is set to `null`, then + // we'll instead set the target back to the empty optional. + if (json.isNull()) { - if (validation == nullptr || validation(jsonVal)) - { - target = conversion(jsonVal); - } + target = T{}; // zero-construct an empty optional + return true; + } + } + + if (json) + { + if (!conv.CanConvert(json)) + { + throw TypeMismatchException{}; + } + + target = conv.FromJson(json); + return true; + } + return false; + } + + // GetValue, forced return type, manual converter + template + std::decay_t GetValue(const Json::Value& json, Converter&& conv) + { + std::decay_t local{}; + GetValue(json, local, std::forward(conv)); + return local; // returns zero-initialized or value + } + + // GetValueForKey, type-deduced, manual converter + template + bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv) + { + if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) }) + { + try + { + return GetValue(*found, target, std::forward(conv)); + } + catch (...) + { + // Wrap any caught exceptions in one that preserves context. + throw KeyedException(key, std::current_exception()); } } + return false; } - void GetInt(const Json::Value& json, - std::string_view key, - int& target); + // GetValueForKey, forced return type, manual converter + template + std::decay_t GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv) + { + std::decay_t local{}; + GetValueForKey(json, key, local, std::forward(conv)); + return local; // returns zero-initialized? + } - void GetUInt(const Json::Value& json, - std::string_view key, - uint32_t& target); + // GetValue, type-deduced, with automatic converter + template + bool GetValue(const Json::Value& json, T& target) + { + return GetValue(json, target, ConversionTrait::Type>{}); + } - void GetDouble(const Json::Value& json, - std::string_view key, - double& target); + // GetValue, forced return type, with automatic converter + template + std::decay_t GetValue(const Json::Value& json) + { + std::decay_t local{}; + GetValue(json, local, ConversionTrait::Type>{}); + return local; // returns zero-initialized or value + } - void GetBool(const Json::Value& json, - std::string_view key, - bool& target); + // GetValueForKey, type-deduced, with automatic converter + template + bool GetValueForKey(const Json::Value& json, std::string_view key, T& target) + { + return GetValueForKey(json, key, target, ConversionTrait::Type>{}); + } + + // GetValueForKey, forced return type, with automatic converter + template + std::decay_t GetValueForKey(const Json::Value& json, std::string_view key) + { + return GetValueForKey(json, key, ConversionTrait::Type>{}); + } + + // Get multiple values for keys (json, k, &v, k, &v, k, &v, ...). + // Uses the default converter for each v. + // Careful: this can cause a template explosion. + constexpr void GetValuesForKeys(const Json::Value& /*json*/) {} - void GetWstring(const Json::Value& json, - std::string_view key, - std::wstring& target); + template + void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args) + { + GetValueForKey(json, key1, val1); + GetValuesForKeys(json, std::forward(args)...); + } }; + +#define JSON_ENUM_MAPPER(...) \ + template<> \ + struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \ + public ::TerminalApp::JsonUtils::EnumMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>> + +#define JSON_FLAG_MAPPER(...) \ + template<> \ + struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \ + public ::TerminalApp::JsonUtils::FlagMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>> + +#define JSON_MAPPINGS(Count) \ + static constexpr std::array mappings diff --git a/src/cascadia/TerminalApp/JsonUtilsNew.h b/src/cascadia/TerminalApp/JsonUtilsNew.h deleted file mode 100644 index 03e9826f4b2..00000000000 --- a/src/cascadia/TerminalApp/JsonUtilsNew.h +++ /dev/null @@ -1,490 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- JsonUtils.h - -Abstract: -- Helpers for the TerminalApp project -Author(s): -- Mike Griese - August 2019 -- Dustin Howett - January 2020 ---*/ - -#pragma once - -#include - -#include "../types/inc/utils.hpp" - -namespace winrt -{ - // If we don't use winrt, nobody will include the ConversionTraits for winrt stuff. - // If nobody includes it, these forward declarations will suffice. - struct guid; - struct hstring; - namespace Windows::Foundation - { - template - struct IReference; - } -} - -namespace TerminalApp::JsonUtils -{ - namespace Detail - { - // Function Description: - // - Returns a string_view to a Json::Value's internal string storage, - // hopefully without copying it. - __declspec(noinline) inline const std::string_view GetStringView(const Json::Value& json) - { - const char* begin{ nullptr }; - const char* end{ nullptr }; - json.getString(&begin, &end); - const std::string_view zeroCopyString{ begin, gsl::narrow_cast(end - begin) }; - return zeroCopyString; - } - - template - struct DeduceOptional - { - using Type = typename std::decay::type; - static constexpr bool IsOptional = false; - }; - - template - struct DeduceOptional> - { - using Type = typename std::decay::type; - static constexpr bool IsOptional = true; - }; - - template - struct DeduceOptional<::winrt::Windows::Foundation::IReference> - { - using Type = typename std::decay::type; - static constexpr bool IsOptional = true; - }; - } - - // These exceptions cannot use localized messages, as we do not have - // guaranteed access to the resource loader. - class TypeMismatchException : public std::runtime_error - { - public: - TypeMismatchException() : - runtime_error("unexpected data type") {} - }; - - class KeyedException : public std::runtime_error - { - public: - KeyedException(const std::string_view key, std::exception_ptr exception) : - runtime_error(fmt::format("error parsing \"{0}\"", key).c_str()), - _key{ key }, - _innerException{ std::move(exception) } {} - - std::string GetKey() const - { - return _key; - } - - [[noreturn]] void RethrowInner() const - { - std::rethrow_exception(_innerException); - } - - private: - std::string _key; - std::exception_ptr _innerException; - }; - - class UnexpectedValueException : public std::runtime_error - { - public: - UnexpectedValueException(const std::string_view value) : - runtime_error(fmt::format("unexpected value \"{0}\"", value).c_str()), - _value{ value } {} - - std::string GetValue() const - { - return _value; - } - - private: - std::string _value; - }; - - template - struct ConversionTrait - { - // Forward-declare these so the linker can pick up specializations from elsewhere! - T FromJson(const Json::Value&); - bool CanConvert(const Json::Value& json); - }; - - template<> - struct ConversionTrait - { - std::string FromJson(const Json::Value& json) - { - return json.asString(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isString(); - } - }; - - template<> - struct ConversionTrait - { - std::wstring FromJson(const Json::Value& json) - { - return til::u8u16(Detail::GetStringView(json)); - } - - bool CanConvert(const Json::Value& json) - { - return json.isString(); - } - }; - -#ifdef WINRT_BASE_H - template<> - struct ConversionTrait : public ConversionTrait - { - // Leverage the wstring converter's validation - winrt::hstring FromJson(const Json::Value& json) - { - return winrt::hstring{ til::u8u16(Detail::GetStringView(json)) }; - } - }; -#endif - - template<> - struct ConversionTrait - { - bool FromJson(const Json::Value& json) - { - return json.asBool(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isBool(); - } - }; - - template<> - struct ConversionTrait - { - int FromJson(const Json::Value& json) - { - return json.asInt(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isInt(); - } - }; - - template<> - struct ConversionTrait - { - unsigned int FromJson(const Json::Value& json) - { - return json.asUInt(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isUInt(); - } - }; - - template<> - struct ConversionTrait - { - float FromJson(const Json::Value& json) - { - return json.asFloat(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isNumeric(); - } - }; - - template<> - struct ConversionTrait - { - double FromJson(const Json::Value& json) - { - return json.asDouble(); - } - - bool CanConvert(const Json::Value& json) - { - return json.isNumeric(); - } - }; - - template<> - struct ConversionTrait - { - GUID FromJson(const Json::Value& json) - { - return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json))); - } - - bool CanConvert(const Json::Value& json) - { - if (!json.isString()) - { - return false; - } - - const auto string{ Detail::GetStringView(json) }; - return string.length() == 38 && string.front() == '{' && string.back() == '}'; - } - }; - - // (GUID and winrt::guid are mutually convertible!) - template<> - struct ConversionTrait : public ConversionTrait - { - }; - - template<> - struct ConversionTrait - { - til::color FromJson(const Json::Value& json) - { - return ::Microsoft::Console::Utils::ColorFromHexString(Detail::GetStringView(json)); - } - - bool CanConvert(const Json::Value& json) - { - if (!json.isString()) - { - return false; - } - - const auto string{ Detail::GetStringView(json) }; - return (string.length() == 7 || string.length() == 4) && string.front() == '#'; - } - }; - - template - struct EnumMapper - { - using BaseEnumMapper = EnumMapper; - using pair_type = std::pair; - T FromJson(const Json::Value& json) - { - const auto name{ Detail::GetStringView(json) }; - for (const auto& pair : TBase::mappings) - { - if (pair.first == name) - { - return pair.second; - } - } - - throw UnexpectedValueException{ name }; - } - - bool CanConvert(const Json::Value& json) - { - return json.isString(); - } - }; - - // FlagMapper is EnumMapper, but it works for bitfields. - // It supports a string (single flag) or an array of strings. - // Does an O(n*m) search; meant for small search spaces! - // - // Cleverly leverage EnumMapper to do the heavy lifting. - template - struct FlagMapper : public EnumMapper - { - private: - // Hide BaseEnumMapper so FlagMapper's consumers cannot see - // it. - using BaseEnumMapper = EnumMapper::BaseEnumMapper; - - public: - using BaseFlagMapper = FlagMapper; - static constexpr T AllSet{ static_cast(~0u) }; - static constexpr T AllClear{ static_cast(0u) }; - - T FromJson(const Json::Value& json) - { - if (json.isString()) - { - return BaseEnumMapper::FromJson(json); - } - else if (json.isArray()) - { - unsigned int seen{ 0 }; - T value{}; - for (const auto& element : json) - { - const auto newFlag{ BaseEnumMapper::FromJson(element) }; - if (++seen > 1 && - ((newFlag == AllClear && value != AllClear) || - (value == AllClear && newFlag != AllClear))) - { - // attempt to combine AllClear (explicitly) with anything else - throw UnexpectedValueException{ element.asString() }; - } - value |= newFlag; - } - return value; - } - - // We'll only get here if CanConvert has failed us. - return AllClear; - } - - bool CanConvert(const Json::Value& json) - { - return BaseEnumMapper::CanConvert(json) || json.isArray(); - } - }; - - // Method Description: - // - Helper that will populate a reference with a value converted from a json object. - // Arguments: - // - json: the json object to convert - // - target: the value to populate with the converted result - // Return Value: - // - a boolean indicating whether the value existed (in this case, was non-null) - // - // GetValue, type-deduced, manual converter - template - bool GetValue(const Json::Value& json, T& target, Converter&& conv) - { - if constexpr (Detail::DeduceOptional::IsOptional) - { - // FOR OPTION TYPES - // - If the json object is set to `null`, then - // we'll instead set the target back to the empty optional. - if (json.isNull()) - { - target = T{}; // zero-construct an empty optional - return true; - } - } - - if (json) - { - if (!conv.CanConvert(json)) - { - throw TypeMismatchException{}; - } - - target = conv.FromJson(json); - return true; - } - return false; - } - - // GetValue, forced return type, manual converter - template - std::decay_t GetValue(const Json::Value& json, Converter&& conv) - { - std::decay_t local{}; - GetValue(json, local, std::forward(conv)); - return local; // returns zero-initialized or value - } - - // GetValueForKey, type-deduced, manual converter - template - bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv) - { - if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) }) - { - try - { - return GetValue(*found, target, std::forward(conv)); - } - catch (...) - { - // Wrap any caught exceptions in one that preserves context. - throw KeyedException(key, std::current_exception()); - } - } - return false; - } - - // GetValueForKey, forced return type, manual converter - template - std::decay_t GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv) - { - std::decay_t local{}; - GetValueForKey(json, key, local, std::forward(conv)); - return local; // returns zero-initialized? - } - - // GetValue, type-deduced, with automatic converter - template - bool GetValue(const Json::Value& json, T& target) - { - return GetValue(json, target, ConversionTrait::Type>{}); - } - - // GetValue, forced return type, with automatic converter - template - std::decay_t GetValue(const Json::Value& json) - { - std::decay_t local{}; - GetValue(json, local, ConversionTrait::Type>{}); - return local; // returns zero-initialized or value - } - - // GetValueForKey, type-deduced, with automatic converter - template - bool GetValueForKey(const Json::Value& json, std::string_view key, T& target) - { - return GetValueForKey(json, key, target, ConversionTrait::Type>{}); - } - - // GetValueForKey, forced return type, with automatic converter - template - std::decay_t GetValueForKey(const Json::Value& json, std::string_view key) - { - return GetValueForKey(json, key, ConversionTrait::Type>{}); - } - - // Get multiple values for keys (json, k, &v, k, &v, k, &v, ...). - // Uses the default converter for each v. - // Careful: this can cause a template explosion. - constexpr void GetValuesForKeys(const Json::Value& /*json*/) {} - - template - void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args) - { - GetValueForKey(json, key1, val1); - GetValuesForKeys(json, std::forward(args)...); - } -}; - -#define JSON_ENUM_MAPPER(...) \ - template<> \ - struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \ - public ::TerminalApp::JsonUtils::EnumMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>> - -#define JSON_FLAG_MAPPER(...) \ - template<> \ - struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \ - public ::TerminalApp::JsonUtils::FlagMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>> - -#define JSON_MAPPINGS(Count) \ - static constexpr std::array mappings diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index b931755c1e3..f8e61ae8379 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -7,6 +7,7 @@ #include "CascadiaSettings.h" using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Graphics::Display; using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; @@ -921,6 +922,102 @@ bool Pane::CanSplit(SplitState splitType) return false; } +// Method Description: +// - This is a helper to determine if a given Pane can be split, but without +// using the ActualWidth() and ActualHeight() methods. This is used during +// processing of many "split-pane" commands, which could happen _before_ we've +// laid out a Pane for the first time. When this happens, the Pane's don't +// have an actual size yet. However, we'd still like to figure out if the pane +// could be split, once they're all laid out. +// - This method assumes that the Pane we're attempting to split is `target`, +// and this method should be called on the root of a tree of Panes. +// - We'll walk down the tree attempting to find `target`. As we traverse the +// tree, we'll reduce the size passed to each subsequent recursive call. The +// size passed to this method represents how much space this Pane _will_ have +// to use. +// * If this pane is a leaf, and it's the pane we're looking for, use the +// available space to calculate which direction to split in. +// * If this pane is _any other leaf_, then just return nullopt, to indicate +// that the `target` Pane is not down this branch. +// * If this pane is a parent, calculate how much space our children will be +// able to use, and recurse into them. +// Arguments: +// - target: The Pane we're attempting to split. +// - splitType: The direction we're attempting to split in. +// - availableSpace: The theoretical space that's available for this pane to be able to split. +// Return Value: +// - nullopt if `target` is not this pane or a child of this pane, otherwise +// true iff we could split this pane, given `availableSpace` +// Note: +// - This method is highly similar to Pane::PreCalculateAutoSplit +std::optional Pane::PreCalculateCanSplit(const std::shared_ptr target, + SplitState splitType, + const winrt::Windows::Foundation::Size availableSpace) const +{ + if (_IsLeaf()) + { + if (target.get() == this) + { + // If this pane is a leaf, and it's the pane we're looking for, use + // the available space to calculate which direction to split in. + const Size minSize = _GetMinSize(); + + if (splitType == SplitState::None) + { + return { false }; + } + + else if (splitType == SplitState::Vertical) + { + const auto widthMinusSeparator = availableSpace.Width - CombinedPaneBorderSize; + const auto newWidth = widthMinusSeparator * Half; + + return { newWidth > minSize.Width }; + } + + else if (splitType == SplitState::Horizontal) + { + const auto heightMinusSeparator = availableSpace.Height - CombinedPaneBorderSize; + const auto newHeight = heightMinusSeparator * Half; + + return { newHeight > minSize.Height }; + } + } + else + { + // If this pane is _any other leaf_, then just return nullopt, to + // indicate that the `target` Pane is not down this branch. + return std::nullopt; + } + } + else + { + // If this pane is a parent, calculate how much space our children will + // be able to use, and recurse into them. + + const bool isVerticalSplit = _splitState == SplitState::Vertical; + const float firstWidth = isVerticalSplit ? + (availableSpace.Width * _desiredSplitPosition) - PaneBorderSize : + availableSpace.Width; + const float secondWidth = isVerticalSplit ? + (availableSpace.Width - firstWidth) - PaneBorderSize : + availableSpace.Width; + const float firstHeight = !isVerticalSplit ? + (availableSpace.Height * _desiredSplitPosition) - PaneBorderSize : + availableSpace.Height; + const float secondHeight = !isVerticalSplit ? + (availableSpace.Height - firstHeight) - PaneBorderSize : + availableSpace.Height; + + const auto firstResult = _firstChild->PreCalculateCanSplit(target, splitType, { firstWidth, firstHeight }); + return firstResult.has_value() ? firstResult : _secondChild->PreCalculateCanSplit(target, splitType, { secondWidth, secondHeight }); + } + + // We should not possibly be getting here - both the above branches should + // return a value. + FAIL_FAST(); +} + // Method Description: // - Split the focused pane in our tree of panes, and place the given // TermControl into the newly created pane. If we're the focused pane, then diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 7041db1f0cb..dfbcf1e7bc2 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -51,7 +51,7 @@ class Pane : public std::enable_shared_from_this void ClearActive(); void SetActive(); - void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, + void UpdateSettings(const winrt::TerminalApp::TerminalSettings& settings, const GUID& profile); void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void Relayout(); @@ -64,7 +64,9 @@ class Pane : public std::enable_shared_from_this const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; std::optional PreCalculateAutoSplit(const std::shared_ptr target, const winrt::Windows::Foundation::Size parentSize) const; - + std::optional PreCalculateCanSplit(const std::shared_ptr target, + winrt::TerminalApp::SplitState splitType, + const winrt::Windows::Foundation::Size availableSpace) const; void Shutdown(); void Close(); diff --git a/src/cascadia/TerminalApp/Profile.cpp b/src/cascadia/TerminalApp/Profile.cpp index 5ee40fd27a2..b98efb9f691 100644 --- a/src/cascadia/TerminalApp/Profile.cpp +++ b/src/cascadia/TerminalApp/Profile.cpp @@ -9,8 +9,10 @@ #include #include "LegacyProfileGeneratorNamespaces.h" +#include "TerminalSettingsSerializationHelpers.h" using namespace TerminalApp; +using namespace winrt::TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::Windows::UI::Xaml; using namespace ::Microsoft::Console; @@ -52,57 +54,6 @@ static constexpr std::string_view BackgroundImageAlignmentKey{ "backgroundImageA static constexpr std::string_view RetroTerminalEffectKey{ "experimental.retroTerminalEffect" }; static constexpr std::string_view AntialiasingModeKey{ "antialiasingMode" }; -// Possible values for closeOnExit -static constexpr std::string_view CloseOnExitAlways{ "always" }; -static constexpr std::string_view CloseOnExitGraceful{ "graceful" }; -static constexpr std::string_view CloseOnExitNever{ "never" }; - -// Possible values for Scrollbar state -static constexpr std::wstring_view AlwaysVisible{ L"visible" }; -static constexpr std::wstring_view AlwaysHide{ L"hidden" }; - -// Possible values for Cursor Shape -static constexpr std::wstring_view CursorShapeVintage{ L"vintage" }; -static constexpr std::wstring_view CursorShapeBar{ L"bar" }; -static constexpr std::wstring_view CursorShapeUnderscore{ L"underscore" }; -static constexpr std::wstring_view CursorShapeFilledbox{ L"filledBox" }; -static constexpr std::wstring_view CursorShapeEmptybox{ L"emptyBox" }; - -// Possible values for Font Weight -static constexpr std::string_view FontWeightThin{ "thin" }; -static constexpr std::string_view FontWeightExtraLight{ "extra-light" }; -static constexpr std::string_view FontWeightLight{ "light" }; -static constexpr std::string_view FontWeightSemiLight{ "semi-light" }; -static constexpr std::string_view FontWeightNormal{ "normal" }; -static constexpr std::string_view FontWeightMedium{ "medium" }; -static constexpr std::string_view FontWeightSemiBold{ "semi-bold" }; -static constexpr std::string_view FontWeightBold{ "bold" }; -static constexpr std::string_view FontWeightExtraBold{ "extra-bold" }; -static constexpr std::string_view FontWeightBlack{ "black" }; -static constexpr std::string_view FontWeightExtraBlack{ "extra-black" }; - -// Possible values for Image Stretch Mode -static constexpr std::string_view ImageStretchModeNone{ "none" }; -static constexpr std::string_view ImageStretchModeFill{ "fill" }; -static constexpr std::string_view ImageStretchModeUniform{ "uniform" }; -static constexpr std::string_view ImageStretchModeUniformTofill{ "uniformToFill" }; - -// Possible values for Image Alignment -static constexpr std::string_view ImageAlignmentCenter{ "center" }; -static constexpr std::string_view ImageAlignmentLeft{ "left" }; -static constexpr std::string_view ImageAlignmentTop{ "top" }; -static constexpr std::string_view ImageAlignmentRight{ "right" }; -static constexpr std::string_view ImageAlignmentBottom{ "bottom" }; -static constexpr std::string_view ImageAlignmentTopLeft{ "topLeft" }; -static constexpr std::string_view ImageAlignmentTopRight{ "topRight" }; -static constexpr std::string_view ImageAlignmentBottomLeft{ "bottomLeft" }; -static constexpr std::string_view ImageAlignmentBottomRight{ "bottomRight" }; - -// Possible values for TextAntialiasingMode -static constexpr std::wstring_view AntialiasingModeGrayscale{ L"grayscale" }; -static constexpr std::wstring_view AntialiasingModeCleartype{ L"cleartype" }; -static constexpr std::wstring_view AntialiasingModeAliased{ L"aliased" }; - Profile::Profile() : Profile(std::nullopt) { @@ -248,8 +199,7 @@ TerminalSettings Profile::CreateTerminalSettings(const std::unordered_map>(json, GuidKey) }) { - const auto guid{ json[JsonKey(GuidKey)] }; - const auto otherGuid = Utils::GuidFromString(GetWstringFromJson(guid)); - if (_guid.value() != otherGuid) + if (otherGuid != _guid) // optional compare takes care of this { return false; } @@ -368,16 +316,17 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const return false; } - const auto& otherSource = json.isMember(JsonKey(SourceKey)) ? json[JsonKey(SourceKey)] : Json::Value::null; + std::optional otherSource; + bool otherHadSource = JsonUtils::GetValueForKey(json, SourceKey, otherSource); // For profiles with a `source`, also check the `source` property. bool sourceMatches = false; if (_source.has_value()) { - if (json.isMember(JsonKey(SourceKey))) + if (otherHadSource) { - const auto otherSourceString = GetWstringFromJson(otherSource); - sourceMatches = otherSourceString == _source.value(); + // If we have a source and the other has a source, compare them! + sourceMatches = otherSource == _source; } else { @@ -395,52 +344,13 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const } else { - // We do not have a source. The only way we match is if source is set to null or "". - if (otherSource.isNull() || (otherSource.isString() && otherSource == "")) - { - sourceMatches = true; - } + // We do not have a source. The only way we match is if source is unset or set to "". + sourceMatches = (!otherSource.has_value() || otherSource.value() == L""); } return sourceMatches; } -// Method Description: -// - Helper function to convert a json value into a value of the Stretch enum. -// Calls into ParseImageStretchMode. Used with JsonUtils::GetOptionalValue. -// Arguments: -// - json: the Json::Value object to parse. -// Return Value: -// - An appropriate value from Windows.UI.Xaml.Media.Stretch -Media::Stretch Profile::_ConvertJsonToStretchMode(const Json::Value& json) -{ - return Profile::ParseImageStretchMode(json.asString()); -} - -// Method Description: -// - Helper function to convert a json value into a value of the Stretch enum. -// Calls into ParseImageAlignment. Used with JsonUtils::GetOptionalValue. -// Arguments: -// - json: the Json::Value object to parse. -// Return Value: -// - A pair of HorizontalAlignment and VerticalAlignment -std::tuple Profile::_ConvertJsonToAlignment(const Json::Value& json) -{ - return Profile::ParseImageAlignment(json.asString()); -} - -// Method Description: -// - Helper function to convert a json value into a bool. -// Used with JsonUtils::GetOptionalValue. -// Arguments: -// - json: the Json::Value object to parse. -// Return Value: -// - A bool -bool Profile::_ConvertJsonToBool(const Json::Value& json) -{ - return json.asBool(); -} - // 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 @@ -456,89 +366,45 @@ bool Profile::_ConvertJsonToBool(const Json::Value& json) void Profile::LayerJson(const Json::Value& json) { // Profile-specific Settings - JsonUtils::GetWstring(json, NameKey, _name); - - JsonUtils::GetOptionalGuid(json, GuidKey, _guid); - - JsonUtils::GetBool(json, HiddenKey, _hidden); + JsonUtils::GetValueForKey(json, NameKey, _name); + JsonUtils::GetValueForKey(json, GuidKey, _guid); + JsonUtils::GetValueForKey(json, HiddenKey, _hidden); // Core Settings - JsonUtils::GetOptionalColor(json, ForegroundKey, _defaultForeground); - - JsonUtils::GetOptionalColor(json, BackgroundKey, _defaultBackground); - - JsonUtils::GetOptionalColor(json, SelectionBackgroundKey, _selectionBackground); - - JsonUtils::GetOptionalColor(json, CursorColorKey, _cursorColor); - - JsonUtils::GetOptionalString(json, ColorSchemeKey, _schemeName); + JsonUtils::GetValueForKey(json, ForegroundKey, _defaultForeground); + JsonUtils::GetValueForKey(json, BackgroundKey, _defaultBackground); + JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _selectionBackground); + JsonUtils::GetValueForKey(json, CursorColorKey, _cursorColor); + JsonUtils::GetValueForKey(json, ColorSchemeKey, _schemeName); // TODO:MSFT:20642297 - Use a sentinel value (-1) for "Infinite scrollback" - JsonUtils::GetInt(json, HistorySizeKey, _historySize); - - JsonUtils::GetBool(json, SnapOnInputKey, _snapOnInput); - - JsonUtils::GetBool(json, AltGrAliasingKey, _altGrAliasing); - - JsonUtils::GetUInt(json, CursorHeightKey, _cursorHeight); - - if (json.isMember(JsonKey(CursorShapeKey))) - { - auto cursorShape{ json[JsonKey(CursorShapeKey)] }; - _cursorShape = _ParseCursorShape(GetWstringFromJson(cursorShape)); - } - JsonUtils::GetOptionalString(json, TabTitleKey, _tabTitle); + JsonUtils::GetValueForKey(json, HistorySizeKey, _historySize); + JsonUtils::GetValueForKey(json, SnapOnInputKey, _snapOnInput); + JsonUtils::GetValueForKey(json, AltGrAliasingKey, _altGrAliasing); + JsonUtils::GetValueForKey(json, CursorHeightKey, _cursorHeight); + JsonUtils::GetValueForKey(json, CursorShapeKey, _cursorShape); + JsonUtils::GetValueForKey(json, TabTitleKey, _tabTitle); // Control Settings - JsonUtils::GetOptionalGuid(json, ConnectionTypeKey, _connectionType); - - JsonUtils::GetWstring(json, CommandlineKey, _commandline); - - JsonUtils::GetWstring(json, FontFaceKey, _fontFace); - - JsonUtils::GetInt(json, FontSizeKey, _fontSize); - - if (json.isMember(JsonKey(FontWeightKey))) - { - auto fontWeight{ json[JsonKey(FontWeightKey)] }; - _fontWeight = _ParseFontWeight(fontWeight); - } - - JsonUtils::GetDouble(json, AcrylicTransparencyKey, _acrylicTransparency); - - JsonUtils::GetBool(json, UseAcrylicKey, _useAcrylic); - - JsonUtils::GetBool(json, SuppressApplicationTitleKey, _suppressApplicationTitle); - - if (json.isMember(JsonKey(CloseOnExitKey))) - { - auto closeOnExit{ json[JsonKey(CloseOnExitKey)] }; - _closeOnExitMode = ParseCloseOnExitMode(closeOnExit); - } - - JsonUtils::GetWstring(json, PaddingKey, _padding); - - JsonUtils::GetOptionalString(json, ScrollbarStateKey, _scrollbarState); - - JsonUtils::GetOptionalString(json, StartingDirectoryKey, _startingDirectory); - - JsonUtils::GetOptionalString(json, IconKey, _icon); - - JsonUtils::GetOptionalString(json, BackgroundImageKey, _backgroundImage); - - JsonUtils::GetOptionalDouble(json, BackgroundImageOpacityKey, _backgroundImageOpacity); - - JsonUtils::GetOptionalValue(json, BackgroundImageStretchModeKey, _backgroundImageStretchMode, &Profile::_ConvertJsonToStretchMode); - - JsonUtils::GetOptionalValue(json, BackgroundImageAlignmentKey, _backgroundImageAlignment, &Profile::_ConvertJsonToAlignment); - - JsonUtils::GetOptionalValue(json, RetroTerminalEffectKey, _retroTerminalEffect, Profile::_ConvertJsonToBool); - - if (json.isMember(JsonKey(AntialiasingModeKey))) - { - auto antialiasingMode{ json[JsonKey(AntialiasingModeKey)] }; - _antialiasingMode = ParseTextAntialiasingMode(GetWstringFromJson(antialiasingMode)); - } + JsonUtils::GetValueForKey(json, FontWeightKey, _fontWeight); + JsonUtils::GetValueForKey(json, ConnectionTypeKey, _connectionType); + JsonUtils::GetValueForKey(json, CommandlineKey, _commandline); + JsonUtils::GetValueForKey(json, FontFaceKey, _fontFace); + JsonUtils::GetValueForKey(json, FontSizeKey, _fontSize); + JsonUtils::GetValueForKey(json, AcrylicTransparencyKey, _acrylicTransparency); + JsonUtils::GetValueForKey(json, UseAcrylicKey, _useAcrylic); + JsonUtils::GetValueForKey(json, SuppressApplicationTitleKey, _suppressApplicationTitle); + JsonUtils::GetValueForKey(json, CloseOnExitKey, _closeOnExitMode); + JsonUtils::GetValueForKey(json, PaddingKey, _padding); + JsonUtils::GetValueForKey(json, ScrollbarStateKey, _scrollbarState); + JsonUtils::GetValueForKey(json, StartingDirectoryKey, _startingDirectory); + JsonUtils::GetValueForKey(json, IconKey, _icon); + JsonUtils::GetValueForKey(json, BackgroundImageKey, _backgroundImage); + JsonUtils::GetValueForKey(json, BackgroundImageOpacityKey, _backgroundImageOpacity); + JsonUtils::GetValueForKey(json, BackgroundImageStretchModeKey, _backgroundImageStretchMode); + JsonUtils::GetValueForKey(json, BackgroundImageAlignmentKey, _backgroundImageAlignment); + JsonUtils::GetValueForKey(json, RetroTerminalEffectKey, _retroTerminalEffect); + JsonUtils::GetValueForKey(json, AntialiasingModeKey, _antialiasingMode); } void Profile::SetFontFace(std::wstring fontFace) noexcept @@ -770,249 +636,6 @@ std::wstring Profile::EvaluateStartingDirectory(const std::wstring& directory) } } -// Method Description: -// - Helper function for converting a user-specified font weight value to its corresponding enum -// Arguments: -// - The value from the settings.json file -// Return Value: -// - The corresponding value which maps to the string provided by the user -winrt::Windows::UI::Text::FontWeight Profile::_ParseFontWeight(const Json::Value& json) -{ - if (json.isUInt()) - { - winrt::Windows::UI::Text::FontWeight weight; - weight.Weight = static_cast(json.asUInt()); - - // We're only accepting variable values between 100 and 990 so we don't go too crazy. - if (weight.Weight >= 100 && weight.Weight <= 990) - { - return weight; - } - } - - if (json.isString()) - { - auto fontWeight = json.asString(); - if (fontWeight == FontWeightThin) - { - return winrt::Windows::UI::Text::FontWeights::Thin(); - } - else if (fontWeight == FontWeightExtraLight) - { - return winrt::Windows::UI::Text::FontWeights::ExtraLight(); - } - else if (fontWeight == FontWeightLight) - { - return winrt::Windows::UI::Text::FontWeights::Light(); - } - else if (fontWeight == FontWeightSemiLight) - { - return winrt::Windows::UI::Text::FontWeights::SemiLight(); - } - else if (fontWeight == FontWeightNormal) - { - return winrt::Windows::UI::Text::FontWeights::Normal(); - } - else if (fontWeight == FontWeightMedium) - { - return winrt::Windows::UI::Text::FontWeights::Medium(); - } - else if (fontWeight == FontWeightSemiBold) - { - return winrt::Windows::UI::Text::FontWeights::SemiBold(); - } - else if (fontWeight == FontWeightBold) - { - return winrt::Windows::UI::Text::FontWeights::Bold(); - } - else if (fontWeight == FontWeightExtraBold) - { - return winrt::Windows::UI::Text::FontWeights::ExtraBold(); - } - else if (fontWeight == FontWeightBlack) - { - return winrt::Windows::UI::Text::FontWeights::Black(); - } - else if (fontWeight == FontWeightExtraBlack) - { - return winrt::Windows::UI::Text::FontWeights::ExtraBlack(); - } - } - - return winrt::Windows::UI::Text::FontWeights::Normal(); -} - -// Method Description: -// - Helper function for converting a user-specified closeOnExit value to its corresponding enum -// Arguments: -// - The value from the settings.json file -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -CloseOnExitMode Profile::ParseCloseOnExitMode(const Json::Value& json) -{ - if (json.isBool()) - { - return json.asBool() ? CloseOnExitMode::Graceful : CloseOnExitMode::Never; - } - - if (json.isString()) - { - auto closeOnExit = json.asString(); - if (closeOnExit == CloseOnExitAlways) - { - return CloseOnExitMode::Always; - } - else if (closeOnExit == CloseOnExitGraceful) - { - return CloseOnExitMode::Graceful; - } - else if (closeOnExit == CloseOnExitNever) - { - return CloseOnExitMode::Never; - } - } - - return CloseOnExitMode::Graceful; -} - -// Method Description: -// - Helper function for converting a user-specified scrollbar state to its corresponding enum -// Arguments: -// - The value from the settings.json file -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -ScrollbarState Profile::ParseScrollbarState(const std::wstring& scrollbarState) -{ - if (scrollbarState == AlwaysVisible) - { - return ScrollbarState::Visible; - } - else if (scrollbarState == AlwaysHide) - { - return ScrollbarState::Hidden; - } - else - { - return ScrollbarState::Visible; - } -} - -// Method Description: -// - Helper function for converting a user-specified image stretch mode -// to the appropriate enum value -// Arguments: -// - The value from the settings.json file -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -Media::Stretch Profile::ParseImageStretchMode(const std::string_view imageStretchMode) -{ - if (imageStretchMode == ImageStretchModeNone) - { - return Media::Stretch::None; - } - else if (imageStretchMode == ImageStretchModeFill) - { - return Media::Stretch::Fill; - } - else if (imageStretchMode == ImageStretchModeUniform) - { - return Media::Stretch::Uniform; - } - else // Fall through to default behavior - { - return Media::Stretch::UniformToFill; - } -} - -// Method Description: -// - Helper function for converting a user-specified image horizontal and vertical -// alignment to the appropriate enum values tuple -// Arguments: -// - The value from the settings.json file -// Return Value: -// - The corresponding enum values tuple which maps to the string provided by the user -std::tuple Profile::ParseImageAlignment(const std::string_view imageAlignment) -{ - if (imageAlignment == ImageAlignmentTopLeft) - { - return std::make_tuple(HorizontalAlignment::Left, - VerticalAlignment::Top); - } - else if (imageAlignment == ImageAlignmentBottomLeft) - { - return std::make_tuple(HorizontalAlignment::Left, - VerticalAlignment::Bottom); - } - else if (imageAlignment == ImageAlignmentLeft) - { - return std::make_tuple(HorizontalAlignment::Left, - VerticalAlignment::Center); - } - else if (imageAlignment == ImageAlignmentTopRight) - { - return std::make_tuple(HorizontalAlignment::Right, - VerticalAlignment::Top); - } - else if (imageAlignment == ImageAlignmentBottomRight) - { - return std::make_tuple(HorizontalAlignment::Right, - VerticalAlignment::Bottom); - } - else if (imageAlignment == ImageAlignmentRight) - { - return std::make_tuple(HorizontalAlignment::Right, - VerticalAlignment::Center); - } - else if (imageAlignment == ImageAlignmentTop) - { - return std::make_tuple(HorizontalAlignment::Center, - VerticalAlignment::Top); - } - else if (imageAlignment == ImageAlignmentBottom) - { - return std::make_tuple(HorizontalAlignment::Center, - VerticalAlignment::Bottom); - } - else // Fall through to default alignment - { - return std::make_tuple(HorizontalAlignment::Center, - VerticalAlignment::Center); - } -} - -// Method Description: -// - Helper function for converting a user-specified cursor style corresponding -// CursorStyle enum value -// Arguments: -// - cursorShapeString: The string value from the settings file to parse -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -CursorStyle Profile::_ParseCursorShape(const std::wstring& cursorShapeString) -{ - if (cursorShapeString == CursorShapeVintage) - { - return CursorStyle::Vintage; - } - else if (cursorShapeString == CursorShapeBar) - { - return CursorStyle::Bar; - } - else if (cursorShapeString == CursorShapeUnderscore) - { - return CursorStyle::Underscore; - } - else if (cursorShapeString == CursorShapeFilledbox) - { - return CursorStyle::FilledBox; - } - else if (cursorShapeString == CursorShapeEmptybox) - { - return CursorStyle::EmptyBox; - } - // default behavior for invalid data - return CursorStyle::Bar; -} - // Method Description: // - If this profile never had a GUID set for it, generate a runtime GUID for // the profile. If a profile had their guid manually set to {0}, this method @@ -1078,17 +701,13 @@ GUID Profile::_GenerateGuidForProfile(const std::wstring& name, const std::optio // - The json's `guid`, or a guid synthesized for it. GUID Profile::GetGuidOrGenerateForJson(const Json::Value& json) noexcept { - std::optional guid{ std::nullopt }; - - JsonUtils::GetOptionalGuid(json, GuidKey, guid); - if (guid) + if (const auto guid{ JsonUtils::GetValueForKey>(json, GuidKey) }) { return guid.value(); } - const auto name = GetWstringFromJson(json[JsonKey(NameKey)]); - std::optional source{ std::nullopt }; - JsonUtils::GetOptionalString(json, SourceKey, source); + const auto name{ JsonUtils::GetValueForKey(json, NameKey) }; + const auto source{ JsonUtils::GetValueForKey>(json, SourceKey) }; return Profile::_GenerateGuidForProfile(name, source); } @@ -1097,28 +716,3 @@ void Profile::SetRetroTerminalEffect(bool value) noexcept { _retroTerminalEffect = value; } - -// Method Description: -// - Helper function for converting a user-specified antialiasing mode -// corresponding TextAntialiasingMode enum value -// Arguments: -// - antialiasingMode: The string value from the settings file to parse -// Return Value: -// - The corresponding enum value which maps to the string provided by the user -TextAntialiasingMode Profile::ParseTextAntialiasingMode(const std::wstring& antialiasingMode) -{ - if (antialiasingMode == AntialiasingModeCleartype) - { - return TextAntialiasingMode::Cleartype; - } - else if (antialiasingMode == AntialiasingModeAliased) - { - return TextAntialiasingMode::Aliased; - } - else if (antialiasingMode == AntialiasingModeGrayscale) - { - return TextAntialiasingMode::Grayscale; - } - // default behavior for invalid data - return TextAntialiasingMode::Grayscale; -} diff --git a/src/cascadia/TerminalApp/Profile.h b/src/cascadia/TerminalApp/Profile.h index bd48592157c..1e145e669c7 100644 --- a/src/cascadia/TerminalApp/Profile.h +++ b/src/cascadia/TerminalApp/Profile.h @@ -15,6 +15,7 @@ Author(s): --*/ #pragma once #include "ColorScheme.h" +#include "SettingsTypes.h" // fwdecl unittest classes namespace TerminalAppLocalTests @@ -35,14 +36,7 @@ constexpr GUID RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID = { 0xf65ddb7e, 0x706b, namespace TerminalApp { class Profile; - - enum class CloseOnExitMode - { - Never = 0, - Graceful, - Always - }; -}; +} class TerminalApp::Profile final { @@ -52,7 +46,7 @@ class TerminalApp::Profile final ~Profile(); - winrt::Microsoft::Terminal::Settings::TerminalSettings CreateTerminalSettings(const std::unordered_map& schemes) const; + winrt::TerminalApp::TerminalSettings CreateTerminalSettings(const std::unordered_map& schemes) const; Json::Value GenerateStub() const; static Profile FromJson(const Json::Value& json); @@ -107,24 +101,8 @@ class TerminalApp::Profile final private: static std::wstring EvaluateStartingDirectory(const std::wstring& directory); - static winrt::Microsoft::Terminal::Settings::ScrollbarState ParseScrollbarState(const std::wstring& scrollbarState); - static winrt::Windows::UI::Xaml::Media::Stretch ParseImageStretchMode(const std::string_view imageStretchMode); - static winrt::Windows::UI::Xaml::Media::Stretch _ConvertJsonToStretchMode(const Json::Value& json); - static std::tuple ParseImageAlignment(const std::string_view imageAlignment); - static std::tuple _ConvertJsonToAlignment(const Json::Value& json); - - static winrt::Windows::UI::Text::FontWeight _ParseFontWeight(const Json::Value& json); - - static CloseOnExitMode ParseCloseOnExitMode(const Json::Value& json); - - static winrt::Microsoft::Terminal::Settings::CursorStyle _ParseCursorShape(const std::wstring& cursorShapeString); - - static winrt::Microsoft::Terminal::Settings::TextAntialiasingMode ParseTextAntialiasingMode(const std::wstring& antialiasingMode); - static GUID _GenerateGuidForProfile(const std::wstring& name, const std::optional& source) noexcept; - static bool _ConvertJsonToBool(const Json::Value& json); - std::optional _guid{ std::nullopt }; std::optional _source{ std::nullopt }; std::wstring _name; @@ -159,7 +137,7 @@ class TerminalApp::Profile final std::optional _backgroundImageStretchMode; std::optional> _backgroundImageAlignment; - std::optional _scrollbarState; + std::optional<::winrt::Microsoft::Terminal::Settings::ScrollbarState> _scrollbarState; CloseOnExitMode _closeOnExitMode; std::wstring _padding; diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 09283b55c6d..6ca22a39ff0 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -543,6 +543,10 @@ Reset tab title + + Run commandline "{0}" in this window + {0} will be replaced with user-defined commandline + Crimson diff --git a/src/cascadia/TerminalApp/SettingsTypes.h b/src/cascadia/TerminalApp/SettingsTypes.h new file mode 100644 index 00000000000..66903ab41a4 --- /dev/null +++ b/src/cascadia/TerminalApp/SettingsTypes.h @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SettingsTypes.h + +Abstract: +- Types used in the settings model (non-exported) +--*/ + +#pragma once + +namespace TerminalApp +{ + enum class CloseOnExitMode + { + Never = 0, + Graceful, + Always + }; + + struct LaunchPosition + { + std::optional x; + std::optional y; + }; +}; diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp index a040106e24c..571b4589516 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp @@ -194,6 +194,10 @@ namespace winrt::TerminalApp::implementation _RenameTabHandlers(*this, *eventArgs); break; } + case ShortcutAction::ExecuteCommandline: + { + _ExecuteCommandlineHandlers(*this, *eventArgs); + } default: return false; } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.h b/src/cascadia/TerminalApp/ShortcutActionDispatch.h index 9b930db980d..9c1dff1575c 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.h +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.h @@ -54,6 +54,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(SetTabColor, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); TYPED_EVENT(OpenTabColorPicker, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); TYPED_EVENT(RenameTab, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); + TYPED_EVENT(ExecuteCommandline, TerminalApp::ShortcutActionDispatch, TerminalApp::ActionEventArgs); // clang-format on private: diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl index 2588c3258b2..8306c04fc46 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl @@ -35,10 +35,11 @@ namespace TerminalApp ToggleFocusMode, ToggleFullscreen, ToggleAlwaysOnTop, + OpenSettings, SetTabColor, OpenTabColorPicker, - OpenSettings, RenameTab, + ExecuteCommandline, ToggleCommandPalette }; @@ -84,5 +85,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SetTabColor; event Windows.Foundation.TypedEventHandler OpenTabColorPicker; event Windows.Foundation.TypedEventHandler RenameTab; + event Windows.Foundation.TypedEventHandler ExecuteCommandline; } } diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 0e9838ec69c..51e54be09ee 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -875,6 +875,10 @@ namespace winrt::TerminalApp::implementation return _rootPane->PreCalculateAutoSplit(_activePane, availableSpace).value_or(SplitState::Vertical); } + bool Tab::PreCalculateCanSplit(SplitState splitType, winrt::Windows::Foundation::Size availableSpace) const + { + return _rootPane->PreCalculateCanSplit(_activePane, splitType, availableSpace).value_or(false); + } DEFINE_EVENT(Tab, ActivePaneChanged, _ActivePaneChangedHandlers, winrt::delegate<>); DEFINE_EVENT(Tab, ColorSelected, _colorSelected, winrt::delegate); DEFINE_EVENT(Tab, ColorCleared, _colorCleared, winrt::delegate<>); diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index be76c1b7912..055d631c421 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -40,12 +40,13 @@ namespace winrt::TerminalApp::implementation float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; SplitState PreCalculateAutoSplit(winrt::Windows::Foundation::Size rootSize) const; + bool PreCalculateCanSplit(SplitState splitType, winrt::Windows::Foundation::Size availableSpace) const; void ResizeContent(const winrt::Windows::Foundation::Size& newSize); void ResizePane(const winrt::TerminalApp::Direction& direction); void NavigateFocus(const winrt::TerminalApp::Direction& direction); - void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile); + void UpdateSettings(const winrt::TerminalApp::TerminalSettings& settings, const GUID& profile); winrt::hstring GetActiveTitle() const; void Shutdown(); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 6ed6d8e9716..30689891f04 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -32,7 +32,6 @@ using namespace winrt::Windows::UI::Text; using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::Microsoft::Terminal::TerminalConnection; -using namespace winrt::Microsoft::Terminal::Settings; using namespace ::TerminalApp; using namespace ::Microsoft::Console; @@ -46,7 +45,8 @@ namespace winrt namespace winrt::TerminalApp::implementation { TerminalPage::TerminalPage() : - _tabs{ winrt::single_threaded_observable_vector() } + _tabs{ winrt::single_threaded_observable_vector() }, + _startupActions{ winrt::single_threaded_vector() } { InitializeComponent(); } @@ -166,7 +166,28 @@ namespace winrt::TerminalApp::implementation _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) { - page->_OpenNewTab(nullptr); + // if alt is pressed, open a pane + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + // Check for DebugTap + bool debugTap = page->_settings->GlobalSettings().DebugFeaturesEnabled() && + WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + if (altPressed && !debugTap) + { + page->_SplitPane(TerminalApp::SplitState::Automatic, + TerminalApp::SplitType::Manual, + nullptr); + } + else + { + page->_OpenNewTab(nullptr); + } } }); _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); @@ -223,7 +244,7 @@ namespace winrt::TerminalApp::implementation if (_startupState == StartupState::NotInitialized) { _startupState = StartupState::InStartup; - if (_startupActions.empty()) + if (_startupActions.Size() == 0) { _OpenNewTab(nullptr); @@ -231,22 +252,27 @@ namespace winrt::TerminalApp::implementation } else { - _ProcessStartupActions(); + _ProcessStartupActions(_startupActions, true); } } } // Method Description: - // - Process all the startup actions in our list of startup actions. We'll - // do this all at once here. + // - Process all the startup actions in the provided list of startup + // actions. We'll do this all at once here. // Arguments: - // - + // - actions: a winrt vector of actions to process. Note that this must NOT + // be an IVector&, because we need the collection to be accessible on the + // other side of the co_await. + // - initial: if true, we're parsing these args during startup, and we + // should fire an Initialized event. // Return Value: // - - winrt::fire_and_forget TerminalPage::_ProcessStartupActions() + winrt::fire_and_forget TerminalPage::_ProcessStartupActions(Windows::Foundation::Collections::IVector actions, + const bool initial) { // If there are no actions left, do nothing. - if (_startupActions.empty()) + if (actions.Size() == 0) { return; } @@ -256,11 +282,20 @@ namespace winrt::TerminalApp::implementation co_await winrt::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal); if (auto page{ weakThis.get() }) { - for (const auto& action : _startupActions) + for (const auto& action : actions) { - _actionDispatch->DoAction(action); + if (auto page{ weakThis.get() }) + { + _actionDispatch->DoAction(action); + } + else + { + return; + } } - + } + if (initial) + { _CompleteInitialization(); } } @@ -590,7 +625,7 @@ namespace winrt::TerminalApp::implementation // currently displayed, it will be shown. // Arguments: // - settings: the TerminalSettings object to use to create the TerminalControl with. - void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings) + void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, TerminalApp::TerminalSettings settings) { // Initialize the new tab @@ -691,7 +726,7 @@ namespace winrt::TerminalApp::implementation // Return value: // - the desired connection TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(GUID profileGuid, - winrt::Microsoft::Terminal::Settings::TerminalSettings settings) + TerminalApp::TerminalSettings settings) { const auto* const profile = _settings->FindProfile(profileGuid); @@ -857,6 +892,7 @@ namespace winrt::TerminalApp::implementation _actionDispatch->SetTabColor({ this, &TerminalPage::_HandleSetTabColor }); _actionDispatch->OpenTabColorPicker({ this, &TerminalPage::_HandleOpenTabColorPicker }); _actionDispatch->RenameTab({ this, &TerminalPage::_HandleRenameTab }); + _actionDispatch->ExecuteCommandline({ this, &TerminalPage::_HandleExecuteCommandline }); } // Method Description: @@ -1331,7 +1367,7 @@ namespace winrt::TerminalApp::implementation try { auto focusedTab = _GetStrongTabImpl(*indexOpt); - winrt::Microsoft::Terminal::Settings::TerminalSettings controlSettings; + TerminalApp::TerminalSettings controlSettings; GUID realGuid; bool profileFound = false; @@ -1364,19 +1400,20 @@ namespace winrt::TerminalApp::implementation const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings); - const auto canSplit = focusedTab->CanSplitPane(splitType); + const float contentWidth = ::base::saturated_cast(_tabContent.ActualWidth()); + const float contentHeight = ::base::saturated_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; - if (!canSplit && _startupState == StartupState::Initialized) + auto realSplitType = splitType; + if (realSplitType == SplitState::Automatic) { - return; + realSplitType = focusedTab->PreCalculateAutoSplit(availableSpace); } - auto realSplitType = splitType; - if (realSplitType == SplitState::Automatic && _startupState < StartupState::Initialized) + const auto canSplit = focusedTab->PreCalculateCanSplit(realSplitType, availableSpace); + if (!canSplit) { - float contentWidth = gsl::narrow_cast(_tabContent.ActualWidth()); - float contentHeight = gsl::narrow_cast(_tabContent.ActualHeight()); - realSplitType = focusedTab->PreCalculateAutoSplit({ contentWidth, contentHeight }); + return; } TermControl newControl{ controlSettings, controlConnection }; @@ -1778,7 +1815,7 @@ namespace winrt::TerminalApp::implementation tab->SetFocused(true); // Raise an event that our title changed - _titleChangeHandlers(*this, Title()); + _titleChangeHandlers(*this, tab->GetActiveTitle()); // Raise an event that our titlebar color changed std::optional color = tab->GetTabColor(); @@ -1930,9 +1967,13 @@ namespace winrt::TerminalApp::implementation // - actions: a list of Actions to process on startup. // Return Value: // - - void TerminalPage::SetStartupActions(std::deque& actions) + void TerminalPage::SetStartupActions(std::vector& actions) { - _startupActions = actions; + // The fastest way to copy all the actions out of the std::vector and + // put them into a winrt::IVector is by making a copy, then moving the + // copy into the winrt vector ctor. + auto listCopy = actions; + _startupActions = winrt::single_threaded_vector(std::move(listCopy)); } winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const @@ -2192,6 +2233,49 @@ namespace winrt::TerminalApp::implementation // TODO GH#3327: Look at what to do with the NC area when we have XAML theming } + // Function Description: + // - This is a helper method to get the commandline out of a + // ExecuteCommandline action, break it into subcommands, and attempt to + // parse it into actions. This is used by _HandleExecuteCommandline for + // processing commandlines in the current WT window. + // Arguments: + // - args: the ExecuteCommandlineArgs to synthesize a list of startup actions for. + // Return Value: + // - an empty list if we failed to parse, otherwise a list of actions to execute. + std::vector TerminalPage::ConvertExecuteCommandlineToActions(const TerminalApp::ExecuteCommandlineArgs& args) + { + if (!args || args.Commandline().empty()) + { + return {}; + } + // Convert the commandline into an array of args with + // CommandLineToArgvW, similar to how the app typically does when + // called from the commandline. + int argc = 0; + wil::unique_any argv{ CommandLineToArgvW(args.Commandline().c_str(), &argc) }; + if (argv) + { + std::vector args; + + // Make sure the first argument is wt.exe, because ParseArgs will + // always skip the program name. The particular value of this first + // string doesn't terribly matter. + args.emplace_back(L"wt.exe"); + for (auto& elem : wil::make_range(argv.get(), argc)) + { + args.emplace_back(elem); + } + winrt::array_view argsView{ args }; + + ::TerminalApp::AppCommandlineArgs appArgs; + if (appArgs.ParseArgs(argsView) == 0) + { + return appArgs.GetStartupActions(); + } + } + return {}; + } + void TerminalPage::_CommandPaletteClosed(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2aa717cfacd..021be7a3e16 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -56,7 +56,8 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; - void SetStartupActions(std::deque& actions); + void SetStartupActions(std::vector& actions); + static std::vector ConvertExecuteCommandlineToActions(const TerminalApp::ExecuteCommandlineArgs& args); winrt::TerminalApp::IDialogPresenter DialogPresenter() const; void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); @@ -106,8 +107,8 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker; StartupState _startupState{ StartupState::NotInitialized }; - std::deque _startupActions; - winrt::fire_and_forget _ProcessStartupActions(); + Windows::Foundation::Collections::IVector _startupActions; + winrt::fire_and_forget _ProcessStartupActions(Windows::Foundation::Collections::IVector actions, const bool initial); void _ShowAboutDialog(); void _ShowCloseWarningDialog(); @@ -117,8 +118,8 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); void _OpenNewTab(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs); - void _CreateNewTabFromSettings(GUID profileGuid, winrt::Microsoft::Terminal::Settings::TerminalSettings settings); - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, winrt::Microsoft::Terminal::Settings::TerminalSettings settings); + void _CreateNewTabFromSettings(GUID profileGuid, TerminalApp::TerminalSettings settings); + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(GUID profileGuid, TerminalApp::TerminalSettings settings); void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); void _FeedbackButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); @@ -221,6 +222,7 @@ namespace winrt::TerminalApp::implementation void _HandleSetTabColor(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleOpenTabColorPicker(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleRenameTab(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); + void _HandleExecuteCommandline(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); void _HandleToggleCommandPalette(const IInspectable& sender, const TerminalApp::ActionEventArgs& args); // Make sure to hook new actions up in _RegisterActionCallbacks! #pragma endregion diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalApp/TerminalSettings.cpp similarity index 87% rename from src/cascadia/TerminalSettings/TerminalSettings.cpp rename to src/cascadia/TerminalApp/TerminalSettings.cpp index 02d8f452e8f..3313736e653 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalApp/TerminalSettings.cpp @@ -6,7 +6,7 @@ #include "TerminalSettings.g.cpp" -namespace winrt::Microsoft::Terminal::Settings::implementation +namespace winrt::TerminalApp::implementation { uint32_t TerminalSettings::GetColorTableEntry(int32_t index) const noexcept { diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalApp/TerminalSettings.h similarity index 84% rename from src/cascadia/TerminalSettings/terminalsettings.h rename to src/cascadia/TerminalApp/TerminalSettings.h index 1bd1aad98ae..02998373e7d 100644 --- a/src/cascadia/TerminalSettings/terminalsettings.h +++ b/src/cascadia/TerminalApp/TerminalSettings.h @@ -19,7 +19,7 @@ Author(s): #include #include -namespace winrt::Microsoft::Terminal::Settings::implementation +namespace winrt::TerminalApp::implementation { struct TerminalSettings : TerminalSettingsT { @@ -49,7 +49,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation GETSET_PROPERTY(bool, SnapOnInput, true); GETSET_PROPERTY(bool, AltGrAliasing, true); GETSET_PROPERTY(uint32_t, CursorColor, DEFAULT_CURSOR_COLOR); - GETSET_PROPERTY(CursorStyle, CursorShape, CursorStyle::Vintage); + GETSET_PROPERTY(Microsoft::Terminal::Settings::CursorStyle, CursorShape, Microsoft::Terminal::Settings::CursorStyle::Vintage); GETSET_PROPERTY(uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT); GETSET_PROPERTY(hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS); GETSET_PROPERTY(bool, CopyOnSelect, false); @@ -78,7 +78,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation BackgroundImageVerticalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment::Center); - GETSET_PROPERTY(IKeyBindings, KeyBindings, nullptr); + GETSET_PROPERTY(Microsoft::Terminal::Settings::IKeyBindings, KeyBindings, nullptr); GETSET_PROPERTY(hstring, Commandline); GETSET_PROPERTY(hstring, StartingDirectory); @@ -86,9 +86,9 @@ namespace winrt::Microsoft::Terminal::Settings::implementation GETSET_PROPERTY(bool, SuppressApplicationTitle); GETSET_PROPERTY(hstring, EnvironmentVariables); - GETSET_PROPERTY(ScrollbarState, ScrollState, ScrollbarState::Visible); + GETSET_PROPERTY(Microsoft::Terminal::Settings::ScrollbarState, ScrollState, Microsoft::Terminal::Settings::ScrollbarState::Visible); - GETSET_PROPERTY(TextAntialiasingMode, AntialiasingMode, TextAntialiasingMode::Grayscale); + GETSET_PROPERTY(Microsoft::Terminal::Settings::TextAntialiasingMode, AntialiasingMode, Microsoft::Terminal::Settings::TextAntialiasingMode::Grayscale); GETSET_PROPERTY(bool, RetroTerminalEffect, false); GETSET_PROPERTY(bool, ForceFullRepaintRendering, false); @@ -102,7 +102,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation }; } -namespace winrt::Microsoft::Terminal::Settings::factory_implementation +namespace winrt::TerminalApp::factory_implementation { BASIC_FACTORY(TerminalSettings); } diff --git a/src/cascadia/TerminalSettings/TerminalSettings.idl b/src/cascadia/TerminalApp/TerminalSettings.idl similarity index 75% rename from src/cascadia/TerminalSettings/TerminalSettings.idl rename to src/cascadia/TerminalApp/TerminalSettings.idl index 15d265b1a9e..3d7d4880107 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.idl +++ b/src/cascadia/TerminalApp/TerminalSettings.idl @@ -1,10 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "ICoreSettings.idl"; -import "IControlSettings.idl"; - -namespace Microsoft.Terminal.Settings +namespace TerminalApp { // Class Description: // TerminalSettings encapsulates all settings that control the @@ -15,8 +12,8 @@ namespace Microsoft.Terminal.Settings // The TerminalControl will pull settings it requires from this object, // and pass along the Core properties to the terminal core. [default_interface] - runtimeclass TerminalSettings : ICoreSettings, - IControlSettings + runtimeclass TerminalSettings : Microsoft.Terminal.Settings.ICoreSettings, + Microsoft.Terminal.Settings.IControlSettings { TerminalSettings(); }; diff --git a/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h new file mode 100644 index 00000000000..40de89d2258 --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalSettingsSerializationHelpers.h @@ -0,0 +1,272 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TerminalSettingsSerializationHelpers.h + +Abstract: +- Specializations of the JsonUtils helpers for things that might end up in a + settings document. + +--*/ + +#pragma once + +#include "pch.h" + +#include "JsonUtils.h" +#include "SettingsTypes.h" + +#include +#include + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::CursorStyle) +{ + static constexpr std::array mappings = { + pair_type{ "bar", ValueType::Bar }, + pair_type{ "vintage", ValueType::Vintage }, + pair_type{ "underscore", ValueType::Underscore }, + pair_type{ "filledBox", ValueType::FilledBox }, + pair_type{ "emptyBox", ValueType::EmptyBox } + }; +}; + +JSON_ENUM_MAPPER(::winrt::Windows::UI::Xaml::Media::Stretch) +{ + static constexpr std::array mappings = { + pair_type{ "uniformToFill", ValueType::UniformToFill }, + pair_type{ "none", ValueType::None }, + pair_type{ "fill", ValueType::Fill }, + pair_type{ "uniform", ValueType::Uniform } + }; +}; + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::ScrollbarState) +{ + static constexpr std::array mappings = { + pair_type{ "visible", ValueType::Visible }, + pair_type{ "hidden", ValueType::Hidden } + }; +}; + +JSON_ENUM_MAPPER(std::tuple<::winrt::Windows::UI::Xaml::HorizontalAlignment, ::winrt::Windows::UI::Xaml::VerticalAlignment>) +{ + // reduce repetition + using HA = ::winrt::Windows::UI::Xaml::HorizontalAlignment; + using VA = ::winrt::Windows::UI::Xaml::VerticalAlignment; + static constexpr std::array mappings = { + pair_type{ "center", std::make_tuple(HA::Center, VA::Center) }, + pair_type{ "topLeft", std::make_tuple(HA::Left, VA::Top) }, + pair_type{ "bottomLeft", std::make_tuple(HA::Left, VA::Bottom) }, + pair_type{ "left", std::make_tuple(HA::Left, VA::Center) }, + pair_type{ "topRight", std::make_tuple(HA::Right, VA::Top) }, + pair_type{ "bottomRight", std::make_tuple(HA::Right, VA::Bottom) }, + pair_type{ "right", std::make_tuple(HA::Right, VA::Center) }, + pair_type{ "top", std::make_tuple(HA::Center, VA::Top) }, + pair_type{ "bottom", std::make_tuple(HA::Center, VA::Bottom) } + }; +}; + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::TextAntialiasingMode) +{ + static constexpr std::array mappings = { + pair_type{ "grayscale", ValueType::Grayscale }, + pair_type{ "cleartype", ValueType::Cleartype }, + pair_type{ "aliased", ValueType::Aliased } + }; +}; + +// Type Description: +// - Helper for converting a user-specified closeOnExit value to its corresponding enum +JSON_ENUM_MAPPER(::TerminalApp::CloseOnExitMode) +{ + JSON_MAPPINGS(3) = { + pair_type{ "always", ValueType::Always }, + pair_type{ "graceful", ValueType::Graceful }, + pair_type{ "never", ValueType::Never }, + }; + + // Override mapping parser to add boolean parsing + CloseOnExitMode FromJson(const Json::Value& json) + { + if (json.isBool()) + { + return json.asBool() ? ValueType::Graceful : ValueType::Never; + } + return EnumMapper::FromJson(json); + } + + bool CanConvert(const Json::Value& json) + { + return EnumMapper::CanConvert(json) || json.isBool(); + } +}; + +// This specialization isn't using JSON_ENUM_MAPPER because we need to have a different +// value type (unsinged int) and return type (FontWeight struct). JSON_ENUM_MAPPER +// expects that the value type _is_ the return type. +template<> +struct ::TerminalApp::JsonUtils::ConversionTrait<::winrt::Windows::UI::Text::FontWeight> : + public ::TerminalApp::JsonUtils::EnumMapper< + unsigned int, + ::TerminalApp::JsonUtils::ConversionTrait<::winrt::Windows::UI::Text::FontWeight>> +{ + // The original parser used the font weight getters Bold(), Normal(), etc. + // They were both cumbersome and *not constant expressions* + JSON_MAPPINGS(11) = { + pair_type{ "thin", 100u }, + pair_type{ "extra-light", 200u }, + pair_type{ "light", 300u }, + pair_type{ "semi-light", 350u }, + pair_type{ "normal", 400u }, + pair_type{ "medium", 500u }, + pair_type{ "semi-bold", 600u }, + pair_type{ "bold", 700u }, + pair_type{ "extra-bold", 800u }, + pair_type{ "black", 900u }, + pair_type{ "extra-black", 950u }, + }; + + // Override mapping parser to add boolean parsing + auto FromJson(const Json::Value& json) + { + unsigned int value{ 400 }; + if (json.isUInt()) + { + value = json.asUInt(); + } + else + { + value = BaseEnumMapper::FromJson(json); + } + + ::winrt::Windows::UI::Text::FontWeight weight{ + static_cast(std::clamp(value, 100u, 990u)) + }; + return weight; + } + + bool CanConvert(const Json::Value& json) + { + return BaseEnumMapper::CanConvert(json) || json.isUInt(); + } +}; + +JSON_ENUM_MAPPER(::winrt::Windows::UI::Xaml::ElementTheme) +{ + JSON_MAPPINGS(3) = { + pair_type{ "system", ValueType::Default }, + pair_type{ "light", ValueType::Light }, + pair_type{ "dark", ValueType::Dark }, + }; +}; + +JSON_ENUM_MAPPER(::winrt::TerminalApp::LaunchMode) +{ + JSON_MAPPINGS(3) = { + pair_type{ "default", ValueType::DefaultMode }, + pair_type{ "maximized", ValueType::MaximizedMode }, + pair_type{ "fullscreen", ValueType::FullscreenMode }, + }; +}; + +JSON_ENUM_MAPPER(::winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode) +{ + JSON_MAPPINGS(3) = { + pair_type{ "equal", ValueType::Equal }, + pair_type{ "titleLength", ValueType::SizeToContent }, + pair_type{ "compact", ValueType::Compact }, + }; +}; + +// Type Description: +// - Helper for converting the initial position string into +// 2 coordinate values. We allow users to only provide one coordinate, +// thus, we use comma as the separator: +// (100, 100): standard input string +// (, 100), (100, ): if a value is missing, we set this value as a default +// (,): both x and y are set to default +// (abc, 100): if a value is not valid, we treat it as default +// (100, 100, 100): we only read the first two values, this is equivalent to (100, 100) +template<> +struct ::TerminalApp::JsonUtils::ConversionTrait<::TerminalApp::LaunchPosition> +{ + ::TerminalApp::LaunchPosition FromJson(const Json::Value& json) + { + ::TerminalApp::LaunchPosition ret; + std::string initialPosition{ json.asString() }; + static constexpr char singleCharDelim = ','; + std::stringstream tokenStream(initialPosition); + std::string token; + uint8_t initialPosIndex = 0; + + // Get initial position values till we run out of delimiter separated values in the stream + // or we hit max number of allowable values (= 2) + // Non-numeral values or empty string will be caught as exception and we do not assign them + for (; std::getline(tokenStream, token, singleCharDelim) && (initialPosIndex < 2); initialPosIndex++) + { + try + { + int32_t position = std::stoi(token); + if (initialPosIndex == 0) + { + ret.x.emplace(position); + } + + if (initialPosIndex == 1) + { + ret.y.emplace(position); + } + } + catch (...) + { + // Do nothing + } + } + return ret; + } + + bool CanConvert(const Json::Value& json) + { + return json.isString(); + } +}; + +// Possible Direction values +JSON_ENUM_MAPPER(::winrt::TerminalApp::Direction) +{ + JSON_MAPPINGS(4) = { + pair_type{ "left", ValueType::Left }, + pair_type{ "right", ValueType::Right }, + pair_type{ "up", ValueType::Up }, + pair_type{ "down", ValueType::Down }, + }; +}; + +// Possible SplitState values +JSON_ENUM_MAPPER(::winrt::TerminalApp::SplitState) +{ + JSON_MAPPINGS(3) = { + pair_type{ "vertical", ValueType::Vertical }, + pair_type{ "horizontal", ValueType::Horizontal }, + pair_type{ "auto", ValueType::Automatic }, + }; +}; + +// Possible SplitType values +JSON_ENUM_MAPPER(::winrt::TerminalApp::SplitType) +{ + JSON_MAPPINGS(1) = { + pair_type{ "duplicate", ValueType::Duplicate }, + }; +}; + +JSON_ENUM_MAPPER(::winrt::TerminalApp::SettingsTarget) +{ + JSON_MAPPINGS(3) = { + pair_type{ "settingsFile", ValueType::SettingsFile }, + pair_type{ "defaultsFile", ValueType::DefaultsFile }, + pair_type{ "allFiles", ValueType::AllFiles }, + }; +}; diff --git a/src/cascadia/TerminalApp/Utils.cpp b/src/cascadia/TerminalApp/Utils.cpp deleted file mode 100644 index 01fa28dfdd1..00000000000 --- a/src/cascadia/TerminalApp/Utils.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "Utils.h" - -// Method Description: -// - Constructs a wstring from a given Json::Value object. Reads the object as -// a std::string using asString, then builds an hstring from that std::string, -// then converts that hstring into a std::wstring. -// Arguments: -// - json: the Json::Value to parse as a string -// Return Value: -// - the wstring equivalent of the value in json -std::wstring GetWstringFromJson(const Json::Value& json) -{ - return winrt::to_hstring(json.asString()).c_str(); -} diff --git a/src/cascadia/TerminalApp/Utils.h b/src/cascadia/TerminalApp/Utils.h index bba97694282..cb23bac450d 100644 --- a/src/cascadia/TerminalApp/Utils.h +++ b/src/cascadia/TerminalApp/Utils.h @@ -13,8 +13,6 @@ Author(s): --*/ #pragma once -std::wstring GetWstringFromJson(const Json::Value& json); - // Method Description: // - Create a std::string from a string_view. We do this because we can't look // up a key in a Json::Value with a string_view directly, so instead we'll use diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json index b4167968f79..a05668723f8 100644 --- a/src/cascadia/TerminalApp/defaults.json +++ b/src/cascadia/TerminalApp/defaults.json @@ -187,7 +187,7 @@ "foreground": "#839496", "background": "#002B36", "cursorColor": "#FFFFFF", - "black": "#073642", + "black": "#002B36", "red": "#DC322F", "green": "#859900", "yellow": "#B58900", @@ -195,7 +195,7 @@ "purple": "#D33682", "cyan": "#2AA198", "white": "#EEE8D5", - "brightBlack": "#002B36", + "brightBlack": "#073642", "brightRed": "#CB4B16", "brightGreen": "#586E75", "brightYellow": "#657B83", @@ -209,7 +209,7 @@ "foreground": "#657B83", "background": "#FDF6E3", "cursorColor": "#002B36", - "black": "#073642", + "black": "#002B36", "red": "#DC322F", "green": "#859900", "yellow": "#B58900", @@ -217,7 +217,7 @@ "purple": "#D33682", "cyan": "#2AA198", "white": "#EEE8D5", - "brightBlack": "#002B36", + "brightBlack": "#073642", "brightRed": "#CB4B16", "brightGreen": "#586E75", "brightYellow": "#657B83", diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index 6c9dc3323b7..2ceada4ea37 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -109,6 +109,7 @@ + @@ -116,6 +117,9 @@ + + ../TerminalSettings.idl + ../ShortcutActionDispatch.idl @@ -178,8 +182,6 @@ - - @@ -187,6 +189,9 @@ + + ../TerminalSettings.idl + Create @@ -257,6 +262,7 @@ + diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters index 5f856a6d4a8..fea379bc66c 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters @@ -8,7 +8,6 @@ - @@ -50,9 +49,6 @@ json - - json - tab @@ -63,7 +59,7 @@ - + settings @@ -92,6 +88,9 @@ settings + + settings + settings @@ -120,7 +119,7 @@ profileGeneration - + settings @@ -140,10 +139,14 @@ tab - commandPalette + + + + settings + diff --git a/src/cascadia/TerminalApp/lib/pch.h b/src/cascadia/TerminalApp/lib/pch.h index 52a51a3bd53..4cb7a49f1e3 100644 --- a/src/cascadia/TerminalApp/lib/pch.h +++ b/src/cascadia/TerminalApp/lib/pch.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 9578166b14d..62bc7a948e2 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -17,6 +17,7 @@ using namespace ::Microsoft::Console::Types; using namespace ::Microsoft::Terminal::Core; +using namespace winrt::Windows::Graphics::Display; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Input; using namespace winrt::Windows::UI::Xaml::Automation::Peers; @@ -55,11 +56,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return initialized; } - TermControl::TermControl() : - TermControl(Settings::TerminalSettings{}, TerminalConnection::ITerminalConnection{ nullptr }) - { - } - TermControl::TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection) : _connection{ connection }, _initializedTerminal{ false }, @@ -628,7 +624,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Then, using the font, get the number of characters that can fit. // Resize our terminal connection to match that size, and initialize the terminal with that size. const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); - THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground dxEngine->SetSelectionBackground(_settings.SelectionBackground()); @@ -737,7 +733,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto ch = e.Character(); const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); - const auto modifiers = _GetPressedModifierKeys(); + auto modifiers = _GetPressedModifierKeys(); + if (e.KeyStatus().IsExtendedKey) + { + modifiers |= ControlKeyStates::EnhancedKey; + } const bool handled = _terminal->SendCharEvent(ch, scanCode, modifiers); e.Handled(handled); } @@ -826,9 +826,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return; } - const auto modifiers = _GetPressedModifierKeys(); + auto modifiers = _GetPressedModifierKeys(); const auto vkey = gsl::narrow_cast(e.OriginalKey()); const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); + if (e.KeyStatus().IsExtendedKey) + { + modifiers |= ControlKeyStates::EnhancedKey; + } // Alt-Numpad# input will send us a character once the user releases // Alt, so we should be ignoring the individual keydowns. The character @@ -2274,28 +2278,65 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // as font size, scrollbar and other control scaling, etc. Make sure the // caller knows what monitor the control is about to appear on. // Return Value: - // - a point containing the requested dimensions in pixels. - winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) + // - a size containing the requested dimensions in pixels. + winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) { + // If the settings have negative or zero row or column counts, ignore those counts. + // (The lower TerminalCore layer also has upper bounds as well, but at this layer + // we may eventually impose different ones depending on how many pixels we can address.) + const auto cols = ::base::saturated_cast(std::max(settings.InitialCols(), 1)); + const auto rows = ::base::saturated_cast(std::max(settings.InitialRows(), 1)); + + const winrt::Windows::Foundation::Size initialSize{ cols, rows }; + + return GetProposedDimensions(initialSize, + settings.FontSize(), + settings.FontWeight(), + settings.FontFace(), + settings.ScrollState(), + settings.Padding(), + dpi); + } + + // Function Description: + // - Determines how much space (in pixels) an app would need to reserve to + // create a control with the settings stored in the settings param. This + // accounts for things like the font size and face, the initialRows and + // initialCols, and scrollbar visibility. The returned sized is based upon + // the provided DPI value + // Arguments: + // - initialSizeInChars: The size to get the proposed dimensions for. + // - fontHeight: The font height to use to calculate the proposed size for. + // - fontWeight: The font weight to use to calculate the proposed size for. + // - fontFace: The font name to use to calculate the proposed size for. + // - scrollState: The ScrollbarState to use to calculate the proposed size for. + // - padding: The padding to use to calculate the proposed size for. + // - dpi: The DPI we should create the terminal at. This affects things such + // as font size, scrollbar and other control scaling, etc. Make sure the + // caller knows what monitor the control is about to appear on. + // Return Value: + // - a size containing the requested dimensions in pixels. + winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(const winrt::Windows::Foundation::Size& initialSizeInChars, + const int32_t& fontHeight, + const winrt::Windows::UI::Text::FontWeight& fontWeight, + const winrt::hstring& fontFace, + const Microsoft::Terminal::Settings::ScrollbarState& scrollState, + const winrt::hstring& padding, + const uint32_t dpi) + { + const auto cols = ::base::saturated_cast(initialSizeInChars.Width); + const auto rows = ::base::saturated_cast(initialSizeInChars.Height); + // Initialize our font information. - const auto fontFace = settings.FontFace(); - const short fontHeight = gsl::narrow_cast(settings.FontSize()); - const auto fontWeight = settings.FontWeight(); // The font width doesn't terribly matter, we'll only be using the // height to look it up // The other params here also largely don't matter. // The family is only used to determine if the font is truetype or // not, but DX doesn't use that info at all. // The Codepage is additionally not actually used by the DX engine at all. - FontInfo actualFont = { fontFace, 0, fontWeight.Weight, { 0, fontHeight }, CP_UTF8, false }; + FontInfo actualFont = { fontFace, 0, fontWeight.Weight, { 0, gsl::narrow_cast(fontHeight) }, CP_UTF8, false }; FontInfoDesired desiredFont = { actualFont }; - // If the settings have negative or zero row or column counts, ignore those counts. - // (The lower TerminalCore layer also has upper bounds as well, but at this layer - // we may eventually impose different ones depending on how many pixels we can address.) - const auto cols = std::max(settings.InitialCols(), 1); - const auto rows = std::max(settings.InitialRows(), 1); - // Create a DX engine and initialize it with our font and DPI. We'll // then use it to measure how much space the requested rows and columns // will take up. @@ -2315,13 +2356,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation double width = cols * fontSize.X; // Reserve additional space if scrollbar is intended to be visible - if (settings.ScrollState() == ScrollbarState::Visible) + if (scrollState == ScrollbarState::Visible) { width += scrollbarSize; } double height = rows * fontSize.Y; - auto thickness = _ParseThicknessFromPadding(settings.Padding()); + auto thickness = _ParseThicknessFromPadding(padding); // GH#2061 - make sure to account for the size the padding _will be_ scaled to width += scale * (thickness.Left + thickness.Right); height += scale * (thickness.Top + thickness.Bottom); @@ -2354,21 +2395,41 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // have a visible character. winrt::Windows::Foundation::Size TermControl::MinimumSize() { - const auto fontSize = _actualFont.GetSize(); - double width = fontSize.X; - double height = fontSize.Y; - // Reserve additional space if scrollbar is intended to be visible - if (_settings.ScrollState() == ScrollbarState::Visible) + if (_initializedTerminal) { - width += ScrollBar().ActualWidth(); - } + const auto fontSize = _actualFont.GetSize(); + double width = fontSize.X; + double height = fontSize.Y; + // Reserve additional space if scrollbar is intended to be visible + if (_settings.ScrollState() == ScrollbarState::Visible) + { + width += ScrollBar().ActualWidth(); + } - // Account for the size of any padding - const auto padding = GetPadding(); - width += padding.Left + padding.Right; - height += padding.Top + padding.Bottom; + // Account for the size of any padding + const auto padding = GetPadding(); + width += padding.Left + padding.Right; + height += padding.Top + padding.Bottom; - return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + } + else + { + // If the terminal hasn't been initialized yet, then the font size will + // have dimensions {1, fontSize.Y}, which can mess with consumers of + // this method. In that case, we'll need to pre-calculate the font + // width, before we actually have a renderer or swapchain. + const winrt::Windows::Foundation::Size minSize{ 1, 1 }; + const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); + const auto dpi = ::base::saturated_cast(USER_DEFAULT_SCREEN_DPI * scaleFactor); + return GetProposedDimensions(minSize, + _settings.FontSize(), + _settings.FontWeight(), + _settings.FontFace(), + _settings.ScrollState(), + _settings.Padding(), + dpi); + } } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3879c4b3835..4499c589efc 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -56,7 +56,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation struct TermControl : TermControlT { - TermControl(); TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection); winrt::fire_and_forget UpdateSettings(Settings::IControlSettings newSettings); @@ -100,7 +99,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation TerminalConnection::ConnectionState ConnectionState() const; - static Windows::Foundation::Point GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi); + static Windows::Foundation::Size GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi); + static Windows::Foundation::Size GetProposedDimensions(const winrt::Windows::Foundation::Size& initialSizeInChars, + const int32_t& fontSize, + const winrt::Windows::UI::Text::FontWeight& fontWeight, + const winrt::hstring& fontFace, + const Microsoft::Terminal::Settings::ScrollbarState& scrollState, + const winrt::hstring& padding, + const uint32_t dpi); // clang-format off // -------------------------------- WinRT Events --------------------------------- diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 0d99a87b530..d9a2ac5cdff 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -32,10 +32,9 @@ namespace Microsoft.Terminal.TerminalControl [default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IDirectKeyListener, IMouseWheelListener { - TermControl(); TermControl(Microsoft.Terminal.Settings.IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); - static Windows.Foundation.Point GetProposedDimensions(Microsoft.Terminal.Settings.IControlSettings settings, UInt32 dpi); + static Windows.Foundation.Size GetProposedDimensions(Microsoft.Terminal.Settings.IControlSettings settings, UInt32 dpi); void UpdateSettings(Microsoft.Terminal.Settings.IControlSettings newSettings); diff --git a/src/cascadia/TerminalControl/ThrottledFunc.cpp b/src/cascadia/TerminalControl/ThrottledFunc.cpp index 41c60b7b2bc..e72d86297e6 100644 --- a/src/cascadia/TerminalControl/ThrottledFunc.cpp +++ b/src/cascadia/TerminalControl/ThrottledFunc.cpp @@ -29,7 +29,7 @@ ThrottledFunc<>::ThrottledFunc(ThrottledFunc::Func func, TimeSpan delay, CoreDis // - void ThrottledFunc<>::Run() { - if (_isRunPending.test_and_set()) + if (_isRunPending.test_and_set(std::memory_order_acquire)) { // already pending return; @@ -44,7 +44,7 @@ void ThrottledFunc<>::Run() if (auto self{ weakThis.lock() }) { timer.Stop(); - self->_isRunPending.clear(); + self->_isRunPending.clear(std::memory_order_release); self->_func(); } }); diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index d92a536c9d6..98945de1cfe 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -39,6 +39,7 @@ namespace Microsoft::Terminal::Core virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD color) noexcept = 0; virtual bool SetCursorStyle(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle cursorStyle) noexcept = 0; + virtual bool SetCursorColor(const DWORD color) noexcept = 0; virtual bool SetDefaultForeground(const DWORD color) noexcept = 0; virtual bool SetDefaultBackground(const DWORD color) noexcept = 0; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index e7719507099..b71471fc7ef 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -486,14 +486,14 @@ bool Terminal::SendKeyEvent(const WORD vkey, // - false if we did not translate the key, and it should be processed into a character. bool Terminal::SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) { - // viewportPos must be within the dimensions of the viewport - const auto viewportDimensions = _mutableViewport.Dimensions(); - if (viewportPos.X < 0 || viewportPos.X >= viewportDimensions.X || viewportPos.Y < 0 || viewportPos.Y >= viewportDimensions.Y) - { - return false; - } - - return _terminalInput->HandleMouse(viewportPos, uiButton, GET_KEYSTATE_WPARAM(states.Value()), wheelDelta); + // GH#6401: VT applications should be able to receive mouse events from outside the + // terminal buffer. This is likely to happen when the user drags the cursor offscreen. + // We shouldn't throw away perfectly good events when they're offscreen, so we just + // clamp them to be within the range [(0, 0), (W, H)]. +#pragma warning(suppress : 26496) // analysis can't tell we're assigning through a reference below + auto clampedPos{ viewportPos }; + _mutableViewport.ToOrigin().Clamp(clampedPos); + return _terminalInput->HandleMouse(clampedPos, uiButton, GET_KEYSTATE_WPARAM(states.Value()), wheelDelta); } // Method Description: diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 64232ef963d..ac8d59d3e02 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -93,6 +93,7 @@ class Microsoft::Terminal::Core::Terminal final : bool SetWindowTitle(std::wstring_view title) noexcept override; bool SetColorTableEntry(const size_t tableIndex, const COLORREF color) noexcept override; bool SetCursorStyle(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle cursorStyle) noexcept override; + bool SetCursorColor(const COLORREF color) noexcept override; bool SetDefaultForeground(const COLORREF color) noexcept override; bool SetDefaultBackground(const COLORREF color) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 4c7f27a88ea..1e27a59702c 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -64,6 +64,14 @@ COORD Terminal::GetCursorPosition() noexcept return newPos; } +bool Terminal::SetCursorColor(const COLORREF color) noexcept +try +{ + _buffer->GetCursor().SetColor(color); + return true; +} +CATCH_LOG_RETURN_FALSE() + // Method Description: // - Moves the cursor down one line, and possibly also to the leftmost column. // Arguments: diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp index cf79384f70c..f119cc266c7 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -146,6 +146,13 @@ try } CATCH_LOG_RETURN_FALSE() +bool TerminalDispatch::SetCursorColor(const DWORD color) noexcept +try +{ + return _terminalApi.SetCursorColor(color); +} +CATCH_LOG_RETURN_FALSE() + bool TerminalDispatch::SetClipboard(std::wstring_view content) noexcept try { diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index 640f8eb242f..4f3dc8e7371 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -35,6 +35,7 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool SetColorTableEntry(const size_t tableIndex, const DWORD color) noexcept override; bool SetCursorStyle(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle cursorStyle) noexcept override; + bool SetCursorColor(const DWORD color) noexcept override; bool SetClipboard(std::wstring_view content) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp index b03cae1c7b8..f8ff0d129da 100644 --- a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp @@ -126,10 +126,10 @@ bool TerminalDispatch::SetGraphicsRendition(const gsl::span true - - KeyChord.idl - - TerminalSettings.idl - @@ -37,13 +32,9 @@ KeyChord.idl - - TerminalSettings.idl - - @@ -53,6 +44,5 @@ - - + \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index b2c3c32a3b5..36ec74db1a7 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -286,9 +286,9 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter auto initialSize = _logic.GetLaunchDimensions(dpix); const short islandWidth = Utils::ClampToShortMax( - static_cast(ceil(initialSize.X)), 1); + static_cast(ceil(initialSize.Width)), 1); const short islandHeight = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Y)), 1); + static_cast(ceil(initialSize.Height)), 1); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index 98ea4790c29..0e40d646652 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -62,6 +62,16 @@ public enum WindowMessage : int /// WM_CHAR = 0x0102, + /// + /// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when a system key is pressed. A system key is F10 or Alt+Something. + /// + WM_SYSKEYDOWN = 0x0104, + + /// + /// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when a system key is released. A system key is F10 or Alt+Something. + /// + WM_SYSKEYUP = 0x0105, + /// /// The WM_MOUSEMOVE message is posted to a window when the cursor moves. If the mouse is not captured, the message is posted to the window that contains the cursor. Otherwise, the message is posted to the window that has captured the mouse. /// @@ -215,10 +225,10 @@ public enum SetWindowPosFlags : uint public static extern void DestroyTerminal(IntPtr terminal); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode, bool keyDown); + public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode, ushort flags, bool keyDown); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode); + public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode, ushort flags); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void TerminalSetTheme(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] TerminalTheme theme, string fontFamily, short fontSize, int newDpi); diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index e7938bb2611..2f5c6839455 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -20,6 +20,20 @@ namespace Microsoft.Terminal.Wpf /// public class TerminalContainer : HwndHost { + private static void UnpackKeyMessage(IntPtr wParam, IntPtr lParam, out ushort vkey, out ushort scanCode, out ushort flags) + { + ulong scanCodeAndFlags = (((ulong)lParam) & 0xFFFF0000) >> 16; + scanCode = (ushort)(scanCodeAndFlags & 0x00FFu); + flags = (ushort)(scanCodeAndFlags & 0xFF00u); + vkey = (ushort)wParam; + } + + private static void UnpackCharMessage(IntPtr wParam, IntPtr lParam, out char character, out ushort scanCode, out ushort flags) + { + UnpackKeyMessage(wParam, lParam, out ushort vKey, out scanCode, out flags); + character = (char)vKey; + } + private ITerminalConnection connection; private IntPtr hwnd; private IntPtr terminal; @@ -124,6 +138,20 @@ internal void SetTheme(TerminalTheme theme, string fontFamily, short fontSize) this.TriggerResize(this.RenderSize); } + /// + /// Gets the selected text from the terminal renderer and clears the selection. + /// + /// The selected text, empty if no text is selected. + internal string GetSelectedText() + { + if (NativeMethods.TerminalIsSelectionActive(this.terminal)) + { + return NativeMethods.TerminalGetSelection(this.terminal); + } + + return string.Empty; + } + /// /// Triggers a refresh of the terminal with the given size. /// @@ -235,29 +263,35 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam this.Focus(); NativeMethods.SetFocus(this.hwnd); break; + case NativeMethods.WindowMessage.WM_SYSKEYDOWN: // fallthrough case NativeMethods.WindowMessage.WM_KEYDOWN: { // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown NativeMethods.TerminalSetCursorVisible(this.terminal, true); - ulong scanCode = (((ulong)lParam) & 0x00FF0000) >> 16; - NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, (ushort)scanCode, true); + UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); + NativeMethods.TerminalSendKeyEvent(this.terminal, vkey, scanCode, flags, true); this.blinkTimer?.Start(); break; } + case NativeMethods.WindowMessage.WM_SYSKEYUP: // fallthrough case NativeMethods.WindowMessage.WM_KEYUP: { // WM_KEYUP lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup - ulong scanCode = (((ulong)lParam) & 0x00FF0000) >> 16; - NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, (ushort)scanCode, false); + UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); + NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, scanCode, flags, false); break; } case NativeMethods.WindowMessage.WM_CHAR: - // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char - NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam, (ushort)((uint)lParam >> 16)); - break; + { + // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char + UnpackCharMessage(wParam, lParam, out char character, out ushort scanCode, out ushort flags); + NativeMethods.TerminalSendCharEvent(this.terminal, character, scanCode, flags); + break; + } + case NativeMethods.WindowMessage.WM_WINDOWPOSCHANGED: var windowpos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS)); if (((NativeMethods.SetWindowPosFlags)windowpos.flags).HasFlag(NativeMethods.SetWindowPosFlags.SWP_NOSIZE)) diff --git a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs index f068d0edfd3..ef8f6b30ae0 100644 --- a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs +++ b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs @@ -70,6 +70,15 @@ public void SetTheme(TerminalTheme theme, string fontFamily, short fontSize) this.termContainer.SetTheme(theme, fontFamily, fontSize); } + /// + /// Gets the selected text in the terminal, clearing the selection. Otherwise returns an empty string. + /// + /// Selected text, empty string if no content is selected. + public string GetSelectedText() + { + return this.termContainer.GetSelectedText(); + } + /// /// Resizes the terminal to the specified rows and columns. /// diff --git a/src/cascadia/ut_app/JsonTests.cpp b/src/cascadia/ut_app/JsonTests.cpp index 9fd573134f7..1afbeaeddff 100644 --- a/src/cascadia/ut_app/JsonTests.cpp +++ b/src/cascadia/ut_app/JsonTests.cpp @@ -28,8 +28,6 @@ namespace TerminalAppUnitTests TEST_METHOD(ParseSimpleColorScheme); TEST_METHOD(ProfileGeneratesGuid); - TEST_METHOD(TestWrongValueType); - TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -169,58 +167,4 @@ namespace TerminalAppUnitTests VERIFY_ARE_EQUAL(profile3.GetGuid(), nullGuid); VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); } - - void JsonTests::TestWrongValueType() - { - // This json blob has a whole bunch of settings with the wrong value - // types - strings for int values, ints for strings, floats for ints, - // etc. When we encounter data that's the wrong data type, we should - // gracefully ignore it, as opposed to throwing an exception, causing us - // to fail to load the settings at all. - - const std::string settings0String{ R"( - { - "defaultProfile" : "{00000000-1111-0000-0000-000000000000}", - "profiles": [ - { - "guid" : "{00000000-1111-0000-0000-000000000000}", - "acrylicOpacity" : "0.5", - "closeOnExit" : "true", - "fontSize" : "10", - "historySize" : 1234.5678, - "padding" : 20, - "snapOnInput" : "false", - "icon" : 4, - "backgroundImageOpacity": false, - "useAcrylic" : 14 - } - ] - })" }; - - const auto settings0Json = VerifyParseSucceeded(settings0String); - - CascadiaSettings settings; - - settings._ParseJsonString(settings0String, false); - // We should not throw an exception trying to parse the settings here. - settings.LayerJson(settings._userSettings); - - VERIFY_ARE_EQUAL(1u, settings._profiles.size()); - auto& profile = settings._profiles.at(0); - Profile defaults{}; - - VERIFY_ARE_EQUAL(defaults._acrylicTransparency, profile._acrylicTransparency); - VERIFY_ARE_EQUAL(defaults._closeOnExitMode, profile._closeOnExitMode); - VERIFY_ARE_EQUAL(defaults._fontSize, profile._fontSize); - VERIFY_ARE_EQUAL(defaults._historySize, profile._historySize); - // A 20 as an int can still be treated as a json string - VERIFY_ARE_EQUAL(L"20", profile._padding); - VERIFY_ARE_EQUAL(defaults._snapOnInput, profile._snapOnInput); - // 4 is a valid string value - VERIFY_ARE_EQUAL(L"4", profile._icon); - // false is not a valid optional - VERIFY_IS_FALSE(profile._backgroundImageOpacity.has_value()); - VERIFY_ARE_EQUAL(defaults._useAcrylic, profile._useAcrylic); - } - } diff --git a/src/cascadia/ut_app/JsonUtilsTests.cpp b/src/cascadia/ut_app/JsonUtilsTests.cpp index 1016fb282a1..f3f3c7c5b2c 100644 --- a/src/cascadia/ut_app/JsonUtilsTests.cpp +++ b/src/cascadia/ut_app/JsonUtilsTests.cpp @@ -3,7 +3,7 @@ #include "precomp.h" -#include "../TerminalApp/JsonUtilsNew.h" +#include "../TerminalApp/JsonUtils.h" using namespace Microsoft::Console; using namespace WEX::Logging; diff --git a/src/common.build.tests.props b/src/common.build.tests.props index 79410878836..02dc8706ef7 100644 --- a/src/common.build.tests.props +++ b/src/common.build.tests.props @@ -5,11 +5,11 @@ INLINE_TEST_METHOD_MARKUP;UNIT_TESTING;%(PreprocessorDefinitions) - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + diff --git a/src/host/ft_uia/Host.Tests.UIA.csproj b/src/host/ft_uia/Host.Tests.UIA.csproj index c963f3d9624..d85b269e2e9 100644 --- a/src/host/ft_uia/Host.Tests.UIA.csproj +++ b/src/host/ft_uia/Host.Tests.UIA.csproj @@ -53,7 +53,10 @@ - ..\..\..\packages\Taef.Redist.Wlk.10.51.200127004\lib\net45\TE.Managed.dll + ..\..\..\packages\Taef.Redist.Wlk.10.57.200731005-develop\lib\net45\TE.Managed.dll + + + ..\..\..\packages\Taef.Redist.Wlk.10.57.200731005-develop\lib\net45\TE.Model.Managed.dll @@ -64,10 +67,10 @@ ..\..\..\packages\Selenium.Support.3.5.0\lib\net40\WebDriver.Support.dll - ..\..\..\packages\Taef.Redist.Wlk.10.51.200127004\lib\net45\Wex.Common.Managed.dll + ..\..\..\packages\Taef.Redist.Wlk.10.57.200731005-develop\lib\net45\Wex.Common.Managed.dll - ..\..\..\packages\Taef.Redist.Wlk.10.51.200127004\lib\net45\Wex.Logger.Interop.dll + ..\..\..\packages\Taef.Redist.Wlk.10.57.200731005-develop\lib\net45\Wex.Logger.Interop.dll @@ -142,11 +145,11 @@ copy $(SolutionDir)\dep\WinAppDriver\* $(OutDir)\ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + - + \ No newline at end of file diff --git a/src/host/ft_uia/VirtualTerminalTests.cs b/src/host/ft_uia/VirtualTerminalTests.cs index 65fdf71b401..6210e297363 100644 --- a/src/host/ft_uia/VirtualTerminalTests.cs +++ b/src/host/ft_uia/VirtualTerminalTests.cs @@ -381,23 +381,23 @@ private static void TestGraphicsCommands(CmdApp app, ViewportArea area, IntPtr h ciActual = area.GetCharInfoAt(hConsole, pt); Verify.AreEqual(ciExpected, ciActual, "Verify that background bright cyan got set."); - Log.Comment("Set underline (SGR.4)"); + Log.Comment("Set overline (SGR.53)"); app.FillCursorPosition(hConsole, ref pt); app.UIRoot.SendKeys("e`"); - ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_GRID_HORIZONTAL; ciActual = area.GetCharInfoAt(hConsole, pt); - Verify.AreEqual(ciExpected, ciActual, "Verify that underline got set."); + Verify.AreEqual(ciExpected, ciActual, "Verify that overline got set."); - Log.Comment("Clear underline (SGR.24)"); + Log.Comment("Clear overline (SGR.55)"); app.FillCursorPosition(hConsole, ref pt); app.UIRoot.SendKeys("d`"); - ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_GRID_HORIZONTAL; ciActual = area.GetCharInfoAt(hConsole, pt); - Verify.AreEqual(ciExpected, ciActual, "Verify that underline got cleared."); + Verify.AreEqual(ciExpected, ciActual, "Verify that overline got cleared."); Log.Comment("Set negative image video (SGR.7)"); app.FillCursorPosition(hConsole, ref pt); @@ -426,14 +426,14 @@ private static void TestGraphicsCommands(CmdApp app, ViewportArea area, IntPtr h ciActual = area.GetCharInfoAt(hConsole, pt); Verify.AreEqual(ciExpected, ciActual, "Verify that we got set back to the original state."); - Log.Comment("Set multiple properties in the same message (SGR.1,37,43,4)"); + Log.Comment("Set multiple properties in the same message (SGR.1,37,43,53)"); app.FillCursorPosition(hConsole, ref pt); app.UIRoot.SendKeys("9`"); ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_COLORS; ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_YELLOW; - ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_GRID_HORIZONTAL; ciActual = area.GetCharInfoAt(hConsole, pt); Verify.AreEqual(ciExpected, ciActual, "Verify that we set foreground bright white, background yellow, and underscore in the same SGR command."); diff --git a/src/host/ft_uia/app.config b/src/host/ft_uia/app.config index bf061caaf76..5d6775b6725 100644 --- a/src/host/ft_uia/app.config +++ b/src/host/ft_uia/app.config @@ -8,7 +8,7 @@ - + diff --git a/src/host/ft_uia/packages.config b/src/host/ft_uia/packages.config index 6629afad84b..101824b09d4 100644 --- a/src/host/ft_uia/packages.config +++ b/src/host/ft_uia/packages.config @@ -5,5 +5,5 @@ - - + + \ No newline at end of file diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 243a50b405e..ed5983c21ca 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -1403,7 +1403,7 @@ void ScreenBufferTests::VtNewlinePastViewport() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -1480,7 +1480,7 @@ void ScreenBufferTests::VtNewlinePastEndOfBuffer() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3382,7 +3382,7 @@ void ScreenBufferTests::ScrollOperations() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3503,7 +3503,7 @@ void ScreenBufferTests::InsertChars() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3663,7 +3663,7 @@ void ScreenBufferTests::DeleteChars() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -3895,7 +3895,7 @@ void ScreenBufferTests::EraseTests() auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; fillAttr.SetCrossedOut(true); fillAttr.SetReverseVideo(true); - fillAttr.SetUnderline(true); + fillAttr.SetUnderlined(true); si.SetAttributes(fillAttr); // But note that the meta attributes are expected to be cleared. auto expectedFillAttr = fillAttr; @@ -5206,7 +5206,7 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors() } if (italics) { - expectedAttr.SetItalics(true); + expectedAttr.SetItalic(true); vtSeq += L"\x1b[3m"; } if (blink) @@ -5315,7 +5315,7 @@ void ScreenBufferTests::TestExtendedTextAttributesWithColors() } if (italics) { - expectedAttr.SetItalics(false); + expectedAttr.SetItalic(false); vtSeq = L"\x1b[23m"; validate(expectedAttr, vtSeq); } @@ -5789,7 +5789,7 @@ void ScreenBufferTests::ScreenAlignmentPattern() // Set the initial attributes. auto initialAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; initialAttr.SetReverseVideo(true); - initialAttr.SetUnderline(true); + initialAttr.SetUnderlined(true); si.SetAttributes(initialAttr); // Set some margins. diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 6755cc7b76e..6f4351b6c26 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -757,8 +757,8 @@ void TextBufferTests::TestMixedRgbAndLegacyUnderline() VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(fgColor, bgColor)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(fgColor, bgColor)); - VERIFY_ARE_EQUAL(attrA.GetLegacyAttributes() & COMMON_LVB_UNDERSCORE, 0); - VERIFY_ARE_EQUAL(attrB.GetLegacyAttributes() & COMMON_LVB_UNDERSCORE, COMMON_LVB_UNDERSCORE); + VERIFY_ARE_EQUAL(attrA.IsUnderlined(), false); + VERIFY_ARE_EQUAL(attrB.IsUnderlined(), true); wchar_t* reset = L"\x1b[0m"; stateMachine.ProcessString(reset); diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 6ef1113b86b..599a29caec4 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -254,6 +254,145 @@ void VtIoTests::DtorTestStackAllocMany() } } +class MockRenderData : public IRenderData, IUiaData +{ +public: + Microsoft::Console::Types::Viewport GetViewport() noexcept override + { + return Microsoft::Console::Types::Viewport{}; + } + + COORD GetTextBufferEndPosition() const noexcept override + { + return COORD{}; + } + + const TextBuffer& GetTextBuffer() noexcept override + { + FAIL_FAST_HR(E_NOTIMPL); + } + + const FontInfo& GetFontInfo() noexcept override + { + FAIL_FAST_HR(E_NOTIMPL); + } + + std::vector GetSelectionRects() noexcept override + { + return std::vector{}; + } + + void LockConsole() noexcept override + { + } + + void UnlockConsole() noexcept override + { + } + + const TextAttribute GetDefaultBrushColors() noexcept override + { + return TextAttribute{}; + } + + std::pair GetAttributeColors(const TextAttribute& /*attr*/) const noexcept override + { + return std::make_pair(COLORREF{}, COLORREF{}); + } + + COORD GetCursorPosition() const noexcept override + { + return COORD{}; + } + + bool IsCursorVisible() const noexcept override + { + return false; + } + + bool IsCursorOn() const noexcept override + { + return false; + } + + ULONG GetCursorHeight() const noexcept override + { + return 42ul; + } + + CursorType GetCursorStyle() const noexcept override + { + return CursorType::FullBox; + } + + ULONG GetCursorPixelWidth() const noexcept override + { + return 12ul; + } + + COLORREF GetCursorColor() const noexcept override + { + return COLORREF{}; + } + + bool IsCursorDoubleWidth() const override + { + return false; + } + + bool IsScreenReversed() const noexcept override + { + return false; + } + + const std::vector GetOverlays() const noexcept override + { + return std::vector{}; + } + + const bool IsGridLineDrawingAllowed() noexcept override + { + return false; + } + + const std::wstring GetConsoleTitle() const noexcept override + { + return std::wstring{}; + } + + const bool IsSelectionActive() const override + { + return false; + } + + const bool IsBlockSelection() const noexcept override + { + return false; + } + + void ClearSelection() override + { + } + + void SelectNewRegion(const COORD /*coordStart*/, const COORD /*coordEnd*/) override + { + } + + const COORD GetSelectionAnchor() const noexcept + { + return COORD{}; + } + + const COORD GetSelectionEnd() const noexcept + { + return COORD{}; + } + + void ColorSelection(const COORD /*coordSelectionStart*/, const COORD /*coordSelectionEnd*/, const TextAttribute /*attr*/) + { + } +}; + void VtIoTests::RendererDtorAndThread() { Log::Comment(NoThrowString().Format( @@ -261,9 +400,10 @@ void VtIoTests::RendererDtorAndThread() for (int i = 0; i < 16; ++i) { + auto data = std::make_unique(); auto thread = std::make_unique(); auto* pThread = thread.get(); - auto pRenderer = std::make_unique(nullptr, nullptr, 0, std::move(thread)); + auto pRenderer = std::make_unique(data.get(), nullptr, 0, std::move(thread)); VERIFY_SUCCEEDED(pThread->Initialize(pRenderer.get())); // Sleep for a hot sec to make sure the thread starts before we enable painting // If you don't, the thread might wait on the paint enabled event AFTER @@ -286,9 +426,10 @@ void VtIoTests::RendererDtorAndThreadAndDx() for (int i = 0; i < 16; ++i) { + auto data = std::make_unique(); auto thread = std::make_unique(); auto* pThread = thread.get(); - auto pRenderer = std::make_unique(nullptr, nullptr, 0, std::move(thread)); + auto pRenderer = std::make_unique(data.get(), nullptr, 0, std::move(thread)); VERIFY_SUCCEEDED(pThread->Initialize(pRenderer.get())); auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index 87ea0738867..111a75ea9fd 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -686,7 +686,7 @@ void VtRendererTest::Xterm256TestExtendedAttributes() } if (italics) { - desiredAttrs.SetItalics(true); + desiredAttrs.SetItalic(true); onSequences.push_back("\x1b[3m"); offSequences.push_back("\x1b[23m"); } @@ -788,15 +788,15 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() break; case GraphicsOptions::Italics: Log::Comment(L"----Set Italics Attribute----"); - textAttributes.SetItalics(true); + textAttributes.SetItalic(true); break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); - textAttributes.SetUnderline(true); + textAttributes.SetUnderlined(true); break; case GraphicsOptions::Overline: Log::Comment(L"----Set Overline Attribute----"); - textAttributes.SetOverline(true); + textAttributes.SetOverlined(true); break; case GraphicsOptions::BlinkOrXterm256Index: Log::Comment(L"----Set Blink Attribute----"); @@ -1289,7 +1289,7 @@ void VtRendererTest::XtermTestAttributesAcrossReset() break; case GraphicsOptions::Underline: Log::Comment(L"----Set Underline Attribute----"); - textAttributes.SetUnderline(true); + textAttributes.SetUnderlined(true); break; case GraphicsOptions::Negative: Log::Comment(L"----Set Negative Attribute----"); diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp index 1ad1fa90302..06d2d9d3ebe 100644 --- a/src/inc/conattrs.hpp +++ b/src/inc/conattrs.hpp @@ -17,7 +17,7 @@ enum class ExtendedAttributes : BYTE Invisible = 0x08, CrossedOut = 0x10, // TODO:GH#2916 add support for these to the parser as well. - Underlined = 0x20, // _technically_ different from LVB_UNDERSCORE, see TODO:GH#2915 + Underlined = 0x20, DoublyUnderlined = 0x40, // Included for completeness, but not currently supported. Faint = 0x80, }; diff --git a/src/inc/til.h b/src/inc/til.h index b1773062cbc..e9504535708 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -15,6 +15,7 @@ #include "til/rectangle.h" #include "til/bitmap.h" #include "til/u8u16convert.h" +#include "til/spsc.h" namespace til // Terminal Implementation Library. Also: "Today I Learned" { diff --git a/src/inc/til/spsc.h b/src/inc/til/spsc.h new file mode 100644 index 00000000000..374306637fb --- /dev/null +++ b/src/inc/til/spsc.h @@ -0,0 +1,643 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// til::spsc::details::arc requires std::atomic::wait() and ::notify_one() and at the time of writing no +// STL supports these. Since both Windows and Linux offer a Futex implementation we can easily implement this though. +// On other platforms we fall back to using a std::condition_variable. +#if __cpp_lib_atomic_wait >= 201907 +#define _TIL_SPSC_DETAIL_POSITION_IMPL_NATIVE 1 +#elif defined(_WIN32_WINNT) && _WIN32_WINNT >= _WIN32_WINNT_WIN8 +#define _TIL_SPSC_DETAIL_POSITION_IMPL_WIN 1 +#elif __linux__ +#define _TIL_SPSC_DETAIL_POSITION_IMPL_LINUX 1 +#else +#define _TIL_SPSC_DETAIL_POSITION_IMPL_FALLBACK 1 +#endif + +// til: Terminal Implementation Library. Also: "Today I Learned". +// spsc: Single Producer Single Consumer. A SPSC queue/channel sends data from exactly one sender to one receiver. +namespace til::spsc +{ + using size_type = uint32_t; + + namespace details + { + static constexpr size_type position_mask = std::numeric_limits::max() >> 2u; // 0b00111.... + static constexpr size_type revolution_flag = 1u << (std::numeric_limits::digits - 2u); // 0b01000.... + static constexpr size_type drop_flag = 1u << (std::numeric_limits::digits - 1u); // 0b10000.... + + struct block_initially_policy + { + using _spsc_policy = int; + static constexpr bool _block_forever = false; + }; + + struct block_forever_policy + { + using _spsc_policy = int; + static constexpr bool _block_forever = true; + }; + + template + using enable_if_wait_policy_t = typename std::remove_reference_t::_spsc_policy; + +#if _TIL_SPSC_DETAIL_POSITION_IMPL_NATIVE + using atomic_size_type = std::atomic; +#else + // atomic_size_type is a fallback if native std::atomic::wait() + // and ::notify_one() methods are unavailable in the STL. + struct atomic_size_type + { + size_type load(std::memory_order order) const noexcept + { + return _value.load(order); + } + + void store(size_type desired, std::memory_order order) noexcept + { +#if _TIL_SPSC_DETAIL_POSITION_IMPL_FALLBACK + // We must use a lock here to prevent us from modifying the value + // in between wait() reading the value and the thread being suspended. + std::lock_guard lock{ _m }; +#endif + _value.store(desired, order); + } + + void wait(size_type old, [[maybe_unused]] std::memory_order order) const noexcept + { +#if _TIL_SPSC_DETAIL_POSITION_IMPL_WIN +#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile + WaitOnAddress(const_cast*>(&_value), &old, sizeof(_value), INFINITE); +#elif _TIL_SPSC_DETAIL_POSITION_IMPL_LINUX + futex(FUTEX_WAIT_PRIVATE, old); +#elif _TIL_SPSC_DETAIL_POSITION_IMPL_FALLBACK + std::unique_lock lock{ _m }; + _cv.wait(lock, [&]() { return _value.load(order) != old; }); +#endif + } + + void notify_one() noexcept + { +#if _TIL_SPSC_DETAIL_POSITION_IMPL_WIN + WakeByAddressSingle(&_value); +#elif _TIL_SPSC_DETAIL_POSITION_IMPL_LINUX + futex(FUTEX_WAKE_PRIVATE, 1); +#elif _TIL_SPSC_DETAIL_POSITION_IMPL_FALLBACK + _cv.notify_one(); +#endif + } + + private: +#if _TIL_SPSC_DETAIL_POSITION_IMPL_LINUX + inline void futex(int futex_op, size_type val) const noexcept + { + // See: https://man7.org/linux/man-pages/man2/futex.2.html + static_assert(sizeof(std::atomic) == 4); + syscall(SYS_futex, &_value, futex_op, val, nullptr, nullptr, 0); + } +#endif + + std::atomic _value{ 0 }; + +#if _TIL_SPSC_DETAIL_POSITION_IMPL_FALLBACK + private: + std::mutex _m; + std::condition_variable _cv; +#endif + }; +#endif + + template + inline T* alloc_raw_memory(size_t size) + { + constexpr auto alignment = alignof(T); + if constexpr (alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) + { + return static_cast(::operator new(size)); + } + else + { + return static_cast(::operator new(size, std::align_val_t(alignment))); + } + } + + template + inline void free_raw_memory(T* ptr) noexcept + { + constexpr auto alignment = alignof(T); + if constexpr (alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) + { + ::operator delete(ptr); + } + else + { + ::operator delete(ptr, std::align_val_t(alignment)); + } + } + + struct acquisition + { + // The index range [begin, end) is the range of slots in the array returned by + // arc::data() that may be written to / read from respectively. + // If a range has been successfully acquired "end > begin" is true. end thus can't be 0. + size_type begin; + size_type end; + + // Upon release() of an acquisition, next is the value that's written to the consumer/producer position. + // It's basically the same as end, but with the revolution flag mixed in. + // If end is equal to capacity, next will be 0 (mixed with the next revolution flag). + size_type next; + + // If the other side of the queue hasn't been destroyed yet, alive will be true. + bool alive; + + constexpr acquisition(size_type begin, size_type end, size_type next, bool alive) : + begin(begin), + end(end), + next(next), + alive(alive) + { + } + }; + + // The following assumes you know what ring/circular buffers are. You can read about them here: + // https://en.wikipedia.org/wiki/Circular_buffer + // + // Furthermore the implementation solves a problem known as the producer-consumer problem: + // https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem + // + // arc follows the classic spsc design and manages a ring buffer with two positions: _producer and _consumer. + // They contain the position the producer / consumer will next write to / read from respectively. + // As usual with ring buffers, these positions are modulo to the _capacity of the underlying buffer. + // The producer's writable range is [_producer, _consumer) and the consumer's readable is [_consumer, _producer). + // + // After you wrote the numbers 0 to 6 into a queue of size 10, a typical state of the ring buffer might be: + // [ 0 | 1 | 2 | 3 | 4 | 5 | 6 | _ | _ | _ | _ ] + // ^ ^ ^ + // _consumer = 0 _producer = 7 _capacity = 10 + // + // As you can see the readable range currently is [_consumer, _producer) = [0, 7). + // The remaining writable range on the other hand is [_producer, _consumer) = [7, 0). + // Wait, what? [7, 0)? How does that work? As all positions are modulo _capacity, 0 mod 10 is the same as 10 mod 10. + // If we only want to read forward in the buffer [7, 0) is thus the same as [7, 10). + // + // If we read 3 items from the queue the contents will be: + // [ _ | _ | _ | 3 | 4 | 5 | 6 | _ | _ | _ | _ ] + // ^ ^ + // _consumer = 3 _producer = 7 + // + // Now the writable range is still [_producer, _consumer), but it wraps around the end of the ring buffer. + // In this case arc will split the range in two and return each separately in acquire(). + // The first returned range will be [_producer, _capacity) and the second [0, _consumer). + // The same logic applies if the readable range wraps around the end of the ring buffer. + // + // As these are symmetric, the logic for acquiring and releasing ranges is the same for both sides. + // The producer will acquire() and release() ranges with its own position as "mine" and the consumer's + // position as "theirs". These arguments are correspondingly flipped for the consumer. + // + // As part of the producer-consumer problem, the producer cannot write more values ahead of the + // consumer than the buffer's capacity. Since both positions are modulo to the capacity we can only + // determine positional differences smaller than the capacity. Due to that both producer and + // consumer store a "revolution_flag" as the second highest bit within their positions. + // This bit is flipped each time the producer/consumer wrap around the end of the ring buffer. + // If the positions are identical, except for their "revolution_flag" value, the producer thus must + // be capacity-many positions ahead of the consumer and must wait until items have been consumed. + // + // Inversely the consumer must wait until the producer has written at least one value ahead. + // This can be detected by checking whether the positions are identical including the revolution_flag. + template + struct arc + { + explicit arc(size_type capacity) noexcept : + _data(alloc_raw_memory(size_t(capacity) * sizeof(T))), + _capacity(capacity) + { + } + + ~arc() + { + auto beg = _consumer.load(std::memory_order_acquire); + auto end = _producer.load(std::memory_order_acquire); + auto differentRevolution = ((beg ^ end) & revolution_flag) != 0; + + beg &= position_mask; + end &= position_mask; + + // The producer position will always be ahead of the consumer, but since we're dealing + // with a ring buffer the producer may be wrapped around the end of the buffer. + // We thus need to deal with 3 potential cases: + // * No valid data. + // If both positions including their revolution bits are identical. + // * Valid data in the middle of the ring buffer. + // If _producer > _consumer. + // * Valid data at both ends of the ring buffer. + // If the revolution bits differ, even if the positions are otherwise identical, + // which they might be if the channel contains exactly as many values as its capacity. + if (end > beg) + { + std::destroy(_data + beg, _data + end); + } + else if (differentRevolution) + { + std::destroy(_data, _data + end); + std::destroy(_data + beg, _data + _capacity); + } + + free_raw_memory(_data); + } + + void drop_producer() + { + drop(_producer); + } + + void drop_consumer() + { + drop(_consumer); + } + + acquisition producer_acquire(size_type slots, bool blocking) noexcept + { + return acquire(_producer, _consumer, revolution_flag, slots, blocking); + } + + void producer_release(acquisition acquisition) noexcept + { + release(_producer, acquisition); + } + + acquisition consumer_acquire(size_type slots, bool blocking) noexcept + { + return acquire(_consumer, _producer, 0, slots, blocking); + } + + void consumer_release(acquisition acquisition) noexcept + { + release(_consumer, acquisition); + } + + T* data() const noexcept + { + return _data; + } + + private: + void drop(atomic_size_type& mine) + { + // Signal the other side we're dropped. See acquire() for the handling of the drop_flag. + // We don't need to use release ordering like release() does as each call to + // any of the producer/consumer methods already results in a call to release(). + // Another release ordered write can't possibly synchronize any more data anyways at this point. + const auto myPos = mine.load(std::memory_order_relaxed); + mine.store(myPos | drop_flag, std::memory_order_relaxed); + mine.notify_one(); + + // The first time SPSCBase is dropped (destroyed) we'll set + // the flag to true and get false, causing us to return early. + // Only the second time we'll get true. + // --> The contents are only deleted when both sides have been dropped. + if (_eitherSideDropped.exchange(true, std::memory_order_relaxed)) + { + delete this; + } + } + + // NOTE: waitMask MUST be either 0 (consumer) or revolution_flag (producer). + acquisition acquire(atomic_size_type& mine, atomic_size_type& theirs, size_type waitMask, size_type slots, bool blocking) noexcept + { + size_type myPos = mine.load(std::memory_order_relaxed); + size_type theirPos; + + while (true) + { + // This acquire read synchronizes with the release write in release(). + theirPos = theirs.load(std::memory_order_acquire); + if ((myPos ^ theirPos) != waitMask) + { + break; + } + if (!blocking) + { + return { + 0, + 0, + 0, + true, + }; + } + + theirs.wait(theirPos, std::memory_order_relaxed); + } + + // If the other side's position contains a drop flag, as a X -> we need to... + // * producer -> stop immediately + // FYI: isProducer == (waitMask != 0). + // * consumer -> finish consuming all values and then stop + // We're finished if the only difference between our + // and the other side's position is the drop flag. + if ((theirPos & drop_flag) != 0 && (waitMask != 0 || (myPos ^ theirPos) == drop_flag)) + { + return { + 0, + 0, + 0, + false, + }; + } + + auto begin = myPos & position_mask; + auto end = theirPos & position_mask; + + // [begin, end) is the writable/readable range for the producer/consumer. + // The following detects whether we'd be wrapping around the end of the ring buffer + // and splits the range into the first half [mine, _capacity). + // If acquire() is called again it'll return [0, theirs). + end = end > begin ? end : _capacity; + + // Of course we also need to ensure to not return more than we've been asked for. + end = std::min(end, begin + slots); + + // "next" will contain the value that's stored into "mine" when release() is called. + // It's basically the same as "end", but with the revolution flag spliced in. + // If we acquired the range [mine, _capacity) "end" will equal _capacity + // and thus wrap around the ring buffer. The next value for "mine" is thus the + // position zero | the flipped "revolution" (and 0 | x == x). + auto revolution = myPos & revolution_flag; + auto next = end != _capacity ? end | revolution : revolution ^ revolution_flag; + + return { + begin, + end, + next, + true, + }; + } + + void release(atomic_size_type& mine, acquisition acquisition) noexcept + { + // This release write synchronizes with the acquire read in acquire(). + mine.store(acquisition.next, std::memory_order_release); + mine.notify_one(); + } + + T* const _data; + const size_type _capacity; + + std::atomic _eitherSideDropped{ false }; + + atomic_size_type _producer; + atomic_size_type _consumer; + }; + + inline void validate_size(size_t v) + { + if (v > static_cast(position_mask)) + { + throw std::overflow_error{ "size too large for spsc" }; + } + } + } + + // Block until at least one item has been written into the sender / read from the receiver. + inline constexpr details::block_initially_policy block_initially{}; + + // Block until all items have been written into the sender / read from the receiver. + inline constexpr details::block_forever_policy block_forever{}; + + template + struct producer + { + explicit producer(details::arc* arc) noexcept : + _arc(arc) {} + + producer(const producer&) = delete; + producer& operator=(const producer&) = delete; + + producer(producer&& other) noexcept + { + drop(); + _arc = std::exchange(other._arc, nullptr); + } + + producer& operator=(producer&& other) noexcept + { + drop(); + _arc = std::exchange(other._arc, nullptr); + } + + ~producer() + { + drop(); + } + + // emplace constructs an item in-place at the end of the queue. + // It returns true, if the item was successfully placed within the queue. + // The return value will be false, if the consumer is gone. + template + bool emplace(Args&&... args) const + { + auto acquisition = _arc->producer_acquire(1, true); + if (!acquisition.end) + { + return false; + } + + auto data = _arc->data(); + auto begin = data + acquisition.begin; + new (begin) T(std::forward(args)...); + + _arc->producer_release(acquisition); + return true; + } + + template + std::pair push(InputIt first, InputIt last) const + { + return push_n(block_forever, first, std::distance(first, last)); + } + + // push writes the items between first and last into the queue. + // The amount of successfully written items is returned as the first pair field. + // The second pair field will be false if the consumer is gone. + template = 0> + std::pair push(WaitPolicy&& policy, InputIt first, InputIt last) const + { + return push_n(std::forward(policy), first, std::distance(first, last)); + } + + template + std::pair push_n(InputIt first, size_t count) const + { + return push_n(block_forever, first, count); + } + + // push_n writes count items from first into the queue. + // The amount of successfully written items is returned as the first pair field. + // The second pair field will be false if the consumer is gone. + template = 0> + std::pair push_n(WaitPolicy&&, InputIt first, size_t count) const + { + details::validate_size(count); + + const auto data = _arc->data(); + auto remaining = static_cast(count); + auto blocking = true; + auto ok = true; + + while (remaining != 0) + { + auto acquisition = _arc->producer_acquire(remaining, blocking); + if (!acquisition.end) + { + ok = acquisition.alive; + break; + } + + const auto begin = data + acquisition.begin; + const auto got = acquisition.end - acquisition.begin; + std::uninitialized_copy_n(first, got, begin); + first += got; + remaining -= got; + + _arc->producer_release(acquisition); + + if constexpr (!std::remove_reference_t::_block_forever) + { + blocking = false; + } + } + + return { count - remaining, ok }; + } + + private: + void drop() + { + if (_arc) + { + _arc->drop_producer(); + } + } + + details::arc* _arc = nullptr; + }; + + template + struct consumer + { + explicit consumer(details::arc* arc) noexcept : + _arc(arc) {} + + consumer(const consumer&) = delete; + consumer& operator=(const consumer&) = delete; + + consumer(consumer&& other) noexcept + { + drop(); + _arc = std::exchange(other._arc, nullptr); + } + + consumer& operator=(consumer&& other) noexcept + { + drop(); + _arc = std::exchange(other._arc, nullptr); + } + + ~consumer() + { + drop(); + } + + // pop returns the next item in the queue, or std::nullopt if the producer is gone. + std::optional pop() const + { + auto acquisition = _arc->consumer_acquire(1, true); + if (!acquisition.end) + { + return std::nullopt; + } + + auto data = _arc->data(); + auto begin = data + acquisition.begin; + + auto item = std::move(*begin); + std::destroy_at(begin); + + _arc->consumer_release(acquisition); + return item; + } + + template + std::pair pop_n(OutputIt first, size_t count) const + { + return pop_n(block_forever, first, count); + } + + // pop_n reads up to count items into first. + // The amount of successfully read items is returned as the first pair field. + // The second pair field will be false if the consumer is gone. + template = 0> + std::pair pop_n(WaitPolicy&&, OutputIt first, size_t count) const + { + details::validate_size(count); + + const auto data = _arc->data(); + auto remaining = static_cast(count); + auto blocking = true; + auto ok = true; + + while (remaining != 0) + { + auto acquisition = _arc->consumer_acquire(remaining, blocking); + if (!acquisition.end) + { + ok = acquisition.alive; + break; + } + + auto beg = data + acquisition.begin; + auto end = data + acquisition.end; + auto got = acquisition.end - acquisition.begin; + first = std::move(beg, end, first); + std::destroy(beg, end); + remaining -= got; + + _arc->consumer_release(acquisition); + + if constexpr (!std::remove_reference_t::_block_forever) + { + blocking = false; + } + } + + return { count - remaining, ok }; + } + + private: + void drop() + { + if (_arc) + { + _arc->drop_consumer(); + } + } + + details::arc* _arc = nullptr; + }; + + // channel returns a bounded, lock-free, single-producer, single-consumer + // FIFO queue ("channel") with the given maximum capacity. + template + std::pair, consumer> channel(uint32_t capacity) + { + if (capacity == 0) + { + throw std::invalid_argument{ "invalid capacity" }; + } + + const auto arc = new details::arc(capacity); + return { std::piecewise_construct, std::forward_as_tuple(arc), std::forward_as_tuple(arc) }; + } +} diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 023b14a8598..2b65f515047 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -123,7 +123,14 @@ bool HandleTerminalMouseEvent(const COORD cMousePosition, // Virtual terminal input mode if (IsInVirtualTerminalInputMode()) { - fWasHandled = gci.GetActiveInputBuffer()->GetTerminalInput().HandleMouse(cMousePosition, uiButton, sModifierKeystate, sWheelDelta); + // GH#6401: VT applications should be able to receive mouse events from outside the + // terminal buffer. This is likely to happen when the user drags the cursor offscreen. + // We shouldn't throw away perfectly good events when they're offscreen, so we just + // clamp them to be within the range [(0, 0), (W, H)]. + auto clampedPosition{ cMousePosition }; + const auto clampViewport{ gci.GetActiveOutputBuffer().GetViewport().ToOrigin() }; + clampViewport.Clamp(clampedPosition); + fWasHandled = gci.GetActiveInputBuffer()->GetTerminalInput().HandleMouse(clampedPosition, uiButton, sModifierKeystate, sWheelDelta); } return fWasHandled; @@ -635,6 +642,25 @@ BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, if (HandleTerminalMouseEvent(MousePosition, Message, GET_KEYSTATE_WPARAM(wParam), sDelta)) { + // GH#6401: Capturing the mouse ensures that we get drag/release events + // even if the user moves outside the window. + // HandleTerminalMouseEvent returns false if the terminal's not in VT mode, + // so capturing/releasing here should not impact other console mouse event + // consumers. + switch (Message) + { + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle()); + break; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + ReleaseCapture(); + break; + } + return FALSE; } } diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index f382dbd7c07..2bbbf220b47 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -26,13 +26,12 @@ Renderer::Renderer(IRenderData* pData, _In_reads_(cEngines) IRenderEngine** const rgpEngines, const size_t cEngines, std::unique_ptr thread) : - _pData(pData), + _pData(THROW_HR_IF_NULL(E_INVALIDARG, pData)), _pThread{ std::move(thread) }, _destructing{ false }, - _clusterBuffer{} + _clusterBuffer{}, + _viewport{ pData->GetViewport() } { - _srViewportPrevious = { 0 }; - for (size_t i = 0; i < cEngines; i++) { IRenderEngine* engine = rgpEngines[i]; @@ -208,7 +207,7 @@ void Renderer::TriggerSystemRedraw(const RECT* const prcDirtyClient) // - void Renderer::TriggerRedraw(const Viewport& region) { - Viewport view = _pData->GetViewport(); + Viewport view = _viewport; SMALL_RECT srUpdateRegion = region.ToExclusive(); if (view.TrimToViewport(&srUpdateRegion)) @@ -357,7 +356,7 @@ void Renderer::TriggerSelection() // - True if something changed and we scrolled. False otherwise. bool Renderer::_CheckViewportAndScroll() { - SMALL_RECT const srOldViewport = _srViewportPrevious; + SMALL_RECT const srOldViewport = _viewport.ToInclusive(); SMALL_RECT const srNewViewport = _pData->GetViewport().ToInclusive(); COORD coordDelta; @@ -369,7 +368,7 @@ bool Renderer::_CheckViewportAndScroll() LOG_IF_FAILED(engine->UpdateViewport(srNewViewport)); } - _srViewportPrevious = srNewViewport; + _viewport = Viewport::FromInclusive(srNewViewport); // If we're keeping some buffers between calls, let them know about the viewport size // so they can prepare the buffers for changes to either preallocate memory at once @@ -861,6 +860,16 @@ IRenderEngine::GridLines Renderer::s_GetGridlines(const TextAttribute& textAttri { lines |= IRenderEngine::GridLines::Right; } + + if (textAttribute.IsCrossedOut()) + { + lines |= IRenderEngine::GridLines::Strikethrough; + } + + if (textAttribute.IsUnderlined()) + { + lines |= IRenderEngine::GridLines::Underline; + } return lines; } diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 6c30e0f4ee6..db6dc6d3875 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -120,7 +120,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); - SMALL_RECT _srViewportPrevious; + Microsoft::Console::Types::Viewport _viewport; static constexpr float _shrinkThreshold = 0.8f; std::vector _clusterBuffer; diff --git a/src/renderer/base/thread.cpp b/src/renderer/base/thread.cpp index c4d4bc36c7d..65fedba6cfb 100644 --- a/src/renderer/base/thread.cpp +++ b/src/renderer/base/thread.cpp @@ -162,17 +162,17 @@ DWORD WINAPI RenderThread::_ThreadProc() { WaitForSingleObject(_hPaintEnabledEvent, INFINITE); - if (!_fNextFrameRequested.exchange(false)) + if (!_fNextFrameRequested.exchange(false, std::memory_order_acq_rel)) { // <-- // If `NotifyPaint` is called at this point, then it will not // set the event because `_fWaiting` is not `true` yet so we have // to check again below. - _fWaiting.store(true); + _fWaiting.store(true, std::memory_order_release); // check again now (see comment above) - if (!_fNextFrameRequested.exchange(false)) + if (!_fNextFrameRequested.exchange(false, std::memory_order_acq_rel)) { // Wait until a next frame is requested. WaitForSingleObject(_hEvent, INFINITE); @@ -193,7 +193,7 @@ DWORD WINAPI RenderThread::_ThreadProc() // expensive operation, we should reset the event to not render // again if nothing changed. - _fWaiting.store(false); + _fWaiting.store(false, std::memory_order_release); // see comment above ResetEvent(_hEvent); @@ -218,13 +218,13 @@ DWORD WINAPI RenderThread::_ThreadProc() void RenderThread::NotifyPaint() { - if (_fWaiting.load()) + if (_fWaiting.load(std::memory_order_acquire)) { SetEvent(_hEvent); } else { - _fNextFrameRequested.store(true); + _fNextFrameRequested.store(true, std::memory_order_release); } } diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 1714b6e10bd..cdd1bc72e58 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -68,6 +68,7 @@ DxEngine::DxEngine() : _invalidateFullRows{ true }, _invalidMap{}, _invalidScroll{}, + _allInvalid{ false }, _firstFrame{ true }, _presentParams{ 0 }, _presentReady{ false }, @@ -847,6 +848,11 @@ void DxEngine::_InvalidateRectangle(const til::rectangle& rc) _invalidMap.set(invalidate); } +bool DxEngine::_IsAllInvalid() const noexcept +{ + return std::llabs(_invalidScroll.y()) >= _invalidMap.size().height(); +} + // Routine Description: // - Invalidates a rectangle described in characters // Arguments: @@ -858,7 +864,10 @@ try { RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion); - _InvalidateRectangle(Viewport::FromExclusive(*psrRegion).ToInclusive()); + if (!_allInvalid) + { + _InvalidateRectangle(Viewport::FromExclusive(*psrRegion).ToInclusive()); + } return S_OK; } @@ -875,7 +884,10 @@ try { RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor); - _InvalidateRectangle(til::rectangle{ *pcoordCursor, til::size{ 1, 1 } }); + if (!_allInvalid) + { + _InvalidateRectangle(til::rectangle{ *pcoordCursor, til::size{ 1, 1 } }); + } return S_OK; } @@ -892,9 +904,12 @@ try { RETURN_HR_IF_NULL(E_INVALIDARG, prcDirtyClient); - // Dirty client is in pixels. Use divide specialization against glyph factor to make conversion - // to cells. - _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell)); + if (!_allInvalid) + { + // Dirty client is in pixels. Use divide specialization against glyph factor to make conversion + // to cells. + _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell)); + } return S_OK; } @@ -908,9 +923,12 @@ CATCH_RETURN(); // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateSelection(const std::vector& rectangles) noexcept { - for (const auto& rect : rectangles) + if (!_allInvalid) { - RETURN_IF_FAILED(Invalidate(&rect)); + for (const auto& rect : rectangles) + { + RETURN_IF_FAILED(Invalidate(&rect)); + } } return S_OK; } @@ -930,11 +948,15 @@ try const til::point deltaCells{ *pcoordDelta }; - if (deltaCells != til::point{ 0, 0 }) + if (!_allInvalid) { - // Shift the contents of the map and fill in revealed area. - _invalidMap.translate(deltaCells, true); - _invalidScroll += deltaCells; + if (deltaCells != til::point{ 0, 0 }) + { + // Shift the contents of the map and fill in revealed area. + _invalidMap.translate(deltaCells, true); + _invalidScroll += deltaCells; + _allInvalid = _IsAllInvalid(); + } } return S_OK; @@ -951,6 +973,7 @@ CATCH_RETURN(); try { _invalidMap.set_all(); + _allInvalid = true; // Since everything is invalidated here, mark this as a "first frame", so // that we won't use incremental drawing on it. The caller of this intended @@ -1209,6 +1232,7 @@ try } _invalidMap.reset_all(); + _allInvalid = false; _invalidScroll = {}; @@ -1460,63 +1484,84 @@ try _d2dBrushForeground->SetColor(_ColorFFromColorRef(color)); - const auto font = _glyphCell; - D2D_POINT_2F target = til::point{ coordTarget } * font; + const D2D1_SIZE_F font = _glyphCell; + const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height }; + const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine); - D2D_POINT_2F start = { 0 }; - D2D_POINT_2F end = { 0 }; - - for (size_t i = 0; i < cchLine; i++) + const auto DrawLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { - // 0.5 pixel offset for crisp lines - start = { target.x + 0.5f, target.y + 0.5f }; + _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _strokeStyle.Get()); + }; - if (lines & GridLines::Top) - { - end = start; - end.x += font.width(); + // NOTE: Line coordinates are centered within the line, so they need to be + // offset by half the stroke width. For the start coordinate we add half + // the stroke width, and for the end coordinate we subtract half the width. - _d2dDeviceContext->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); - } + if (lines & (GridLines::Left | GridLines::Right)) + { + const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f; + const auto startY = target.y + halfGridlineWidth; + const auto endY = target.y + font.height - halfGridlineWidth; if (lines & GridLines::Left) { - end = start; - end.y += font.height(); + auto x = target.x + halfGridlineWidth; + for (size_t i = 0; i < cchLine; i++, x += font.width) + { + DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth); + } + } - _d2dDeviceContext->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); + if (lines & GridLines::Right) + { + auto x = target.x + font.width - halfGridlineWidth; + for (size_t i = 0; i < cchLine; i++, x += font.width) + { + DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth); + } } + } - // NOTE: Watch out for inclusive/exclusive rectangles here. - // We have to remove 1 from the font size for the bottom and right lines to ensure that the - // starting point remains within the clipping rectangle. - // For example, if we're drawing a letter at 0,0 and the font size is 8x16.... - // The bottom left corner inclusive is at 0,15 which is Y (0) + Font Height (16) - 1 = 15. - // The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7. + if (lines & (GridLines::Top | GridLines::Bottom)) + { + const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f; + const auto startX = target.x + halfGridlineWidth; + const auto endX = target.x + fullRunWidth - halfGridlineWidth; - // 0.5 pixel offset for crisp lines; -0.5 on the Y to fit _in_ the cell, not outside it. - start = { target.x + 0.5f, target.y + font.height() - 0.5f }; + if (lines & GridLines::Top) + { + const auto y = target.y + halfGridlineWidth; + DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth); + } if (lines & GridLines::Bottom) { - end = start; - end.x += font.width() - 1.f; - - _d2dDeviceContext->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); + const auto y = target.y + font.height - halfGridlineWidth; + DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth); } + } - start = { target.x + font.width() - 0.5f, target.y + 0.5f }; + // In the case of the underline and strikethrough offsets, the stroke width + // is already accounted for, so they don't require further adjustments. - if (lines & GridLines::Right) - { - end = start; - end.y += font.height() - 1.f; + if (lines & GridLines::Underline) + { + const auto halfUnderlineWidth = _lineMetrics.underlineWidth / 2.0f; + const auto startX = target.x + halfUnderlineWidth; + const auto endX = target.x + fullRunWidth - halfUnderlineWidth; + const auto y = target.y + _lineMetrics.underlineOffset; - _d2dDeviceContext->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); - } + DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth); + } - // Move to the next character in this run. - target.x += font.width(); + if (lines & GridLines::Strikethrough) + { + const auto halfStrikethroughWidth = _lineMetrics.strikethroughWidth / 2.0f; + const auto startX = target.x + halfStrikethroughWidth; + const auto endX = target.x + fullRunWidth - halfStrikethroughWidth; + const auto y = target.y + _lineMetrics.strikethroughOffset; + + DrawLine(startX, y, endX, y, _lineMetrics.strikethroughWidth); } return S_OK; @@ -1683,7 +1728,8 @@ try _dpi, _dwriteTextFormat, _dwriteTextAnalyzer, - _dwriteFontFace)); + _dwriteFontFace, + _lineMetrics)); _glyphCell = fiFontInfo.GetSize(); @@ -1772,13 +1818,15 @@ float DxEngine::GetScaling() const noexcept Microsoft::WRL::ComPtr format; Microsoft::WRL::ComPtr analyzer; Microsoft::WRL::ComPtr face; + LineMetrics lineMetrics; return _GetProposedFont(pfiFontInfoDesired, pfiFontInfo, iDpi, format, analyzer, - face); + face, + lineMetrics); } // Routine Description: @@ -2047,7 +2095,8 @@ CATCH_RETURN(); const int dpi, Microsoft::WRL::ComPtr& textFormat, Microsoft::WRL::ComPtr& textAnalyzer, - Microsoft::WRL::ComPtr& fontFace) const noexcept + Microsoft::WRL::ComPtr& fontFace, + LineMetrics& lineMetrics) const noexcept { try { @@ -2210,6 +2259,34 @@ CATCH_RETURN(); false, scaled, unscaled); + + // There is no font metric for the grid line width, so we use a small + // multiple of the font size, which typically rounds to a pixel. + lineMetrics.gridlineWidth = std::round(fontSize * 0.025f); + + // All other line metrics are in design units, so to get a pixel value, + // we scale by the font size divided by the design-units-per-em. + const auto scale = fontSize / fontMetrics.designUnitsPerEm; + lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale); + lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale); + lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale); + lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale); + + // We always want the lines to be visible, so if a stroke width ends up + // at zero after rounding, we need to make it at least 1 pixel. + lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f); + lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f); + lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.0f); + + // Offsets are relative to the base line of the font, so we subtract + // from the ascent to get an offset relative to the top of the cell. + lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset; + lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset; + + // We also add half the stroke width to the offset, since the line + // coordinates designate the center of the line. + lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f; + lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f; } CATCH_RETURN(); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 708f3418e87..5eeb781a57f 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -142,6 +142,16 @@ namespace Microsoft::Console::Render bool _isEnabled; bool _isPainting; + struct LineMetrics + { + float gridlineWidth; + float underlineOffset; + float underlineWidth; + float strikethroughOffset; + float strikethroughWidth; + }; + + LineMetrics _lineMetrics; til::size _displaySizePixels; til::size _glyphCell; ::Microsoft::WRL::ComPtr _boxDrawingEffect; @@ -157,6 +167,7 @@ namespace Microsoft::Console::Render bool _invalidateFullRows; til::bitmap _invalidMap; til::point _invalidScroll; + bool _allInvalid; bool _presentReady; std::vector _presentDirty; @@ -266,11 +277,13 @@ namespace Microsoft::Console::Render const int dpi, ::Microsoft::WRL::ComPtr& textFormat, ::Microsoft::WRL::ComPtr& textAnalyzer, - ::Microsoft::WRL::ComPtr& fontFace) const noexcept; + ::Microsoft::WRL::ComPtr& fontFace, + LineMetrics& lineMetrics) const noexcept; [[nodiscard]] til::size _GetClientSize() const; void _InvalidateRectangle(const til::rectangle& rc); + bool _IsAllInvalid() const noexcept; [[nodiscard]] D2D1_COLOR_F _ColorFFromColorRef(const COLORREF color) noexcept; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index c375f8b764a..ad3b7089f09 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -96,6 +96,16 @@ namespace Microsoft::Console::Render std::vector cursorInvertRects; + struct LineMetrics + { + int gridlineWidth; + int underlineOffset; + int underlineWidth; + int strikethroughOffset; + int strikethroughWidth; + }; + + LineMetrics _lineMetrics; COORD _coordFontLast; int _iCurrentDpi; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index b416510f99e..2008b220399 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -459,40 +459,58 @@ using namespace Microsoft::Console::Render; auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); }); // Get the font size so we know the size of the rectangle lines we'll be inscribing. - COORD const coordFontSize = _GetFontSize(); + const auto fontWidth = _GetFontSize().X; + const auto fontHeight = _GetFontSize().Y; + const auto widthOfAllCells = fontWidth * gsl::narrow_cast(cchLine); + + const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) { + return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY); + }; - // For each length of the line, inscribe the various lines as specified by the enum - for (size_t i = 0; i < cchLine; i++) + if (lines & GridLines::Left) { - if (lines & GridLines::Top) + auto x = ptTarget.x; + for (size_t i = 0; i < cchLine; i++, x += fontWidth) { - RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y, coordFontSize.X, 1, PATCOPY))); + RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight)); } + } - if (lines & GridLines::Left) + if (lines & GridLines::Right) + { + // NOTE: We have to subtract the stroke width from the cell width + // to ensure the x coordinate remains inside the clipping rectangle. + auto x = ptTarget.x + fontWidth - _lineMetrics.gridlineWidth; + for (size_t i = 0; i < cchLine; i++, x += fontWidth) { - RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y, 1, coordFontSize.Y, PATCOPY))); + RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight)); } + } - // NOTE: Watch out for inclusive/exclusive rectangles here. - // We have to remove 1 from the font size for the bottom and right lines to ensure that the - // starting point remains within the clipping rectangle. - // For example, if we're drawing a letter at 0,0 and the font size is 8x16.... - // The bottom left corner inclusive is at 0,15 which is Y (0) + Font Height (16) - 1 = 15. - // The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7. + if (lines & GridLines::Top) + { + const auto y = ptTarget.y; + RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth)); + } - if (lines & GridLines::Bottom) - { - RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y + coordFontSize.Y - 1, coordFontSize.X, 1, PATCOPY))); - } + if (lines & GridLines::Bottom) + { + // NOTE: We have to subtract the stroke width from the cell height + // to ensure the y coordinate remains inside the clipping rectangle. + const auto y = ptTarget.y + fontHeight - _lineMetrics.gridlineWidth; + RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth)); + } - if (lines & GridLines::Right) - { - RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x + coordFontSize.X - 1, ptTarget.y, 1, coordFontSize.Y, PATCOPY))); - } + if (lines & GridLines::Underline) + { + const auto y = ptTarget.y + _lineMetrics.underlineOffset; + RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth)); + } - // Move to the next character in this run. - ptTarget.x += coordFontSize.X; + if (lines & GridLines::Strikethrough) + { + const auto y = ptTarget.y + _lineMetrics.strikethroughOffset; + RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth)); } return S_OK; diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 65eec1aaa4e..e7ad9e2f6ef 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -232,6 +232,43 @@ GdiEngine::~GdiEngine() // Save off the font metrics for various other calculations RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics))); + // There is no font metric for the grid line width, so we use a small + // multiple of the font size, which typically rounds to a pixel. + const auto fontSize = _tmFontMetrics.tmHeight - _tmFontMetrics.tmInternalLeading; + _lineMetrics.gridlineWidth = std::lround(fontSize * 0.025); + + OUTLINETEXTMETRICW outlineMetrics; + if (GetOutlineTextMetricsW(_hdcMemoryContext, sizeof(outlineMetrics), &outlineMetrics)) + { + // For TrueType fonts, the other line metrics can be obtained from + // the font's outline text metric structure. + _lineMetrics.underlineOffset = outlineMetrics.otmsUnderscorePosition; + _lineMetrics.underlineWidth = outlineMetrics.otmsUnderscoreSize; + _lineMetrics.strikethroughOffset = outlineMetrics.otmsStrikeoutPosition; + _lineMetrics.strikethroughWidth = outlineMetrics.otmsStrikeoutSize; + } + else + { + // If we can't obtain the outline metrics for the font, we just pick + // some reasonable values for the offsets and widths. + _lineMetrics.underlineOffset = -std::lround(fontSize * 0.05); + _lineMetrics.underlineWidth = _lineMetrics.gridlineWidth; + _lineMetrics.strikethroughOffset = std::lround(_tmFontMetrics.tmAscent / 3.0); + _lineMetrics.strikethroughWidth = _lineMetrics.gridlineWidth; + } + + // We always want the lines to be visible, so if a stroke width ends + // up being zero, we need to make it at least 1 pixel. + _lineMetrics.gridlineWidth = std::max(_lineMetrics.gridlineWidth, 1); + _lineMetrics.underlineWidth = std::max(_lineMetrics.underlineWidth, 1); + _lineMetrics.strikethroughWidth = std::max(_lineMetrics.strikethroughWidth, 1); + + // Offsets are relative to the base line of the font, so we subtract + // from the ascent to get an offset relative to the top of the cell. + const auto ascent = _tmFontMetrics.tmAscent; + _lineMetrics.underlineOffset = ascent - _lineMetrics.underlineOffset; + _lineMetrics.strikethroughOffset = ascent - _lineMetrics.strikethroughOffset; + // Now find the size of a 0 in this current font and save it for conversions done later. _coordFontLast = Font.GetSize(); diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index a70960754c0..388ba308960 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -35,7 +35,9 @@ namespace Microsoft::Console::Render Top = 0x1, Bottom = 0x2, Left = 0x4, - Right = 0x8 + Right = 0x8, + Underline = 0x10, + Strikethrough = 0x20 }; virtual ~IRenderEngine() = 0; diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp index dc6afd71711..3fd69ce3ab0 100644 --- a/src/renderer/vt/VtSequences.cpp +++ b/src/renderer/vt/VtSequences.cpp @@ -361,7 +361,7 @@ using namespace Microsoft::Console::Render; // - isUnderlined: If true, we'll underline the text. Otherwise we'll remove the underline. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_SetUnderline(const bool isUnderlined) noexcept +[[nodiscard]] HRESULT VtEngine::_SetUnderlined(const bool isUnderlined) noexcept { return _Write(isUnderlined ? "\x1b[4m" : "\x1b[24m"); } @@ -372,7 +372,7 @@ using namespace Microsoft::Console::Render; // - isOverlined: If true, we'll overline the text. Otherwise we'll remove the overline. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_SetOverline(const bool isOverlined) noexcept +[[nodiscard]] HRESULT VtEngine::_SetOverlined(const bool isOverlined) noexcept { return _Write(isOverlined ? "\x1b[53m" : "\x1b[55m"); } @@ -383,7 +383,7 @@ using namespace Microsoft::Console::Render; // - isItalic: If true, we'll italicize the text. Otherwise we'll remove the italics. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_SetItalics(const bool isItalic) noexcept +[[nodiscard]] HRESULT VtEngine::_SetItalic(const bool isItalic) noexcept { return _Write(isItalic ? "\x1b[3m" : "\x1b[23m"); } diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index d2848a80539..ec7dac6460a 100644 --- a/src/renderer/vt/Xterm256Engine.cpp +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -67,20 +67,20 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, if (textAttributes.IsUnderlined() != _lastTextAttributes.IsUnderlined()) { - RETURN_IF_FAILED(_SetUnderline(textAttributes.IsUnderlined())); - _lastTextAttributes.SetUnderline(textAttributes.IsUnderlined()); + RETURN_IF_FAILED(_SetUnderlined(textAttributes.IsUnderlined())); + _lastTextAttributes.SetUnderlined(textAttributes.IsUnderlined()); } if (textAttributes.IsOverlined() != _lastTextAttributes.IsOverlined()) { - RETURN_IF_FAILED(_SetOverline(textAttributes.IsOverlined())); - _lastTextAttributes.SetOverline(textAttributes.IsOverlined()); + RETURN_IF_FAILED(_SetOverlined(textAttributes.IsOverlined())); + _lastTextAttributes.SetOverlined(textAttributes.IsOverlined()); } if (textAttributes.IsItalic() != _lastTextAttributes.IsItalic()) { - RETURN_IF_FAILED(_SetItalics(textAttributes.IsItalic())); - _lastTextAttributes.SetItalics(textAttributes.IsItalic()); + RETURN_IF_FAILED(_SetItalic(textAttributes.IsItalic())); + _lastTextAttributes.SetItalic(textAttributes.IsItalic()); } if (textAttributes.IsBlinking() != _lastTextAttributes.IsBlinking()) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 64a29f26e82..34101935ab4 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -155,8 +155,8 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, } if (textAttributes.IsUnderlined() != _lastTextAttributes.IsUnderlined()) { - RETURN_IF_FAILED(_SetUnderline(textAttributes.IsUnderlined())); - _lastTextAttributes.SetUnderline(textAttributes.IsUnderlined()); + RETURN_IF_FAILED(_SetUnderlined(textAttributes.IsUnderlined())); + _lastTextAttributes.SetUnderlined(textAttributes.IsUnderlined()); } return S_OK; diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 952be678925..d9ddbcf7c56 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -187,9 +187,9 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _SetBold(const bool isBold) noexcept; [[nodiscard]] HRESULT _SetFaint(const bool isFaint) noexcept; - [[nodiscard]] HRESULT _SetUnderline(const bool isUnderlined) noexcept; - [[nodiscard]] HRESULT _SetOverline(const bool isUnderlined) noexcept; - [[nodiscard]] HRESULT _SetItalics(const bool isItalic) noexcept; + [[nodiscard]] HRESULT _SetUnderlined(const bool isUnderlined) noexcept; + [[nodiscard]] HRESULT _SetOverlined(const bool isOverlined) noexcept; + [[nodiscard]] HRESULT _SetItalic(const bool isItalic) noexcept; [[nodiscard]] HRESULT _SetBlinking(const bool isBlinking) noexcept; [[nodiscard]] HRESULT _SetInvisible(const bool isInvisible) noexcept; [[nodiscard]] HRESULT _SetCrossedOut(const bool isCrossedOut) noexcept; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index ada5924cc8e..a80accff61a 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -135,10 +135,10 @@ bool AdaptDispatch::SetGraphicsRendition(const gsl::span_attribute = TextAttribute{ 0 }; - _testGetSet->_expectedAttribute = TextAttribute{ COMMON_LVB_UNDERSCORE }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetUnderlined(true); break; case DispatchTypes::GraphicsOptions::Overline: Log::Comment(L"Testing graphics 'Overline'"); @@ -1317,6 +1317,12 @@ class AdapterTest _testGetSet->_expectedAttribute = TextAttribute{ 0 }; _testGetSet->_expectedAttribute.SetInvisible(true); break; + case DispatchTypes::GraphicsOptions::CrossedOut: + Log::Comment(L"Testing graphics 'Crossed Out'"); + _testGetSet->_attribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + _testGetSet->_expectedAttribute.SetCrossedOut(true); + break; case DispatchTypes::GraphicsOptions::NotBoldOrFaint: Log::Comment(L"Testing graphics 'No Bold or Faint'"); _testGetSet->_attribute = TextAttribute{ 0 }; @@ -1326,7 +1332,8 @@ class AdapterTest break; case DispatchTypes::GraphicsOptions::NoUnderline: Log::Comment(L"Testing graphics 'No Underline'"); - _testGetSet->_attribute = TextAttribute{ COMMON_LVB_UNDERSCORE }; + _testGetSet->_attribute = TextAttribute{ 0 }; + _testGetSet->_attribute.SetUnderlined(true); _testGetSet->_expectedAttribute = TextAttribute{ 0 }; break; case DispatchTypes::GraphicsOptions::NoOverline: @@ -1345,6 +1352,12 @@ class AdapterTest _testGetSet->_attribute.SetInvisible(true); _testGetSet->_expectedAttribute = TextAttribute{ 0 }; break; + case DispatchTypes::GraphicsOptions::NotCrossedOut: + Log::Comment(L"Testing graphics 'Not Crossed Out'"); + _testGetSet->_attribute = TextAttribute{ 0 }; + _testGetSet->_attribute.SetCrossedOut(true); + _testGetSet->_expectedAttribute = TextAttribute{ 0 }; + break; case DispatchTypes::GraphicsOptions::ForegroundBlack: Log::Comment(L"Testing graphics 'Foreground Color Black'"); _testGetSet->_attribute = TextAttribute{ FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY }; diff --git a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj index 02e923f575e..df36be81a1b 100644 --- a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj +++ b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj @@ -6,15 +6,12 @@ ParserUnitTests TerminalParser.UnitTests ConParser.Unit.Tests - DynamicLibrary + DynamicLibrary - - - @@ -26,13 +23,11 @@ Create - ..;%(AdditionalIncludeDirectories) - {18d09a24-8240-42d6-8cb6-236eee820263} @@ -44,9 +39,17 @@ {3ae13314-1939-4dfa-9c14-38ca0834050c} - - + + + - + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters index adc0b0fec1f..40253dffc1c 100644 --- a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters +++ b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters @@ -36,4 +36,10 @@ Header Files - + + + + + + + \ No newline at end of file diff --git a/src/terminal/parser/ut_parser/packages.config b/src/terminal/parser/ut_parser/packages.config index 42477ca456a..3a630c09c2c 100644 --- a/src/terminal/parser/ut_parser/packages.config +++ b/src/terminal/parser/ut_parser/packages.config @@ -1,4 +1,4 @@  - - + + \ No newline at end of file diff --git a/src/til/ut_til/SPSCTests.cpp b/src/til/ut_til/SPSCTests.cpp new file mode 100644 index 00000000000..77f3d111613 --- /dev/null +++ b/src/til/ut_til/SPSCTests.cpp @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +struct drop_indicator +{ + explicit drop_indicator(int& counter) noexcept : + _counter(&counter) {} + + drop_indicator(const drop_indicator&) = delete; + drop_indicator& operator=(const drop_indicator&) = delete; + + drop_indicator(drop_indicator&& other) noexcept + { + _counter = std::exchange(other._counter, nullptr); + } + + drop_indicator& operator=(drop_indicator&& other) noexcept + { + _counter = std::exchange(other._counter, nullptr); + } + + ~drop_indicator() + { + if (_counter) + { + ++*_counter; + } + } + +private: + int* _counter = nullptr; +}; + +template +void drop(T&& val) +{ + auto _ = std::move(val); +} + +class SPSCTests +{ + BEGIN_TEST_CLASS(SPSCTests) + TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout + END_TEST_CLASS() + + TEST_METHOD(DropEmptyTest); + TEST_METHOD(DropSameRevolutionTest); + TEST_METHOD(DropDifferentRevolutionTest); + TEST_METHOD(IntegrationTest); +}; + +void SPSCTests::DropEmptyTest() +{ + auto [tx, rx] = til::spsc::channel(5); + int counter = 0; + + for (int i = 0; i < 5; ++i) + { + tx.emplace(counter); + } + VERIFY_ARE_EQUAL(counter, 0); + + for (int i = 0; i < 5; ++i) + { + rx.pop(); + } + VERIFY_ARE_EQUAL(counter, 5); + + for (int i = 0; i < 3; ++i) + { + tx.emplace(counter); + } + VERIFY_ARE_EQUAL(counter, 5); + + drop(tx); + VERIFY_ARE_EQUAL(counter, 5); + + for (int i = 0; i < 3; ++i) + { + rx.pop(); + } + VERIFY_ARE_EQUAL(counter, 8); + + drop(rx); + VERIFY_ARE_EQUAL(counter, 8); +} + +void SPSCTests::DropSameRevolutionTest() +{ + auto [tx, rx] = til::spsc::channel(5); + int counter = 0; + + for (int i = 0; i < 5; ++i) + { + tx.emplace(counter); + } + VERIFY_ARE_EQUAL(counter, 0); + + drop(tx); + VERIFY_ARE_EQUAL(counter, 0); + + for (int i = 0; i < 3; ++i) + { + rx.pop(); + } + VERIFY_ARE_EQUAL(counter, 3); + + drop(rx); + VERIFY_ARE_EQUAL(counter, 5); +} + +void SPSCTests::DropDifferentRevolutionTest() +{ + auto [tx, rx] = til::spsc::channel(5); + int counter = 0; + + for (int i = 0; i < 4; ++i) + { + tx.emplace(counter); + } + VERIFY_ARE_EQUAL(counter, 0); + + for (int i = 0; i < 3; ++i) + { + rx.pop(); + } + VERIFY_ARE_EQUAL(counter, 3); + + for (int i = 0; i < 4; ++i) + { + tx.emplace(counter); + } + VERIFY_ARE_EQUAL(counter, 3); + + // At this point we emplace()d 8 items and pop()ed 3 in a channel with a capacity of 5. + // Both producer and consumer positions will be 3 and only differ in their revolution flag. + // This ensures that the arc destructor works even if the + // two positions within the circular buffer are identical (modulo the capacity). + + drop(tx); + VERIFY_ARE_EQUAL(counter, 3); + + drop(rx); + VERIFY_ARE_EQUAL(counter, 8); +} + +void SPSCTests::IntegrationTest() +{ + auto [tx, rx] = til::spsc::channel(7); + + std::thread t([tx = std::move(tx)]() { + std::array buffer{}; + std::generate(buffer.begin(), buffer.end(), [v = 0]() mutable { return v++; }); + + for (int i = 0; i < 37; ++i) + { + tx.emplace(i); + } + for (int i = 0; i < 3; ++i) + { + tx.push(buffer.begin(), buffer.end()); + } + }); + + std::array buffer{}; + + for (int i = 0; i < 3; ++i) + { + rx.pop_n(buffer.data(), buffer.size()); + for (int j = 0; j < 11; ++j) + { + VERIFY_ARE_EQUAL(i * 11 + j, buffer[j]); + } + } + for (int i = 33; i < 37; ++i) + { + auto actual = rx.pop(); + VERIFY_ARE_EQUAL(i, actual); + } + for (int i = 0; i < 33; ++i) + { + auto expected = i % 11; + auto actual = rx.pop(); + VERIFY_ARE_EQUAL(expected, actual); + } + + t.join(); +} diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 84ee72de877..82b7908bb7c 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -22,6 +22,7 @@ Create + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 20492e2c64c..5633f453b7d 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -15,6 +15,7 @@ + diff --git a/src/tools/vtapp/Program.cs b/src/tools/vtapp/Program.cs index 20cff98abb7..6b2017c004b 100644 --- a/src/tools/vtapp/Program.cs +++ b/src/tools/vtapp/Program.cs @@ -236,11 +236,11 @@ static void Main(string[] args) break; case 'e': Console.Write(CSI); - Console.Write("4m"); + Console.Write("53m"); break; case 'd': Console.Write(CSI); - Console.Write("24m"); + Console.Write("55m"); break; case 'r': Console.Write(CSI); @@ -260,7 +260,7 @@ static void Main(string[] args) break; case '9': Console.Write(CSI); - Console.Write("1;37;43;4m"); + Console.Write("1;37;43;53m"); break; case '(': Console.Write(CSI); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 3245620e369..7d12fb42762 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -145,7 +145,9 @@ const COORD UiaTextRangeBase::GetEndpoint(TextPatternRangeEndpoint endpoint) con // - true if range is degenerate, false otherwise. bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const COORD val) noexcept { - const auto bufferSize = _getBufferSize(); + // GH#6402: Get the actual buffer size here, instead of the one + // constrained by the virtual bottom. + const auto bufferSize = _pData->GetTextBuffer().GetSize(); switch (endpoint) { case TextPatternRangeEndpoint_End: @@ -284,6 +286,8 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc } else { + // TODO GH#6986: properly handle "end of buffer" as last character + // instead of last cell // expand to document _start = bufferSize.Origin(); _end = bufferSize.EndExclusive(); @@ -393,7 +397,9 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ // set of coords. std::vector coords; - const auto bufferSize = _getBufferSize(); + // GH#6402: Get the actual buffer size here, instead of the one + // constrained by the virtual bottom. + const auto bufferSize = _pData->GetTextBuffer().GetSize(); // these viewport vars are converted to the buffer coordinate space const auto viewport = bufferSize.ConvertToOrigin(_pData->GetViewport()); diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index bef3078a0d2..78c5416eddb 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -189,7 +189,7 @@ function Invoke-OpenConsoleTests() } $OpenConsolePath = "$env:OpenConsoleroot\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe" $RunTePath = "$env:OpenConsoleRoot\tools\runte.cmd" - $TaefExePath = "$env:OpenConsoleRoot\packages\Taef.Redist.Wlk.10.51.200127004\build\Binaries\$Platform\te.exe" + $TaefExePath = "$env:OpenConsoleRoot\packages\Taef.Redist.Wlk.10.57.200731005-develop\build\Binaries\$Platform\te.exe" $BinDir = "$env:OpenConsoleRoot\bin\$OpenConsolePlatform\$Configuration" [xml]$TestConfig = Get-Content "$env:OpenConsoleRoot\tools\tests.xml" diff --git a/tools/bcz.cmd b/tools/bcz.cmd index f70269aaf34..3262b0b39e1 100644 --- a/tools/bcz.cmd +++ b/tools/bcz.cmd @@ -116,8 +116,12 @@ set "__PROJECT_NAME=!_OUTPUT!" rem If we're trying to clean build, make sure to update the target here. if "%_MSBUILD_TARGET%" == "Build" ( set __MSBUILD_TARGET=%__PROJECT_NAME% -) else if "%_MSBUILD_TARGET%" == "Clean,Build" ( +) else if "%_MSBUILD_TARGET%" == "Clean;Build" ( set __MSBUILD_TARGET=%__PROJECT_NAME%:Rebuild +) else ( + echo. + echo Oops... build bug in the neighborhood of configuring a build target. + echo. ) rem This statement will propagate our internal variables up to the calling rem scope. Because they're all on one line, the value of our local variables diff --git a/tools/razzle.cmd b/tools/razzle.cmd index 0851160f9ef..9dedd23caca 100644 --- a/tools/razzle.cmd +++ b/tools/razzle.cmd @@ -104,7 +104,7 @@ shift goto :ARGS_LOOP :POST_ARGS_LOOP -set TAEF=%OPENCON%\packages\Taef.Redist.Wlk.10.51.200127004\build\Binaries\%ARCH%\TE.exe +set TAEF=%OPENCON%\packages\Taef.Redist.Wlk.10.57.200731005-develop\build\Binaries\%ARCH%\TE.exe rem Set this envvar so setup won't repeat itself set OpenConBuild=true