Terminal-Shell integration - a proposed specification
Shell integration refers to a protocol
between a terminal emulator and a shell (or other REPL)
to provide various features, some shown below.
The main idea is that the shell sends extra escape sequences to mark
the various parts of a command: prompt, input, and output.
Terminals that have supported some variation of this concept include
These protocols are not consistent and have missing funtionality,
so I have been working on
a proposed specification.
The rest of this article will show some of the supported features,
and discuss some unresolved issues.
If you want to try out the current state, please
and follow the
Best results (and the following screenshots) use the current
gitgub master of Fish,
(in the DomTerm sources).
Visually separate commands from each other
Shell integration enables each command (with its associated input and output) to be separated from other commands. The default DomTerm styling uses a thin partially-transparent green line you can see in the screenshot below
Different styling for prompt, input, and output
It is helpful to use a different styling for input and output. A simple version is to add distinctive styling for the prompt, but adding styling for entire input lines is even better. The DomTerm default styling (for light mode) is a pale green background for the prompt, a light yellow for the actual input, and a slighly darker yellow for the rest of the input line. Using pale background colors makes it less likely that it will conflict with user color preferences, including syntax coloring.
Note that the actual user input are distictively styled,
even spaces. It is helpful to see what are the actual input characters,
to avoid surprises in selection or cursor movement.
echo -n foo \ illustrates this -
the input line ends with two spaces:
The final space is removed by the fish parser, but the space before
is escaped by a backslash, as you can see in the output.
⏎ symbol is emitted by fish to indicate
a missing final newline in the output. The output ends in two spaces: one
foo in the input and one following the backspash.)
Button to hide/show command output
One useful feature enabled by shell integration
is a button to hide (or show) the output of a command.
DomTerm puts a downward triangle in the right
gutter of the first
line if there is any output or more than one input line.
Clicking on the button hides all but the first line, and changes
the triangle to a right-pointing one
(that can be clicked to restore the hidden lines).
DomTerm places the hide/show button in the same
that is also used to mark wrapped long lines (as shown above).
The same area is also used to show error status (below),
and for the cursor when at the end of a line.
Show error status
When a command finishes, the application can send the exit status
in an escape sequence to the terminal.
DomTerm handles a non-zero exit status by displaying a red circle
in the right gutter of the last line of the command.
(If there is no output, as with the
the circle appears in the last input line.)
Hovering over the red circle displays the exit code.
Move cursor using mouse
People new to shells usually expect that they can move the input cursor using a mouse click, and are surprised when it doesn't work. Experienced shell users also sometimes want this ability, for example making an edit to a long command. This can be implemented using xterm-style mouse handling, but that is non-trivial to implement. It also has some undesirable side-effects, including disabling the default mouse behavior for selection and for context menus.
A simpler solution is to configure a terminal emulator to map a mouse click to the appropriate number of cursor-motion keystrokes. By default, the terminal can send the escape sequences emitted for the Left and Right arrow keys. This works well for many unmodified REPL applications. (However, to avoid surprises, this has to be specifically enabled by adding a special escape sequence to the prompt.)
DomTerm also allows you to
force mouse-click translation:
If the Alt modifier is pressed during a left mouse-click,
the DomTerm sends the appropriate arrow-key sequence to move the cursor,
even if translation has not been eplicitly requested.
This is useful for an application like
which understands arrow keys but does not have mouse handling by default.
Motion is easy to handle when moving within a single line.
Some input editors (such as
fish_ support multi-line input areas.
Things get more complicated when the mouse-click is a different line
than the current position. When the terminal calculates the arrow-key
sequence it must be able to
ignore prompts and non-significant spacing, such as the indentation
fish emits, or the space before any right-edge prompt.
Another issue is what kind cursor motion the terminal should send:
fish you only want to send Left/Right arrows,
since Up/Down moves through the history.
The draft protocol and the DomTerm implementation support multiple options.
Integrating the selection and clipboard
How to integrate the system selection and clipboard with the input editor raises a number of questions that deserve a separate article. The way Emacs integrates the "region" with the selection seems a good approach (but the devil is in the details).
In DomTerm, if you make a selection that ends inside the input area, then DomTerm moves the cursor to the end of the selection (in the same way as if you clicked):
If you type Shift-Right or Shift-Left (optionally with Ctrl) the selection is extended and the cursor moved to the new endpoint. For example, typing Ctrl-Shift-Right extends the selection by one word:
Typing Ctrl-Shift-X copies the selection to the clipboard, and deletes it by emulating the appropriate number of Delete or Backspace key-presses.
Coming soon is having Delete or Backspace delete the selection, without copying to the clipboard.
Re-flow on window re-size
Many terminals will automatically re-flow (re-break) wrapped lines when the window width is changed. The terminal will also send a notification to the application, which may also re-display the output to take into account the changed size. The re-display done by the terminal may be conflict with that done by the application. For fullscreen applications it's not a problem because the applications can clear the buffer and re-paint, which clear any conflicting re-flow done by the terminal. For shells it is trickier: Suppose the current input fits on one line, but the window is made narrower. The terminal re-wraps the line as two lines. Then the application gets the window-changed notification, and redisplays the input line - but without taking into account the terminal's re-wrap, making a mess of the display.
This is a hard problem: Having the application assume re-wrap has happened is unreliable, because another re-size and re-wrap may have happened in the meanwhile. It is better to disable terminal re-wrap for the current line (the one containing the cursor), but that breaks in the case of multi-line input if the application re-sends previous input lines.
The proposed specification recommends that on re-size the application re-sends a prompt-start escape sequence, but not the command-start escape sequence. The terminal can if needed move the cursor to the first input line when it sees the prompt-start sequence. This seems to work pretty well.
(Better would be to let the terminal can handle the re-wrap, and the application not do re-display at all. This avoids the race condition, and it makes old input lines wrap the same way as the current line. However, that may require more radical protocol changes: It works best if the application can act as if the available width is unlimited, and delegate line-breaking it to the terminals.)
Select only real user input
When selecting and copying text that includes input it is usually more useful to ignore indentation, prompts, and extra spacing (such as between user input and a right prompt).