terminal/doc/specs/#607 - Commandline Argument...

34 KiB
Raw Permalink Blame History

author created on last updated issue id
Mike Griese @zadjii-msft 2019-11-08 2020-01-15

Commandline Arguments for the Windows Terminal

Abstract

This spec outlines the changes necessary for Windows Terminal to support commandline arguments. These arguments can be used to enable customized launch scenarios for the Terminal, such as booting directly into a specific profile or directory.

Inspiration

Since the addition of the "execution alias" wt.exe which enables launching the Windows Terminal from the commandline, we've always wanted to support arguments to enable custom launch scenarios. This need was amplified by requests like:

  • #576, which wanted to add jumplist entries for the Windows Terminal, but was blocked because there was no way of communicating to the Terminal which profile it wanted to launch
  • #1060 - being able to right-click in explorer to "open a Windows Terminal Here" is great, but would be more powerful if it could also provide options to open specific profiles in that directory.
  • #2068 - We want the user to be able to (from inside the Terminal) not only open a new window with the default profile, but also open the new window with a specific profile.

Additionally, the final design for the arguments was heavily inspired by the arguments available to tmux, which also enables robust startup configuration through commandline arguments.

User Stories

Lets consider some different ways that a user or developer might want want to use commandline arguments, to help guide the design.

  1. A user wants to open the Windows Terminal with their default profile.
  • This one is easy, it's already provided with simply wt.
  1. A user wants to open the Windows Terminal with a specific profile from their list of profiles.
  2. A user wants to open the Windows Terminal with their default profile, but running a different commandline than usual.
  3. A user wants to know the list of arguments supported by wt.exe.
  4. A user wants to see their list of profiles, so they can open one in particular
  5. A user wants to open their settings file, without needing to open the Terminal window.
  6. A user wants to know what version of the Windows Terminal they are running, without needing to open the Terminal window.
  7. A user wants to open the Windows Terminal at a specific location on the screen
  8. A user wants to open the Windows Terminal in a specific directory.
  9. A user wants to open the Windows Terminal with a specific size
  10. A user wants to open the Windows Terminal with only the default settings, ignoring their user settings.
  11. A user wants to open the Windows Terminal with multiple tabs open simultaneously, each with different profiles, starting directories, even commandlines
  12. A user wants to open the Windows Terminal with multiple tabs and panes open simultaneously, each with different profiles, starting directories, even commandlines, and specific split sizes
  13. A user wants to use a file to provide a reusable startup configuration with many steps, to avoid needing to type the commandline each time.

Solution Design

Proposal 1 - Parameters

Initially, I had considered arguments in the following style:

  • --help: Display the help message
  • --version: Display version info for the Windows Terminal
  • --list-profiles: Display a list of the available profiles
    • --all to also show "hidden" profiles
    • --verbose? To also display GUIDs?
  • --open-settings: Open the settings file
  • --profile <profile name>: Start with the given profile, by name
  • --guid <profile guid>: Start with the given profile, by GUID
  • --startingDirectory <path>: Start in the given directory
  • --initialRows <rows>, --initialCols <rows>: Start with a specific size
  • --initialPosition <x,y>: Start at an initial location on the screen
  • -- <commandline>: Start with this commandline instead

However, this style of arguments makes it very challenging to start multiple tabs or panes simultaneously. How would a user start multiple panes, each with a different commandline? As configurations become more complex, these commandlines would quickly become hard to parse and understand for the user.

Proposal 2 - Commands and Parameters

Instead, we'll try to separate these arguments by their responsibilities. Some of these arguments cause something to happen, like help, version, or open-settings. Other arguments act more like modifiers, like for example --profile or --startingDirectory, which provide additional information to the action of opening a new tab. Lets try and define these concepts more clearly.

Commands are arguments that cause something to happen. They're provided in kebab-case, and can have some number of optional or required "parameters".

Parameters are arguments that provide additional information to "commands". They can be provided in either a long form or a short form. In the long form, they're provided in --camelCase, with two hyphens preceding the argument name. In short form, they're provided as just a single character preceded by a hyphen, like so: -c.

Let's enumerate some possible example commandlines, with explanations, to demonstrate:

Sample Commandlines


# Runs the user's "Windows Powershell" profile in a new tab (user story 2)
wt new-tab --profile "Windows Powershell"
wt --profile "Windows Powershell"
wt -p "Windows Powershell"

