diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 4e75f04be35..cf19440c293 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -87,6 +87,31 @@ ], "type": "string" }, + "NewTerminalArgs": { + "properties": { + "commandline": { + "description": "A commandline to use instead of the profile's", + "type": "string" + }, + "tabTitle": { + "description": "An initial tabTitle to use instead of the profile's", + "type": "string" + }, + "startingDirectory": { + "description": "A startingDirectory to use instead of the profile's", + "type": "string" + }, + "profile": { + "description": "Either the GUID or name of a profile to use, instead of launching the default", + "type": "string" + }, + "index": { + "type": "integer", + "description": "The index of the profile in the new tab dropdown to open" + } + }, + "type": "object" + }, "ShortcutAction": { "properties": { "action": { @@ -119,13 +144,10 @@ "description": "Arguments corresponding to a New Tab Action", "allOf": [ { "$ref": "#/definitions/ShortcutAction" }, + { "$ref": "#/definitions/NewTerminalArgs" }, { "properties": { - "action": { "type":"string", "pattern": "newTab" }, - "index": { - "type": "integer", - "description": "The index in the new tab dropdown to open in a new tab" - } + "action": { "type":"string", "pattern": "newTab" } } } ] @@ -185,6 +207,7 @@ "description": "Arguments corresponding to a Split Pane Action", "allOf": [ { "$ref": "#/definitions/ShortcutAction" }, + { "$ref": "#/definitions/NewTerminalArgs" }, { "properties": { "action": { "type": "string", "pattern": "splitPane" }, diff --git a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp index a263c39c107..275fcd5bf78 100644 --- a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp @@ -6,6 +6,7 @@ #include "../TerminalApp/ColorScheme.h" #include "../TerminalApp/CascadiaSettings.h" #include "JsonTestClass.h" +#include "TestUtils.h" using namespace Microsoft::Console; using namespace TerminalApp; @@ -46,42 +47,6 @@ namespace TerminalAppLocalTests InitializeJsonReader(); return true; } - - // Function Description: - // - This is a helper to retrieve the ActionAndArgs from the keybindings - // for a given chord. - // Arguments: - // - bindings: The AppKeyBindings to lookup the ActionAndArgs from. - // - kc: The key chord to look up the bound ActionAndArgs for. - // Return Value: - // - The ActionAndArgs bound to the given key, or nullptr if nothing is bound to it. - static const ActionAndArgs GetActionAndArgs(const implementation::AppKeyBindings& bindings, - const KeyChord& kc) - { - std::wstring buffer{ L"" }; - if (WI_IsFlagSet(kc.Modifiers(), KeyModifiers::Ctrl)) - { - buffer += L"Ctrl+"; - } - if (WI_IsFlagSet(kc.Modifiers(), KeyModifiers::Shift)) - { - buffer += L"Shift+"; - } - if (WI_IsFlagSet(kc.Modifiers(), KeyModifiers::Alt)) - { - buffer += L"Alt+"; - } - buffer += static_cast(MapVirtualKeyW(kc.Vkey(), MAPVK_VK_TO_CHAR)); - Log::Comment(NoThrowString().Format(L"Looking for key:%s", buffer.c_str())); - - const auto keyIter = bindings._keyShortcuts.find(kc); - VERIFY_IS_TRUE(keyIter != bindings._keyShortcuts.end(), L"Expected to find an action bound to the given KeyChord"); - if (keyIter != bindings._keyShortcuts.end()) - { - return keyIter->second; - } - return nullptr; - }; }; void KeyBindingsTests::ManyKeysSameAction() @@ -233,7 +198,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` without args parses as Copy(TrimWhitespace=true)")); KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -244,7 +209,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copyTextWithoutNewlines` parses as Copy(TrimWhitespace=false)")); KeyChord kc{ false, true, false, static_cast('C') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -255,7 +220,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); KeyChord kc{ true, false, true, static_cast('C') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -266,7 +231,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` with args parses them correctly")); KeyChord kc{ false, true, true, static_cast('C') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value @@ -277,68 +242,73 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `newTab` without args parses as NewTab(Index=null)")); KeyChord kc{ true, false, false, static_cast('T') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_IS_NULL(realArgs.ProfileIndex()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_NULL(realArgs.TerminalArgs().ProfileIndex()); } { Log::Comment(NoThrowString().Format( L"Verify that `newTab` parses args correctly")); KeyChord kc{ true, false, true, static_cast('T') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_IS_NOT_NULL(realArgs.ProfileIndex()); - VERIFY_ARE_EQUAL(0, realArgs.ProfileIndex().Value()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); + VERIFY_ARE_EQUAL(0, realArgs.TerminalArgs().ProfileIndex().Value()); } { Log::Comment(NoThrowString().Format( L"Verify that `newTabProfile0` parses as NewTab(Index=0)")); KeyChord kc{ false, true, true, static_cast('T') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTabProfile0, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_IS_NOT_NULL(realArgs.ProfileIndex()); - VERIFY_ARE_EQUAL(0, realArgs.ProfileIndex().Value()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); + VERIFY_ARE_EQUAL(0, realArgs.TerminalArgs().ProfileIndex().Value()); } { Log::Comment(NoThrowString().Format( L"Verify that `newTab` with an index greater than the legacy " L"args afforded parses correctly")); KeyChord kc{ true, false, true, static_cast('Y') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_IS_NOT_NULL(realArgs.ProfileIndex()); - VERIFY_ARE_EQUAL(11, realArgs.ProfileIndex().Value()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); + VERIFY_ARE_EQUAL(11, realArgs.TerminalArgs().ProfileIndex().Value()); } { Log::Comment(NoThrowString().Format( L"Verify that `newTabProfile8` parses as NewTab(Index=8)")); KeyChord kc{ false, true, true, static_cast('Y') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::NewTabProfile8, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value - VERIFY_IS_NOT_NULL(realArgs.ProfileIndex()); - VERIFY_ARE_EQUAL(8, realArgs.ProfileIndex().Value()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex()); + VERIFY_ARE_EQUAL(8, realArgs.TerminalArgs().ProfileIndex().Value()); } { Log::Comment(NoThrowString().Format( L"Verify that `copy` ignores args it doesn't understand")); KeyChord kc{ true, false, true, static_cast('B') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -350,7 +320,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `copy` null as it's `args` parses as the default option")); KeyChord kc{ true, false, true, static_cast('B') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -362,7 +332,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `increaseFontSize` without args parses as AdjustFontSize(Delta=1)")); KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::IncreaseFontSize, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -374,7 +344,7 @@ namespace TerminalAppLocalTests Log::Comment(NoThrowString().Format( L"Verify that `decreaseFontSize` without args parses as AdjustFontSize(Delta=-1)")); KeyChord kc{ true, false, false, static_cast('G') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::DecreaseFontSize, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -405,7 +375,7 @@ namespace TerminalAppLocalTests { KeyChord kc{ true, false, false, static_cast('A') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitVertical, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -414,7 +384,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('B') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitHorizontal, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); @@ -423,7 +393,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('C') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + 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); @@ -432,7 +402,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('D') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + 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); @@ -441,7 +411,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('E') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + 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); @@ -450,7 +420,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('F') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + 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); @@ -459,7 +429,7 @@ namespace TerminalAppLocalTests } { KeyChord kc{ true, false, false, static_cast('G') }; - auto actionAndArgs = GetActionAndArgs(*appKeyBindings, kc); + 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); diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index e9412f25f5e..ca3a99e21cf 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -6,6 +6,7 @@ #include "../TerminalApp/ColorScheme.h" #include "../TerminalApp/CascadiaSettings.h" #include "JsonTestClass.h" +#include "TestUtils.h" #include using namespace Microsoft::Console; @@ -13,6 +14,8 @@ using namespace TerminalApp; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; +using namespace winrt::TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings; namespace TerminalAppLocalTests { @@ -60,6 +63,8 @@ namespace TerminalAppLocalTests TEST_METHOD(TestCloseOnExitParsing); TEST_METHOD(TestCloseOnExitCompatibilityShim); + TEST_METHOD(TestTerminalArgsForBinding); + TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -1542,4 +1547,307 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings._profiles[0].GetCloseOnExitMode()); VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings._profiles[1].GetCloseOnExitMode()); } + + void SettingsTests::TestTerminalArgsForBinding() + { + const std::string settingsJson{ R"( + { + "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name": "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "historySize": 1, + "commandline": "cmd.exe" + }, + { + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "historySize": 2, + "commandline": "pwsh.exe" + }, + { + "name": "profile2", + "historySize": 3, + "commandline": "wsl.exe" + } + ], + "keybindings": [ + { "keys": ["ctrl+a"], "command": { "action": "splitPane", "split": "vertical" } }, + { "keys": ["ctrl+b"], "command": { "action": "splitPane", "split": "vertical", "profile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } }, + { "keys": ["ctrl+c"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile1" } }, + { "keys": ["ctrl+d"], "command": { "action": "splitPane", "split": "vertical", "profile": "profile2" } }, + { "keys": ["ctrl+e"], "command": { "action": "splitPane", "split": "horizontal", "commandline": "foo.exe" } }, + { "keys": ["ctrl+f"], "command": { "action": "splitPane", "split": "horizontal", "profile": "profile1", "commandline": "foo.exe" } }, + { "keys": ["ctrl+g"], "command": { "action": "newTab" } }, + { "keys": ["ctrl+h"], "command": { "action": "newTab", "startingDirectory": "c:\\foo" } }, + { "keys": ["ctrl+i"], "command": { "action": "newTab", "profile": "profile2", "startingDirectory": "c:\\foo" } }, + { "keys": ["ctrl+j"], "command": { "action": "newTab", "tabTitle": "bar" } }, + { "keys": ["ctrl+k"], "command": { "action": "newTab", "profile": "profile2", "tabTitle": "bar" } }, + { "keys": ["ctrl+l"], "command": { "action": "newTab", "profile": "profile1", "tabTitle": "bar", "startingDirectory": "c:\\foo", "commandline":"foo.exe" } } + ] + })" }; + + const auto guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"); + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + + VerifyParseSucceeded(settingsJson); + CascadiaSettings settings{}; + settings._ParseJsonString(settingsJson, false); + settings.LayerJson(settings._userSettings); + settings._ValidateSettings(); + + auto appKeyBindings = settings._globals._keybindings; + VERIFY_ARE_EQUAL(3u, settings.GetProfiles().size()); + + const auto profile2Guid = settings._profiles.at(2).GetGuid(); + VERIFY_ARE_NOT_EQUAL(GUID{ 0 }, profile2Guid); + + VERIFY_ARE_EQUAL(12u, appKeyBindings->_keyShortcuts.size()); + + { + KeyChord kc{ true, false, false, static_cast('A') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('B') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('C') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"pwsh.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('D') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Vertical, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('E') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('F') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_ARE_EQUAL(winrt::TerminalApp::SplitState::Horizontal, realArgs.SplitStyle()); + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('G') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('H') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('I') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('J') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid0, guid); + VERIFY_ARE_EQUAL(L"cmd.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(1, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('K') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); + VERIFY_ARE_EQUAL(L"profile2", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(profile2Guid, guid); + VERIFY_ARE_EQUAL(L"wsl.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(3, termSettings.HistorySize()); + } + { + KeyChord kc{ true, false, false, static_cast('L') }; + auto actionAndArgs = TestUtils::GetActionAndArgs(*appKeyBindings, kc); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); + const auto& realArgs = actionAndArgs.Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + // Verify the args have the expected value + VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Commandline().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().StartingDirectory().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().TabTitle().empty()); + VERIFY_IS_FALSE(realArgs.TerminalArgs().Profile().empty()); + VERIFY_ARE_EQUAL(L"foo.exe", realArgs.TerminalArgs().Commandline()); + VERIFY_ARE_EQUAL(L"c:\\foo", realArgs.TerminalArgs().StartingDirectory()); + VERIFY_ARE_EQUAL(L"bar", realArgs.TerminalArgs().TabTitle()); + VERIFY_ARE_EQUAL(L"profile1", realArgs.TerminalArgs().Profile()); + + const auto [guid, termSettings] = settings.BuildSettings(realArgs.TerminalArgs()); + VERIFY_ARE_EQUAL(guid1, guid); + VERIFY_ARE_EQUAL(L"foo.exe", termSettings.Commandline()); + VERIFY_ARE_EQUAL(L"bar", termSettings.StartingTitle()); + VERIFY_ARE_EQUAL(L"c:\\foo", termSettings.StartingDirectory()); + VERIFY_ARE_EQUAL(2, termSettings.HistorySize()); + } + } } diff --git a/src/cascadia/LocalTests_TerminalApp/TestUtils.h b/src/cascadia/LocalTests_TerminalApp/TestUtils.h new file mode 100644 index 00000000000..ea1df508e1f --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/TestUtils.h @@ -0,0 +1,53 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TestUtils.h + +Abstract: +- This file has helper functions for writing tests for the TerminalApp project. + +Author(s): + Mike Griese (migrie) December-2019 +--*/ + +class TerminalAppLocalTests::TestUtils +{ +public: + // Function Description: + // - This is a helper to retrieve the ActionAndArgs from the keybindings + // for a given chord. + // Arguments: + // - bindings: The AppKeyBindings to lookup the ActionAndArgs from. + // - kc: The key chord to look up the bound ActionAndArgs for. + // Return Value: + // - The ActionAndArgs bound to the given key, or nullptr if nothing is bound to it. + static const winrt::TerminalApp::ActionAndArgs GetActionAndArgs(const winrt::TerminalApp::implementation::AppKeyBindings& bindings, + const winrt::Microsoft::Terminal::Settings::KeyChord& kc) + { + std::wstring buffer{ L"" }; + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Ctrl)) + { + buffer += L"Ctrl+"; + } + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Shift)) + { + buffer += L"Shift+"; + } + if (WI_IsFlagSet(kc.Modifiers(), winrt::Microsoft::Terminal::Settings::KeyModifiers::Alt)) + { + buffer += L"Alt+"; + } + buffer += static_cast(MapVirtualKeyW(kc.Vkey(), MAPVK_VK_TO_CHAR)); + WEX::Logging::Log::Comment(WEX::Common::NoThrowString().Format(L"Looking for key:%s", buffer.c_str())); + + const auto keyIter = bindings._keyShortcuts.find(kc); + VERIFY_IS_TRUE(keyIter != bindings._keyShortcuts.end(), L"Expected to find an action bound to the given KeyChord"); + if (keyIter != bindings._keyShortcuts.end()) + { + return keyIter->second; + } + return nullptr; + }; +}; diff --git a/src/cascadia/TerminalApp/ActionArgs.cpp b/src/cascadia/TerminalApp/ActionArgs.cpp index f343e519fdc..b3af876bace 100644 --- a/src/cascadia/TerminalApp/ActionArgs.cpp +++ b/src/cascadia/TerminalApp/ActionArgs.cpp @@ -6,6 +6,7 @@ #include "ActionArgs.h" #include "ActionEventArgs.g.cpp" +#include "NewTerminalArgs.g.cpp" #include "CopyTextArgs.g.cpp" #include "NewTabArgs.g.cpp" #include "SwitchToTabArgs.g.cpp" diff --git a/src/cascadia/TerminalApp/ActionArgs.h b/src/cascadia/TerminalApp/ActionArgs.h index ff32d187a1d..0cbd7ff2193 100644 --- a/src/cascadia/TerminalApp/ActionArgs.h +++ b/src/cascadia/TerminalApp/ActionArgs.h @@ -6,6 +6,7 @@ // HEY YOU: When adding ActionArgs types, make sure to add the corresponding // *.g.cpp to ActionArgs.cpp! #include "ActionEventArgs.g.h" +#include "NewTerminalArgs.g.h" #include "CopyTextArgs.g.h" #include "NewTabArgs.g.h" #include "SwitchToTabArgs.g.h" @@ -34,6 +35,63 @@ namespace winrt::TerminalApp::implementation GETSET_PROPERTY(bool, Handled, false); }; + struct NewTerminalArgs : public NewTerminalArgsT + { + NewTerminalArgs() = default; + GETSET_PROPERTY(winrt::hstring, Commandline, L""); + GETSET_PROPERTY(winrt::hstring, StartingDirectory, L""); + GETSET_PROPERTY(winrt::hstring, TabTitle, L""); + GETSET_PROPERTY(Windows::Foundation::IReference, ProfileIndex, nullptr); + GETSET_PROPERTY(winrt::hstring, Profile, L""); + + static constexpr std::string_view CommandlineKey{ "commandline" }; + static constexpr std::string_view StartingDirectoryKey{ "startingDirectory" }; + static constexpr std::string_view TabTitleKey{ "tabTitle" }; + static constexpr std::string_view ProfileIndexKey{ "index" }; + static constexpr std::string_view ProfileKey{ "profile" }; + + public: + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_Commandline == _Commandline && + otherAsUs->_StartingDirectory == _StartingDirectory && + otherAsUs->_TabTitle == _TabTitle && + otherAsUs->_ProfileIndex == _ProfileIndex && + otherAsUs->_Profile == _Profile; + } + return false; + }; + static winrt::TerminalApp::NewTerminalArgs FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + 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()); + } + return *args; + } + }; + struct CopyTextArgs : public CopyTextArgsT { CopyTextArgs() = default; @@ -66,9 +124,7 @@ namespace winrt::TerminalApp::implementation struct NewTabArgs : public NewTabArgsT { NewTabArgs() = default; - GETSET_PROPERTY(Windows::Foundation::IReference, ProfileIndex, nullptr); - - static constexpr std::string_view ProfileIndexKey{ "index" }; + GETSET_PROPERTY(winrt::TerminalApp::NewTerminalArgs, TerminalArgs, nullptr); public: bool Equals(const IActionArgs& other) @@ -76,7 +132,7 @@ namespace winrt::TerminalApp::implementation auto otherAsUs = other.try_as(); if (otherAsUs) { - return otherAsUs->_ProfileIndex == _ProfileIndex; + return otherAsUs->_TerminalArgs == _TerminalArgs; } return false; }; @@ -84,10 +140,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 profileIndex{ json[JsonKey(ProfileIndexKey)] }) - { - args->_ProfileIndex = profileIndex.asInt(); - } + args->_TerminalArgs = NewTerminalArgs::FromJson(json); return *args; } }; @@ -265,6 +318,7 @@ namespace winrt::TerminalApp::implementation { SplitPaneArgs() = default; GETSET_PROPERTY(winrt::TerminalApp::SplitState, SplitStyle, winrt::TerminalApp::SplitState::None); + GETSET_PROPERTY(winrt::TerminalApp::NewTerminalArgs, TerminalArgs, nullptr); static constexpr std::string_view SplitKey{ "split" }; @@ -274,7 +328,8 @@ namespace winrt::TerminalApp::implementation auto otherAsUs = other.try_as(); if (otherAsUs) { - return otherAsUs->_SplitStyle == _SplitStyle; + return otherAsUs->_SplitStyle == _SplitStyle && + otherAsUs->_TerminalArgs == _TerminalArgs; } return false; }; @@ -282,6 +337,7 @@ 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()); @@ -294,4 +350,5 @@ namespace winrt::TerminalApp::implementation namespace winrt::TerminalApp::factory_implementation { BASIC_FACTORY(ActionEventArgs); + BASIC_FACTORY(NewTerminalArgs); } diff --git a/src/cascadia/TerminalApp/ActionArgs.idl b/src/cascadia/TerminalApp/ActionArgs.idl index 9f8671d3f34..e55f0338ce8 100644 --- a/src/cascadia/TerminalApp/ActionArgs.idl +++ b/src/cascadia/TerminalApp/ActionArgs.idl @@ -30,6 +30,17 @@ namespace TerminalApp Horizontal = 2 }; + [default_interface] runtimeclass NewTerminalArgs { + NewTerminalArgs(); + String Commandline; + String StartingDirectory; + String TabTitle; + String Profile; // Either a GUID or a profile's name if the GUID isn't a match + // ProfileIndex can be null (for "use the default"), so this needs to be + // a IReference, so it's nullable + Windows.Foundation.IReference ProfileIndex { get; }; + }; + [default_interface] runtimeclass ActionEventArgs : IActionEventArgs { ActionEventArgs(IActionArgs args); @@ -42,9 +53,7 @@ namespace TerminalApp [default_interface] runtimeclass NewTabArgs : IActionArgs { - // ProfileIndex can be null (for "use the default"), so this needs to be - // a IReference, so it's nullable - Windows.Foundation.IReference ProfileIndex { get; }; + NewTerminalArgs TerminalArgs { get; }; }; [default_interface] runtimeclass SwitchToTabArgs : IActionArgs @@ -70,5 +79,6 @@ namespace TerminalApp [default_interface] runtimeclass SplitPaneArgs : IActionArgs { SplitState SplitStyle { get; }; + NewTerminalArgs TerminalArgs { get; }; }; } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 8c18463a042..68a29622501 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -98,7 +98,7 @@ namespace winrt::TerminalApp::implementation } else if (const auto& realArgs = args.ActionArgs().try_as()) { - _SplitPane(realArgs.SplitStyle(), std::nullopt); + _SplitPane(realArgs.SplitStyle(), realArgs.TerminalArgs()); args.Handled(true); } } @@ -137,19 +137,12 @@ namespace winrt::TerminalApp::implementation { if (args == nullptr) { - _OpenNewTab(std::nullopt); + _OpenNewTab(nullptr); args.Handled(true); } else if (const auto& realArgs = args.ActionArgs().try_as()) { - if (realArgs.ProfileIndex() == nullptr) - { - _OpenNewTab(std::nullopt); - } - else - { - _OpenNewTab(realArgs.ProfileIndex().Value()); - } + _OpenNewTab(realArgs.TerminalArgs()); args.Handled(true); } } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 58b1164cc34..a12b92b51d2 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -13,6 +13,7 @@ namespace TerminalAppLocalTests { class SettingsTests; class KeyBindingsTests; + class TestUtils; } namespace winrt::TerminalApp::implementation @@ -64,6 +65,7 @@ namespace winrt::TerminalApp::implementation friend class TerminalAppLocalTests::SettingsTests; friend class TerminalAppLocalTests::KeyBindingsTests; + friend class TerminalAppLocalTests::TestUtils; }; } diff --git a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp index 3f2efc5fae3..26dc7f2759e 100644 --- a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp @@ -222,7 +222,9 @@ std::function LegacyParseNewTabWithProfileArgs( { auto pfn = [index](const Json::Value & /*value*/) -> IActionArgs { auto args = winrt::make_self(); - args->ProfileIndex(index); + auto newTerminalArgs = winrt::make_self(); + newTerminalArgs->ProfileIndex(index); + args->TerminalArgs(*newTerminalArgs); return *args; }; return pfn; diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 7e1233e53bb..522f911067a 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -343,7 +343,7 @@ namespace winrt::TerminalApp::implementation } // Use the default profile to determine how big of a window we need. - TerminalSettings settings = _settings->MakeSettings(std::nullopt); + const auto [_, settings] = _settings->BuildSettings(nullptr); // TODO MSFT:21150597 - If the global setting "Always show tab bar" is // set or if "Show tabs in title bar" is set, then we'll need to add diff --git a/src/cascadia/TerminalApp/CascadiaSettings.cpp b/src/cascadia/TerminalApp/CascadiaSettings.cpp index 629d0007c2f..f520ec517bf 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettings.cpp @@ -115,35 +115,6 @@ const Profile* CascadiaSettings::FindProfile(GUID profileGuid) const noexcept return nullptr; } -// Method Description: -// - Create a TerminalSettings object from the given profile. -// If the profileGuidArg is not provided, this method will use the default -// profile. -// The TerminalSettings object that is created can be used to initialize both -// the Control's settings, and the Core settings of the terminal. -// Arguments: -// - profileGuidArg: an optional GUID to use to lookup the profile to create the -// settings from. If this arg is not provided, or the GUID does not match a -// profile, then this method will use the default profile. -// Return Value: -// - -TerminalSettings CascadiaSettings::MakeSettings(std::optional profileGuidArg) const -{ - GUID profileGuid = profileGuidArg ? profileGuidArg.value() : _globals.GetDefaultProfile(); - const Profile* const profile = FindProfile(profileGuid); - if (profile == nullptr) - { - throw E_INVALIDARG; - } - - TerminalSettings result = profile->CreateTerminalSettings(_globals.GetColorSchemes()); - - // Place our appropriate global settings into the Terminal Settings - _globals.ApplyToSettings(result); - - return result; -} - // Method Description: // - Returns an iterable collection of all of our Profiles. // Arguments: @@ -452,3 +423,169 @@ void CascadiaSettings::_ValidateAllSchemesExist() _warnings.push_back(::TerminalApp::SettingsLoadWarnings::UnknownColorScheme); } } + +// Method Description: +// - Create a TerminalSettings object for the provided newTerminalArgs. We'll +// use the newTerminalArgs to look up the profile that should be used to +// create these TerminalSettings. Then, we'll apply settings contained in the +// newTerminalArgs to the profile's settings, to enable customization on top +// of the profile's default values. +// Arguments: +// - newTerminalArgs: An object that may contain a profile name or GUID to +// actually use. If the Profile value is not a guid, we'll treat it as a name, +// and attempt to look the profile up by name instead. +// * Additionally, we'll use other values (such as Commandline, +// StartingDirectory) in this object to override the settings directly from +// the profile. +// Return Value: +// - the GUID of the created profile, and a fully initialized TerminalSettings object +std::tuple CascadiaSettings::BuildSettings(const NewTerminalArgs& newTerminalArgs) const +{ + const GUID profileGuid = _GetProfileForArgs(newTerminalArgs); + auto settings = BuildSettings(profileGuid); + + if (newTerminalArgs) + { + // Override commandline, starting directory if they exist in newTerminalArgs + if (!newTerminalArgs.Commandline().empty()) + { + settings.Commandline(newTerminalArgs.Commandline()); + } + if (!newTerminalArgs.StartingDirectory().empty()) + { + settings.StartingDirectory(newTerminalArgs.StartingDirectory()); + } + if (!newTerminalArgs.TabTitle().empty()) + { + settings.StartingTitle(newTerminalArgs.TabTitle()); + } + } + + return { profileGuid, settings }; +} + +// Method Description: +// - Create a TerminalSettings object for the profile with a GUID matching the +// provided GUID. If no profile matches this GUID, then this method will +// throw. +// Arguments: +// - profileGuid: The GUID of a profile to use to create a settings object for. +// Return Value: +// - the GUID of the created profile, and a fully initialized TerminalSettings object +TerminalSettings CascadiaSettings::BuildSettings(GUID profileGuid) const +{ + const Profile* const profile = FindProfile(profileGuid); + THROW_HR_IF_NULL(E_INVALIDARG, profile); + + TerminalSettings result = profile->CreateTerminalSettings(_globals.GetColorSchemes()); + + // Place our appropriate global settings into the Terminal Settings + _globals.ApplyToSettings(result); + + return result; +} + +// Method Description: +// - Helper to get the GUID of a profile, given an optional index and a possible +// "profile" value to override that. +// - First, we'll try looking up the profile for the given index. This will +// either get us the GUID of the Nth profile, or the GUID of the default +// profile. +// - Then, if there was a Profile set in the NewTerminalArgs, we'll use that to +// try and look the profile up by either GUID or name. +// Arguments: +// - index: if provided, the index in the list of profiles to get the GUID for. +// If omitted, instead use the default profile's GUID +// - newTerminalArgs: An object that may contain a profile name or GUID to +// actually use. If the Profile value is not a guid, we'll treat it as a name, +// and attempt to look the profile up by name instead. +// Return Value: +// - the GUID of the profile corresponding to this combination of index and NewTerminalArgs +GUID CascadiaSettings::_GetProfileForArgs(const NewTerminalArgs& newTerminalArgs) const +{ + std::optional profileIndex{ std::nullopt }; + if (newTerminalArgs && + newTerminalArgs.ProfileIndex() != nullptr) + { + profileIndex = newTerminalArgs.ProfileIndex().Value(); + } + GUID profileGuid = _GetProfileForIndex(profileIndex); + + if (newTerminalArgs) + { + const auto profileString = newTerminalArgs.Profile(); + + // First, try and parse the "profile" argument as a GUID. If it's a + // GUID, and the GUID of one of our profiles, then use that as the + // profile GUID instead. If it's not, then try looking it up as a + // name of a profile. If it's still not that, then just ignore it. + if (!profileString.empty()) + { + bool wasGuid = false; + + // Do a quick heuristic check - is the profile 38 chars long (the + // length of a GUID string), and does it start with '{'? Because if + // it doesn't, it's _definitely_ not a GUID. + if (profileString.size() == 38 && profileString[0] == L'{') + { + try + { + const auto newGUID = Utils::GuidFromString(profileString.c_str()); + if (FindProfile(newGUID)) + { + profileGuid = newGUID; + wasGuid = true; + } + } + CATCH_LOG(); + } + + // Here, we were unable to use the profile string as a GUID to + // lookup a profile. Instead, try using the string to look the + // Profile up by name. + if (!wasGuid) + { + const auto guidFromName = FindGuid(profileString.c_str()); + if (guidFromName.has_value()) + { + profileGuid = guidFromName.value(); + } + } + } + } + + return profileGuid; +} + +// Method Description: +// - Helper to find the profile GUID for a the profile at the given index in the +// list of profiles. If no index is provided, this instead returns the default +// profile's guid. This is used by the NewTabProfile ShortcutActions to +// create a tab for the Nth profile in the list of profiles. +// Arguments: +// - index: if provided, the index in the list of profiles to get the GUID for. +// If omitted, instead return the default profile's GUID +// Return Value: +// - the Nth profile's GUID, or the default profile's GUID +GUID CascadiaSettings::_GetProfileForIndex(std::optional index) const +{ + GUID profileGuid; + if (index) + { + const auto realIndex = index.value(); + // If we don't have that many profiles, then do nothing. + if (realIndex < 0 || + realIndex >= gsl::narrow_cast(_profiles.size())) + { + return _globals.GetDefaultProfile(); + } + const auto& selectedProfile = _profiles.at(realIndex); + profileGuid = selectedProfile.GetGuid(); + } + else + { + // get Guid for the default profile + profileGuid = _globals.GetDefaultProfile(); + } + return profileGuid; +} diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index 31cc4948f79..d782786c694 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -51,7 +51,8 @@ class TerminalApp::CascadiaSettings final static const CascadiaSettings& GetCurrentAppSettings(); - winrt::Microsoft::Terminal::Settings::TerminalSettings MakeSettings(std::optional profileGuid) const; + std::tuple BuildSettings(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; + winrt::Microsoft::Terminal::Settings::TerminalSettings BuildSettings(GUID profileGuid) const; GlobalAppSettings& GlobalSettings(); @@ -98,6 +99,9 @@ class TerminalApp::CascadiaSettings final static std::optional _ReadUserSettings(); static std::optional _ReadFile(HANDLE hFile); + GUID _GetProfileForIndex(std::optional index) const; + GUID _GetProfileForArgs(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) const; + void _ValidateSettings(); void _ValidateProfilesExist(); void _ValidateProfilesHaveGuid(); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 5ba822f1463..8e6158a5be1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -5,6 +5,7 @@ #include "TerminalPage.h" #include "ActionAndArgs.h" #include "Utils.h" +#include "../../types/inc/utils.hpp" #include @@ -25,6 +26,7 @@ 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; namespace winrt { @@ -116,7 +118,7 @@ namespace winrt::TerminalApp::implementation _newTabButton.Click([weakThis](auto&&, auto&&) { if (auto page{ weakThis.get() }) { - page->_OpenNewTab(std::nullopt); + page->_OpenNewTab(nullptr); } }); _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); @@ -124,7 +126,7 @@ namespace winrt::TerminalApp::implementation _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); _CreateNewTabFlyout(); - _OpenNewTab(std::nullopt); + _OpenNewTab(nullptr); _tabContent.SizeChanged({ this, &TerminalPage::_OnContentSizeChanged }); } @@ -284,7 +286,9 @@ namespace winrt::TerminalApp::implementation auto actionAndArgs = winrt::make_self(); actionAndArgs->Action(ShortcutAction::NewTab); auto newTabArgs = winrt::make_self(); - newTabArgs->ProfileIndex(profileIndex); + auto newTerminalArgs = winrt::make_self(); + newTerminalArgs->ProfileIndex(profileIndex); + newTabArgs->TerminalArgs(*newTerminalArgs); actionAndArgs->Args(*newTabArgs); profileKeyChord = keyBindings.GetKeyBindingForActionWithArgs(*actionAndArgs); } @@ -322,7 +326,9 @@ namespace winrt::TerminalApp::implementation profileMenuItem.Click([profileIndex, weakThis](auto&&, auto&&) { if (auto page{ weakThis.get() }) { - page->_OpenNewTab({ profileIndex }); + auto newTerminalArgs = winrt::make_self(); + newTerminalArgs->ProfileIndex(profileIndex); + page->_OpenNewTab(*newTerminalArgs); } }); newTabFlyout.Items().Append(profileMenuItem); @@ -391,48 +397,27 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Open a new tab. This will create the TerminalControl hosting the - // terminal, and add a new Tab to our list of tabs. The method can - // optionally be provided a profile index, which will be used to create - // a tab using the profile in that index. - // If no index is provided, the default profile will be used. + // terminal, and add a new Tab to our list of tabs. The method can + // optionally be provided a NewTerminalArgs, which will be used to create + // a tab using the values in that object. // Arguments: - // - profileIndex: an optional index into the list of profiles to use to - // initialize this tab up with. - void TerminalPage::_OpenNewTab(std::optional profileIndex) + // - newTerminalArgs: An object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + void TerminalPage::_OpenNewTab(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) { - GUID profileGuid; + const auto [profileGuid, settings] = _settings->BuildSettings(newTerminalArgs); - if (profileIndex) - { - const auto realIndex = profileIndex.value(); - const auto profiles = _settings->GetProfiles(); - - // If we don't have that many profiles, then do nothing. - if (realIndex >= gsl::narrow(profiles.size())) - { - return; - } - - const auto& selectedProfile = profiles[realIndex]; - profileGuid = selectedProfile.GetGuid(); - } - else - { - // Getting Guid for default profile - const auto globalSettings = _settings->GlobalSettings(); - profileGuid = globalSettings.GetDefaultProfile(); - } - - TerminalSettings settings = _settings->MakeSettings(profileGuid); _CreateNewTabFromSettings(profileGuid, settings); const int tabCount = static_cast(_tabs.size()); + const bool usedManualProfile = (newTerminalArgs != nullptr) && (newTerminalArgs.ProfileIndex().Value() || newTerminalArgs.Profile().empty()); TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider "TabInformation", TraceLoggingDescription("Event emitted upon new tab creation in TerminalApp"), TraceLoggingInt32(tabCount, "TabCount", "Count of tabs curently opened in TerminalApp"), - TraceLoggingBool(profileIndex.has_value(), "ProfileSpecified", "Whether the new tab specified a profile explicitly"), + TraceLoggingBool(usedManualProfile, "ProfileSpecified", "Whether the new tab specified a profile explicitly"), TraceLoggingGuid(profileGuid, "ProfileGuid", "The GUID of the profile spawned in the new tab"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); @@ -724,9 +709,11 @@ namespace winrt::TerminalApp::implementation const auto& _tab = _tabs.at(focusedTabIndex); const auto& profileGuid = _tab->GetFocusedProfile(); - const auto& settings = _settings->MakeSettings(profileGuid); - - _CreateNewTabFromSettings(profileGuid.value(), settings); + if (profileGuid.has_value()) + { + const auto settings = _settings->BuildSettings(profileGuid.value()); + _CreateNewTabFromSettings(profileGuid.value(), settings); + } } // Method Description: @@ -959,9 +946,11 @@ namespace winrt::TerminalApp::implementation // Arguments: // - splitType: one value from the TerminalApp::SplitState enum, indicating how the // new pane should be split from its parent. - // - profile: The profile GUID to associate with the newly created pane. If - // this is nullopt, use the default profile. - void TerminalPage::_SplitPane(const TerminalApp::SplitState splitType, const std::optional& profileGuid) + // - newTerminalArgs: An object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + void TerminalPage::_SplitPane(const TerminalApp::SplitState splitType, + const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs) { // Do nothing if we're requesting no split. if (splitType == TerminalApp::SplitState::None) @@ -969,9 +958,7 @@ namespace winrt::TerminalApp::implementation return; } - const auto realGuid = profileGuid ? profileGuid.value() : - _settings->GlobalSettings().GetDefaultProfile(); - const auto controlSettings = _settings->MakeSettings(realGuid); + const auto [realGuid, controlSettings] = _settings->BuildSettings(newTerminalArgs); const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings); @@ -1374,7 +1361,7 @@ namespace winrt::TerminalApp::implementation for (auto& profile : profiles) { const GUID profileGuid = profile.GetGuid(); - TerminalSettings settings = _settings->MakeSettings(profileGuid); + const auto settings = _settings->BuildSettings(profileGuid); for (auto& tab : _tabs) { diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 28cb5c26929..466c6a91ed5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -67,7 +67,7 @@ namespace winrt::TerminalApp::implementation void _CreateNewTabFlyout(); void _OpenNewTabDropdown(); - void _OpenNewTab(std::optional profileIndex); + 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); @@ -102,7 +102,7 @@ namespace winrt::TerminalApp::implementation // Todo: add more event implementations here // MSFT:20641986: Add keybindings for New Window void _Scroll(int delta); - void _SplitPane(const winrt::TerminalApp::SplitState splitType, const std::optional& profileGuid); + void _SplitPane(const winrt::TerminalApp::SplitState splitType, const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs = nullptr); void _ResizePane(const Direction& direction); void _ScrollPage(int delta); void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Settings::KeyChord& keyChord);