32 KiB
author | created on | last updated | issue id |
---|---|---|---|
Mike Griese | 2022-08-22 | 2024-06-13 | 1595 |
Windows Terminal - Snippets
Abstract
The command line is a highly powerful tool. However, its power is dependent on the user's knowledge of the specific commands, flags and parameters needed to perform tasks from the command-line. For simple everyday commands, this might not be so hard. For longer commands, or ones used less frequently, there's quite a bit of mental overhead trying to recall the exact syntax. For teams, it might be helpful to share these tasks with everyone on the project. The Terminal can be an avenue by which complicated tasks can be remembered, shared, discovered, and recalled by the user simply thinking "what do I want to do", rather than "how do I do it".
Background
Note:
This largely builds off of work in the Suggestions UI, for displaying these tasks to the user. Make sure to read that spec first.
Inspiration
The primordial version of this idea was probably #keep - a command-line tool I wrote for stashing long command-lines and directories, and recalling them with just a number. We've had many variations on this idea over the years - #1595 was probably the first such request on the Terminal repo. ITerm2 also had a similar feature. Theirs was more directly tied to shell integration (that menu is populated from commands that they know were run in the shell). In the absence of shell integration though, it should be able to save these commands to a menu manually.
Consider VsCode Tasks. These are files which can be placed in the root of a workspace, and share common tasks between users of that workspace. They've got support for starting processes, with a set of args. These args can also be picked at runtime, and custom sets of arguments can be specified for individual arguments.
It is hard to say that the ultimate vision here isn't partially inspired by the "workflows" of Warp, or by Fig. These are modern tools that seek to augment the command-line experience, by making the command-line more approachable. Warp quite clearly has the same concept in "workflows" - scripts which the user can build and Warp (a Terminal emulator) can insert quickly. Fig, on the other hand, is more focused on just simplifying the command-line experience. Fig is more about providing additional metadata to the user as they're typing. They are also working on workflows, so there's clearly quite a bit of ecosystem-wide demand for more discoverable command-line tooling.
We've had verbatim feedback that developers already attempt to record useful commandlines in various different ways - in OneNotes, in shell scripts, in aliases. Furthermore, developers often share these commands with the rest of their teams. Providing a unified way to easily store, browse, and use these command lines should be valuable to developers already doing this. A static file in their project containing commands for the whole team seems like a simple solution to this problem.
User Stories
Story | Size | Description |
---|---|---|
A | ✅ Done | Users can bring up a menu of command line tasks and quickly execute them |
B | ✅ Done | Fragment apps can provide tasks to a users settings |
C | 🚶 Walk | The user can save commands straight to their settings with a wt command |
D | 🚶 Walk | Users can have different tasks enabled for different profiles(/shells?) |
E | 🚶 Walk | The Terminal displays a Snippets Pane for easy browsing of relevant snippets |
F | 🏃♂️ Run | The terminal can automatically look for command fragments in the tree of the CWD |
G | 🏃♂️ Run | Snippets with multiple lines can be sent only conditionally on the success of the previous command (with shell integration) |
H | ✅ Done | Snippets can be filtered by text the user has already typed |
I | 🚀 Sprint | Snippets can have prompt-able sections of input |
J | 🚀 Sprint | Community tasks are hosted in a public GH repo |
K | 🚀 Sprint | A simple UX (either web or in Terminal) is exposed for interacting with public GH repo of tasks |
Elevator Pitch
The Terminal can remember long command-lines and display them with user-friendly descriptions of what they actually do. These tasks can be searched by intent, rather than the particular combination of flags. They can be shared with members of your team, so everyone has easy access to common tasks for a project.
Why not just aliases / native script files?
Why not just take these tasks and put them into a shell alias? For longer tasks,
why not just stick them in a .ps1
/.bat
/.sh
/etc file? This is an option
that's existed since the time immemorial. However, this still requires the user
to remember that they've created these aliases/scripts, remember where they're
stored, and remember how they work.
By providing a dedicated UI for these command-lines, they can always be at your
fingertips. No need to remember what the alias for a particular command-line is -
just look up what you want to do. Aliases and scripts are no longer scattered
across .bashrc
, .bash_profile
, .profile
, etc, they can all be stashed in
the Terminal config, or in the project they're relevant to. By stashing them
alongside the code, then anyone else coming to work on the code can have
immediate access to useful sets of tasks.
Aliases have a tendency towards more experienced shell users. This proposal instead brings the power of these aliases and scripts right to the foreground, with a cross-shell mechanism of exposing them to even beginners. With fragment extensions, tools can bundle common workflows together with their application so the Terminal can automatically load them for the user.
Business Justification
It will delight developers.
Scenario Details
Implementation Details
For the most part, this is already implemented as the sendInput
action. These
actions send text to the terminal already, and work quite well as snippets.
Basics
We'll want to also augment sendInput
to add support for input
as an array of
strings, not only a single string value. When the input is a list of strings,
then the terminal can send each string, separated by the enter key.
We can also add a waitForSuccess
parameter to sendInput
(with a default
value of false
). If that's set to true
, and shell integration is enabled,
then the Terminal will wait to send each command until the previous command
exits.
As another minor improvement, we'll add a description
property to Commands.
This will allow users to add additional information to snippets which we can
surface. Additionally, extension authors could provide more details as well.
As a matter of renaming, we'll also update "source": "tasks"
for the
SuggestionsSource
enum to instead be snippets
(and gracefully update that
where we find it). "tasks" was an older name for this feature, and "snippets"
will better align with our partners in VsCode.
Multi-line snippets example
Consider the following script:
$s=Invoke-GitHubGraphQlApi "query{organization(login:`"Microsoft`"){projectV2(number: 159) { id } } }"
$tasks = get-GitHubIssue -Labels "Issue-Task" -state open
$bugs = get-GitHubIssue -Labels "Issue-Bug" -state open
$issues = $tasks + $bugs
$issues | ? {$_.labels.Name -NotContains "Needs-Triage" } | ? { $_.milestone.title -Ne "Icebox ❄" } | ? type -Ne "PullRequest" | select -expand node_id | % {
$resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ;
}
As just a raw sendInput action with a single input
, this would look like the following:
{
"command":
{
"action": "sendInput",
"input": "$s=Invoke-GitHubGraphQlApi \"query{organization(login:`\"Microsoft`\"){projectV2(number: 159) { id } } }\"\r\n$tasks = get-GitHubIssue -Labels \"Issue-Task\" -state open\r\n$bugs = get-GitHubIssue -Labels \"Issue-Bug\" -state open\r\n$issues = $tasks + $bugs\r\n$issues | ? {$_.labels.Name -NotContains \"Needs-Triage\" } | ? { $_.milestone.title -Ne \"Icebox ❄\" } | ? type -Ne \"PullRequest\" | select -expand node_id | % {\r\n $resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ;\r\n}"
},
"name": "Upload to project board",
"description": "Sync all our issues and bugs that have been triaged and are actually on the backlog to the big-ol project",
},
This JSON is basically entirely unusable. Since JSON doesn't support multiline
strings, then every line has to be joined to a single line, separated by \r\n
.
Instead, the following version of this command uses an array for the input
parameter. This then implies that each string should be sent in sequence, with
enter between them.
{
"command":
{
"action": "sendInput",
"input":
[
"$s=Invoke-GitHubGraphQlApi \"query{organization(login:`\"Microsoft`\"){projectV2(number: 159) { id } } }\"",
"$tasks = get-GitHubIssue -Labels \"Issue-Task\" -state open",
"$bugs = get-GitHubIssue -Labels \"Issue-Bug\" -state open",
"$issues = $tasks + $bugs",
"$issues | ? {$_.labels.Name -NotContains \"Needs-Triage\" } | ? { $_.milestone.title -Ne \"Icebox ❄\" } | ? type -Ne \"PullRequest\" | select -expand node_id | % {",
" $resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ;",
"}",
""
]
},
"name": "Upload to project board",
"description": "Sync all our issues and bugs that have been triaged and are actually on the backlog to the big-ol project",
},
This is slightly more maintainable. Assuming the user also has shell integration
enabled, they could also set "waitForSuccess": true
, and if any part of the
script fails, then the rest of it won't be sent to the shell[1].
Fragment actions
This was already added in #16185. These will allow third-party developers to
create apps which add additional snippets to the Terminal. These will require
app developers to add id
s to each action they add in this way. Users can then
bind that action id
to a keybinding, if they so choose.
Case in point: https://github.com/abduvik/just-enough-series/tree/master/courses/docker+docker-compose. Something like that should be able to be easily added directly to the Terminal.
Snippets pane
With non-terminal content landing in 1.21 Preview, it's now simple to add
additional types of panes to add to the Terminal. We'll support a new pane
"type": "snippets"
, to support opening the Snippets pane.
This will be a pane with a TreeView
in it and a text box to filter results
(ala the Command Palette).
Each item in the TreeView will be a kind of FilteredCommand
, with a play
button to support quickly running the command.
This pane could also support all the different suggestion sources that the
Suggestions UI supports - recentCommands
could be plumbed into it from the
currently active This pane could also support checkboxes to filter different
suggestion sources.
Per-Project Snippets (.wt.json
)
Users may also want to leave snippets in the root of their repo, for others to
use as well. To support this, the Terminal will automatically look for a
.wt.json
file in any directories that are parents of the CWD of the shell, and
load actions from that file as well. The syntax for this file will be a modified
version of the standard settings schema. As an example:
{
"$version": "1.0.0",
"snippets":
[
{
"input": "bx",
"name": "Build project",
"description": "Build the project in the CWD"
},
{
"input": "bcz",
"name": "Clean & build solution",
"icon": "\uE8e6",
"description": "Start over. Go get your coffee. "
},
{
"input": "nuget push -ApiKey az -source TerminalDependencies %userprofile%\\Downloads" ,
"name": "Upload package to nuget feed",
"icon": "\uE898",
"description": "Go download a .nupkg, put it in ~/Downloads, and use this to push to our private feed."
},
]
}
Instead of actions
, the top-level list is snippets
. These snippet objects
are a simplification of the Command
object. They have a name
, description
,
and icon
properties, just like a Command
. However, instead of an arbitrary
action
, we will just have the SendInput
action's args as properties directly
in the object.
Additionally, we'll also support a $version
field, in case we ever want to
make breaking changes to the schema. When this is missing, we'll just assume the
version to be 1.0.0
, which is this originally proposed schema.
By default, a TermControl
is always initialized with the CWD set to the
startingDirectory
of a profile. So, even for users that don't have shell
integration enabled, the Terminal will still be able to load snippets from the
.wt.json
in the profile's startingDirectory
. If the user has shell
integration configured to tell the Terminal about the CWD, then we'll refresh
that list as the user changes directories.
- In
Terminal.Settings.Model
, we will store a cached map of path->actions.- that if multiple panes are all in the same CWD, they don't need to individually re-read the file from disk and regenerate that part of the map.
- I believe it should be impossible for a keybinding to be bound to a local action. Even if it has an ID, the file won't be loaded when we build the keymap, and we don't really want the keymap changing based on CWD. Also, with the actions living in an entirely separate map of CWD->actions, the keybindings in the main map won't be able to easily get to them. See also Security considerations for more.
- If the Snippets pane or Suggestions UI is opened with
snippets
as a source, then we'll just append the appropriate list of suggestions for the active control's CWD.- We don't need to have the control raise an event when the CWD changes - we can lazy-load these actions when a UI element that requires it is first invoked.
- These local snippets will not be included in the Command Palette.
(
sendInput
actions in the settings file and in fragments still will be, however)- The Command Palette is quite tightly bound to our own ActionMap settings model, accumulated through complicated layering of defaults, fragments, and user settings. It's not trivially mutable at runtime in the way that context-sensitive snippets would require.
- The Suggestions UI and Snippets pane are both surfaces that are better
equipped to handle context-relevant actions, especially where the context is
a
TermControl
. - The Suggestions UI and snippets pane will give us more flexibility in
customizing the experience specifically for snippets. Case in point - we'll
want to filter the suggestions UI based on both the
Name
and theInput
of the send input command. Contrast that with the Command Palette which is currently only capable of filtering based on names.
- If we find multiple
.wt.json
files in the ancestors of the CWD (e.g. forc:\a\b\c\d\
, there's ac:\a\.wt.json
and ac:\a\b\c\.wt.json
), then we'll add each one separately to the map of paths->directories. When requesting the actual actions forc:\a\b\c\d\
, we'll layer the ones fromc:\a\
before the ones fromc:\a\b\c
, so that deeper descendants take precedence.- For example,
c:\a\.wt.json
has a snippet withinput: "foo", name: "Build"
, andc:\a\b\c\.wt.json
has a snippet withinput: "bar", name: "Build"
. When the user is underc:\a\b\c
, the Terminal will showbar
when the user selects theBuild
suggestion. Otherwise, if the user is underc:\a
, then the Terminal will showfoo
.
- For example,
- If we fail to parse the
.wt.json
file, then we'll ignore it. For parsing errors, we'll want to display warnings to the user:- If the user had opened the suggestions UI, we can display a Toast the first time we fail to load it to show the error.
- In the snippets pane, we can have static text along the lines of "failed to
parse snippets found in
path/to/file
" at the top of the pane.
Saving snippets from the commandline
This has already been prototyped in #16513
Users should be able to save commands as snippets directly from the commandline.
Consider: you've just run the command that worked the way you need it to. You
shouldn't have to open the settings to then separately copy-paste the command in
to save it. It should be as easy as Up, Home, wt x-save
,
Enter.
This will be powered by a saveSnippet
action behind the scenes. However, we
won't actually parse these actions from a user's settings file. They don't
really make sense to have the action to save a snippet to the settings file, in
the settings file already.
The exact syntax of x-save
is as follows:
x-save
subcommand
x-save [--name,-n name][--description,-d description][-- commandline]
Saves a given commandline as a sendInput action to the Terminal settings file. This will immediately write the Terminal settings file.
Parameters:
--name,-n name
: The name to assign to thename
parameter of the saved command. If omitted, then the parameter will be left blank, and the command will use the auto-generated "Send input:..." name in menus.--description,-d
: The description to optionally assign to the command.commandline
: The commandline to save as theinput
of thesendInput
action.
If the save
subcommand is ran without any other subcommands, the Terminal will
imply the -w 0
arguments, to attempt to send this action to the current
Terminal window (unless of course, -w
was manually provided on the
commandline). This is done to avoid a new terminal window popping up, just to
inform the user a command was saved. When run with other subcommands, then the
action will just be ran in the same window as all the other subcommands.
[!NOTE] In team discussions, we've decided to accept this for now as experimental. We have some concerns about how effective we'll be at dealing with all the weird edge cases of string escaping. For now, we'll keep this subcommand as
x-save
, wherex-
implies "experimental".Perhaps once we add a dialog for saving these snippets, we can promote it out of experimental.
UI/UX Design
For the most part, we'll be using the Suggestions UI to display tasks to the user. This is a text cursor-relative UI surface that can quickly display actions to the user, in the context of what they're working on.
The following are some examples from VsCode and Warp. These are meant to be illustrative of what these menus already look like in the wild:
A prototype of saving a command directly to the user's settings, then invoking it via the suggestions UI
A prototype of reading tasks from the CWD
The initial version of the snippets pane:
Tenets
Other potential issues
Something like wt save ping 8.8.8.8 > foo.txt
isn't going to work the way
users want. The shell is gonna get the first crack at parsing that commandline,
and is going to try and redirect the output of wt
to foo.txt
.
Predictably, local snippets won't work over ssh or other remote connections.
Terminal is only able to read files off the local filesystem. We'll at best be
able to read snippets from the directory they ssh
'd from locally.
Implementation Plan
🐣 Crawl
- The Command Palette and Suggestions UI need to be able to display both the command name and a tooltip for the comment
- #1595 Add the Suggestions UI, with support for
tasks
- Fragments can add actions to a user's settings
🚶 Walk
- The terminal can look for a settings file of tasks in a profile's
startingDirectory
(regardless of shell integration being enabled) - #5790 - profile specific actions
- #12857 Ability to save selected text as a
sendInput
action - #12861 Re-evaluate showing some sort of "ghost text" or other preview for snippets
🏃♂️ Run
- When the user
cd
s to a directory (with shell integration enabled), the terminal can load the tasks from that directory tree - #10436 Users can manage all their fragments extensions directly in the Settings UI
- The suggestions UI & snippets pane can filter not only on name of a command, but for snippets, the input as well.
- #12927 Enlighten the suggestions UI to support (a yet undeclared syntax for) snippets with prompt-able sections in them
Conclusion
Snippets are something that developers immediately understand the value of. After talking with users, everyone we talked with immediately understood the concept, and you could see the gears turning on ways to integrate this into their own workflows.
Future Considerations
- We may want to add additional params to the
save
subcommand in the future, to configure where the snippet is saved:--local
: Save to the.wt.json
in the CWD, if there is one (or create one)--parent
: Save to the.wt.json
in the first ancestor of the CWD, if there is one. Otherwise create one here.--settings
: Manually save to the settings file?--profile
: save to this profile???? Not sure if this is actually possible. Maybe with theWT_SESSION_ID
env var to figure out which profile is in use for the pane with that ID- This would probably require per-profile actions, which are woefully under specified
--local
/--parent
/--settings
was well received in team discussion - maybe we should just do them now.
- Longer workflows might be better exposed as notebooks. We've already got a mind to support markdown in a notebook-like experience in the Terminal. For longer scripts that may need rich markup between commands, that will likely be a better UX.
- For what it is worth, Warp uses .yaml files for their "workflows". As an
example, see
clone_all_repos_in_org.yaml
. We may want to straight up just seamlessly support that syntax as well.- Converting them to WT-compatible json is fairly trivial [2].
- We may want to consider supporting YAML like this for
wt import
, ala #10083 - Similarly, we could import the YAML in the settings UI in a fashion similar to how we import color schemes:
- We may want to consider supporting YAML like this for
- Furthermore, the commands are all licensed under Apache 2.0, which means they can be easily consumed by other OSS projects and shared with other developers.
- This leads us to the next future consideration:
- Converting them to WT-compatible json is fairly trivial [2].
- Discoverability will be important. Perhaps the actions page could have a
toggle to immediately filter to "snippets"? Which then also displays some text
like "Tip: save snippets directly from the commandline with
wt save <the commandline>
". - We should easily be able to put "Save command as snippet" into the quick fix menu next to an individual prompt, when shell integration is enabled.
- We should most definitely add a dialog for saving snippets directly in the Terminal.
- We'd have inputs for the commandline, name, description.
- Obviously, it'd be easy to have a "Add new" button (to open that dialog) on the snippets pane.
- We could have
wt save
open that dialog pre-populated, rather than just saving the command directly. - We could even also find a way to pre-populate that dialog with the recent commands (from shell integration)!
- As a potential v2.0 of the snippets file schema, we may look to the
.vscode/tasks.json
schema for inspiration. That file supports much more complex task definitions. Notably, with the ability to prompt the user for different inputs, for different parameter values. This is something that would play well off of #12927 - We may want to consider a future property of snippets like
shell
, which specifies which shells a snippet can be used with. We could then only filter to the snippets that will work with the current shell. This is left for the future because we don't have a reliable way of knowing what shell application the user is currently running. - We may want to consider promoting
sendInput
actions to a top-levelsnippets
array insettings.json
in the future. That might make sharing them from one user's settings, to a.wt.json
, and back, a little easier.
Community Snippets
The big stretch version of this feature.
It would be supremely cool to have a community curated list of Snippets, for various tools. Stored publicly on a GitHub repo (a la the winget-pkgs repo). Users can submit Snippets with descriptions of what the Snippet does. The Terminal can plug into that repo automatically and fetch the latest community commands, immediately giving the user access to a wide berth of common Snippets. That could easily be done as another suggestion source.
Profiles in .wt.json
If we've got a .wt.json
in a given directory, should we be dynamically
adding/removing other settings too? Wouldn't profiles also make sense? Take for
example, the Terminal repo. We've got a PowerShell build environment and a CMD
one. What if we could drop two profiles in the .wt.json
file, with the
commandline
's set up to call those scripts as needed?
However, what does that even mean? We wouldn't know that file exists till we see it the first time. Maybe there's room to integrate that with Dev Home ala microsoft/DevHome/3005. Though, that probably makes the most sense as a winget DSC to create a fragment profile instead.
Resources
Footnotes
[1]: Shell integration would be a strict requirement
for that parameter to work as intended. Without also enabling shell integration,
then the Terminal would only send the first line of the script, then wait
forever for a FTCS_COMMAND_FINISHED
.
[2]: For your consideration, I made a python script
that will take the Warp workflow YAML and convert it into json that the Terminal
can load. Go checkout dump_workflows.py
to see it. It's
super straightforward.