# Runs the user's default profile in a new tab, running cmd.exe (user story 3)
wt cmd.exe

# display the help text (user story 4)
wt help
wt --help
wt -h
wt -?
wt /?

# output the list of profiles (user story 5)
wt list-profiles

# open the settings file, without opening the Terminal window (user story 6)
wt open-settings

# Display version info for the Windows Terminal (user story 7)
wt version
wt --version
wt -v

# Start the default profile in directory "c:/Users/Foo/dev/MyProject" (user story 9)
wt new-tab --startingDirectory "c:/Users/Foo/dev/MyProject"
wt --startingDirectory "c:/Users/Foo/dev/MyProject"
wt -d "c:/Users/Foo/dev/MyProject"
# Windows-style paths work too
wt -d "c:\Users\Foo\dev\MyProject"

# Runs the user's "Windows Powershell" profile in a new tab in directory
#  "c:/Users/Foo/dev/MyProject" (user story 2, 9)
wt new-tab --profile "Windows Powershell" --startingDirectory "c:/Users/Foo/dev/MyProject"
wt --profile "Windows Powershell" --startingDirectory "c:/Users/Foo/dev/MyProject"
wt -p "Windows Powershell" -d "c:/Users/Foo/dev/MyProject"

# open a new tab with the "Windows Powershell" profile, and another with the
#  "cmd" profile (user story 12)
wt new-tab --profile "Windows Powershell" ; new-tab --profile "cmd"
wt --profile "Windows Powershell" ; new-tab --profile "cmd"
wt --profile "Windows Powershell" ; --profile "cmd"
wt --p "Windows Powershell" ; --p "cmd"

# run "my-commandline.exe with some args" in a new tab
wt new-tab my-commandline.exe with some args
wt my-commandline.exe with some args

# run "my-commandline.exe with some args and a ; literal semicolon" in a new
#  tab, and in another tab, run "another.exe running in a second tab"
wt my-commandline.exe with some args and a \; literal semicolon ; new-tab another.exe running in a second tab

# Start cmd.exe, then split it vertically (with the first taking 70% of it's
#  space, and the new pane taking 30%), and run wsl.exe in that pane (user story 13)
wt cmd.exe ; split-pane --target 0 -V -% 30 wsl.exe
wt cmd.exe ; split-pane -% 30 wsl.exe

# Create a new window with the default profile, create a vertical split with the
#  default profile, then create a horizontal split in the second pane and run
#  "media.exe" (user story 13)
wt new-tab ; split-pane -V ; split-pane --target 1 -H media.exe
wt new-tab ; split-pane -V ; split-pane -t 1 -H media.exe

wt Syntax

The wt commandline is divided into two main sections: "Options", and "Commands":

wt [options] [command ; ]...

Options are a list of flags and other parameters that can control the behavior of the wt commandline as a whole. Commands are a semicolon-delimited list of commands and arguments for those commands.

If no command is specified in a command, then the command is assumed to be a new-tab command by default. So, for example, wt cmd.exe is interpreted the same as wt new-tab cmd.exe.

To take this a step further, empty commands surrounded by semicolons will also be interpreted as new-tab commands with the default parameters, so wt ; ; ; can be used to open the windows terminal with 4 new tabs. Effectively, that commandline expands to wt new-tab ; new-tab ; new-tab ; new-tab.

Options

--help,-h,-?,/?,

Runs the help command.

--version,-v

Runs the version command.

--session,-s session-id

Run these commands in the given Windows Terminal session. Enables opening new tabs in already running Windows Terminal windows. This feature is dependent upon other planned work landing, so is only provided as an example, of what it might look like. See Future Considerations for more details.

--file,-f configuration-file

Run these commands in the given Windows Terminal session. Enables opening new tabs in already running Windows Terminal windows. See Future Considerations for more details.

Commands

help

help

Display the help message.

version

version

Display version info for the Windows Terminal.

open-settings

open-settings [--defaults,-d]

Open the settings file. If this command is provided alone, it does not open the terminal window.

Parameters:

  • --defaults,-d: Open the defaults.json file instead of the profiles.json file.

list-profiles

list-profiles [--all,-A] [--showGuids,-g]

