diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 7e0edef7bd..e3b24e2439 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -11,6 +11,8 @@ #include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" #include "Utils.h" +#include + using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Text; @@ -1335,6 +1337,80 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleNewProfile(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (!args) + { + return; + } + + const auto& realArgs = args.ActionArgs().try_as(); + if (!realArgs) + { + return; + } + + try + { + auto newProfile = _settings.CreateNewProfile(); + if (!newProfile) + { + return; + } + + if (!realArgs.Name().empty()) + { + newProfile.Name(realArgs.Name()); + } + + if (!realArgs.StartingDirectory().empty()) + { + newProfile.StartingDirectory(realArgs.StartingDirectory()); + } + + if (!realArgs.Commandline().empty()) + { + newProfile.Commandline(realArgs.Commandline()); + } + + // Resolve color scheme: either a specific one, or pick a random one. + winrt::hstring schemeName; + if (realArgs.RandomColorScheme()) + { + const auto schemes = _settings.GlobalSettings().ColorSchemes(); + const auto schemeCount = schemes.Size(); + if (schemeCount > 0) + { + std::vector names; + names.reserve(schemeCount); + for (const auto& kvp : schemes) + { + names.emplace_back(kvp.Key()); + } + std::random_device rd; + std::mt19937 gen{ rd() }; + std::uniform_int_distribution dist{ 0, names.size() - 1 }; + schemeName = names[dist(gen)]; + } + } + else if (!realArgs.ColorScheme().empty()) + { + schemeName = realArgs.ColorScheme(); + } + + if (!schemeName.empty()) + { + newProfile.DefaultAppearance().DarkColorSchemeName(schemeName); + newProfile.DefaultAppearance().LightColorSchemeName(schemeName); + } + + _settings.WriteSettingsToDisk(); + args.Handled(true); + } + CATCH_LOG(); + } + void TerminalPage::ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord) { // If we haven't ever loaded the TeachingTip, then do so now and diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 950984019b..493cdb0664 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -209,6 +209,7 @@ void AppCommandlineArgs::_buildParser() _buildSwapPaneParser(); _buildFocusPaneParser(); _buildSaveSnippetParser(); + _buildNewProfileParser(); } // Method Description: @@ -603,6 +604,115 @@ void AppCommandlineArgs::_buildSaveSnippetParser() setupSubcommand(_saveCommand); } +// Method Description: +// - Adds the `new-profile` subcommand and related options to the commandline parser. +// This subcommand creates a new profile in the user's settings file, using the +// current working directory as the starting directory by default. It does not +// open a new tab or window. +// Arguments: +// - +// Return Value: +// - +void AppCommandlineArgs::_buildNewProfileParser() +{ + _newProfileCommand = _app.add_subcommand("new-profile", RS_A(L"CmdNewProfileDesc")); + + auto setupSubcommand = [this](auto* subcommand) { + subcommand->add_option("--name,-n", _newProfileName, RS_A(L"CmdNewProfileNameArgDesc")); + subcommand->add_option("--startingDirectory,-d", _newProfileStartingDirectory, RS_A(L"CmdNewProfileStartingDirArgDesc")); + subcommand->add_option("--colorScheme,-c", _newProfileColorScheme, RS_A(L"CmdNewProfileColorSchemeArgDesc")); + subcommand->add_flag("--random,-r", _newProfileRandomColorScheme, RS_A(L"CmdNewProfileRandomColorSchemeArgDesc")); + subcommand->add_option("command,", _commandline, RS_A(L"CmdCommandArgDesc")); + subcommand->positionals_at_end(true); + + // When ParseCommand is called, if this subcommand was provided, this + // callback function will be triggered on the same thread. We can be sure + // that `this` will still be safe - this function just lets us know this + // command was parsed. + subcommand->callback([&, this]() { + ActionAndArgs newProfileAction{}; + newProfileAction.Action(ShortcutAction::NewProfile); + NewProfileArgs args{}; + + // Capture the current working directory now, while we're still + // running in the spawned wt.exe process. The action will be + // delegated to the existing monarch wt.exe process, which will + // have a different CWD. + std::wstring cwd; + try + { + cwd = wil::GetCurrentDirectoryW(); + } + CATCH_LOG(); + + // Use the --startingDirectory if provided, else use the CWD we captured. + std::wstring startingDir; + if (!_newProfileStartingDirectory.empty()) + { + startingDir = winrt::to_hstring(_newProfileStartingDirectory).c_str(); + } + else + { + startingDir = cwd; + } + args.StartingDirectory(winrt::hstring{ startingDir }); + + // Use the --name if provided, else use the leaf of the starting directory. + std::wstring name; + if (!_newProfileName.empty()) + { + name = winrt::to_hstring(_newProfileName).c_str(); + } + else if (!startingDir.empty()) + { + try + { + name = std::filesystem::path{ startingDir }.filename().wstring(); + if (name.empty()) + { + // path ended in a separator (e.g. "C:\") - try the parent + name = std::filesystem::path{ startingDir }.parent_path().filename().wstring(); + } + } + CATCH_LOG(); + } + args.Name(winrt::hstring{ name }); + + if (!_newProfileColorScheme.empty()) + { + args.ColorScheme(winrt::to_hstring(_newProfileColorScheme)); + } + args.RandomColorScheme(_newProfileRandomColorScheme); + + if (!_commandline.empty()) + { + std::ostringstream cmdlineBuffer; + for (const auto& arg : _commandline) + { + if (cmdlineBuffer.tellp() != 0) + { + cmdlineBuffer << ' '; + } + if (arg.find(" ") != std::string::npos) + { + cmdlineBuffer << '"' << arg << '"'; + } + else + { + cmdlineBuffer << arg; + } + } + args.Commandline(winrt::to_hstring(cmdlineBuffer.str())); + } + + newProfileAction.Args(args); + _startupActions.push_back(newProfileAction); + }); + }; + + setupSubcommand(_newProfileCommand); +} + // Method Description: // - Add the `NewTerminalArgs` parameters to the given subcommand. This enables // that subcommand to support all the properties in a NewTerminalArgs. @@ -777,7 +887,8 @@ bool AppCommandlineArgs::_noCommandsProvided() *_focusPaneShort || *_newPaneShort.subcommand || *_newPaneCommand.subcommand || - *_saveCommand); + *_saveCommand || + *_newProfileCommand); } // Method Description: @@ -815,6 +926,11 @@ void AppCommandlineArgs::_resetStateToDefault() _focusPaneTarget = -1; _loadPersistedLayoutIdx = -1; + _newProfileName.clear(); + _newProfileStartingDirectory.clear(); + _newProfileColorScheme.clear(); + _newProfileRandomColorScheme = false; + // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out // the "global" fullscreen flag (-F). @@ -1002,21 +1118,23 @@ bool AppCommandlineArgs::ShouldExitEarly() const noexcept // - void AppCommandlineArgs::ValidateStartupCommands() { - // If we only have a single x-save command, then set our target to the - // current terminal window. This will prevent us from spawning a new - // window just to save the commandline. + // If we only have a single x-save or new-profile command, then set our + // target to the current terminal window. This will prevent us from spawning + // a new window just to save the commandline or create a profile. if (_startupActions.size() == 1 && - _startupActions.front().Action() == ShortcutAction::SaveSnippet && + (_startupActions.front().Action() == ShortcutAction::SaveSnippet || + _startupActions.front().Action() == ShortcutAction::NewProfile) && _windowTarget.empty()) { _windowTarget = "0"; } // If we parsed no commands, or the first command we've parsed is not a new // tab action, prepend a new-tab command to the front of the list. - // (also, we don't need to do this if the only action is a x-save) + // (also, we don't need to do this if the only action is a x-save or new-profile) else if (_startupActions.empty() || (_startupActions.front().Action() != ShortcutAction::NewTab && - _startupActions.front().Action() != ShortcutAction::SaveSnippet)) + _startupActions.front().Action() != ShortcutAction::SaveSnippet && + _startupActions.front().Action() != ShortcutAction::NewProfile)) { // Build the NewTab action from the values we've parsed on the commandline. NewTerminalArgs newTerminalArgs{}; diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 0a9de3999c..ef5c5e629e 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -93,6 +93,7 @@ private: CLI::App* _focusPaneCommand; CLI::App* _focusPaneShort; CLI::App* _saveCommand; + CLI::App* _newProfileCommand; // Are you adding a new sub-command? Make sure to update _noCommandsProvided! @@ -125,6 +126,11 @@ private: int _focusPaneTarget{ -1 }; std::string _saveInputName; std::string _keyChordOption; + + std::string _newProfileName; + std::string _newProfileStartingDirectory; + std::string _newProfileColorScheme; + bool _newProfileRandomColorScheme{ false }; // Are you adding more args here? Make sure to reset them in _resetStateToDefault const Commandline* _currentCommandline{ nullptr }; @@ -150,6 +156,7 @@ private: void _buildMovePaneParser(); void _buildSwapPaneParser(); void _buildFocusPaneParser(); + void _buildNewProfileParser(); bool _noCommandsProvided(); void _resetStateToDefault(); int _handleExit(const CLI::App& command, const CLI::Error& e); diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 2fe41f21a0..1d7ffff6b3 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -296,6 +296,21 @@ Save command line as input action + + Create a new profile from the current working directory and persist it to settings without opening a new tab or window + + + The name of the new profile. Defaults to the leaf of the current working directory + + + The starting directory of the new profile. Defaults to the current working directory + + + The name of the color scheme to assign to the new profile + + + Assign a random color scheme to the new profile + An optional argument diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 60e9178129..64e254d19c 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -34,6 +34,7 @@ #include "AddMarkArgs.g.cpp" #include "FindMatchArgs.g.cpp" #include "SaveSnippetArgs.g.cpp" +#include "NewProfileArgs.g.cpp" #include "ToggleCommandPaletteArgs.g.cpp" #include "SuggestionsArgs.g.cpp" #include "NewWindowArgs.g.cpp" @@ -924,6 +925,20 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return {}; } + winrt::hstring NewProfileArgs::GenerateName(const winrt::WARC::ResourceContext& context) const + { + auto str = std::wstring{ RS_switchable_(L"NewProfileNamePrefix") }; + if (!Name().empty()) + { + fmt::format_to(std::back_inserter(str), FMT_COMPILE(L", name: {}"), Name()); + } + if (!Commandline().empty()) + { + fmt::format_to(std::back_inserter(str), FMT_COMPILE(L", commandline: {}"), Commandline()); + } + return winrt::hstring{ str }; + } + static winrt::hstring _FormatColorString(const Control::SelectionColor& selectionColor) { if (!selectionColor) diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 45ba2cb144..fbe04ed528 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -35,6 +35,7 @@ #include "AddMarkArgs.g.h" #include "MoveTabArgs.g.h" #include "SaveSnippetArgs.g.h" +#include "NewProfileArgs.g.h" #include "ToggleCommandPaletteArgs.g.h" #include "SuggestionsArgs.g.h" #include "FindMatchArgs.g.h" @@ -225,6 +226,14 @@ protected: \ X(winrt::hstring, Commandline, "commandline", args->Commandline().empty(), ArgTypeHint::None, L"") \ X(winrt::hstring, KeyChord, "keyChord", false, ArgTypeHint::None, L"") +//////////////////////////////////////////////////////////////////////////////// +#define NEW_PROFILE_ARGS(X) \ + X(winrt::hstring, Name, "name", false, ArgTypeHint::None, L"") \ + X(winrt::hstring, StartingDirectory, "startingDirectory", false, ArgTypeHint::None, L"") \ + X(winrt::hstring, ColorScheme, "colorScheme", false, ArgTypeHint::None, L"") \ + X(winrt::hstring, Commandline, "commandline", false, ArgTypeHint::None, L"") \ + X(bool, RandomColorScheme, "randomColorScheme", false, ArgTypeHint::None, false) + //////////////////////////////////////////////////////////////////////////////// #define SUGGESTIONS_ARGS(X) \ X(SuggestionsSource, Source, "source", false, ArgTypeHint::None, SuggestionsSource::Tasks) \ @@ -930,6 +939,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ACTION_ARGS_STRUCT(SaveSnippetArgs, SAVE_TASK_ARGS); + ACTION_ARGS_STRUCT(NewProfileArgs, NEW_PROFILE_ARGS); + ACTION_ARGS_STRUCT(SuggestionsArgs, SUGGESTIONS_ARGS); ACTION_ARGS_STRUCT(FindMatchArgs, FIND_MATCH_ARGS); @@ -1066,6 +1077,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(MoveTabArgs); BASIC_FACTORY(OpenSettingsArgs); BASIC_FACTORY(SaveSnippetArgs); + BASIC_FACTORY(NewProfileArgs); BASIC_FACTORY(FindMatchArgs); BASIC_FACTORY(NewWindowArgs); BASIC_FACTORY(FocusPaneArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 67c241e239..0f591bed13 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -394,6 +394,17 @@ namespace Microsoft.Terminal.Settings.Model String KeyChord; }; + [default_interface] runtimeclass NewProfileArgs : IActionArgs, IActionArgsDescriptorAccess + { + NewProfileArgs(); + NewProfileArgs(String Name, String StartingDirectory, String ColorScheme, String Commandline, Boolean RandomColorScheme); + String Name; + String StartingDirectory; + String ColorScheme; + String Commandline; + Boolean RandomColorScheme; + }; + [default_interface] runtimeclass NewWindowArgs : IActionArgs, IActionArgsDescriptorAccess { NewWindowArgs(INewContentArgs contentArgs); diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 062d303c33..04ab53240a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -106,6 +106,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::MultipleActions, USES_RESOURCE(L"MultipleActions") }, { ShortcutAction::NewTab, USES_RESOURCE(L"NewTabCommandKey") }, { ShortcutAction::NewWindow, USES_RESOURCE(L"NewWindowCommandKey") }, + { ShortcutAction::NewProfile, USES_RESOURCE(L"NewProfileNamePrefix") }, { ShortcutAction::NextTab, USES_RESOURCE(L"NextTabCommandKey") }, { ShortcutAction::OpenAbout, USES_RESOURCE(L"OpenAboutCommandKey") }, { ShortcutAction::OpenCWD, USES_RESOURCE(L"OpenCWDCommandKey") }, diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index b9855d9e30..a800795aef 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -164,7 +164,9 @@ // They don't need to be parsed by the settings model, or saved as actions to // JSON. #define INTERNAL_SHORTCUT_ACTIONS \ - ON_ALL_ACTIONS(SaveSnippet) + ON_ALL_ACTIONS(SaveSnippet) \ + ON_ALL_ACTIONS(NewProfile) -#define INTERNAL_SHORTCUT_ACTIONS_WITH_ARGS \ - ON_ALL_ACTIONS_WITH_ARGS(SaveSnippet)\ +#define INTERNAL_SHORTCUT_ACTIONS_WITH_ARGS \ + ON_ALL_ACTIONS_WITH_ARGS(SaveSnippet) \ + ON_ALL_ACTIONS_WITH_ARGS(NewProfile) diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index a21afe9e88..08dd3f54b8 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -734,6 +734,9 @@ Save Snippet + + New profile + Open quick fix menu