terminal/src/cascadia/TerminalApp/AppCommandlineArgs.cpp
Schuyler Rosefield 75e2b5fae7
Persist window layout cont. save multiple windows (#11083)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
Continuation of https://github.com/microsoft/terminal/pull/10972 to handle multiple windows, requires that to be merged first. 

<!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> 
## References

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
* [x] Also closes #766
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [ ] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [x] Schema updated.
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
Rough changelog:
Normally saving is triggered to occur every 30s, or sooner if a window is created/closed. The existing behavior of saving on last close is maintained to bypass that throttling. The automatic saving allows for crash recovery. Additionally all window layouts will be saved upon taking the `quit` action.

For loading we will check if we are the first window, that there are any saved layouts, and if the setting is enabled, and then depending on if we were given command line args or startup actions.

- create a new window for each saved layout, or
- take the first layout for our self and then a new window for each other layout.

This also saves the layout when the quit action is taken.

Misc changes
- A -s,--saved argument was added to the command line to facilitate opening all of the windows with the right settings. This also means that while a terminal session is running you can do wt -s idx to open a copy of window idx. There isn't a stable ordering of which idx each window gets saved as (it is whatever the iteration order of _peasants is), so it is just a cute hack for now.
- All position calculation has been moved up to AppHost this does mean we need to awkwardly pass around positions in a couple of unexpected places, but no solution was perfect.
- Renamed "Open tabs from a previous session" to "Open windows from a previous session". (not reflected in video below)
- Now save runtime tab color and window names
- Only enabled for non-elevated windows
- Add some change tracking to ApplicationState

<!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
![output](https://user-images.githubusercontent.com/6185249/131163473-d649d204-a589-41ad-b9d9-c4c0528cb684.gif)
2021-09-27 21:18:39 +00:00

1072 lines
44 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "AppCommandlineArgs.h"
#include "../types/inc/utils.hpp"
#include <LibraryResources.h>
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace TerminalApp;
// Either a ; at the start of a line, or a ; preceded by any non-\ char.
const std::wregex AppCommandlineArgs::_commandDelimiterRegex{ LR"(^;|[^\\];)" };
AppCommandlineArgs::AppCommandlineArgs()
{
_buildParser();
_resetStateToDefault();
}
// Method Description:
// - Attempt to parse a given command as a single commandline. If the command
// doesn't have a subcommand, we'll try parsing the commandline again, as a
// new-tab command.
// - Actions generated by this command are added to our _startupActions list.
// Arguments:
// - command: The individual commandline to parse as a command.
// Return Value:
// - 0 if the commandline was successfully parsed
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs::ParseCommand(const Commandline& command)
{
const int argc = static_cast<int>(command.Argc());
// Stash a pointer to the current Commandline instance we're parsing.
// When we're trying to parse the commandline for a new-tab/split-pane
// subcommand, we'll need to inspect the original Args from this
// Commandline to find the entirety of the commandline args for the new
// terminal instance. Discard the pointer when we leave this method. The
// pointer will be safe for usage, since the parse callback will be
// executed on the same thread, higher on the stack.
_currentCommandline = &command;
auto clearPointer = wil::scope_exit([this]() { _currentCommandline = nullptr; });
try
{
// CLI11 needs a mutable vector<string>, so copy out the args here.
// * When we're using the vector<string> parse(), it also expects that
// there isn't a leading executable name in the args, so slice that
// out.
// - In AppCommandlineArgs::BuildCommands, we'll make sure each
// subsequent command in a single commandline starts with a wt.exe.
// Our very first argument might not be "wt.exe", it could be `wt`,
// or `wtd.exe`, etc. Regardless, we want to ignore the first arg of
// every Commandline
// * Not only that, but this particular overload of parse() wants the
// args _reversed_ here.
std::vector<std::string> args{ command.Args().begin() + 1, command.Args().end() };
std::reverse(args.begin(), args.end());
// Revert our state to the initial state. As this function can be called
// multiple times during the parsing of a single commandline (once for each
// sub-command), we don't want the leftover state from previous calls to
// pollute this run's state.
_resetStateToDefault();
// Manually check for the "/?" or "-?" flags, to manually trigger the help text.
if (argc == 2 && (NixHelpFlag == til::at(command.Args(), 1) || WindowsHelpFlag == til::at(command.Args(), 1)))
{
throw CLI::CallForHelp();
}
// attempt to parse the commandline prefix of the form [options][subcommand]
_app.parse(args);
auto remainingParams = _app.remaining_size();
// If we parsed the commandline, and _no_ subcommands were provided, try
// parse the remaining suffix as a "new-tab" command.
if (_noCommandsProvided())
{
_newTabCommand.subcommand->parse(args);
remainingParams = _newTabCommand.subcommand->remaining_size();
}
// if after parsing the prefix and (optionally) the implicit tab subcommand
// we still have unparsed parameters we need to fail
if (remainingParams > 0)
{
throw CLI::ExtrasError(args);
}
}
catch (const CLI::ParseError& e)
{
return _handleExit(_app, e);
}
return 0;
}
// Method Description:
// - Calls App::exit() for the provided command, and collects it's output into
// our _exitMessage buffer.
// Arguments:
// - command: Either the root App object, or a subcommand for which to call exit() on.
// - e: the CLI::Error to process as the exit reason for parsing.
// Return Value:
// - 0 if the command exited successfully
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs::_handleExit(const CLI::App& command, const CLI::Error& e)
{
// Create some streams to collect the output that would otherwise go to stdout.
std::ostringstream out;
std::ostringstream err;
const auto result = command.exit(e, out, err);
// I believe only CallForHelp will return 0
if (result == 0)
{
_exitMessage = out.str();
}
else
{
_exitMessage = err.str();
}
// We're displaying an error message - we should always exit instead of
// actually starting the Terminal.
_shouldExitEarly = true;
return result;
}
// Method Description:
// - Add each subcommand and options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildParser()
{
// We define or parser as a prefix command, to support "implicit new tab subcommand" scenario.
// In this scenario we will try to parse the prefix that contains parameters like launch mode,
// but will not encounter an explicit command.
// Instead we will encounter an argument that doesn't belong to the prefix indicating the prefix is over.
// Then we will try to parse the remaining arguments as a new tab subcommand.
// E.g., for "wt.exe -M -d c:/", we will use -M for the launch mode, but once we will encounter -d
// we will know that the prefix is over and try to handle the suffix as a new tab subcommand
_app.prefix_command();
// -v,--version: Displays version info
auto versionCallback = [this](int64_t /*count*/) {
// Set our message to display the application name and the current version.
_exitMessage = fmt::format("{0}\n{1}",
til::u16u8(CascadiaSettings::ApplicationDisplayName()),
til::u16u8(CascadiaSettings::ApplicationVersion()));
// Theoretically, we don't need to exit now, since this isn't really
// an error case. However, in practice, it feels weird to have `wt
// -v` open a new tab, and makes enough sense that `wt -v ;
// split-pane` (or whatever) just displays the version and exits.
_shouldExitEarly = true;
};
_app.add_flag_function("-v,--version", versionCallback, RS_A(L"CmdVersionDesc"));
// Launch mode related flags
// -M,--maximized: Maximizes the window on launch
// -F,--fullscreen: Fullscreens the window on launch
// -f,--focus: Sets the terminal into the Focus mode
// While fullscreen excludes both maximized and focus mode, the user can combine between the maximized and focused (-fM)
auto maximizedCallback = [this](int64_t /*count*/) {
_launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::FocusMode) ?
LaunchMode::MaximizedFocusMode :
LaunchMode::MaximizedMode;
};
auto fullscreenCallback = [this](int64_t /*count*/) {
_launchMode = LaunchMode::FullscreenMode;
};
auto focusCallback = [this](int64_t /*count*/) {
_launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::MaximizedMode) ?
LaunchMode::MaximizedFocusMode :
LaunchMode::FocusMode;
};
auto maximized = _app.add_flag_function("-M,--maximized", maximizedCallback, RS_A(L"CmdMaximizedDesc"));
auto fullscreen = _app.add_flag_function("-F,--fullscreen", fullscreenCallback, RS_A(L"CmdFullscreenDesc"));
auto focus = _app.add_flag_function("-f,--focus", focusCallback, RS_A(L"CmdFocusDesc"));
maximized->excludes(fullscreen);
focus->excludes(fullscreen);
_app.add_option("-w,--window",
_windowTarget,
RS_A(L"CmdWindowTargetArgDesc"));
_app.add_option("-s,--saved",
_loadPersistedLayoutIdx,
RS_A(L"CmdSavedLayoutArgDesc"));
// Subcommands
_buildNewTabParser();
_buildSplitPaneParser();
_buildFocusTabParser();
_buildMoveFocusParser();
_buildMovePaneParser();
_buildSwapPaneParser();
_buildFocusPaneParser();
}
// Method Description:
// - Adds the `new-tab` subcommand and related options to the commandline parser.
// - Additionally adds the `nt` subcommand, which is just a shortened version of `new-tab`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildNewTabParser()
{
_newTabCommand.subcommand = _app.add_subcommand("new-tab", RS_A(L"CmdNewTabDesc"));
_newTabShort.subcommand = _app.add_subcommand("nt", RS_A(L"CmdNTDesc"));
auto setupSubcommand = [this](auto& subcommand) {
_addNewTerminalArgs(subcommand);
// 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.subcommand->callback([&, this]() {
// Build the NewTab action from the values we've parsed on the commandline.
ActionAndArgs newTabAction{};
newTabAction.Action(ShortcutAction::NewTab);
// _getNewTerminalArgs MUST be called before parsing any other options,
// as it might clear those options while finding the commandline
NewTabArgs args{ _getNewTerminalArgs(subcommand) };
newTabAction.Args(args);
_startupActions.push_back(newTabAction);
});
};
setupSubcommand(_newTabCommand);
setupSubcommand(_newTabShort);
}
// Method Description:
// - Adds the `split-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `sp` subcommand, which is just a shortened version of `split-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildSplitPaneParser()
{
_newPaneCommand.subcommand = _app.add_subcommand("split-pane", RS_A(L"CmdSplitPaneDesc"));
_newPaneShort.subcommand = _app.add_subcommand("sp", RS_A(L"CmdSPDesc"));
auto setupSubcommand = [this](auto& subcommand) {
_addNewTerminalArgs(subcommand);
subcommand._horizontalOption = subcommand.subcommand->add_flag("-H,--horizontal",
_splitHorizontal,
RS_A(L"CmdSplitPaneHorizontalArgDesc"));
subcommand._verticalOption = subcommand.subcommand->add_flag("-V,--vertical",
_splitVertical,
RS_A(L"CmdSplitPaneVerticalArgDesc"));
subcommand._verticalOption->excludes(subcommand._horizontalOption);
auto* sizeOpt = subcommand.subcommand->add_option("-s,--size",
_splitPaneSize,
RS_A(L"CmdSplitPaneSizeArgDesc"));
subcommand._duplicateOption = subcommand.subcommand->add_flag("-D,--duplicate",
_splitDuplicate,
RS_A(L"CmdSplitPaneDuplicateArgDesc"));
sizeOpt->check(CLI::Range(0.01f, 0.99f));
// 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.subcommand->callback([&, this]() {
// Build the SplitPane action from the values we've parsed on the commandline.
ActionAndArgs splitPaneActionAndArgs{};
splitPaneActionAndArgs.Action(ShortcutAction::SplitPane);
// _getNewTerminalArgs MUST be called before parsing any other options,
// as it might clear those options while finding the commandline
auto terminalArgs{ _getNewTerminalArgs(subcommand) };
auto style{ SplitDirection::Automatic };
// Make sure to use the `Option`s here to check if they were set -
// _getNewTerminalArgs might reset them while parsing a commandline
if ((*subcommand._horizontalOption || *subcommand._verticalOption))
{
if (_splitHorizontal)
{
style = SplitDirection::Down;
}
else if (_splitVertical)
{
style = SplitDirection::Right;
}
}
const auto splitMode{ subcommand._duplicateOption && _splitDuplicate ? SplitType::Duplicate : SplitType::Manual };
SplitPaneArgs args{ splitMode, style, _splitPaneSize, terminalArgs };
splitPaneActionAndArgs.Args(args);
_startupActions.push_back(splitPaneActionAndArgs);
});
};
setupSubcommand(_newPaneCommand);
setupSubcommand(_newPaneShort);
}
// Method Description:
// - Adds the `move-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMovePaneParser()
{
_movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc"));
_movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc"));
auto setupSubcommand = [this](auto* subcommand) {
subcommand->add_option("-t,--tab",
_movePaneTabIndex,
RS_A(L"CmdMovePaneTabArgDesc"));
// 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]() {
// Build the action from the values we've parsed on the commandline.
ActionAndArgs movePaneAction{};
if (_movePaneTabIndex >= 0)
{
movePaneAction.Action(ShortcutAction::MovePane);
MovePaneArgs args{ static_cast<unsigned int>(_movePaneTabIndex) };
movePaneAction.Args(args);
_startupActions.push_back(movePaneAction);
}
});
};
setupSubcommand(_movePaneCommand);
setupSubcommand(_movePaneShort);
}
// Method Description:
// - Adds the `focus-tab` subcommand and related options to the commandline parser.
// - Additionally adds the `ft` subcommand, which is just a shortened version of `focus-tab`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildFocusTabParser()
{
_focusTabCommand = _app.add_subcommand("focus-tab", RS_A(L"CmdFocusTabDesc"));
_focusTabShort = _app.add_subcommand("ft", RS_A(L"CmdFTDesc"));
auto setupSubcommand = [this](auto* subcommand) {
auto* indexOpt = subcommand->add_option("-t,--target",
_focusTabIndex,
RS_A(L"CmdFocusTabTargetArgDesc"));
auto* nextOpt = subcommand->add_flag("-n,--next",
_focusNextTab,
RS_A(L"CmdFocusTabNextArgDesc"));
auto* prevOpt = subcommand->add_flag("-p,--previous",
_focusPrevTab,
RS_A(L"CmdFocusTabPrevArgDesc"));
nextOpt->excludes(prevOpt);
indexOpt->excludes(prevOpt);
indexOpt->excludes(nextOpt);
// 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]() {
// Build the action from the values we've parsed on the commandline.
ActionAndArgs focusTabAction{};
if (_focusTabIndex >= 0)
{
focusTabAction.Action(ShortcutAction::SwitchToTab);
SwitchToTabArgs args{ static_cast<unsigned int>(_focusTabIndex) };
focusTabAction.Args(args);
_startupActions.push_back(focusTabAction);
}
else if (_focusNextTab || _focusPrevTab)
{
focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab);
// GH#10070 - make sure to not use the MRU order when switching
// tabs on the commandline. That wouldn't make any sense!
focusTabAction.Args(_focusNextTab ?
static_cast<IActionArgs>(NextTabArgs(TabSwitcherMode::Disabled)) :
static_cast<IActionArgs>(PrevTabArgs(TabSwitcherMode::Disabled)));
_startupActions.push_back(std::move(focusTabAction));
}
});
};
setupSubcommand(_focusTabCommand);
setupSubcommand(_focusTabShort);
}
static const std::map<std::string, FocusDirection> focusDirectionMap = {
{ "left", FocusDirection::Left },
{ "right", FocusDirection::Right },
{ "up", FocusDirection::Up },
{ "down", FocusDirection::Down },
{ "previous", FocusDirection::Previous },
{ "nextInOrder", FocusDirection::NextInOrder },
{ "previousInOrder", FocusDirection::PreviousInOrder },
{ "first", FocusDirection::First },
};
// Method Description:
// - Adds the `move-focus` subcommand and related options to the commandline parser.
// - Additionally adds the `mf` subcommand, which is just a shortened version of `move-focus`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMoveFocusParser()
{
_moveFocusCommand = _app.add_subcommand("move-focus", RS_A(L"CmdMoveFocusDesc"));
_moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc"));
auto setupSubcommand = [this](auto* subcommand) {
auto* directionOpt = subcommand->add_option("direction",
_moveFocusDirection,
RS_A(L"CmdMoveFocusDirectionArgDesc"));
directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
directionOpt->required();
// 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]() {
if (_moveFocusDirection != FocusDirection::None)
{
MoveFocusArgs args{ _moveFocusDirection };
ActionAndArgs actionAndArgs{};
actionAndArgs.Action(ShortcutAction::MoveFocus);
actionAndArgs.Args(args);
_startupActions.push_back(std::move(actionAndArgs));
}
});
};
setupSubcommand(_moveFocusCommand);
setupSubcommand(_moveFocusShort);
}
// Method Description:
// - Adds the `swap-pane` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildSwapPaneParser()
{
_swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc"));
auto setupSubcommand = [this](auto* subcommand) {
auto* directionOpt = subcommand->add_option("direction",
_swapPaneDirection,
RS_A(L"CmdSwapPaneDirectionArgDesc"));
directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
directionOpt->required();
// 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]() {
if (_swapPaneDirection != FocusDirection::None)
{
SwapPaneArgs args{ _swapPaneDirection };
ActionAndArgs actionAndArgs{};
actionAndArgs.Action(ShortcutAction::SwapPane);
actionAndArgs.Args(args);
_startupActions.push_back(std::move(actionAndArgs));
}
});
};
setupSubcommand(_swapPaneCommand);
}
// Method Description:
// - Adds the `focus-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildFocusPaneParser()
{
_focusPaneCommand = _app.add_subcommand("focus-pane", RS_A(L"CmdFocusPaneDesc"));
_focusPaneShort = _app.add_subcommand("fp", RS_A(L"CmdFPDesc"));
auto setupSubcommand = [this](auto* subcommand) {
auto* targetOpt = subcommand->add_option("-t,--target",
_focusPaneTarget,
RS_A(L"CmdFocusPaneTargetArgDesc"));
targetOpt->required();
targetOpt->check(CLI::NonNegativeNumber);
// 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]() {
// Build the action from the values we've parsed on the commandline.
if (_focusPaneTarget >= 0)
{
ActionAndArgs focusPaneAction{};
focusPaneAction.Action(ShortcutAction::FocusPane);
FocusPaneArgs args{ static_cast<uint32_t>(_focusPaneTarget) };
focusPaneAction.Args(args);
_startupActions.push_back(focusPaneAction);
}
});
};
setupSubcommand(_focusPaneCommand);
setupSubcommand(_focusPaneShort);
}
// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
// that subcommand to support all the properties in a NewTerminalArgs.
// Arguments:
// - subcommand: the command to add the args to.
// Return Value:
// - <none>
void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand)
{
subcommand.profileNameOption = subcommand.subcommand->add_option("-p,--profile",
_profileName,
RS_A(L"CmdProfileArgDesc"));
subcommand.startingDirectoryOption = subcommand.subcommand->add_option("-d,--startingDirectory",
_startingDirectory,
RS_A(L"CmdStartingDirArgDesc"));
subcommand.titleOption = subcommand.subcommand->add_option("--title",
_startingTitle,
RS_A(L"CmdTitleArgDesc"));
subcommand.tabColorOption = subcommand.subcommand->add_option("--tabColor",
_startingTabColor,
RS_A(L"CmdTabColorArgDesc"));
subcommand.suppressApplicationTitleOption = subcommand.subcommand->add_flag(
"--suppressApplicationTitle,!--useApplicationTitle",
_suppressApplicationTitle,
RS_A(L"CmdSuppressApplicationTitleDesc"));
subcommand.colorSchemeOption = subcommand.subcommand->add_option("--colorScheme",
_startingColorScheme,
RS_A(L"CmdColorSchemeArgDesc"));
// Using positionals_at_end allows us to support "wt new-tab -d wsl -d Ubuntu"
// without CLI11 thinking that we've specified -d twice.
// There's an alternate construction where we make all subcommands "prefix commands",
// which lets us get all remaining non-option args provided at the end, but that
// doesn't support "wt new-tab -- wsl -d Ubuntu -- sleep 10" because the first
// -- breaks out of the subcommand (instead of the subcommand options).
// See https://github.com/CLIUtils/CLI11/issues/417 for more info.
subcommand.commandlineOption = subcommand.subcommand->add_option("command", _commandline, RS_A(L"CmdCommandArgDesc"));
subcommand.subcommand->positionals_at_end(true);
}
// Method Description:
// - Build a NewTerminalArgs instance from the data we've parsed
// Arguments:
// - <none>
// Return Value:
// - A fully initialized NewTerminalArgs corresponding to values we've currently parsed.
NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand)
{
NewTerminalArgs args{};
if (!_commandline.empty())
{
std::ostringstream cmdlineBuffer;
for (const auto& arg : _commandline)
{
if (cmdlineBuffer.tellp() != 0)
{
// If there's already something in here, prepend a space
cmdlineBuffer << ' ';
}
if (arg.find(" ") != std::string::npos)
{
cmdlineBuffer << '"' << arg << '"';
}
else
{
cmdlineBuffer << arg;
}
}
args.Commandline(winrt::to_hstring(cmdlineBuffer.str()));
}
if (*subcommand.profileNameOption)
{
args.Profile(winrt::to_hstring(_profileName));
}
if (*subcommand.startingDirectoryOption)
{
args.StartingDirectory(winrt::to_hstring(_startingDirectory));
}
if (*subcommand.titleOption)
{
args.TabTitle(winrt::to_hstring(_startingTitle));
}
if (*subcommand.tabColorOption)
{
try
{
// This is gonna throw whenever the string that's currently being parsed
// isn't a valid hex string. Let's just eat anything this throws because
// we should only lock in the TabColor arg when the user gives a valid hex
// str, and we shouldn't crash when the user gives us anything else.
const auto tabColor = Microsoft::Console::Utils::ColorFromHexString(_startingTabColor);
args.TabColor(static_cast<winrt::Windows::UI::Color>(tabColor));
}
catch (...)
{
}
}
if (*subcommand.suppressApplicationTitleOption)
{
args.SuppressApplicationTitle(_suppressApplicationTitle);
}
if (*subcommand.colorSchemeOption)
{
args.ColorScheme(winrt::to_hstring(_startingColorScheme));
}
return args;
}
// Method Description:
// - This function should return true if _no_ subcommands were parsed from the
// given commandline. In that case, we'll fall back to trying the commandline
// as a new tab command.
// Arguments:
// - <none>
// Return Value:
// - true if no sub commands were parsed.
bool AppCommandlineArgs::_noCommandsProvided()
{
return !(*_newTabCommand.subcommand ||
*_newTabShort.subcommand ||
*_focusTabCommand ||
*_focusTabShort ||
*_moveFocusCommand ||
*_moveFocusShort ||
*_movePaneCommand ||
*_movePaneShort ||
*_swapPaneCommand ||
*_focusPaneCommand ||
*_focusPaneShort ||
*_newPaneShort.subcommand ||
*_newPaneCommand.subcommand);
}
// Method Description:
// - Reset any state we might have accumulated back to its default values. Since
// we'll be re-using these members across the parsing of many commandlines, we
// need to make sure the state from one run doesn't pollute the following one.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_resetStateToDefault()
{
_profileName.clear();
_startingDirectory.clear();
_startingTitle.clear();
_startingTabColor.clear();
_commandline.clear();
_suppressApplicationTitle = false;
_splitVertical = false;
_splitHorizontal = false;
_splitPaneSize = 0.5f;
_splitDuplicate = false;
_movePaneTabIndex = -1;
_focusTabIndex = -1;
_focusNextTab = false;
_focusPrevTab = false;
_moveFocusDirection = FocusDirection::None;
_swapPaneDirection = FocusDirection::None;
_focusPaneTarget = -1;
_loadPersistedLayoutIdx = -1;
// 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).
// Same with _windowTarget.
}
// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
// Commandline represents a single command to parse. These commands can be
// separated by ";", which indicates the start of the next commandline. If the
// user would like to provide ';' in the text of the commandline, they can
// escape it as "\;".
// Arguments:
// - args: an array of arguments to parse into Commandlines
// Return Value:
// - a list of Commandline objects, where each one represents a single
// commandline to parse.
std::vector<Commandline> AppCommandlineArgs::BuildCommands(winrt::array_view<const winrt::hstring>& args)
{
std::vector<Commandline> commands;
commands.emplace_back(Commandline{});
// For each arg in argv:
// Check the string for a delimiter.
// * If there isn't a delimiter, add the arg to the current commandline.
// * If there is a delimiter, split the string at that delimiter. Add the
// first part of the string to the current command, and start a new
// command with the second bit.
for (const auto& arg : args)
{
_addCommandsForArg(commands, { arg });
}
return commands;
}
// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
// Commandline represents a single command to parse. These commands can be
// separated by ";", which indicates the start of the next commandline. If the
// user would like to provide ';' in the text of the commandline, they can
// escape it as "\;".
// Arguments:
// - argc: the number of arguments provided in argv
// - argv: a c-style array of wchar_t strings. These strings can include spaces in them.
// Return Value:
// - a list of Commandline objects, where each one represents a single
// commandline to parse.
std::vector<Commandline> AppCommandlineArgs::BuildCommands(const std::vector<const wchar_t*>& args)
{
std::vector<Commandline> commands;
// Initialize a first Commandline without a leading `wt.exe` argument. When
// we're run from the commandline, `wt.exe` (or whatever the exe's name is)
// will be the first argument passed to us
commands.resize(1);
// For each arg in argv:
// Check the string for a delimiter.
// * If there isn't a delimiter, add the arg to the current commandline.
// * If there is a delimiter, split the string at that delimiter. Add the
// first part of the string to the current command, and start a new
// command with the second bit.
for (const auto& arg : args)
{
_addCommandsForArg(commands, { arg });
}
return commands;
}
// Function Description:
// - Update and append Commandline objects for the given arg to the given list
// of commands. Each Commandline represents a single command to parse. These
// commands can be separated by ";", which indicates the start of the next
// commandline. If the user would like to provide ';' in the text of the
// commandline, they can escape it as "\;".
// - As we parse arg, if it doesn't contain a delimiter in it, we'll add it to
// the last command in commands. Otherwise, we'll generate a new Commandline
// object for each command in arg.
// Arguments:
// - commands: a list of Commandline objects to modify and append to
// - arg: a single argument that should be parsed into args to append to the
// current command, or create more Commandlines
// Return Value:
// <none>
void AppCommandlineArgs::_addCommandsForArg(std::vector<Commandline>& commands, std::wstring_view arg)
{
std::wstring remaining{ arg };
std::wsmatch match;
// Keep looking for matches until we've found no unescaped delimiters,
// or we've hit the end of the string.
std::regex_search(remaining, match, AppCommandlineArgs::_commandDelimiterRegex);
do
{
if (match.empty())
{
// Easy case: no delimiter. Add it to the current command.
commands.back().AddArg(remaining);
break;
}
else
{
// Harder case: There was a match.
const bool matchedFirstChar = match.position(0) == 0;
// If the match was at the beginning of the string, then the
// next arg should be "", since there was no content before the
// delimiter. Otherwise, add one, since the regex will include
// the last character of the string before the delimiter.
const auto delimiterPosition = matchedFirstChar ? match.position(0) : match.position(0) + 1;
const auto nextArg = remaining.substr(0, delimiterPosition);
if (!nextArg.empty())
{
commands.back().AddArg(nextArg);
}
// Create a new commandline
commands.emplace_back(Commandline{});
// Initialize it with "wt.exe" as the first arg, as if that command
// was passed individually by the user on the commandline.
commands.back().AddArg(std::wstring{ AppCommandlineArgs::PlaceholderExeName });
// Look for the next match in the string, but updating our
// remaining to be the text after the match.
remaining = match.suffix().str();
std::regex_search(remaining, match, AppCommandlineArgs::_commandDelimiterRegex);
}
} while (!remaining.empty());
}
// Method Description:
// - Returns the deque of actions we've buffered as a result of parsing commands.
// Arguments:
// - <none>
// Return Value:
// - the deque of actions we've buffered as a result of parsing commands.
std::vector<ActionAndArgs>& AppCommandlineArgs::GetStartupActions()
{
return _startupActions;
}
// Method Description:
// - Returns whether we should start listening for inbound PTY connections
// coming from the operating system default application feature.
// Arguments:
// - <none>
// Return Value:
// - True if the listener should be started. False otherwise.
bool AppCommandlineArgs::IsHandoffListener() const noexcept
{
return _isHandoffListener;
}
// Method Description:
// - Get the string of text that should be displayed to the user on exit. This
// is usually helpful for cases where the user entered some sort of invalid
// commandline. It's additionally also used when the user has requested the
// help text.
// Arguments:
// - <none>
// Return Value:
// - The help text, or an error message, generated from parsing the input
// provided by the user.
const std::string& AppCommandlineArgs::GetExitMessage()
{
return _exitMessage;
}
// Method Description:
// - Returns true if we should exit the application before even starting the
// window. We might want to do this if we're displaying an error message or
// the version string, or if we want to open the settings file.
// Arguments:
// - <none>
// Return Value:
// - true iff we should exit the application before even starting the window
bool AppCommandlineArgs::ShouldExitEarly() const noexcept
{
return _shouldExitEarly;
}
// Method Description:
// - Ensure that the first command in our list of actions is a NewTab action.
// This makes sure that if the user passes a commandline like "wt split-pane
// -H", we _first_ create a new tab, so there's always at least one tab.
// - If the first command in our queue of actions is a NewTab action, this does
// nothing.
// - This should only be called once - if the first NewTab action is popped from
// our _startupActions, calling this again will add another.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::ValidateStartupCommands()
{
// Only check over the actions list for the potential to add a new-tab
// command if we are not starting for the purposes of receiving an inbound
// handoff connection from the operating system.
if (!_isHandoffListener)
{
// 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.
if (_startupActions.empty() ||
_startupActions.front().Action() != ShortcutAction::NewTab)
{
// Build the NewTab action from the values we've parsed on the commandline.
NewTerminalArgs newTerminalArgs{};
NewTabArgs args{ newTerminalArgs };
ActionAndArgs newTabAction{ ShortcutAction::NewTab, args };
// push the arg onto the front
_startupActions.insert(_startupActions.begin(), 1, newTabAction);
}
}
}
std::optional<uint32_t> AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept
{
return _loadPersistedLayoutIdx >= 0 ?
std::optional{ static_cast<uint32_t>(_loadPersistedLayoutIdx) } :
std::nullopt;
}
std::optional<winrt::Microsoft::Terminal::Settings::Model::LaunchMode> AppCommandlineArgs::GetLaunchMode() const noexcept
{
return _launchMode;
}
// Method Description:
// - Attempts to parse an array of commandline args into a list of
// commands to execute, and then parses these commands. As commands are
// successfully parsed, they will generate ShortcutActions for us to be
// able to execute. If we fail to parse any commands, we'll return the
// error code from the failure to parse that command, and stop processing
// additional commands.
// - The first arg in args should be the program name "wt" (or some variant). It
// will be ignored during parsing.
// Arguments:
// - args: an array of strings to process as a commandline. These args can contain spaces
// Return Value:
// - 0 if the commandline was successfully parsed
int AppCommandlineArgs::ParseArgs(winrt::array_view<const winrt::hstring>& args)
{
for (const auto& arg : args)
{
if (arg == L"-Embedding")
{
_isHandoffListener = true;
return 0;
}
}
auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args);
for (auto& cmdBlob : commands)
{
// On one hand, it seems like we should be able to have one
// AppCommandlineArgs for parsing all of them, and collect the
// results one at a time.
//
// On the other hand, re-using a CLI::App seems to leave state from
// previous parsings around, so we could get mysterious behavior
// where one command affects the values of the next.
//
// From https://cliutils.github.io/CLI11/book/chapters/options.html:
// > If that option is not given, CLI11 will not touch the initial
// > value. This allows you to set up defaults by simply setting
// > your value beforehand.
//
// So we pretty much need the to either manually reset the state
// each command, or build new ones.
const auto result = ParseCommand(cmdBlob);
// If this succeeded, result will be 0. Otherwise, the caller should
// exit(result), to exit the program.
if (result != 0)
{
return result;
}
}
// If all the args were successfully parsed, we'll have some commands
// built in _appArgs, which we'll use when the application starts up.
return 0;
}
// Method Description:
// - Attempts to parse an array of commandline args into a list of
// commands to execute, and then parses these commands. As commands are
// successfully parsed, they will generate ShortcutActions for us to be
// able to execute. If we fail to parse any commands, we'll return the
// error code from the failure to parse that command, and stop processing
// additional commands.
// - The first arg in args should be the program name "wt" (or some variant). It
// will be ignored during parsing.
// Arguments:
// - args: ExecuteCommandlineArgs describing the command line to parse
// Return Value:
// - 0 if the commandline was successfully parsed
int AppCommandlineArgs::ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args)
{
if (!args || args.Commandline().empty())
{
return 0;
}
// Convert the commandline into an array of args with
// CommandLineToArgvW, similar to how the app typically does when
// called from the commandline.
int argc = 0;
wil::unique_any<LPWSTR*, decltype(&::LocalFree), ::LocalFree> argv{ CommandLineToArgvW(args.Commandline().c_str(), &argc) };
if (argv)
{
std::vector<winrt::hstring> args;
// Make sure the first argument is wt.exe, because ParseArgs will
// always skip the program name. The particular value of this first
// string doesn't terribly matter.
args.emplace_back(L"wt.exe");
for (auto& elem : wil::make_range(argv.get(), argc))
{
args.emplace_back(elem);
}
winrt::array_view<const winrt::hstring> argsView{ args };
return ParseArgs(argsView);
}
return 0;
}
// Method Description:
// - Allows disabling addition of help-related info in the exit message
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::DisableHelpInExitMessage()
{
_app.set_help_flag();
_app.set_help_all_flag();
}
// Method Description:
// - Resets the state to allow external consumers to reuse this instance
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::FullResetState()
{
_resetStateToDefault();
_currentCommandline = nullptr;
_launchMode = std::nullopt;
_startupActions.clear();
_exitMessage = "";
_shouldExitEarly = false;
_isHandoffListener = false;
_windowTarget = {};
}
std::string_view AppCommandlineArgs::GetTargetWindow() const noexcept
{
return _windowTarget;
}