Displays a list of each of the available profiles. Each profile displays it's name, separated by newlines.

Parameters:

  • --all,-A: Show all profiles, including profiles marked "hidden": true.
  • --showGuids,-g: In addition to showing names, also list each profile's guid. These GUIDs should probably be listed first on each line, to make parsing output easier.

new-tab

new-tab [--initialPosition x,y]|[--maximized]|[--fullscreen] [--initialRows rows] [--initialCols cols] [terminal_parameters]

Opens a new tab with the given customizations. On its first invocation, also opens a new window. Subsequent new-tab commands will all open new tabs in the same window.

Parameters:

  • --initialPosition x,y: Create the new Windows Terminal window at the given location on the screen in pixels. This parameter is only used when initially creating the window, and ignored for subsequent new-tab commands. When combined with any of --maximized or --fullscreen, an error message will be displayed to the user, indicating that an invalid combination of arguments was provided.
  • --initialRows rows: Create the terminal window with rows rows (in characters). If omitted, uses the value from the user's settings. This parameter is only used when initially creating the window, and ignored for subsequent new-tab commands. When combined with any of --maximized or --fullscreen, an error message will be displayed to the user, indicating that an invalid combination of arguments was provided.
  • --initialCols cols: Create the terminal window with cols cols (in characters). If omitted, uses the value from the user's settings. This parameter is only used when initially creating the window, and ignored for subsequent new-tab commands. When combined with any of --maximized or --fullscreen, an error message will be displayed to the user, indicating that an invalid combination of arguments was provided.
  • [terminal_parameters]: See [terminal_parameters].

split-pane

split-pane [--target,-t target-pane] [-H]|[-V] [--percent,-% split-percentage] [terminal_parameters]

Creates a new pane in the currently focused tab by splitting the given pane vertically or horizontally.

Parameters:

  • --target,-t target-pane: Creates a new split in the given target-pane. Each pane has a unique index (per-tab) which can be used to identify them. These indices are assigned in the order the panes were created. If omitted, defaults to the index of the currently focused pane.
  • -H, -V: Used to indicate which direction to split the pane. -V is "vertically" (think [|]), and -H is "horizontally" (think [-]). If omitted, defaults to "auto", which splits the current pane in whatever the larger dimension is. If both -H and -V are provided, defaults to vertical.
  • --percent,-% split-percentage: Designates the amount of space that the new pane should take as a percentage of the parent's space. If omitted, the pane will take 50% by default.
  • [terminal_parameters]: See [terminal_parameters].

focus-tab

focus-tab [--target,-t tab-index]

Moves focus to a given tab.

Parameters:

  • --target,-t tab-index: moves focus to the tab at index tab-index. If omitted, defaults to 0 (the first tab).

focus-pane

focus-pane [--target,-t target-pane]

Moves focus within the currently focused tab to a given pane.

Parameters:

  • --target,-t target-pane: moves focus to the given target-pane. Each pane has a unique index (per-tab) which can be used to identify them. These indices are assigned in the order the panes were created. If omitted, defaults to the index of the currently focused pane (which is effectively a no-op).

move-focus

move-focus [--direction,-d direction]

Moves focus within the currently focused tab in the given direction.

Parameters:

  • --direction,-d direction: moves focus in the given direction. direction should be one of [left, right, up, down]. If omitted, does not move the focus at all (resulting in a no-op).

[terminal_parameters]

Some of the preceding commands are used to create a new terminal instance. These commands are listed above as accepting [terminal_parameters] as a parameter. For these commands, [terminal_parameters] can be any of the following:

[--profile,-p profile-name] [--startingDirectory,-d starting-directory] [commandline]

  • --profile,-p profile-name: Use the given profile to open the new tab/pane, where profile-name is the name or guid of a profile. If profile-name does not match any profiles, uses the default.
  • --startingDirectory,-d starting-directory: Overrides the value of startingDirectory of the specified profile, to start in starting-directory instead.
  • commandline: A commandline to replace the default commandline of the selected profile. If the user wants to use a ; in this commandline, it should be escaped as \;.

Fundamentally, there's no reason that all the current profile settings couldn't be overridden by commandline arguments. Practically, it might be unreasonable to create short form arguments for each and every Profile property, but the long form would certainly be reasonable.

