Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Shell Integration #576

Closed
ctwise opened this issue Mar 2, 2017 · 15 comments
Closed

Implement Shell Integration #576

ctwise opened this issue Mar 2, 2017 · 15 comments
Labels
out-of-scope type/enhancement Features or improvements to existing features

Comments

@ctwise
Copy link

ctwise commented Mar 2, 2017

iTerm2 has implemented (and documented) proprietary escape code for shell integration. This integration is used to provide command-line markers, jump between command-line entries in the terminal history, track successful commands, provide a terminal-level command history, track the current host and current directory. These escape codes originated with the (now defunct) FinalTerm project.

Please implement these escape codes - https://iterm2.com/documentation-escape-codes.html

Shell Integration/FinalTerm
iTerm2's Shell Integration feature is made possible by proprietary escape sequences pioneered by the FinalTerm emulator. FinalTerm is defunct, but the escape sequences are documented here.

Definitions
OSC stands for Operating System Command. In practice it refers to this sequence of two ASCII characters: 27, 93 (esc ]).
ST stands for String Terminator. It terminates an OSC sequence and consists either of two ASCII characters 27, 92 (esc ) or ASCII 7 (bel).
OSC sequences always begin with OSC, are followed by a sequence of characters, and are terminated with ST.

Most OSC codes begin with a number (one or more decimal digits), which we'll call the "command" in this document. If the command takes parameters it will be followed by a semicolon and the structure of the rest of the body of the OSC sequence is dependent on the command. Well-behaved terminal emulators ignore OSC codes with unrecognized commands.

Concepts
The goal of the FinalTerm escape sequences is to mark up a shell's output with semantic information about where the prompt begins, where the user-entered command begins, and where the command's output begins and ends.

[PROMPT]prompt% [COMMAND_START] ls -l
[COMMAND_EXECUTED]
-rw-r--r-- 1 user group 127 May 1 2016 filename
[COMMAND_FINISHED]

Escape Sequences
FinalTerm originally defined various escape sequences in its original spec that are not supported by iTerm2 and are not described in this document. The best remaining references to these codes are in iTerm2's source code.

FTCS_PROMPT

OSC 1 3 3 ; A ST

Sent just before start of shell prompt.

FTCS_COMMAND_START

OSC 1 3 3 ; B ST

Sent just after end of shell prompt, before the user-entered command.

FTCS_COMMAND_EXECUTED

OSC 1 3 3 ; C ST

Sent just before start of command output. All text between FTCS_COMMAND_START and FTCS_COMMAND_EXECUTED at the time FTCS_COMMAND_EXECUTED is received excluding terminal whitespace is considered the command the user entered. It is expected that user-entered commands will be edited interactively, so the screen contents are captured without regard to how they came to contain their state. If the cursor's location is before (above, or if on the same line, left of) its location when FTCS_COMMAND_START was received, then the command will be treated as the empty string.

FTCS_COMMAND_FINISHED

OSC 1 3 3 ; D ; Ps ST

OSC 1 3 3 ; D ST (for cancellation only)

The interpretation of this command depends on which FTCS was most recently received prior to FTCS_COMMAND_FINISHED.

This command may be sent after FTCS_COMMAND_START to indicate that a command was aborted. All state associated with the preceding prompt and the command until its receipt will be deleted. Either form is accepted for an abort. If the Ps argument is provided to an abort it will be ignored.

If this command is sent after FTCS_COMMAND_EXECUTED, then it indicates the end of command prompt. Ps is the command's exit status, a number in the range 0-255 represented as one or more ASCII decimal digits. A status of 0 is considered "success" and nonzero indicates "failure." The terminal may choose to indicate this visually.

If neither FTCS_COMMAND_START nor FTCS_COMMAND_EXECUTED was sent prior to FTCS_COMMAND_FINISHED it should be ignored.

iTerm2 Extensions
iTerm2 extends FinalTerm's suite of escape sequences.

SetUserVar

OSC 1 3 3 7 ; S e t U s e r V a r = Ps1 = Ps2 ST

