add a subcommand for creating a new profile from the CWD

This commit is contained in:
Mike Griese
2026-06-16 13:26:48 -05:00
parent 8fe6c21ef8
commit ed2b77c078
10 changed files with 270 additions and 10 deletions

View File

@@ -11,6 +11,8 @@
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
#include "Utils.h"
#include <random>
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<NewProfileArgs>();
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<winrt::hstring> 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<size_t> 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

View File

@@ -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:
// - <none>
// Return Value:
// - <none>
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<std::wstring>();
}
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
// - <none>
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{};

View File

@@ -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);

View File

@@ -296,6 +296,21 @@
<data name="SaveSnippetDesc" xml:space="preserve">
<value>Save command line as input action</value>
</data>
<data name="CmdNewProfileDesc" xml:space="preserve">
<value>Create a new profile from the current working directory and persist it to settings without opening a new tab or window</value>
</data>
<data name="CmdNewProfileNameArgDesc" xml:space="preserve">
<value>The name of the new profile. Defaults to the leaf of the current working directory</value>
</data>
<data name="CmdNewProfileStartingDirArgDesc" xml:space="preserve">
<value>The starting directory of the new profile. Defaults to the current working directory</value>
</data>
<data name="CmdNewProfileColorSchemeArgDesc" xml:space="preserve">
<value>The name of the color scheme to assign to the new profile</value>
</data>
<data name="CmdNewProfileRandomColorSchemeArgDesc" xml:space="preserve">
<value>Assign a random color scheme to the new profile</value>
</data>
<data name="SaveSnippetArgDesc" xml:space="preserve">
<value>An optional argument</value>
</data>

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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") },

View File

@@ -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)

View File

@@ -734,6 +734,9 @@
<data name="SaveSnippetNamePrefix" xml:space="preserve">
<value>Save Snippet</value>
</data>
<data name="NewProfileNamePrefix" xml:space="preserve">
<value>New profile</value>
</data>
<data name="QuickFixCommandKey" xml:space="preserve">
<value>Open quick fix menu</value>
</data>