The arguments listed above represent both special cases of the profile settings like guid and name, as well as high priority properties to add as arguments.

  • It doesn't really make sense to override name or guid, so those have been repurposed as arguments for selecting a profile.
  • commandline is a bit of a unique case - we're not explicitly using an argument to identify the start of the commandline here. This is to help avoid the need to parse and escape arguments to the client commandline.
  • startingDirectory is a highly requested commandline argument, so that's been given priority in this spec.

Implementation Details

Following an investigation performed the week of Nov 18th, 2019, I've determined that we should be able to use the CLI11 open-source library to parse our arguments. We'll need to add some additional logic on top of CLI11 in order to properly separate commands with ;, but that's not impossible to achieve.

CLI11 will allow us to parse commandlines as a series of options, with a possible sub-command that takes its own set of parameters. This functionality will be used to enable our options & commands style of parameters.

When commands are parsed, each command will build an ActionAndArgs that can be used to tell the terminal what steps to perform on startup. The Terminal already uses these ActionAndArgs to perform actions like opening new tabs, panes, moving focus, etc.

In my initial investigation, it seemed as though the Terminal did not initialize the size of child controls initially. This meant that it wasn't possible to immediately create all the splits and tabs for the Terminal as passed on the commandline, because they'd open at a size of 0x0. To mitigate this, we'll handle dispatching these startup actions one at a time, waiting until the Terminal for an action is initialized or the command is otherwise completed before dispatching the next one.

This is a perhaps fragile way of handling the initialization. Ideally, there should be a way to dispatch all the commands immediately, before the Terminal fully initializes, so that the UI pops up in the state as specified in the commandline. This will be an area of active investigation as implementation is developed, to make the initialization of many commands as seamless as possible.

Implementation plan

As this is a very complex feature, there will need to be a number of steps taken in the codebase to enable this functionality in a way that users are expecting. The following is a suggestion of the individual changelists that could be made to iteratively work towards fulling implementing this functionality.

  • Refactor ShortcutAction dispatching into its own class
    • Right now, the AppKeyBindings is responsible for triggering all ActionAndArgs events, but only based upon keystrokes while the Terminal is running. As we'll be re-using ActionAndArgs for handling startup events, we'll need a more generic way of dispatching those events.
  • Add a SplitPane ShortcutAction, with a single parameter split, which accepts either vertical, horizontal, or auto.
    • Make sure to convert the legacy SplitVertical and SplitHorizontal to use SplitPane with that arg set appropriately.
  • Add a TerminalParameters winrt object to NewTabArgs and SplitPane args. TerminalParameters will include the following properties:
runtimeclass TerminalParameters {
    String ProfileName;
    String ProfileGuid;
    String StartingDirectory;
    String Commandline;
}
  • These represent the arguments in [terminal_parameters]. When set, they'll both newTab and splitPane will accept [profile, guid, commandline, startingDirectory] as optional parameters, and when they're set, they'll override the default values used when creating a new terminal instance.
    • profile and guid will be used to look up the profile to create by name, guid, respectively, as opposed to the default profile.
    • The others will override their respective properties from the TerminalSettings created for that profile.
  • Add an optional "percent" argument to SplitPane, that enables a pane to be split with a specified percent of the parent pane.
  • Add support to TerminalApp for parsing commandline arguments, and constructing a list of ActionAndArgs based on those commands.
    • This will include adding tests that validate a particular commandline generates the given sequence of ActionAndArgs.
    • This will not include performing those actions, or passing the commandline from the WindowsTerminal executable to the TerminalApp library for parsing. This change does not add any user-facing functional behavior, but is self-contained enough that it can be its own changelist, without depending upon other functionality.
  • When parsing a new-tab command, configure the TerminalApp::AppLogic to set some initial state about itself, to handle the new-tab arguments [--initialPosition, --maximized, --initialRows, --initialCols]. Only set this state for the first new-tab parsed. These settings will overwrite the corresponding global properties on launch.
  • When parsing a help command or a list-profiles command, trigger a event on AppLogic. This event should be able to be handled by WindowsTerminal (AppHost), and used to display a MessageBox with the given text. (see Potential Issues for a discussion on this).
  • Add support for performing actions passed on the commandline. This includes:
    • Passing the commandline into the TerminalApp for parsing.
    • Performing ActionAndArgs that are parsed by the Terminal.
    • At this point, the user should be able to pass the following commands to the Terminal:
      • new-tab
      • split-pane
      • move-focus
      • focus-tab
      • open-settings
      • help
      • list-profiles
  • Add a ShortcutAction for FocusPane, which accepts a single parameter index.
    • We'll need to track each Pane's ID as Panes are created, so that we can quickly switch to the nth Pane.
    • This is in order to support the -t,--target parameter of split-pane.