Sets the value of a user-defined variable. iTerm2 keeps a dictionary of key-value pairs which may be used within iTerm2 as string substitutions, such as in the Badge.

Ps1 is the key.

Ps2 is the base64-encoded value.

ShellIntegrationVersion

OSC 1 3 3 7 ; S h e l l I n t e g r a t i o n V e r s i o n = Pn ; Ps ST

OSC 1 3 3 7 ; S h e l l I n t e g r a t i o n V e r s i o n = Pn ST (deprecated)

Reports the current version of the shell integration script.

Pn is the version.

Ps is the name of the shell (e.g., bash).

iTerm2 has a baked-in notion of the "current" version and if it sees a lower number the user will be prompted to upgrade. The version number is specific to the shell.

RemoteHost

OSC 1 3 3 7 ; R e m o t e H o s t = Ps1 @ Ps2 ST

Reports the user name and hostname.

Ps1 is username. Ps2 is fully-qualified hostname.

CurrentDir

OSC 1 3 3 7 ; C u r r e n t D i r = Ps1 ST

Reports the current directory.

Ps1 is the current directory.

@parisk parisk added the feature label Mar 2, 2017
@parisk
Copy link
Contributor

parisk commented Mar 2, 2017

This is a very interesting proposal, despite implementing handling for proprietary escape codes into the core of xterm.js is not likely to happen.

What we could do that sounds a good idea to me is make escape code handling more pluggable, so it can be extended by the user.

This way "advanced" xterm.js users can provide tighter integration between the consumer app and what runs in the terminal. TBH I would really like us to use this in SourceLair as well, it has it's fair set of use cases.

I would like to hear @Tyriar's opinion on this, since he is the one that has dealt with input handling mostly.

@Tyriar
Copy link
Member

Tyriar commented Mar 2, 2017

I was actually looking into something similar to this using a pretty different approach in microsoft/vscode#20676

What I don't like about this shell integration is that it doesn't really work for Windows, but it would probably lead to better and more reliable results than in microsoft/vscode#20676. We could probably make the Parser pluggable and allow addons to access it to implement this.

@ctwise
Copy link
Author

ctwise commented Mar 2, 2017

In case you're interested, iTerm 2 provides shell integration for Bash, ZSH and Fish. Here's the link for the bash integration - https://iterm2.com/misc/bash_startup.in

iTerm2 will then automatically mark command prompts with a triangle in the left-hand column. Based on the error code of the command it will color failed commands red. You can also hop between prompts in your history which is truly handy when you have a lot of output.

2017-03-02 13-27-19

@ctwise
Copy link
Author

ctwise commented Mar 2, 2017

More information on what iTerm 2 does with the integration is here - https://iterm2.com/documentation-shell-integration.html

@Tyriar
Copy link
Member

Tyriar commented Mar 2, 2017

Also for tcsh it appears https://iterm2.com/misc/tcsh_startup.in

@feamsr00
Copy link

Sorry to barge in :) Considering the functional model of the parser, it would seem to be very logical for it to emit parse events. Is it possible to just get the parser to do that for now? That might be a better move while waiting for extension component architecture to evolve. Especially if XT starts to have a more evented arch anyway.

This way implementers (that need ctrl codes) can start planing\testing rough functionality and when (if?) components\observers are developed, implementers can just wrap the new functionality in the freshly prescribed components.

@PerBothner
Copy link
Contributor

I know of three incompatible sets of escape sequences for shell integration:

  • The iterm2 escapes, originally from the now-defunct FinalTerm, discussed above. They start with "\e]133;"letter\007".

  • The ExtraTerm escapes. I haven't found actual documention, but they appear to have the form "\e&cookie;number\007, where cookie is to protect against garbled output.

  • The DomTerm escapes. The newer version starts with "\e]119;shellid\007. (Using an optional shellid works better for nested shells/repls.)

@PerBothner
Copy link
Contributor

I wrote a proposal for a shell integration proptocol.

Comments (unless xterm.js-specific) should preferably be discussed here, for now.

