diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index dff45c936b4..169ca5dbe9b 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -58,6 +58,7 @@ NCHITTEST NCLBUTTONDBLCLK NCRBUTTONDBLCLK NOAGGREGATION +NOASYNC NOPROGRESS NOREDIRECTIONBITMAP ntprivapi @@ -82,6 +83,7 @@ shobjidl SIZENS smoothstep GETDESKWALLPAPER +SHELLEXECUTEINFOW snprintf spsc sregex diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 322edd0a583..d927f598fd2 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -80,6 +80,7 @@ "moveFocus", "moveTab", "newTab", + "newWindow", "nextTab", "openNewTabDropdown", "openSettings", @@ -584,6 +585,18 @@ ], "required": [ "direction" ] }, + "NewWindowAction": { + "description": "Arguments corresponding to a New Window Action", + "allOf": [ + { "$ref": "#/definitions/ShortcutAction" }, + { "$ref": "#/definitions/NewTerminalArgs" }, + { + "properties": { + "action": { "type":"string", "pattern": "newWindow" } + } + } + ] + }, "Keybinding": { "additionalProperties": false, "properties": { @@ -609,6 +622,7 @@ { "$ref": "#/definitions/ScrollDownAction" }, { "$ref": "#/definitions/MoveTabAction" }, { "$ref": "#/definitions/FindMatchAction" }, + { "$ref": "#/definitions/NewWindowAction" }, { "type": "null" } ] }, diff --git a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index 11d4064288b..110c0a5b938 100644 --- a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -42,6 +42,8 @@ namespace SettingsModelLocalTests TEST_METHOD(TestAutogeneratedName); TEST_METHOD(TestLayerOnAutogeneratedName); + TEST_METHOD(TestGenerateCommandline); + TEST_CLASS_SETUP(ClassSetup) { InitializeJsonReader(); @@ -361,4 +363,145 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle()); } } + + void CommandTests::TestGenerateCommandline() + { + const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; + + const std::string commands0String{ R"([ + { + "name":"action0", + "command": { "action": "newWindow" } + }, + { + "name":"action1", + "command": { "action": "newTab", "profile": "foo" } + }, + { + "name":"action2", + "command": { "action": "newWindow", "profile": "foo" } + }, + { + "name":"action3", + "command": { "action": "newWindow", "commandline": "bar.exe" } + }, + { + "name":"action4", + "command": { "action": "newWindow", "commandline": "pop.exe ya ha ha" } + }, + { + "name":"action5", + "command": { "action": "newWindow", "commandline": "pop.exe \"ya ha ha\"" } + }, + { + "name":"action6", + "command": { "action": "newWindow", "startingDirectory":"C:\\foo", "commandline": "bar.exe" } + }, + ])" }; + + const auto commands0Json = VerifyParseSucceeded(commands0String); + + IMap commands = winrt::single_threaded_map(); + VERIFY_ARE_EQUAL(0u, commands.Size()); + auto warnings = implementation::Command::LayerJson(commands, commands0Json); + VERIFY_ARE_EQUAL(0u, warnings.size()); + VERIFY_ARE_EQUAL(7u, commands.Size()); + + { + auto command = commands.Lookup(L"action0"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + VERIFY_ARE_EQUAL(L"", cmdline); + } + + { + auto command = commands.Lookup(L"action1"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + VERIFY_ARE_EQUAL(L"--profile \"foo\"", cmdline); + } + + { + auto command = commands.Lookup(L"action2"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + VERIFY_ARE_EQUAL(L"--profile \"foo\"", cmdline); + } + + { + auto command = commands.Lookup(L"action3"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + VERIFY_ARE_EQUAL(L"-- \"bar.exe\"", cmdline); + } + + { + auto command = commands.Lookup(L"action4"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(L"-- \"pop.exe ya ha ha\"", terminalArgs.ToCommandline()); + } + + { + auto command = commands.Lookup(L"action5"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(L"-- \"pop.exe \"ya ha ha\"\"", terminalArgs.ToCommandline()); + } + + { + auto command = commands.Lookup(L"action6"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.Action()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.Action().Action()); + const auto& realArgs = command.Action().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\foo\" -- \"bar.exe\"", terminalArgs.ToCommandline()); + } + } } diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index e50c249986c..48b9cffe290 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "OpenTerminalHere.h" +#include "../WinRTUtils/inc/WtExeUtils.h" #include // TODO GH#6112: Localize these strings @@ -10,103 +11,10 @@ static constexpr std::wstring_view VerbDisplayName{ L"Open in Windows Terminal" static constexpr std::wstring_view VerbDevBuildDisplayName{ L"Open in Windows Terminal (Dev Build)" }; static constexpr std::wstring_view VerbName{ L"WindowsTerminalOpenHere" }; -static constexpr std::wstring_view WtExe{ L"wt.exe" }; -static constexpr std::wstring_view WtdExe{ L"wtd.exe" }; -static constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; - -static constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; - // This code is aggressively copied from // https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/ // Win7Samples/winui/shell/appshellintegration/ExplorerCommandVerb/ExplorerCommandVerb.cpp -// Function Description: -// - This is a helper to determine if we're running as a part of the Dev Build -// Package or the release package. We'll need to return different text, icons, -// and use different commandlines depending on which one the user requested. -// - Uses a C++11 "magic static" to make sure this is only computed once. -// - If we can't determine if it's the dev build or not, we'll default to true -// Arguments: -// - -// Return Value: -// - true if we believe this extension is being run in the dev build package. -static bool IsDevBuild() -{ - // use C++11 magic statics to make sure we only do this once. - static bool isDevBuild = []() -> bool { - try - { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - const auto id = package.Id(); - const std::wstring name{ id.FullName() }; - // Does our PFN start with WindowsTerminalDev? - return name.rfind(L"WindowsTerminalDev", 0) == 0; - } - CATCH_LOG(); - return true; - }(); - - return isDevBuild; -} - -// Function Description: -// - Helper function for getting the path to the appropriate executable to use -// for this instance of the shell extension. If we're running the dev build, -// it should be a `wtd.exe`, but if we're preview or release, we want to make -// sure to get the correct `wt.exe` that corresponds to _us_. -// - If we're unpackaged, this needs to get us `WindowsTerminal.exe`, because -// the `wt*exe` alias won't have been installed for this install. -// Arguments: -// - -// Return Value: -// - the full path to the exe, one of `wt.exe`, `wtd.exe`, or `WindowsTerminal.exe`. -static std::wstring _getExePath() -{ - // use C++11 magic statics to make sure we only do this once. - static const std::wstring exePath = []() -> std::wstring { - // First, check a packaged location for the exe. If we've got a package - // family name, that means we're one of the packaged Dev build, packaged - // Release build, or packaged Preview build. - // - // If we're the preview or release build, there's no way of knowing if the - // `wt.exe` on the %PATH% is us or not. Fortunately, _our_ execution alias - // is located in "%LOCALAPPDATA%\Microsoft\WindowsApps\", _always_, so we can use that to look up the exe easier. - try - { - const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; - const auto id = package.Id(); - const std::wstring pfn{ id.FamilyName() }; - if (!pfn.empty()) - { - const std::filesystem::path windowsAppsPath{ wil::ExpandEnvironmentStringsW(LocalAppDataAppsPath.data()) }; - const std::filesystem::path wtPath = windowsAppsPath / pfn / (IsDevBuild() ? WtdExe : WtExe); - return wtPath; - } - } - CATCH_LOG(); - - // If we're here, then we couldn't resolve our exe from the package. This - // means we're running unpackaged. We should just use the - // WindowsTerminal.exe that's sitting in the directory next to us. - try - { - HMODULE hModule = GetModuleHandle(nullptr); - THROW_LAST_ERROR_IF(hModule == nullptr); - std::wstring dllPathString; - THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, dllPathString)); - const std::filesystem::path dllPath{ dllPathString }; - const std::filesystem::path rootDir = dllPath.parent_path(); - std::filesystem::path wtPath = rootDir / WindowsTerminalExe; - return wtPath; - } - CATCH_LOG(); - - return L"wt.exe"; - }(); - return exePath; -} - // Method Description: // - This method is called when the user activates the context menu item. We'll // launch the Terminal using the current working directory. @@ -148,7 +56,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); // Append a "\." to the given path, so that this will work in "C:\" - std::wstring cmdline = fmt::format(L"\"{}\" -d \"{}\\.\"", _getExePath(), pszName.get()); + std::wstring cmdline = fmt::format(L"\"{}\" -d \"{}\\.\"", GetWtExePath(), pszName.get()); RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( nullptr, cmdline.data(), diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 227f7285f90..c7b75fc605e 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -5,6 +5,8 @@ #include "App.h" #include "TerminalPage.h" +#include "../WinRTUtils/inc/WtExeUtils.h" +#include "../../types/inc/utils.hpp" #include "Utils.h" using namespace winrt::Windows::ApplicationModel::DataTransfer; @@ -581,4 +583,89 @@ namespace winrt::TerminalApp::implementation } } + // Function Description: + // - Helper to launch a new WT instance. It can either launch the instance + // elevated or unelevated. + // - To launch elevated, it will as the shell to elevate the process for us. + // This might cause a UAC prompt. The elevation is performed on a + // background thread, as to not block the UI thread. + // Arguments: + // - elevate: If true, launch the new Terminal elevated using `runas` + // - newTerminalArgs: A NewTerminalArgs describing the terminal instance + // that should be spawned. The Profile should be filled in with the GUID + // of the profile we want to launch. + // Return Value: + // - + // Important: Don't take the param by reference, since we'll be doing work + // on another thread. + fire_and_forget _OpenNewWindow(const bool elevate, + const NewTerminalArgs newTerminalArgs) + { + // Hop to the BG thread + co_await winrt::resume_background(); + + // This will get us the correct exe for dev/preview/release. If you + // don't stick this in a local, it'll get mangled by ShellExecute. I + // have no idea why. + const auto exePath{ GetWtExePath() }; + + // Build the commandline to pass to wt for this set of NewTerminalArgs + // `-w -1` will ensure a new window is created. + winrt::hstring cmdline{ + fmt::format(L"-w -1 new-tab {}", + newTerminalArgs ? newTerminalArgs.ToCommandline().c_str() : + L"") + }; + + // Build the args to ShellExecuteEx. We need to use ShellExecuteEx so we + // can pass the SEE_MASK_NOASYNC flag. That flag allows us to safely + // call this on the background thread, and have ShellExecute _not_ call + // back to us on the main thread. Without this, if you close the + // Terminal quickly after the UAC prompt, the elevated WT will never + // actually spawn. + SHELLEXECUTEINFOW seInfo{ 0 }; + seInfo.cbSize = sizeof(seInfo); + seInfo.fMask = SEE_MASK_NOASYNC; + // `runas` will cause the shell to launch this child process elevated. + // `open` will just run the executable normally. + seInfo.lpVerb = elevate ? L"runas" : L"open"; + seInfo.lpFile = exePath.c_str(); + seInfo.lpParameters = cmdline.c_str(); + seInfo.nShow = SW_SHOWNORMAL; + LOG_IF_WIN32_BOOL_FALSE(ShellExecuteExW(&seInfo)); + + co_return; + } + + void TerminalPage::_HandleNewWindow(const IInspectable& /*sender*/, + const ActionEventArgs& actionArgs) + { + NewTerminalArgs newTerminalArgs{ nullptr }; + // If the caller provided NewTerminalArgs, then try to use those + if (actionArgs) + { + if (const auto& realArgs = actionArgs.ActionArgs().try_as()) + { + newTerminalArgs = realArgs.TerminalArgs(); + } + } + // Otherwise, if no NewTerminalArgs were provided, then just use a + // default-constructed one. The default-constructed one implies that + // nothing about the launch should be modified (just use the default + // profile). + if (!newTerminalArgs) + { + newTerminalArgs = NewTerminalArgs(); + } + + auto [profileGuid, settings] = TerminalSettings::BuildSettings(_settings, + newTerminalArgs, + *_bindings); + + // Manually fill in the evaluated profile. + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profileGuid)); + _OpenNewWindow(false, newTerminalArgs); + actionArgs.Handled(true); + } + } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp index 033f1a37813..90eede9d057 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.cpp @@ -60,12 +60,6 @@ namespace winrt::TerminalApp::implementation _NewTabHandlers(*this, eventArgs); break; } - - case ShortcutAction::NewWindow: - { - _NewWindowHandlers(*this, eventArgs); - break; - } case ShortcutAction::CloseWindow: { _CloseWindowHandlers(*this, eventArgs); @@ -266,6 +260,11 @@ namespace winrt::TerminalApp::implementation _TogglePaneReadOnlyHandlers(*this, eventArgs); break; } + case ShortcutAction::NewWindow: + { + _NewWindowHandlers(*this, eventArgs); + break; + } default: return false; } diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.h b/src/cascadia/TerminalApp/ShortcutActionDispatch.h index 0f2488e40ce..bdcf59e7a17 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.h +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.h @@ -27,7 +27,6 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(OpenNewTabDropdown, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(DuplicateTab, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(NewTab, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); - TYPED_EVENT(NewWindow, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(CloseWindow, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(CloseTab, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(ClosePane, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); @@ -67,6 +66,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(BreakIntoDebugger, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(FindMatch, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); TYPED_EVENT(TogglePaneReadOnly, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); + TYPED_EVENT(NewWindow, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); // clang-format on private: diff --git a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl index 906de95498d..4a8b366b945 100644 --- a/src/cascadia/TerminalApp/ShortcutActionDispatch.idl +++ b/src/cascadia/TerminalApp/ShortcutActionDispatch.idl @@ -13,7 +13,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler NewTab; event Windows.Foundation.TypedEventHandler OpenNewTabDropdown; event Windows.Foundation.TypedEventHandler DuplicateTab; - event Windows.Foundation.TypedEventHandler NewWindow; event Windows.Foundation.TypedEventHandler CloseWindow; event Windows.Foundation.TypedEventHandler CloseTab; event Windows.Foundation.TypedEventHandler ClosePane; @@ -53,5 +52,6 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler BreakIntoDebugger; event Windows.Foundation.TypedEventHandler FindMatch; event Windows.Foundation.TypedEventHandler TogglePaneReadOnly; + event Windows.Foundation.TypedEventHandler NewWindow; } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index b96a0e7cc09..ca62d6d7491 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1133,6 +1133,7 @@ namespace winrt::TerminalApp::implementation _actionDispatch->BreakIntoDebugger({ this, &TerminalPage::_HandleBreakIntoDebugger }); _actionDispatch->FindMatch({ this, &TerminalPage::_HandleFindMatch }); _actionDispatch->TogglePaneReadOnly({ this, &TerminalPage::_HandleTogglePaneReadOnly }); + _actionDispatch->NewWindow({ this, &TerminalPage::_HandleNewWindow }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 67ca1841d60..9779ef792d7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -317,6 +317,7 @@ namespace winrt::TerminalApp::implementation void _HandleBreakIntoDebugger(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _HandleFindMatch(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); void _HandleTogglePaneReadOnly(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + void _HandleNewWindow(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); // Make sure to hook new actions up in _RegisterActionCallbacks! #pragma endregion diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 501c43db175..9d2c714392f 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -19,7 +19,6 @@ static constexpr std::string_view ExecuteCommandlineKey{ "wt" }; static constexpr std::string_view FindKey{ "find" }; static constexpr std::string_view MoveFocusKey{ "moveFocus" }; static constexpr std::string_view NewTabKey{ "newTab" }; -static constexpr std::string_view NewWindowKey{ "newWindow" }; static constexpr std::string_view NextTabKey{ "nextTab" }; static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" }; static constexpr std::string_view OpenSettingsKey{ "openSettings" }; // TODO GH#2557: Add args for OpenSettings @@ -53,6 +52,7 @@ static constexpr std::string_view MoveTabKey{ "moveTab" }; static constexpr std::string_view BreakIntoDebuggerKey{ "breakIntoDebugger" }; static constexpr std::string_view FindMatchKey{ "findMatch" }; static constexpr std::string_view TogglePaneReadOnlyKey{ "toggleReadOnlyMode" }; +static constexpr std::string_view NewWindowKey{ "newWindow" }; static constexpr std::string_view ActionKey{ "action" }; @@ -84,7 +84,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { FindKey, ShortcutAction::Find }, { MoveFocusKey, ShortcutAction::MoveFocus }, { NewTabKey, ShortcutAction::NewTab }, - { NewWindowKey, ShortcutAction::NewWindow }, { NextTabKey, ShortcutAction::NextTab }, { OpenNewTabDropdownKey, ShortcutAction::OpenNewTabDropdown }, { OpenSettingsKey, ShortcutAction::OpenSettings }, @@ -119,6 +118,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { UnboundKey, ShortcutAction::Invalid }, { FindMatchKey, ShortcutAction::FindMatch }, { TogglePaneReadOnlyKey, ShortcutAction::TogglePaneReadOnly }, + { NewWindowKey, ShortcutAction::NewWindow }, }; using ParseResult = std::tuple>; @@ -150,6 +150,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::MoveTab, MoveTabArgs::FromJson }, { ShortcutAction::ToggleCommandPalette, ToggleCommandPaletteArgs::FromJson }, { ShortcutAction::FindMatch, FindMatchArgs::FromJson }, + { ShortcutAction::NewWindow, NewWindowArgs::FromJson }, { ShortcutAction::Invalid, nullptr }, }; @@ -288,7 +289,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::Invalid, L"" }, { ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") }, { ShortcutAction::NewTab, RS_(L"NewTabCommandKey") }, - { ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") }, { ShortcutAction::NextTab, RS_(L"NextTabCommandKey") }, { ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") }, { ShortcutAction::OpenSettings, RS_(L"OpenSettingsCommandKey") }, @@ -321,6 +321,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") }, { ShortcutAction::FindMatch, L"" }, // Intentionally omitted, must be generated by GenerateName { ShortcutAction::TogglePaneReadOnly, RS_(L"TogglePaneReadOnlyCommandKey") }, + { ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") }, }; }(); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 218368125d5..919f1927fff 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -25,6 +25,7 @@ #include "MoveTabArgs.g.cpp" #include "FindMatchArgs.g.cpp" #include "ToggleCommandPaletteArgs.g.cpp" +#include "NewWindowArgs.g.cpp" #include @@ -76,6 +77,53 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return winrt::hstring{ s.substr(0, s.size() - 2) }; } + winrt::hstring NewTerminalArgs::ToCommandline() const + { + std::wstringstream ss; + + if (!_Profile.empty()) + { + ss << fmt::format(L"--profile \"{}\" ", _Profile); + } + // The caller is always expected to provide the evaluated profile in the + // NewTerminalArgs, not the index + // + // else if (_ProfileIndex) + // { + // ss << fmt::format(L"profile index: {}, ", _ProfileIndex.Value()); + // } + + if (!_StartingDirectory.empty()) + { + ss << fmt::format(L"--startingDirectory \"{}\" ", _StartingDirectory); + } + + if (!_TabTitle.empty()) + { + ss << fmt::format(L"--title \"{}\" ", _TabTitle); + } + + if (_TabColor) + { + const til::color tabColor{ _TabColor.Value() }; + ss << fmt::format(L"--tabColor \"{}\" ", tabColor.ToHexString(true)); + } + + if (!_Commandline.empty()) + { + ss << fmt::format(L"-- \"{}\" ", _Commandline); + } + + auto s = ss.str(); + if (s.empty()) + { + return L""; + } + + // Chop off the last " " + return winrt::hstring{ s.substr(0, s.size() - 1) }; + } + winrt::hstring CopyTextArgs::GenerateName() const { std::wstringstream ss; @@ -444,4 +492,21 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } return L""; } + + winrt::hstring NewWindowArgs::GenerateName() const + { + winrt::hstring newTerminalArgsStr; + if (_TerminalArgs) + { + newTerminalArgsStr = _TerminalArgs.GenerateName(); + } + + if (newTerminalArgsStr.empty()) + { + return RS_(L"NewWindowCommandKey"); + } + return winrt::hstring{ + fmt::format(L"{}, {}", RS_(L"NewWindowCommandKey"), newTerminalArgsStr) + }; + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 31898031cdc..2c14247a397 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -27,6 +27,7 @@ #include "MoveTabArgs.g.h" #include "ToggleCommandPaletteArgs.g.h" #include "FindMatchArgs.g.h" +#include "NewWindowArgs.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" #include "JsonUtils.h" @@ -76,6 +77,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation public: hstring GenerateName() const; + hstring ToCommandline() const; bool Equals(const Model::NewTerminalArgs& other) { @@ -883,6 +885,40 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; + struct NewWindowArgs : public NewWindowArgsT + { + NewWindowArgs() = default; + NewWindowArgs(const Model::NewTerminalArgs& terminalArgs) : + _TerminalArgs{ terminalArgs } {}; + GETSET_PROPERTY(Model::NewTerminalArgs, TerminalArgs, nullptr); + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_TerminalArgs.Equals(_TerminalArgs); + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + args->_TerminalArgs = NewTerminalArgs::FromJson(json); + return { *args, {} }; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_TerminalArgs = _TerminalArgs.Copy(); + return *copy; + } + }; + } namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation @@ -899,4 +935,5 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(MoveTabArgs); BASIC_FACTORY(OpenSettingsArgs); BASIC_FACTORY(FindMatchArgs); + BASIC_FACTORY(NewWindowArgs); } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 498cc475db3..f51a999a0b9 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -93,6 +93,7 @@ namespace Microsoft.Terminal.Settings.Model Boolean Equals(NewTerminalArgs other); String GenerateName(); + String ToCommandline(); }; [default_interface] runtimeclass ActionEventArgs : IActionEventArgs @@ -217,4 +218,10 @@ namespace Microsoft.Terminal.Settings.Model FindMatchArgs(FindMatchDirection direction); FindMatchDirection Direction { get; }; }; + + [default_interface] runtimeclass NewWindowArgs : IActionArgs + { + NewWindowArgs(NewTerminalArgs terminalArgs); + NewTerminalArgs TerminalArgs { get; }; + }; } diff --git a/src/cascadia/TerminalSettingsModel/KeyMapping.idl b/src/cascadia/TerminalSettingsModel/KeyMapping.idl index 7df3dbb489f..a51a601afee 100644 --- a/src/cascadia/TerminalSettingsModel/KeyMapping.idl +++ b/src/cascadia/TerminalSettingsModel/KeyMapping.idl @@ -13,7 +13,6 @@ namespace Microsoft.Terminal.Settings.Model OpenNewTabDropdown, DuplicateTab, NewTab, - NewWindow, CloseWindow, CloseTab, ClosePane, @@ -53,8 +52,9 @@ namespace Microsoft.Terminal.Settings.Model TabSearch, MoveTab, BreakIntoDebugger, + TogglePaneReadOnly, FindMatch, - TogglePaneReadOnly + NewWindow }; [default_interface] runtimeclass ActionAndArgs { diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index b3b4888ec06..0d0f5f7729a 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -303,6 +303,7 @@ { "command": { "action" : "moveTab", "direction": "forward" }}, { "command": { "action" : "moveTab", "direction": "backward" }}, { "command": "newTab", "keys": "ctrl+shift+t" }, + { "command": "newWindow", "keys": "ctrl+shift+n" }, { "command": { "action": "newTab", "index": 0 }, "keys": "ctrl+shift+1" }, { "command": { "action": "newTab", "index": 1 }, "keys": "ctrl+shift+2" }, { "command": { "action": "newTab", "index": 2 }, "keys": "ctrl+shift+3" }, diff --git a/src/cascadia/WinRTUtils/inc/WtExeUtils.h b/src/cascadia/WinRTUtils/inc/WtExeUtils.h new file mode 100644 index 00000000000..1fa7a57082d --- /dev/null +++ b/src/cascadia/WinRTUtils/inc/WtExeUtils.h @@ -0,0 +1,93 @@ + +static constexpr std::wstring_view WtExe{ L"wt.exe" }; +static constexpr std::wstring_view WtdExe{ L"wtd.exe" }; +static constexpr std::wstring_view WindowsTerminalExe{ L"WindowsTerminal.exe" }; + +static constexpr std::wstring_view LocalAppDataAppsPath{ L"%LOCALAPPDATA%\\Microsoft\\WindowsApps\\" }; + +// Function Description: +// - This is a helper to determine if we're running as a part of the Dev Build +// Package or the release package. We'll need to return different text, icons, +// and use different commandlines depending on which one the user requested. +// - Uses a C++11 "magic static" to make sure this is only computed once. +// - If we can't determine if it's the dev build or not, we'll default to true +// Arguments: +// - +// Return Value: +// - true if we believe this extension is being run in the dev build package. +_TIL_INLINEPREFIX bool IsDevBuild() +{ + // use C++11 magic statics to make sure we only do this once. + static bool isDevBuild = []() -> bool { + try + { + const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; + const auto id = package.Id(); + const std::wstring name{ id.FullName() }; + // Does our PFN start with WindowsTerminalDev? + return name.rfind(L"WindowsTerminalDev", 0) == 0; + } + CATCH_LOG(); + return true; + }(); + + return isDevBuild; +} + +// Function Description: +// - Helper function for getting the path to the appropriate executable to use +// for this instance of the shell extension. If we're running the dev build, +// it should be a `wtd.exe`, but if we're preview or release, we want to make +// sure to get the correct `wt.exe` that corresponds to _us_. +// - If we're unpackaged, this needs to get us `WindowsTerminal.exe`, because +// the `wt*exe` alias won't have been installed for this install. +// Arguments: +// - +// Return Value: +// - the full path to the exe, one of `wt.exe`, `wtd.exe`, or `WindowsTerminal.exe`. +_TIL_INLINEPREFIX std::wstring GetWtExePath() +{ + // use C++11 magic statics to make sure we only do this once. + static const std::wstring exePath = []() -> std::wstring { + // First, check a packaged location for the exe. If we've got a package + // family name, that means we're one of the packaged Dev build, packaged + // Release build, or packaged Preview build. + // + // If we're the preview or release build, there's no way of knowing if the + // `wt.exe` on the %PATH% is us or not. Fortunately, _our_ execution alias + // is located in "%LOCALAPPDATA%\Microsoft\WindowsApps\", _always_, so we can use that to look up the exe easier. + try + { + const auto package{ winrt::Windows::ApplicationModel::Package::Current() }; + const auto id = package.Id(); + const std::wstring pfn{ id.FamilyName() }; + if (!pfn.empty()) + { + const std::filesystem::path windowsAppsPath{ wil::ExpandEnvironmentStringsW(LocalAppDataAppsPath.data()) }; + const std::filesystem::path wtPath = windowsAppsPath / pfn / (IsDevBuild() ? WtdExe : WtExe); + return wtPath; + } + } + CATCH_LOG(); + + // If we're here, then we couldn't resolve our exe from the package. This + // means we're running unpackaged. We should just use the + // WindowsTerminal.exe that's sitting in the directory next to us. + try + { + HMODULE hModule = GetModuleHandle(nullptr); + THROW_LAST_ERROR_IF(hModule == nullptr); + std::wstring dllPathString; + THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, dllPathString)); + const std::filesystem::path dllPath{ dllPathString }; + const std::filesystem::path rootDir = dllPath.parent_path(); + std::filesystem::path wtPath = rootDir / WindowsTerminalExe; + return wtPath; + } + CATCH_LOG(); + + return L"wt.exe"; + }(); + return exePath; +}