Capabilities

Accessibility

As a commandline feature, the accessibility of this feature will largely be tied to the ability of the commandline environment to expose accessibility notifications. Both conhost.exe and the Windows Terminal already support basic accessibility patterns, so users using this feature from either of those terminals will be reliant upon their accessibility implementations.

Security

As we'll be parsing user input, that's always subject to worries about buffer length, input values, etc. Fortunately, most of this should be handled for us by the operating system, and passed to us as a commandline via winMain and CommandLineToArgvW. We should still take extra care in parsing these args.

Reliability

This change should not have any particular reliability concerns.

Compatibility

This change should not regress any existing behaviors.

Performance, Power, and Efficiency

This change should not particularly impact startup time or any of these other categories.

Potential Issues

Commandline escaping

Escaping commandlines is notoriously tricky to do correctly. Since we're using ; to delimit commands, which might want to also use ; in the commandline itself, we'll use \; as an escaped ; within the commandline. This is an area we've been caught in before, so extensive testing will be necessary to make sure this works as expected.

Painfully, powershell uses ; as a separator between commands as well. So, if someone wanted to call a wt commandline in powershell with multiple commands, the user would need to also escape those semicolons for powershell first. That means a command like wt new-tab ; split-pane would need to be wt new-tab `; split-pane in powershell, and wt new-tab ; split-pane commandline \; with \; semicolons would need to become wt new-tab `; split-pane commandline \`; with \`; semicolons, using \`; to first escape the semicolon for powershell, then the backslash to escape it for wt.

Alternatively, the user could choose to escape the semicolons with quotes (either single or double), like so: wt new-tab ';' split-pane "commandline \; with \; semicolons".

This would get a little ridiculous when using powershell commands that also have semicolons possible escaped within them:

wt.exe ";" split-pane "powershell Write-Output 'Hello World' > foo.txt; type foo.txt"

We've decided that although this behavior is uncomfortable in powershell, there doesn't seem to be any option out there that's less painful. This is a reasonable option that makes enough logical sense. Users familiar with powershell will understand the need to escape commandlines like this.

As noted by @jantari:

PowerShell has the --% (stop parsing) operator, which instructs it to stop interpreting anything after it and just pass it on verbatim. So, the semicolon-problem could also be addressed by the following syntax:

# wt.exe still needs to be interpreted by PowerShell as it's a command in PATH, but nothing after it
wt.exe --% cmd.exe ; split-pane --target-pane 0 -V -% 30 wsl.exe

/SUBSYSTEM:Windows or /SUBSYSTEM:Console?

When you create an application on Windows, you must link it as either a Windows or a Console application. When the application is launched from a commandline shell as a Windows application, the shell will immediately return to the foreground of the console, which means that any console output emitted by the process will be intermixed with the shell. However, if an application is linked as a Console application, and it's launched from the Start Menu, Run dialog, or any other context that's not a console, then the OS will automatically create a console to host the commandline application. That means that briefly, a console window will appear on the screen, even if we decide that we just want to launch our application's window.

This basically leaves us with two bad scenarios. Either we're a Console application, and a console window always flashes on screen for every non-commandline invocation of the Terminal, or we're a Windows application, and console output we log (including help messages) can get mixed with shell output. Neither of these are particularly good.

python et. al. often ship with two executables, a python.exe which is a Console application, and a pythonw.exe, which is a Windows application. This however has led to loads of confusion, and even with plentiful documentation, would likely result in users being confused about what does what. For situations like launching the Terminal in the CWD of explorer.exe, users would need to use wtw.exe -d . to prevent the console window from appearing. However, when calling Windows Terminal from a commandline environment, users who call wtw.exe /? would likely get unexpected behavior, because they should have instead called wt.exe /?.

To avoid this confusion, I propose we follow the example of msiexec /?. This is a Windows application that uses a MessageBox to display its help text. While this is less convenient for users coming exclusively from a commandline environment, it's also the least bad option available to us.

  • It's less confusing than having control returned to the shell
  • It's not as bad as forcing the creation of a console window for non-commandline launches.
  • There's precedent for this kind of dialog (we're not inventing a new pattern here).

What happens if new-tab isn't the first command?

Consider the following commandline:

wt.exe split-pane -V ; new-tab

In the future, maybe we could presume in this case that the commands are intended for the current Windows Terminal window, though that's not functionality that will arrive in 1.0. Even when sessions are supported like that, I'm not sure that when we're parsing a commandline, we'll be able to know what session we're currently running in. That might make it challenging to dispatch this kind of command to "the current WT window".

Additionally, what would happen if this was run in a conhost window, that wasn't attached to a Terminal session? We wouldn't be able to tell the current session to split-pane, since there wouldn't be one. What would we do then? Display an error message somehow?

I don't believe that implying the current Windows Terminal session is the correct behavior here. Instead we should either:

  • Assume that there's an implicit new-tab command that's run first, to create the window, then run split-pane in that tab.
  • Immediately display an error that the commandline is invalid, and that a commandline should start with a new-tab ; ?

In my initial implementation, I resolved this by assuming there was an implicit new-tab command, and that felt right. The team has discussed this, and concluded that's the correct behavior. In the words of @DHowett-MSFT:

In favor of "implicit new-tab": wt.exe without any arguments is already an implicit new-window or new-tab; we can't claw back the implicitness and ease of use in that one, so I think in the spirit of keeping that going WT should automatically do anything necessary to service a command (wt split-pane should operate in a new tab or new window, etc.)

We should also make sure that when we add support for the open-settings command, that command by itself should not imply a new-tab. wt open-settings should simply open the settings in the user's chosen .json editor, without needing to open a terminal window.

Future considerations

  • These are some additional argument ideas which are dependent on other features that might not land for a long time. These features were still considered as a part of the design of this solution, though their implementation is purely hypothetical for the time being.
    • Instead of launching a new Windows Terminal window, attach this new terminal to an existing one. This would require the work outlined in #2080, so support a "manager" process that could coordinate sessions like this.
      • This would be something like wt --session [some-session-id] [commands], where --session [some-session-id] would tell us that [more-commands] are intended for the given other session/window. That way, you could open a new tab in another window with wt --session 0 cmd.exe (for example).
    • list-sessions: A command to display all the active Windows terminal instances and their session ID's, in a way compatible with the above command. Again, heavily dependent upon the implementation of #2080.
    • --elevated: Should it be possible for us to request an elevated session of ourselves, this argument could be used to indicate the process should launch in an elevated context. This is considered in pursuit of #632.
    • --file,-f configuration-file: Used for loading a configuration file to give a list of commands. This file can enable a user to have a re-usable configuration saved somewhere on their machine. When dealing with a file full of startup commands, we'll assume all of them are intended for the given window. So the first new-tab in the file will create the window, and all subsequent new-tab commands will create tabs in that same window.
  • In the past we've had requests (like #756) for having the terminal start with multiple tabs/panes by default. This might be a path to enabling that scenario. One could imagine the profiles.json file including a defaultConfiguration property, with a path to a .conf file filled with commands. We'd parse that file on window creation just the same as if it was parsed on the commandline. If the user provides a file on the commandline, we'll just ignore that value from profiles.json.
  • When working on "New Window", we'll want the user to be able to open a new window with not only the default profile, but also a specific profile. This will help us enable that scenario.
  • We might want to look into RegisterArgumentCompleter in powershell to enable letting the user auto-complete our args in powershell.
  • If we're careful, we could maybe create short form aliases for all the commands, so the user wouldn't need to type them all out every time. new-tab could become nt, split-pane becomes sp, etc. A commandline could look like wt ; sp less some-log.txt ; fp -t 0 then.

Resources

Feature Request: wt.exe supports command line arguments (profile, command, directory, etc.) #607 Add "open Windows terminal here" into right-click context menu #1060

Feature Request: Task Bar jumplist should show items from profile #576 Draft spec for adding profiles to the Windows jumplist #1357

Spec for tab tear off and default app #2080

[Question] Configuring Windows Terminal profile to always launch elevated #632

New window key binding not working #2068

Feature Request: Start with multiple tabs open #756