Complications (compared to the simple ITerm2/FinalTerm protocol) that I try to address include multi-line inputs (with optional continuation prompt); implicit input-area termination (in case the only customization hook available to the user is a prompt string); and right-prompts. Another feature I want to make more common is being able to move the input cursor using mouse clicks, implemented (when enabled) by translating to arrow keys. - see the proposal.

I will be implementing and testing the proposed protocol in DomTerm, whiicg already supports most of the funtionality.

@Tyriar
Copy link
Member

Tyriar commented Oct 7, 2019

With the addition of the parser hooks added (parser.addCsiHandler, etc.) this can now be implemented completely in an application embedding xterm.js. I think this makes more sense than trying to put support for shell integration inside xterm.js itself as shell integration is not a standard tech.

@ZedYeung
Copy link

With the addition of the parser hooks added (parser.addCsiHandler, etc.) this can now be implemented completely in an application embedding xterm.js. I think this makes more sense than trying to put support for shell integration inside xterm.js itself as shell integration is not a standard tech.

Any example? How could we utilize this to integrate shell? e.g. capture the input command like ls

@Tyriar
Copy link
Member

Tyriar commented Oct 29, 2019

If you're after an example of how to use addCsiHandler generally you can see how we use it internally to handle various sequences:

this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params));
this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params));
this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params));
this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params));
this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params));
this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params));
this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params));
this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params));
this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params));
this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params));
this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params));
this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params));
this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params));
this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params));
this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params));
this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params));
this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params));
this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params));
this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params));
this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params));
this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params));
this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params));
this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params));
this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params));
this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params));
this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params));
this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params));
this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params));
this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params));
this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params));
this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params));
this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params));
this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params));
this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params));
this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params));
this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params));
this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params));
this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params));
this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params));
this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params));
this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params));
this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params));
this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params));
this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params));
this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params));
this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params));

@jerch
Copy link
Member

jerch commented Oct 29, 2019

Additionally this might need some clarification about the involved things:

Per's proposal is a nice way to get "markup support" in terminals that would help typical terminal programs like shells, pagers etc. with several tasks. But that's one side of the story - the terminal. You can add those features to xterm.js with the commands @Tyriar has shown.

The other side - for real "shell integration" you'd need a shell (or pager or....) supporting those sequences to actually do something with it. Now you have 2 ways to get there - write your own shell supporting it or patch/advertise support in other common shells.

e.g. capture the input command like ls

This might work with good results for some programs, depending on the quality of your heuristics. Still it is doomed to fail for others.

@PerBothner
Copy link
Contributor

My proposal has been moved to here.

This article shows examples of what you can do with the proposal, with screenshots using DomTerm. (I prefer a styling for "commands" that is relatively minimalistic.)

Not sure what you mean by "capture the input command like ls". Perhaps you mean capturing the output? My proposal doesn't directly do that, but does provide a building-block to do that: Specifically explicitly delineating what is the output and what isn't. DomTerm has a builtin pager, like an automatic less filter.

@SterlingButters
Copy link

If you're after an example of how to use addCsiHandler generally you can see how we use it internally to handle various sequences:

this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params));
this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params));
this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params));
this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params));
this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params));
this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params));
this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params));
this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params));
this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params));
this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params));
this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params));
this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params));
this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params));
this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params));
this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params));
this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params));
this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params));
this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params));
this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params));
this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params));
this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params));
this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params));
this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params));
this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params));
this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params));
this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params));
this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params));
this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params));
this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params));
this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params));
this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params));
this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params));
this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params));
this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params));
this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params));
this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params));
this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params));
this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params));
this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params));
this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params));
this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params));
this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params));
this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params));
this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params));
this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params));
this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params));

@Tyriar @jerch Could you expound on the usage of the parser and how one might achieve correct usage?

@Tyriar
Copy link
Member

Tyriar commented Feb 25, 2020

@SterlingButters there's an issue to create a guide on the website on this xtermjs/xtermjs.org#116

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
out-of-scope type/enhancement Features or improvements to existing features
Projects
None yet
Development

No branches or pull requests

8 participants