| // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * Constructor for the VT escape sequence interpreter. |
| * |
| * The interpreter operates on a terminal object capable of performing cursor |
| * move operations, painting characters, etc. |
| * |
| * This interpreter is intended to be compatible with xterm, though it |
| * ignores some of the more esoteric escape sequences. |
| * |
| * Control sequences are documented in hterm/doc/ControlSequences.md. |
| * |
| * @param {hterm.Terminal} terminal Terminal to use with the interpreter. |
| */ |
| hterm.VT = function(terminal) { |
| /** |
| * The display terminal object associated with this virtual terminal. |
| */ |
| this.terminal = terminal; |
| |
| terminal.onMouse = this.onTerminalMouse_.bind(this); |
| this.mouseReport = this.MOUSE_REPORT_DISABLED; |
| this.mouseCoordinates = this.MOUSE_COORDINATES_X10; |
| |
| // We only want to report mouse moves between cells, not between pixels. |
| this.lastMouseDragResponse_ = null; |
| |
| // Parse state left over from the last parse. You should use the parseState |
| // instance passed into your parse routine, rather than reading |
| // this.parseState_ directly. |
| this.parseState_ = new hterm.VT.ParseState(this.parseUnknown_); |
| |
| // Any "leading modifiers" for the escape sequence, such as '?', ' ', or the |
| // other modifiers handled in this.parseCSI_. |
| this.leadingModifier_ = ''; |
| |
| // Any "trailing modifiers". Same character set as a leading modifier, |
| // except these are found after the numeric arguments. |
| this.trailingModifier_ = ''; |
| |
| // Whether or not to respect the escape codes for setting terminal width. |
| this.allowColumnWidthChanges_ = false; |
| |
| // The amount of time we're willing to wait for the end of an OSC sequence. |
| this.oscTimeLimit_ = 20000; |
| |
| /** |
| * Whether to accept the 8-bit control characters. |
| * |
| * An 8-bit control character is one with the eighth bit set. These |
| * didn't work on 7-bit terminals so they all have two byte equivalents. |
| * Most hosts still only use the two-byte versions. |
| * |
| * We ignore 8-bit control codes by default. This is in order to avoid |
| * issues with "accidental" usage of codes that need to be terminated. |
| * The "accident" usually involves cat'ing binary data. |
| */ |
| this.enable8BitControl = false; |
| |
| /** |
| * Whether to allow the OSC 52 sequence to write to the system clipboard. |
| */ |
| this.enableClipboardWrite = true; |
| |
| /** |
| * Respect the host's attempt to change the cursor blink status using |
| * the DEC Private mode 12. |
| */ |
| this.enableDec12 = false; |
| |
| /** |
| * Respect the host's attempt to clear the scrollback buffer using CSI-J-3. |
| */ |
| this.enableCsiJ3 = true; |
| |
| /** |
| * The expected encoding method for data received from the host. |
| */ |
| this.characterEncoding = 'utf-8'; |
| |
| /** |
| * If true, emit warnings when we encounter a control character or escape |
| * sequence that we don't recognize or explicitly ignore. |
| * |
| * We disable this by default as the console logging can be expensive when |
| * dumping binary files (e.g. `cat /dev/zero`) to the point where you can't |
| * recover w/out restarting. |
| */ |
| this.warnUnimplemented = false; |
| |
| /** |
| * The set of available character maps (used by G0...G3 below). |
| */ |
| this.characterMaps = new hterm.VT.CharacterMaps(); |
| |
| /** |
| * The default G0...G3 character maps. |
| * We default to the US/ASCII map everywhere as that aligns with other |
| * terminals, and it makes it harder to accidentally switch to the graphics |
| * character map (Ctrl-N). Any program that wants to use the graphics map |
| * will usually select it anyways since there's no guarantee what state any |
| * of the maps are in at any particular time. |
| */ |
| this.G0 = this.G1 = this.G2 = this.G3 = |
| this.characterMaps.getMap('B'); |
| |
| /** |
| * The 7-bit visible character set. |
| * |
| * This is a mapping from inbound data to display glyph. The GL set |
| * contains the 94 bytes from 0x21 to 0x7e. |
| * |
| * The default GL set is 'B', US ASCII. |
| */ |
| this.GL = 'G0'; |
| |
| /** |
| * The 8-bit visible character set. |
| * |
| * This is a mapping from inbound data to display glyph. The GR set |
| * contains the 94 bytes from 0xa1 to 0xfe. |
| */ |
| this.GR = 'G0'; |
| |
| /** |
| * The current encoding of the terminal. |
| * |
| * We only support ECMA-35 and UTF-8, so go with a boolean here. |
| * The encoding can be locked too. |
| */ |
| this.codingSystemUtf8_ = false; |
| this.codingSystemLocked_ = false; |
| |
| // Construct a regular expression to match the known one-byte control chars. |
| // This is used in parseUnknown_ to quickly scan a string for the next |
| // control character. |
| this.cc1Pattern_ = null; |
| this.updateEncodingState_(); |
| }; |
| |
| /** |
| * No mouse events. |
| */ |
| hterm.VT.prototype.MOUSE_REPORT_DISABLED = 0; |
| |
| /** |
| * DECSET mode 9. |
| * |
| * Report mouse down events only. |
| */ |
| hterm.VT.prototype.MOUSE_REPORT_PRESS = 1; |
| |
| /** |
| * DECSET mode 1000. |
| * |
| * Report mouse down/up events only. |
| */ |
| hterm.VT.prototype.MOUSE_REPORT_CLICK = 2; |
| |
| /** |
| * DECSET mode 1002. |
| * |
| * Report mouse down/up and movement while a button is down. |
| */ |
| hterm.VT.prototype.MOUSE_REPORT_DRAG = 3; |
| |
| /** |
| * DEC mode for X10 coorindates (the default). |
| */ |
| hterm.VT.prototype.MOUSE_COORDINATES_X10 = 0; |
| |
| /** |
| * DEC mode 1005 for UTF-8 coorindates. |
| */ |
| hterm.VT.prototype.MOUSE_COORDINATES_UTF8 = 1; |
| |
| /** |
| * DEC mode 1006 for SGR coorindates. |
| */ |
| hterm.VT.prototype.MOUSE_COORDINATES_SGR = 2; |
| |
| /** |
| * ParseState constructor. |
| * |
| * This object tracks the current state of the parse. It has fields for the |
| * current buffer, position in the buffer, and the parse function. |
| * |
| * @param {function} defaultFunc The default parser function. |
| * @param {string} opt_buf Optional string to use as the current buffer. |
| */ |
| hterm.VT.ParseState = function(defaultFunction, opt_buf) { |
| this.defaultFunction = defaultFunction; |
| this.buf = opt_buf || null; |
| this.pos = 0; |
| this.func = defaultFunction; |
| this.args = []; |
| // Whether any of the arguments in the args array have subarguments. |
| // e.g. All CSI sequences are integer arguments separated by semi-colons, |
| // so subarguments are further colon separated. |
| this.subargs = null; |
| }; |
| |
| /** |
| * Reset the parser function, buffer, and position. |
| */ |
| hterm.VT.ParseState.prototype.reset = function(opt_buf) { |
| this.resetParseFunction(); |
| this.resetBuf(opt_buf || ''); |
| this.resetArguments(); |
| }; |
| |
| /** |
| * Reset the parser function only. |
| */ |
| hterm.VT.ParseState.prototype.resetParseFunction = function() { |
| this.func = this.defaultFunction; |
| }; |
| |
| /** |
| * Reset the buffer and position only. |
| * |
| * @param {string} buf Optional new value for buf, defaults to null. |
| */ |
| hterm.VT.ParseState.prototype.resetBuf = function(opt_buf) { |
| this.buf = (typeof opt_buf == 'string') ? opt_buf : null; |
| this.pos = 0; |
| }; |
| |
| /** |
| * Reset the arguments list only. |
| * |
| * Typically we reset arguments before parsing a sequence that uses them rather |
| * than always trying to make sure they're in a good state. This can lead to |
| * confusion during debugging where args from a previous sequence appear to be |
| * "sticking around" in other sequences (which in reality don't use args). |
| * |
| * @param {string} opt_arg_zero Optional initial value for args[0]. |
| */ |
| hterm.VT.ParseState.prototype.resetArguments = function(opt_arg_zero) { |
| this.args.length = 0; |
| if (typeof opt_arg_zero != 'undefined') |
| this.args[0] = opt_arg_zero; |
| }; |
| |
| /** |
| * Parse an argument as an integer. |
| * |
| * This assumes the inputs are already in the proper format. e.g. This won't |
| * handle non-numeric arguments. |
| * |
| * An "0" argument is treated the same as "" which means the default value will |
| * be applied. This is what most terminal sequences expect. |
| * |
| * @param {string} argstr The argument to parse directly. |
| * @param {number=} defaultValue Default value if argstr is empty. |
| * @return {number} The parsed value. |
| */ |
| hterm.VT.ParseState.prototype.parseInt = function(argstr, defaultValue) { |
| if (defaultValue === undefined) |
| defaultValue = 0; |
| |
| if (argstr) { |
| const ret = parseInt(argstr, 10); |
| // An argument of zero is treated as the default value. |
| return ret == 0 ? defaultValue : ret; |
| } |
| return defaultValue; |
| }; |
| |
| /** |
| * Get an argument as an integer. |
| * |
| * @param {number} argnum The argument number to retrieve. |
| * @param {number=} defaultValue Default value if the argument is empty. |
| * @return {number} The parsed value. |
| */ |
| hterm.VT.ParseState.prototype.iarg = function(argnum, defaultValue) { |
| return this.parseInt(this.args[argnum], defaultValue); |
| }; |
| |
| /** |
| * Check whether an argument has subarguments. |
| * |
| * @param {number} argnum The argument number to check. |
| * @return {number} Whether the argument has subarguments. |
| */ |
| hterm.VT.ParseState.prototype.argHasSubargs = function(argnum) { |
| return this.subargs && this.subargs[argnum]; |
| }; |
| |
| /** |
| * Mark an argument as having subarguments. |
| * |
| * @param {number} argnum The argument number that has subarguments. |
| */ |
| hterm.VT.ParseState.prototype.argSetSubargs = function(argnum) { |
| if (this.subargs === null) |
| this.subargs = {}; |
| this.subargs[argnum] = true; |
| }; |
| |
| /** |
| * Advance the parse position. |
| * |
| * @param {integer} count The number of bytes to advance. |
| */ |
| hterm.VT.ParseState.prototype.advance = function(count) { |
| this.pos += count; |
| }; |
| |
| /** |
| * Return the remaining portion of the buffer without affecting the parse |
| * position. |
| * |
| * @return {string} The remaining portion of the buffer. |
| */ |
| hterm.VT.ParseState.prototype.peekRemainingBuf = function() { |
| return this.buf.substr(this.pos); |
| }; |
| |
| /** |
| * Return the next single character in the buffer without affecting the parse |
| * position. |
| * |
| * @return {string} The next character in the buffer. |
| */ |
| hterm.VT.ParseState.prototype.peekChar = function() { |
| return this.buf.substr(this.pos, 1); |
| }; |
| |
| /** |
| * Return the next single character in the buffer and advance the parse |
| * position one byte. |
| * |
| * @return {string} The next character in the buffer. |
| */ |
| hterm.VT.ParseState.prototype.consumeChar = function() { |
| return this.buf.substr(this.pos++, 1); |
| }; |
| |
| /** |
| * Return true if the buffer is empty, or the position is past the end. |
| */ |
| hterm.VT.ParseState.prototype.isComplete = function() { |
| return this.buf == null || this.buf.length <= this.pos; |
| }; |
| |
| /** |
| * Reset the VT back to baseline state. |
| */ |
| hterm.VT.prototype.reset = function() { |
| this.G0 = this.G1 = this.G2 = this.G3 = |
| this.characterMaps.getMap('B'); |
| |
| this.GL = 'G0'; |
| this.GR = 'G0'; |
| |
| this.mouseReport = this.MOUSE_REPORT_DISABLED; |
| this.mouseCoordinates = this.MOUSE_COORDINATES_X10; |
| this.lastMouseDragResponse_ = null; |
| }; |
| |
| /** |
| * Handle terminal mouse events. |
| * |
| * See the "Mouse Tracking" section of [xterm]. |
| */ |
| hterm.VT.prototype.onTerminalMouse_ = function(e) { |
| // Short circuit a few events to avoid unnecessary processing. |
| if (this.mouseReport == this.MOUSE_REPORT_DISABLED) |
| return; |
| else if (this.mouseReport != this.MOUSE_REPORT_DRAG && e.type == 'mousemove') |
| return; |
| |
| // Temporary storage for our response. |
| var response; |
| |
| // Modifier key state. |
| var mod = 0; |
| if (this.mouseReport != this.MOUSE_REPORT_PRESS) { |
| if (e.shiftKey) |
| mod |= 4; |
| if (e.metaKey || (this.terminal.keyboard.altIsMeta && e.altKey)) |
| mod |= 8; |
| if (e.ctrlKey) |
| mod |= 16; |
| } |
| |
| // X & Y coordinate reporting. |
| let x; |
| let y; |
| // Normally X10 has a limit of 255, but since we only want to emit UTF-8 valid |
| // streams, we limit ourselves to 127 to avoid setting the 8th bit. If we do |
| // re-enable this, we should re-enable the hterm_vt_tests.js too. |
| let limit = 127; |
| switch (this.mouseCoordinates) { |
| case this.MOUSE_COORDINATES_UTF8: |
| // UTF-8 mode is the same as X10 but with higher limits. |
| limit = 2047; |
| case this.MOUSE_COORDINATES_X10: |
| // X10 reports coordinates by encoding into strings. |
| x = String.fromCharCode(lib.f.clamp(e.terminalColumn + 32, 32, limit)); |
| y = String.fromCharCode(lib.f.clamp(e.terminalRow + 32, 32, limit)); |
| break; |
| case this.MOUSE_COORDINATES_SGR: |
| // SGR reports coordinates by transmitting the numbers directly. |
| x = e.terminalColumn; |
| y = e.terminalRow; |
| break; |
| } |
| |
| switch (e.type) { |
| case 'wheel': |
| // Mouse wheel is treated as button 1 or 2 plus an additional 64. |
| b = (((e.deltaY * -1) > 0) ? 0 : 1) + 64; |
| b |= mod; |
| if (this.mouseCoordinates == this.MOUSE_COORDINATES_SGR) { |
| response = `\x1b[<${b};${x};${y}M`; |
| } else { |
| // X10 based modes (including UTF8) add 32 for legacy encoding reasons. |
| response = '\x1b[M' + String.fromCharCode(b + 32) + x + y; |
| } |
| |
| // Keep the terminal from scrolling. |
| e.preventDefault(); |
| break; |
| |
| case 'mousedown': |
| // Buttons are encoded as button number. |
| var b = Math.min(e.button, 2); |
| // X10 based modes (including UTF8) add 32 for legacy encoding reasons. |
| if (this.mouseCoordinates != this.MOUSE_COORDINATES_SGR) |
| b += 32; |
| |
| // And mix in the modifier keys. |
| b |= mod; |
| |
| if (this.mouseCoordinates == this.MOUSE_COORDINATES_SGR) |
| response = `\x1b[<${b};${x};${y}M`; |
| else |
| response = '\x1b[M' + String.fromCharCode(b) + x + y; |
| break; |
| |
| case 'mouseup': |
| if (this.mouseReport != this.MOUSE_REPORT_PRESS) { |
| if (this.mouseCoordinates == this.MOUSE_COORDINATES_SGR) { |
| // SGR mode can report the released button. |
| response = `\x1b[<${e.button};${x};${y}m`; |
| } else { |
| // X10 mode has no indication of which button was released. |
| response = '\x1b[M\x23' + x + y; |
| } |
| } |
| break; |
| |
| case 'mousemove': |
| if (this.mouseReport == this.MOUSE_REPORT_DRAG && e.buttons) { |
| // Standard button bits. The XTerm protocol only reports the first |
| // button press (e.g. if left & right are pressed, right is ignored), |
| // and it only supports the first three buttons. If none of them are |
| // pressed, then XTerm flags it as a release. We'll do the same. |
| // X10 based modes (including UTF8) add 32 for legacy encoding reasons. |
| b = this.mouseCoordinates == this.MOUSE_COORDINATES_SGR ? 0 : 32; |
| |
| // Priority here matches XTerm: left, middle, right. |
| if (e.buttons & 0x1) { |
| // Report left button. |
| b += 0; |
| } else if (e.buttons & 0x4) { |
| // Report middle button. |
| b += 1; |
| } else if (e.buttons & 0x2) { |
| // Report right button. |
| b += 2; |
| } else { |
| // Release higher buttons. |
| b += 3; |
| } |
| |
| // Add 32 to indicate mouse motion. |
| b += 32; |
| |
| // And mix in the modifier keys. |
| b |= mod; |
| |
| if (this.mouseCoordinates == this.MOUSE_COORDINATES_SGR) |
| response = `\x1b[<${b};${x};${y}M`; |
| else |
| response = '\x1b[M' + String.fromCharCode(b) + x + y; |
| |
| // If we were going to report the same cell because we moved pixels |
| // within, suppress the report. This is what xterm does and cuts |
| // down on duplicate messages. |
| if (this.lastMouseDragResponse_ == response) |
| response = ''; |
| else |
| this.lastMouseDragResponse_ = response; |
| } |
| |
| break; |
| |
| case 'click': |
| case 'dblclick': |
| break; |
| |
| default: |
| console.error('Unknown mouse event: ' + e.type, e); |
| break; |
| } |
| |
| if (response) |
| this.terminal.io.sendString(response); |
| }; |
| |
| /** |
| * Interpret a string of characters, displaying the results on the associated |
| * terminal object. |
| * |
| * The buffer will be decoded according to the 'receive-encoding' preference. |
| */ |
| hterm.VT.prototype.interpret = function(buf) { |
| this.parseState_.resetBuf(buf); |
| |
| while (!this.parseState_.isComplete()) { |
| var func = this.parseState_.func; |
| var pos = this.parseState_.pos; |
| var buf = this.parseState_.buf; |
| |
| this.parseState_.func.call(this, this.parseState_); |
| |
| if (this.parseState_.func == func && this.parseState_.pos == pos && |
| this.parseState_.buf == buf) { |
| throw 'Parser did not alter the state!'; |
| } |
| } |
| }; |
| |
| /** |
| * Set the encoding of the terminal. |
| * |
| * @param {string} encoding The name of the encoding to set. |
| */ |
| hterm.VT.prototype.setEncoding = function(encoding) { |
| switch (encoding) { |
| default: |
| console.warn('Invalid value for "terminal-encoding": ' + encoding); |
| // Fall through. |
| case 'iso-2022': |
| this.codingSystemUtf8_ = false; |
| this.codingSystemLocked_ = false; |
| break; |
| case 'utf-8-locked': |
| this.codingSystemUtf8_ = true; |
| this.codingSystemLocked_ = true; |
| break; |
| case 'utf-8': |
| this.codingSystemUtf8_ = true; |
| this.codingSystemLocked_ = false; |
| break; |
| } |
| |
| this.updateEncodingState_(); |
| }; |
| |
| /** |
| * Refresh internal state when the encoding changes. |
| */ |
| hterm.VT.prototype.updateEncodingState_ = function() { |
| // If we're in UTF8 mode, don't suport 8-bit escape sequences as we'll never |
| // see those -- everything should be UTF8! |
| var cc1 = Object.keys(hterm.VT.CC1) |
| .filter((e) => !this.codingSystemUtf8_ || e.charCodeAt() < 0x80) |
| .map((e) => '\\x' + lib.f.zpad(e.charCodeAt().toString(16), 2)) |
| .join(''); |
| this.cc1Pattern_ = new RegExp(`[${cc1}]`); |
| }; |
| |
| /** |
| * The default parse function. |
| * |
| * This will scan the string for the first 1-byte control character (C0/C1 |
| * characters from [CTRL]). Any plain text coming before the code will be |
| * printed to the terminal, then the control character will be dispatched. |
| */ |
| hterm.VT.prototype.parseUnknown_ = function(parseState) { |
| var self = this; |
| |
| function print(str) { |
| if (!self.codingSystemUtf8_ && self[self.GL].GL) |
| str = self[self.GL].GL(str); |
| |
| self.terminal.print(str); |
| } |
| |
| // Search for the next contiguous block of plain text. |
| var buf = parseState.peekRemainingBuf(); |
| var nextControl = buf.search(this.cc1Pattern_); |
| |
| if (nextControl == 0) { |
| // We've stumbled right into a control character. |
| this.dispatch('CC1', buf.substr(0, 1), parseState); |
| parseState.advance(1); |
| return; |
| } |
| |
| if (nextControl == -1) { |
| // There are no control characters in this string. |
| print(buf); |
| parseState.reset(); |
| return; |
| } |
| |
| print(buf.substr(0, nextControl)); |
| this.dispatch('CC1', buf.substr(nextControl, 1), parseState); |
| parseState.advance(nextControl + 1); |
| }; |
| |
| /** |
| * Parse a Control Sequence Introducer code and dispatch it. |
| * |
| * See [CSI] for some useful information about these codes. |
| */ |
| hterm.VT.prototype.parseCSI_ = function(parseState) { |
| var ch = parseState.peekChar(); |
| var args = parseState.args; |
| |
| const finishParsing = () => { |
| // Resetting the arguments isn't strictly necessary, but it makes debugging |
| // less confusing (otherwise args will stick around until the next sequence |
| // that needs arguments). |
| parseState.resetArguments(); |
| // We need to clear subargs since we explicitly set it. |
| parseState.subargs = null; |
| parseState.resetParseFunction(); |
| }; |
| |
| if (ch >= '@' && ch <= '~') { |
| // This is the final character. |
| this.dispatch('CSI', this.leadingModifier_ + this.trailingModifier_ + ch, |
| parseState); |
| finishParsing(); |
| |
| } else if (ch == ';') { |
| // Parameter delimiter. |
| if (this.trailingModifier_) { |
| // Parameter delimiter after the trailing modifier. That's a paddlin'. |
| finishParsing(); |
| |
| } else { |
| if (!args.length) { |
| // They omitted the first param, we need to supply it. |
| args.push(''); |
| } |
| |
| args.push(''); |
| } |
| |
| } else if (ch >= '0' && ch <= '9' || ch == ':') { |
| // Next byte in the current parameter. |
| |
| if (this.trailingModifier_) { |
| // Numeric parameter after the trailing modifier. That's a paddlin'. |
| finishParsing(); |
| } else { |
| if (!args.length) { |
| args[0] = ch; |
| } else { |
| args[args.length - 1] += ch; |
| } |
| |
| // Possible sub-parameters. |
| if (ch == ':') |
| parseState.argSetSubargs(args.length - 1); |
| } |
| |
| } else if (ch >= ' ' && ch <= '?') { |
| // Modifier character. |
| if (!args.length) { |
| this.leadingModifier_ += ch; |
| } else { |
| this.trailingModifier_ += ch; |
| } |
| |
| } else if (this.cc1Pattern_.test(ch)) { |
| // Control character. |
| this.dispatch('CC1', ch, parseState); |
| |
| } else { |
| // Unexpected character in sequence, bail out. |
| finishParsing(); |
| } |
| |
| parseState.advance(1); |
| }; |
| |
| /** |
| * Skip over the string until the next String Terminator (ST, 'ESC \') or |
| * Bell (BEL, '\x07'). |
| * |
| * The string is accumulated in parseState.args[0]. Make sure to reset the |
| * arguments (with parseState.resetArguments) before starting the parse. |
| * |
| * You can detect that parsing in complete by checking that the parse |
| * function has changed back to the default parse function. |
| * |
| * @return {boolean} If true, parsing is ongoing or complete. If false, we've |
| * exceeded the max string sequence. |
| */ |
| hterm.VT.prototype.parseUntilStringTerminator_ = function(parseState) { |
| var buf = parseState.peekRemainingBuf(); |
| var args = parseState.args; |
| // Since we might modify parse state buffer locally, if we want to advance |
| // the parse state buffer later on, we need to know how many chars we added. |
| let bufInserted = 0; |
| |
| if (!args.length) { |
| args[0] = ''; |
| args[1] = new Date(); |
| } else { |
| // If our saved buffer ends with an escape, it's because we were hoping |
| // it's an ST split across two buffers. Move it from our saved buffer |
| // to the start of our current buffer for processing anew. |
| if (args[0].slice(-1) == '\x1b') { |
| args[0] = args[0].slice(0, -1); |
| buf = '\x1b' + buf; |
| bufInserted = 1; |
| } |
| } |
| |
| const nextTerminator = buf.search(/[\x1b\x07]/); |
| const terminator = buf[nextTerminator]; |
| let foundTerminator; |
| |
| // If the next escape we see is not a start of a ST, fall through. This will |
| // either be invalid (embedded escape), or we'll queue it up (wait for \\). |
| if (terminator == '\x1b' && buf[nextTerminator + 1] != '\\') |
| foundTerminator = false; |
| else |
| foundTerminator = (nextTerminator != -1); |
| |
| if (!foundTerminator) { |
| // No terminator here, have to wait for the next string. |
| |
| args[0] += buf; |
| |
| var abortReason; |
| |
| // Special case: If our buffering happens to split the ST (\e\\), we have to |
| // buffer the content temporarily. So don't reject a trailing escape here, |
| // instead we let it timeout or be rejected in the next pass. |
| if (terminator == '\x1b' && nextTerminator != buf.length - 1) |
| abortReason = 'embedded escape: ' + nextTerminator; |
| |
| if (new Date() - args[1] > this.oscTimeLimit_) |
| abortReason = 'timeout expired: ' + (new Date() - args[1]); |
| |
| if (abortReason) { |
| if (this.warnUnimplemented) |
| console.log('parseUntilStringTerminator_: aborting: ' + abortReason, |
| args[0]); |
| parseState.reset(args[0]); |
| return false; |
| } |
| |
| parseState.advance(buf.length - bufInserted); |
| return true; |
| } |
| |
| args[0] += buf.substr(0, nextTerminator); |
| |
| parseState.resetParseFunction(); |
| parseState.advance(nextTerminator + |
| (terminator == '\x1b' ? 2 : 1) - bufInserted); |
| |
| return true; |
| }; |
| |
| /** |
| * Dispatch to the function that handles a given CC1, ESC, or CSI or VT52 code. |
| */ |
| hterm.VT.prototype.dispatch = function(type, code, parseState) { |
| var handler = hterm.VT[type][code]; |
| if (!handler) { |
| if (this.warnUnimplemented) |
| console.warn('Unknown ' + type + ' code: ' + JSON.stringify(code)); |
| return; |
| } |
| |
| if (handler == hterm.VT.ignore) { |
| if (this.warnUnimplemented) |
| console.warn('Ignored ' + type + ' code: ' + JSON.stringify(code)); |
| return; |
| } |
| |
| if (parseState.subargs && !handler.supportsSubargs) { |
| if (this.warnUnimplemented) |
| console.warn('Ignored ' + type + ' code w/subargs: ' + JSON.stringify(code)); |
| return; |
| } |
| |
| if (type == 'CC1' && code > '\x7f' && !this.enable8BitControl) { |
| // It's kind of a hack to put this here, but... |
| // |
| // If we're dispatching a 'CC1' code, and it's got the eighth bit set, |
| // but we're not supposed to handle 8-bit codes? Just ignore it. |
| // |
| // This prevents an errant (DCS, '\x90'), (OSC, '\x9d'), (PM, '\x9e') or |
| // (APC, '\x9f') from locking up the terminal waiting for its expected |
| // (ST, '\x9c') or (BEL, '\x07'). |
| console.warn('Ignoring 8-bit control code: 0x' + |
| code.charCodeAt(0).toString(16)); |
| return; |
| } |
| |
| handler.apply(this, [parseState, code]); |
| }; |
| |
| /** |
| * Set one of the ANSI defined terminal mode bits. |
| * |
| * Invoked in response to SM/RM. |
| * |
| * Unexpected and unimplemented values are silently ignored. |
| */ |
| hterm.VT.prototype.setANSIMode = function(code, state) { |
| if (code == 4) { // Insert Mode (IRM) |
| this.terminal.setInsertMode(state); |
| } else if (code == 20) { // Automatic Newline (LNM) |
| this.terminal.setAutoCarriageReturn(state); |
| } else if (this.warnUnimplemented) { |
| console.warn('Unimplemented ANSI Mode: ' + code); |
| } |
| }; |
| |
| /** |
| * Set or reset one of the DEC Private modes. |
| * |
| * Invoked in response to DECSET/DECRST. |
| */ |
| hterm.VT.prototype.setDECMode = function(code, state) { |
| switch (parseInt(code, 10)) { |
| case 1: // DECCKM |
| this.terminal.keyboard.applicationCursor = state; |
| break; |
| |
| case 3: // DECCOLM |
| if (this.allowColumnWidthChanges_) { |
| this.terminal.setWidth(state ? 132 : 80); |
| |
| this.terminal.clearHome(); |
| this.terminal.setVTScrollRegion(null, null); |
| } |
| break; |
| |
| case 5: // DECSCNM |
| this.terminal.setReverseVideo(state); |
| break; |
| |
| case 6: // DECOM |
| this.terminal.setOriginMode(state); |
| break; |
| |
| case 7: // DECAWM |
| this.terminal.setWraparound(state); |
| break; |
| |
| case 9: // Report on mouse down events only (X10). |
| this.mouseReport = ( |
| state ? this.MOUSE_REPORT_PRESS : this.MOUSE_REPORT_DISABLED); |
| this.terminal.syncMouseStyle(); |
| break; |
| |
| case 12: // Start blinking cursor |
| if (this.enableDec12) |
| this.terminal.setCursorBlink(state); |
| break; |
| |
| case 25: // DECTCEM |
| this.terminal.setCursorVisible(state); |
| break; |
| |
| case 30: // Show scrollbar |
| this.terminal.setScrollbarVisible(state); |
| break; |
| |
| case 40: // Allow 80 - 132 (DECCOLM) Mode |
| this.terminal.allowColumnWidthChanges_ = state; |
| break; |
| |
| case 45: // Reverse-wraparound Mode |
| this.terminal.setReverseWraparound(state); |
| break; |
| |
| case 67: // Backarrow key sends backspace (DECBKM) |
| this.terminal.keyboard.backspaceSendsBackspace = state; |
| break; |
| |
| case 1000: // Report on mouse clicks only (X11). |
| this.mouseReport = ( |
| state ? this.MOUSE_REPORT_CLICK : this.MOUSE_REPORT_DISABLED); |
| this.terminal.syncMouseStyle(); |
| break; |
| |
| case 1002: // Report on mouse clicks and drags |
| this.mouseReport = ( |
| state ? this.MOUSE_REPORT_DRAG : this.MOUSE_REPORT_DISABLED); |
| this.terminal.syncMouseStyle(); |
| break; |
| |
| case 1004: // Report on window focus change. |
| this.terminal.reportFocus = state; |
| break; |
| |
| case 1005: // Extended coordinates in UTF-8 mode. |
| this.mouseCoordinates = ( |
| state ? this.MOUSE_COORDINATES_UTF8 : this.MOUSE_COORDINATES_X10); |
| break; |
| |
| case 1006: // Extended coordinates in SGR mode. |
| this.mouseCoordinates = ( |
| state ? this.MOUSE_COORDINATES_SGR : this.MOUSE_COORDINATES_X10); |
| break; |
| |
| case 1007: // Enable Alternate Scroll Mode. |
| this.terminal.scrollWheelArrowKeys_ = state; |
| break; |
| |
| case 1010: // Scroll to bottom on tty output |
| this.terminal.scrollOnOutput = state; |
| break; |
| |
| case 1011: // Scroll to bottom on key press |
| this.terminal.scrollOnKeystroke = state; |
| break; |
| |
| case 1036: // Send ESC when Meta modifies a key |
| this.terminal.keyboard.metaSendsEscape = state; |
| break; |
| |
| case 1039: // Send ESC when Alt modifies a key |
| if (state) { |
| if (!this.terminal.keyboard.previousAltSendsWhat_) { |
| this.terminal.keyboard.previousAltSendsWhat_ = |
| this.terminal.keyboard.altSendsWhat; |
| this.terminal.keyboard.altSendsWhat = 'escape'; |
| } |
| } else if (this.terminal.keyboard.previousAltSendsWhat_) { |
| this.terminal.keyboard.altSendsWhat = |
| this.terminal.keyboard.previousAltSendsWhat_; |
| this.terminal.keyboard.previousAltSendsWhat_ = null; |
| } |
| break; |
| |
| case 47: // Use Alternate Screen Buffer |
| case 1047: |
| this.terminal.setAlternateMode(state); |
| break; |
| |
| case 1048: // Save cursor as in DECSC. |
| if (state) |
| this.terminal.saveCursorAndState(); |
| else |
| this.terminal.restoreCursorAndState(); |
| break; |
| |
| case 1049: // 1047 + 1048 + clear. |
| if (state) { |
| this.terminal.saveCursorAndState(); |
| this.terminal.setAlternateMode(state); |
| this.terminal.clear(); |
| } else { |
| this.terminal.setAlternateMode(state); |
| this.terminal.restoreCursorAndState(); |
| } |
| |
| break; |
| |
| case 2004: // Bracketed paste mode. |
| this.terminal.setBracketedPaste(state); |
| break; |
| |
| default: |
| if (this.warnUnimplemented) |
| console.warn('Unimplemented DEC Private Mode: ' + code); |
| break; |
| } |
| }; |
| |
| /** |
| * Function shared by control characters and escape sequences that are |
| * ignored. |
| */ |
| hterm.VT.ignore = function() {}; |
| |
| /** |
| * Collection of control characters expressed in a single byte. |
| * |
| * This includes the characters from the C0 and C1 sets (see [CTRL]) that we |
| * care about. Two byte versions of the C1 codes are defined in the |
| * hterm.VT.ESC collection. |
| * |
| * The 'CC1' mnemonic here refers to the fact that these are one-byte Control |
| * Codes. It's only used in this source file and not defined in any of the |
| * referenced documents. |
| */ |
| hterm.VT.CC1 = {}; |
| |
| /** |
| * Collection of two-byte and three-byte sequences starting with ESC. |
| */ |
| hterm.VT.ESC = {}; |
| |
| /** |
| * Collection of CSI (Control Sequence Introducer) sequences. |
| * |
| * These sequences begin with 'ESC [', and may take zero or more arguments. |
| */ |
| hterm.VT.CSI = {}; |
| |
| /** |
| * Collection of OSC (Operating System Control) sequences. |
| * |
| * These sequences begin with 'ESC ]', followed by a function number and a |
| * string terminated by either ST or BEL. |
| */ |
| hterm.VT.OSC = {}; |
| |
| /** |
| * Collection of VT52 sequences. |
| * |
| * When in VT52 mode, other sequences are disabled. |
| */ |
| hterm.VT.VT52 = {}; |
| |
| /** |
| * Null (NUL). |
| * |
| * Silently ignored. |
| */ |
| hterm.VT.CC1['\x00'] = hterm.VT.ignore; |
| |
| /** |
| * Enquiry (ENQ). |
| * |
| * Transmit answerback message. |
| * |
| * The default answerback message in xterm is an empty string, so we just |
| * ignore this. |
| */ |
| hterm.VT.CC1['\x05'] = hterm.VT.ignore; |
| |
| /** |
| * Ring Bell (BEL). |
| */ |
| hterm.VT.CC1['\x07'] = function() { |
| this.terminal.ringBell(); |
| }; |
| |
| /** |
| * Backspace (BS). |
| * |
| * Move the cursor to the left one character position, unless it is at the |
| * left margin, in which case no action occurs. |
| */ |
| hterm.VT.CC1['\x08'] = function() { |
| this.terminal.cursorLeft(1); |
| }; |
| |
| /** |
| * Horizontal Tab (HT). |
| * |
| * Move the cursor to the next tab stop, or to the right margin if no further |
| * tab stops are present on the line. |
| */ |
| hterm.VT.CC1['\x09'] = function() { |
| this.terminal.forwardTabStop(); |
| }; |
| |
| /** |
| * Line Feed (LF). |
| * |
| * This code causes a line feed or a new line operation. See Automatic |
| * Newline (LNM). |
| */ |
| hterm.VT.CC1['\x0a'] = function() { |
| this.terminal.formFeed(); |
| }; |
| |
| /** |
| * Vertical Tab (VT). |
| * |
| * Interpreted as LF. |
| */ |
| hterm.VT.CC1['\x0b'] = hterm.VT.CC1['\x0a']; |
| |
| /** |
| * Form Feed (FF). |
| * |
| * Interpreted as LF. |
| */ |
| hterm.VT.CC1['\x0c'] = hterm.VT.CC1['\x0a']; |
| |
| /** |
| * Carriage Return (CR). |
| * |
| * Move cursor to the left margin on the current line. |
| */ |
| hterm.VT.CC1['\x0d'] = function() { |
| this.terminal.setCursorColumn(0); |
| }; |
| |
| /** |
| * Shift Out (SO), aka Lock Shift 0 (LS1). |
| * |
| * Invoke G1 character set in GL. |
| */ |
| hterm.VT.CC1['\x0e'] = function() { |
| this.GL = 'G1'; |
| }; |
| |
| /** |
| * Shift In (SI), aka Lock Shift 0 (LS0). |
| * |
| * Invoke G0 character set in GL. |
| */ |
| hterm.VT.CC1['\x0f'] = function() { |
| this.GL = 'G0'; |
| }; |
| |
| /** |
| * Transmit On (XON). |
| * |
| * Not currently implemented. |
| * |
| * TODO(rginda): Implement? |
| */ |
| hterm.VT.CC1['\x11'] = hterm.VT.ignore; |
| |
| /** |
| * Transmit Off (XOFF). |
| * |
| * Not currently implemented. |
| * |
| * TODO(rginda): Implement? |
| */ |
| hterm.VT.CC1['\x13'] = hterm.VT.ignore; |
| |
| /** |
| * Cancel (CAN). |
| * |
| * If sent during a control sequence, the sequence is immediately terminated |
| * and not executed. |
| * |
| * It also causes the error character to be displayed. |
| */ |
| hterm.VT.CC1['\x18'] = function(parseState) { |
| // If we've shifted in the G1 character set, shift it back out to |
| // the default character set. |
| if (this.GL == 'G1') { |
| this.GL = 'G0'; |
| } |
| parseState.resetParseFunction(); |
| this.terminal.print('?'); |
| }; |
| |
| /** |
| * Substitute (SUB). |
| * |
| * Interpreted as CAN. |
| */ |
| hterm.VT.CC1['\x1a'] = hterm.VT.CC1['\x18']; |
| |
| /** |
| * Escape (ESC). |
| */ |
| hterm.VT.CC1['\x1b'] = function(parseState) { |
| function parseESC(parseState) { |
| var ch = parseState.consumeChar(); |
| |
| if (ch == '\x1b') |
| return; |
| |
| this.dispatch('ESC', ch, parseState); |
| |
| if (parseState.func == parseESC) |
| parseState.resetParseFunction(); |
| } |
| |
| parseState.func = parseESC; |
| }; |
| |
| /** |
| * Delete (DEL). |
| */ |
| hterm.VT.CC1['\x7f'] = hterm.VT.ignore; |
| |
| // 8 bit control characters and their two byte equivalents, below... |
| |
| /** |
| * Index (IND). |
| * |
| * Like newline, only keep the X position |
| */ |
| hterm.VT.CC1['\x84'] = |
| hterm.VT.ESC['D'] = function() { |
| this.terminal.lineFeed(); |
| }; |
| |
| /** |
| * Next Line (NEL). |
| * |
| * Like newline, but doesn't add lines. |
| */ |
| hterm.VT.CC1['\x85'] = |
| hterm.VT.ESC['E'] = function() { |
| this.terminal.setCursorColumn(0); |
| this.terminal.cursorDown(1); |
| }; |
| |
| /** |
| * Horizontal Tabulation Set (HTS). |
| */ |
| hterm.VT.CC1['\x88'] = |
| hterm.VT.ESC['H'] = function() { |
| this.terminal.setTabStop(this.terminal.getCursorColumn()); |
| }; |
| |
| /** |
| * Reverse Index (RI). |
| * |
| * Move up one line. |
| */ |
| hterm.VT.CC1['\x8d'] = |
| hterm.VT.ESC['M'] = function() { |
| this.terminal.reverseLineFeed(); |
| }; |
| |
| /** |
| * Single Shift 2 (SS2). |
| * |
| * Select of G2 Character Set for the next character only. |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CC1['\x8e'] = |
| hterm.VT.ESC['N'] = hterm.VT.ignore; |
| |
| /** |
| * Single Shift 3 (SS3). |
| * |
| * Select of G3 Character Set for the next character only. |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CC1['\x8f'] = |
| hterm.VT.ESC['O'] = hterm.VT.ignore; |
| |
| /** |
| * Device Control String (DCS). |
| * |
| * Indicate a DCS sequence. See Device-Control functions in [XTERM]. |
| * Not currently implemented. |
| * |
| * TODO(rginda): Consider implementing DECRQSS, the rest don't seem applicable. |
| */ |
| hterm.VT.CC1['\x90'] = |
| hterm.VT.ESC['P'] = function(parseState) { |
| parseState.resetArguments(); |
| parseState.func = this.parseUntilStringTerminator_; |
| }; |
| |
| /** |
| * Start of Guarded Area (SPA). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CC1['\x96'] = |
| hterm.VT.ESC['V'] = hterm.VT.ignore; |
| |
| /** |
| * End of Guarded Area (EPA). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CC1['\x97'] = |
| hterm.VT.ESC['W'] = hterm.VT.ignore; |
| |
| /** |
| * Start of String (SOS). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CC1['\x98'] = |
| hterm.VT.ESC['X'] = hterm.VT.ignore; |
| |
| /** |
| * Single Character Introducer (SCI, also DECID). |
| * |
| * Return Terminal ID. Obsolete form of 'ESC [ c' (DA). |
| */ |
| hterm.VT.CC1['\x9a'] = |
| hterm.VT.ESC['Z'] = function() { |
| this.terminal.io.sendString('\x1b[?1;2c'); |
| }; |
| |
| /** |
| * Control Sequence Introducer (CSI). |
| * |
| * The lead into most escape sequences. See [CSI]. |
| */ |
| hterm.VT.CC1['\x9b'] = |
| hterm.VT.ESC['['] = function(parseState) { |
| parseState.resetArguments(); |
| this.leadingModifier_ = ''; |
| this.trailingModifier_ = ''; |
| parseState.func = this.parseCSI_; |
| }; |
| |
| /** |
| * String Terminator (ST). |
| * |
| * Used to terminate DCS/OSC/PM/APC commands which may take string arguments. |
| * |
| * We don't directly handle it here, as it's only used to terminate other |
| * sequences. See the 'parseUntilStringTerminator_' method. |
| */ |
| hterm.VT.CC1['\x9c'] = |
| hterm.VT.ESC['\\'] = hterm.VT.ignore; |
| |
| /** |
| * Operating System Command (OSC). |
| * |
| * Commands relating to the operating system. |
| */ |
| hterm.VT.CC1['\x9d'] = |
| hterm.VT.ESC[']'] = function(parseState) { |
| parseState.resetArguments(); |
| |
| function parseOSC(parseState) { |
| if (!this.parseUntilStringTerminator_(parseState)) { |
| // The string sequence was too long. |
| return; |
| } |
| |
| if (parseState.func == parseOSC) { |
| // We're not done parsing the string yet. |
| return; |
| } |
| |
| // We're done. |
| var ary = parseState.args[0].match(/^(\d+);?(.*)$/); |
| if (ary) { |
| parseState.args[0] = ary[2]; |
| this.dispatch('OSC', ary[1], parseState); |
| } else { |
| console.warn('Invalid OSC: ' + JSON.stringify(parseState.args[0])); |
| } |
| |
| // Resetting the arguments isn't strictly necessary, but it makes debugging |
| // less confusing (otherwise args will stick around until the next sequence |
| // that needs arguments). |
| parseState.resetArguments(); |
| } |
| |
| parseState.func = parseOSC; |
| }; |
| |
| /** |
| * Privacy Message (PM). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CC1['\x9e'] = |
| hterm.VT.ESC['^'] = function(parseState) { |
| parseState.resetArguments(); |
| parseState.func = this.parseUntilStringTerminator_; |
| }; |
| |
| /** |
| * Application Program Control (APC). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CC1['\x9f'] = |
| hterm.VT.ESC['_'] = function(parseState) { |
| parseState.resetArguments(); |
| parseState.func = this.parseUntilStringTerminator_; |
| }; |
| |
| /** |
| * ESC \x20 - Unclear to me where these originated, possibly in xterm. |
| * |
| * Not currently implemented: |
| * ESC \x20 F - Select 7 bit escape codes in responses (S7C1T). |
| * ESC \x20 G - Select 8 bit escape codes in responses (S8C1T). |
| * NB: We currently assume S7C1T always. |
| * |
| * Will not implement: |
| * ESC \x20 L - Set ANSI conformance level 1. |
| * ESC \x20 M - Set ANSI conformance level 2. |
| * ESC \x20 N - Set ANSI conformance level 3. |
| */ |
| hterm.VT.ESC['\x20'] = function(parseState) { |
| parseState.func = function(parseState) { |
| var ch = parseState.consumeChar(); |
| if (this.warnUnimplemented) |
| console.warn('Unimplemented sequence: ESC 0x20 ' + ch); |
| parseState.resetParseFunction(); |
| }; |
| }; |
| |
| /** |
| * DEC 'ESC #' sequences. |
| */ |
| hterm.VT.ESC['#'] = function(parseState) { |
| parseState.func = function(parseState) { |
| var ch = parseState.consumeChar(); |
| if (ch == '8') { |
| // DEC Screen Alignment Test (DECALN). |
| this.terminal.setCursorPosition(0, 0); |
| this.terminal.fill('E'); |
| } |
| |
| parseState.resetParseFunction(); |
| }; |
| }; |
| |
| /** |
| * Designate Other Coding System (DOCS). |
| */ |
| hterm.VT.ESC['%'] = function(parseState) { |
| parseState.func = function(parseState) { |
| var ch = parseState.consumeChar(); |
| |
| // If we've locked the encoding, then just eat the bytes and return. |
| if (this.codingSystemLocked_) { |
| if (ch == '/') |
| parseState.consumeChar(); |
| parseState.resetParseFunction(); |
| return; |
| } |
| |
| // Process the encoding requests. |
| switch (ch) { |
| case '@': |
| // Switch to ECMA 35. |
| this.setEncoding('iso-2022'); |
| break; |
| |
| case 'G': |
| // Switch to UTF-8. |
| this.setEncoding('utf-8'); |
| break; |
| |
| case '/': |
| // One way transition to something else. |
| ch = parseState.consumeChar(); |
| switch (ch) { |
| case 'G': // UTF-8 Level 1. |
| case 'H': // UTF-8 Level 2. |
| case 'I': // UTF-8 Level 3. |
| // We treat all UTF-8 levels the same. |
| this.setEncoding('utf-8-locked'); |
| break; |
| |
| default: |
| if (this.warnUnimplemented) |
| console.warn('Unknown ESC % / argument: ' + JSON.stringify(ch)); |
| break; |
| } |
| break; |
| |
| default: |
| if (this.warnUnimplemented) |
| console.warn('Unknown ESC % argument: ' + JSON.stringify(ch)); |
| break; |
| } |
| |
| parseState.resetParseFunction(); |
| }; |
| }; |
| |
| /** |
| * Character Set Selection (SCS). |
| * |
| * ESC ( Ps - Set G0 character set (VT100). |
| * ESC ) Ps - Set G1 character set (VT220). |
| * ESC * Ps - Set G2 character set (VT220). |
| * ESC + Ps - Set G3 character set (VT220). |
| * ESC - Ps - Set G1 character set (VT300). |
| * ESC . Ps - Set G2 character set (VT300). |
| * ESC / Ps - Set G3 character set (VT300). |
| * |
| * All other sequences are echoed to the terminal. |
| */ |
| hterm.VT.ESC['('] = |
| hterm.VT.ESC[')'] = |
| hterm.VT.ESC['*'] = |
| hterm.VT.ESC['+'] = |
| hterm.VT.ESC['-'] = |
| hterm.VT.ESC['.'] = |
| hterm.VT.ESC['/'] = function(parseState, code) { |
| parseState.func = function(parseState) { |
| var ch = parseState.consumeChar(); |
| if (ch == '\x1b') { |
| parseState.resetParseFunction(); |
| parseState.func(); |
| return; |
| } |
| |
| var map = this.characterMaps.getMap(ch); |
| if (map !== undefined) { |
| if (code == '(') { |
| this.G0 = map; |
| } else if (code == ')' || code == '-') { |
| this.G1 = map; |
| } else if (code == '*' || code == '.') { |
| this.G2 = map; |
| } else if (code == '+' || code == '/') { |
| this.G3 = map; |
| } |
| } else if (this.warnUnimplemented) { |
| console.log('Invalid character set for "' + code + '": ' + ch); |
| } |
| |
| parseState.resetParseFunction(); |
| }; |
| }; |
| |
| /** |
| * Back Index (DECBI). |
| * |
| * VT420 and up. Not currently implemented. |
| */ |
| hterm.VT.ESC['6'] = hterm.VT.ignore; |
| |
| /** |
| * Save Cursor (DECSC). |
| */ |
| hterm.VT.ESC['7'] = function() { |
| this.terminal.saveCursorAndState(); |
| }; |
| |
| /** |
| * Restore Cursor (DECRC). |
| */ |
| hterm.VT.ESC['8'] = function() { |
| this.terminal.restoreCursorAndState(); |
| }; |
| |
| /** |
| * Forward Index (DECFI). |
| * |
| * VT210 and up. Not currently implemented. |
| */ |
| hterm.VT.ESC['9'] = hterm.VT.ignore; |
| |
| /** |
| * Application keypad (DECKPAM). |
| */ |
| hterm.VT.ESC['='] = function() { |
| this.terminal.keyboard.applicationKeypad = true; |
| }; |
| |
| /** |
| * Normal keypad (DECKPNM). |
| */ |
| hterm.VT.ESC['>'] = function() { |
| this.terminal.keyboard.applicationKeypad = false; |
| }; |
| |
| /** |
| * Cursor to lower left corner of screen. |
| * |
| * Will not implement. |
| * |
| * This is only recognized by xterm when the hpLowerleftBugCompat resource is |
| * set. |
| */ |
| hterm.VT.ESC['F'] = hterm.VT.ignore; |
| |
| /** |
| * Full Reset (RIS). |
| */ |
| hterm.VT.ESC['c'] = function() { |
| this.terminal.reset(); |
| }; |
| |
| /** |
| * Memory lock/unlock. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.ESC['l'] = |
| hterm.VT.ESC['m'] = hterm.VT.ignore; |
| |
| /** |
| * Lock Shift 2 (LS2) |
| * |
| * Invoke the G2 Character Set as GL. |
| */ |
| hterm.VT.ESC['n'] = function() { |
| this.GL = 'G2'; |
| }; |
| |
| /** |
| * Lock Shift 3 (LS3) |
| * |
| * Invoke the G3 Character Set as GL. |
| */ |
| hterm.VT.ESC['o'] = function() { |
| this.GL = 'G3'; |
| }; |
| |
| /** |
| * Lock Shift 2, Right (LS3R) |
| * |
| * Invoke the G3 Character Set as GR. |
| */ |
| hterm.VT.ESC['|'] = function() { |
| this.GR = 'G3'; |
| }; |
| |
| /** |
| * Lock Shift 2, Right (LS2R) |
| * |
| * Invoke the G2 Character Set as GR. |
| */ |
| hterm.VT.ESC['}'] = function() { |
| this.GR = 'G2'; |
| }; |
| |
| /** |
| * Lock Shift 1, Right (LS1R) |
| * |
| * Invoke the G1 Character Set as GR. |
| */ |
| hterm.VT.ESC['~'] = function() { |
| this.GR = 'G1'; |
| }; |
| |
| /** |
| * Change icon name and window title. |
| * |
| * We only change the window title. |
| */ |
| hterm.VT.OSC['0'] = function(parseState) { |
| this.terminal.setWindowTitle(parseState.args[0]); |
| }; |
| |
| /** |
| * Change window title. |
| */ |
| hterm.VT.OSC['2'] = hterm.VT.OSC['0']; |
| |
| /** |
| * Set/read color palette. |
| */ |
| hterm.VT.OSC['4'] = function(parseState) { |
| // Args come in as a single 'index1;rgb1 ... ;indexN;rgbN' string. |
| // We split on the semicolon and iterate through the pairs. |
| var args = parseState.args[0].split(';'); |
| |
| var pairCount = parseInt(args.length / 2); |
| var colorPalette = this.terminal.getTextAttributes().colorPalette; |
| var responseArray = []; |
| |
| for (var pairNumber = 0; pairNumber < pairCount; ++pairNumber) { |
| var colorIndex = parseInt(args[pairNumber * 2]); |
| var colorValue = args[pairNumber * 2 + 1]; |
| |
| if (colorIndex >= colorPalette.length) |
| continue; |
| |
| if (colorValue == '?') { |
| // '?' means we should report back the current color value. |
| colorValue = lib.colors.rgbToX11(colorPalette[colorIndex]); |
| if (colorValue) |
| responseArray.push(colorIndex + ';' + colorValue); |
| |
| continue; |
| } |
| |
| colorValue = lib.colors.x11ToCSS(colorValue); |
| if (colorValue) |
| colorPalette[colorIndex] = colorValue; |
| } |
| |
| if (responseArray.length) |
| this.terminal.io.sendString('\x1b]4;' + responseArray.join(';') + '\x07'); |
| }; |
| |
| /** |
| * Hyperlinks. |
| * |
| * The first argument is optional and colon separated: |
| * id=<id> |
| * The second argument is the link itself. |
| * |
| * Calling with a non-blank URI starts it. A blank URI stops it. |
| * |
| * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda |
| */ |
| hterm.VT.OSC['8'] = function(parseState) { |
| const args = parseState.args[0].split(';'); |
| let id = null; |
| let uri = null; |
| |
| if (args.length != 2 || args[1].length == 0) { |
| // Reset link. |
| } else { |
| // Pull out any colon separated parameters in the first argument. |
| const params = args[0].split(':'); |
| id = ''; |
| params.forEach((param) => { |
| const idx = param.indexOf('='); |
| if (idx == -1) |
| return; |
| |
| const key = param.slice(0, idx); |
| const value = param.slice(idx + 1); |
| switch (key) { |
| case 'id': |
| id = value; |
| break; |
| default: |
| // Ignore unknown keys. |
| break; |
| } |
| }); |
| |
| // The URI is in the second argument. |
| uri = args[1]; |
| } |
| |
| const attrs = this.terminal.getTextAttributes(); |
| attrs.uri = uri; |
| attrs.uriId = id; |
| }; |
| |
| /** |
| * iTerm2 growl notifications. |
| */ |
| hterm.VT.OSC['9'] = function(parseState) { |
| // This just dumps the entire string as the message. |
| hterm.notify({'body': parseState.args[0]}); |
| }; |
| |
| /** |
| * Change VT100 text foreground color. |
| */ |
| hterm.VT.OSC['10'] = function(parseState) { |
| // Args come in as a single string, but extra args will chain to the following |
| // OSC sequences. |
| var args = parseState.args[0].split(';'); |
| if (!args) |
| return; |
| |
| var colorArg; |
| var colorX11 = lib.colors.x11ToCSS(args.shift()); |
| if (colorX11) |
| this.terminal.setForegroundColor(colorX11); |
| |
| if (args.length > 0) { |
| parseState.args[0] = args.join(';'); |
| hterm.VT.OSC['11'].apply(this, [parseState]); |
| } |
| }; |
| |
| /** |
| * Change VT100 text background color. |
| */ |
| hterm.VT.OSC['11'] = function(parseState) { |
| // Args come in as a single string, but extra args will chain to the following |
| // OSC sequences. |
| var args = parseState.args[0].split(';'); |
| if (!args) |
| return; |
| |
| var colorArg; |
| var colorX11 = lib.colors.x11ToCSS(args.shift()); |
| if (colorX11) |
| this.terminal.setBackgroundColor(colorX11); |
| |
| if (args.length > 0) { |
| parseState.args[0] = args.join(';'); |
| hterm.VT.OSC['12'].apply(this, [parseState]); |
| } |
| }; |
| |
| /** |
| * Change text cursor color. |
| */ |
| hterm.VT.OSC['12'] = function(parseState) { |
| // Args come in as a single string, but extra args will chain to the following |
| // OSC sequences. |
| var args = parseState.args[0].split(';'); |
| if (!args) |
| return; |
| |
| var colorArg; |
| var colorX11 = lib.colors.x11ToCSS(args.shift()); |
| if (colorX11) |
| this.terminal.setCursorColor(colorX11); |
| |
| /* Note: If we support OSC 13+, we'd chain it here. |
| if (args.length > 0) { |
| parseState.args[0] = args.join(';'); |
| hterm.VT.OSC['13'].apply(this, [parseState]); |
| } |
| */ |
| }; |
| |
| /** |
| * Set the cursor shape. |
| * |
| * Parameter is expected to be in the form "CursorShape=number", where number is |
| * one of: |
| * |
| * 0 - Block |
| * 1 - I-Beam |
| * 2 - Underline |
| * |
| * This is a bit of a de-facto standard supported by iTerm 2 and Konsole. See |
| * also: DECSCUSR. |
| * |
| * Invalid numbers will restore the cursor to the block shape. |
| */ |
| hterm.VT.OSC['50'] = function(parseState) { |
| var args = parseState.args[0].match(/CursorShape=(.)/i); |
| if (!args) { |
| console.warn('Could not parse OSC 50 args: ' + parseState.args[0]); |
| return; |
| } |
| |
| switch (args[1]) { |
| case '1': // CursorShape=1: I-Beam. |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM); |
| break; |
| |
| case '2': // CursorShape=2: Underline. |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE); |
| break; |
| |
| default: // CursorShape=0: Block. |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK); |
| } |
| }; |
| |
| /** |
| * Set/read system clipboard. |
| * |
| * Read is not implemented due to security considerations. A remote app |
| * that is able to both write and read to the clipboard could essentially |
| * take over your session. |
| * |
| * The clipboard data will be decoded according to the 'receive-encoding' |
| * preference. |
| */ |
| hterm.VT.OSC['52'] = function(parseState) { |
| if (!this.enableClipboardWrite) |
| return; |
| |
| // Args come in as a single 'clipboard;b64-data' string. The clipboard |
| // parameter is used to select which of the X clipboards to address. Since |
| // we're not integrating with X, we treat them all the same. |
| var args = parseState.args[0].match(/^[cps01234567]*;(.*)/); |
| if (!args) |
| return; |
| |
| let data; |
| try { |
| data = window.atob(args[1]); |
| } catch (e) { |
| // If the user sent us invalid base64 content, silently ignore it. |
| return; |
| } |
| if (this.characterEncoding == 'utf-8') { |
| const decoder = new TextDecoder(); |
| const bytes = lib.codec.stringToCodeUnitArray(data, Uint8Array); |
| data = decoder.decode(bytes); |
| } |
| if (data) |
| this.terminal.copyStringToClipboard(data); |
| }; |
| |
| /** |
| * Reset color palette. |
| */ |
| hterm.VT.OSC['104'] = function(parseState) { |
| const attrs = this.terminal.getTextAttributes(); |
| |
| // If there are no args, we reset the entire palette. |
| if (!parseState.args[0]) { |
| attrs.resetColorPalette(); |
| return; |
| } |
| |
| // Args come in as a single 'index1;index2;...;indexN' string. |
| // Split on the semicolon and iterate through the colors. |
| const args = parseState.args[0].split(';'); |
| args.forEach((c) => attrs.resetColor(c)); |
| }; |
| |
| /** |
| * Reset foreground color. |
| */ |
| hterm.VT.OSC['110'] = function(parseState) { |
| this.terminal.setForegroundColor(); |
| }; |
| |
| /** |
| * Reset background color. |
| */ |
| hterm.VT.OSC['111'] = function(parseState) { |
| this.terminal.setBackgroundColor(); |
| }; |
| |
| /** |
| * Reset text cursor color. |
| */ |
| hterm.VT.OSC['112'] = function(parseState) { |
| this.terminal.setCursorColor(); |
| }; |
| |
| /** |
| * iTerm2 extended sequences. |
| * |
| * We only support image display atm. |
| */ |
| hterm.VT.OSC['1337'] = function(parseState) { |
| // Args come in as a set of key value pairs followed by data. |
| // File=name=<base64>;size=123;inline=1:<base64 data> |
| let args = parseState.args[0].match(/^File=([^:]*):([\s\S]*)$/m); |
| if (!args) { |
| if (this.warnUnimplemented) |
| console.log(`iTerm2 1337: unsupported sequence: ${args[1]}`); |
| return; |
| } |
| |
| const options = { |
| name: '', |
| size: 0, |
| preserveAspectRatio: true, |
| inline: false, |
| width: 'auto', |
| height: 'auto', |
| align: 'left', |
| type: '', |
| buffer: lib.codec.stringToCodeUnitArray(atob(args[2]), Uint8Array).buffer, |
| }; |
| // Walk the "key=value;" sets. |
| args[1].split(';').forEach((ele) => { |
| const kv = ele.match(/^([^=]+)=(.*)$/m); |
| if (!kv) |
| return; |
| |
| // Sanitize values nicely. |
| switch (kv[1]) { |
| case 'name': |
| try { |
| options.name = window.atob(kv[2]); |
| } catch (e) {} |
| break; |
| case 'size': |
| try { |
| options.size = parseInt(kv[2]); |
| } catch (e) {} |
| break; |
| case 'width': |
| options.width = kv[2]; |
| break; |
| case 'height': |
| options.height = kv[2]; |
| break; |
| case 'preserveAspectRatio': |
| options.preserveAspectRatio = !(kv[2] == '0'); |
| break; |
| case 'inline': |
| options.inline = !(kv[2] == '0'); |
| break; |
| // hterm-specific keys. |
| case 'align': |
| options.align = kv[2]; |
| break; |
| case 'type': |
| options.type = kv[2]; |
| break; |
| default: |
| // Ignore unknown keys. Don't want remote stuffing our JS env. |
| break; |
| } |
| }); |
| |
| // This is a bit of a hack. If the buffer has data following the image, we |
| // need to delay processing of it until after we've finished with the image. |
| // Otherwise while we wait for the the image to load asynchronously, the new |
| // text data will intermingle with the image. |
| if (options.inline) { |
| const io = this.terminal.io; |
| const queued = parseState.peekRemainingBuf(); |
| parseState.advance(queued.length); |
| this.terminal.displayImage(options); |
| io.print(queued); |
| } else |
| this.terminal.displayImage(options); |
| }; |
| |
| /** |
| * URxvt perl modules. |
| * |
| * This is the escape system used by rxvt-unicode and its perl modules. |
| * Obviously we don't support perl or custom modules, so we list a few common |
| * ones that we find useful. |
| * |
| * Technically there is no format here, but most modules obey: |
| * <module name>;<module args, usually ; delimited> |
| */ |
| hterm.VT.OSC['777'] = function(parseState) { |
| var ary; |
| var urxvtMod = parseState.args[0].split(';', 1)[0]; |
| |
| switch (urxvtMod) { |
| case 'notify': |
| // Format: |
| // notify;title;message |
| var title, message; |
| ary = parseState.args[0].match(/^[^;]+;([^;]*)(;([\s\S]*))?$/); |
| if (ary) { |
| title = ary[1]; |
| message = ary[3]; |
| } |
| hterm.notify({'title': title, 'body': message}); |
| break; |
| |
| default: |
| console.warn('Unknown urxvt module: ' + parseState.args[0]); |
| break; |
| } |
| }; |
| |
| /** |
| * Insert (blank) characters (ICH). |
| */ |
| hterm.VT.CSI['@'] = function(parseState) { |
| this.terminal.insertSpace(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Up (CUU). |
| */ |
| hterm.VT.CSI['A'] = function(parseState) { |
| this.terminal.cursorUp(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Down (CUD). |
| */ |
| hterm.VT.CSI['B'] = function(parseState) { |
| this.terminal.cursorDown(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Forward (CUF). |
| */ |
| hterm.VT.CSI['C'] = function(parseState) { |
| this.terminal.cursorRight(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Backward (CUB). |
| */ |
| hterm.VT.CSI['D'] = function(parseState) { |
| this.terminal.cursorLeft(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Next Line (CNL). |
| * |
| * This is like Cursor Down, except the cursor moves to the beginning of the |
| * line as well. |
| */ |
| hterm.VT.CSI['E'] = function(parseState) { |
| this.terminal.cursorDown(parseState.iarg(0, 1)); |
| this.terminal.setCursorColumn(0); |
| }; |
| |
| /** |
| * Cursor Preceding Line (CPL). |
| * |
| * This is like Cursor Up, except the cursor moves to the beginning of the |
| * line as well. |
| */ |
| hterm.VT.CSI['F'] = function(parseState) { |
| this.terminal.cursorUp(parseState.iarg(0, 1)); |
| this.terminal.setCursorColumn(0); |
| }; |
| |
| /** |
| * Cursor Horizontal Absolute (CHA). |
| * |
| * Xterm calls this Cursor Character Absolute. |
| */ |
| hterm.VT.CSI['G'] = function(parseState) { |
| this.terminal.setCursorColumn(parseState.iarg(0, 1) - 1); |
| }; |
| |
| /** |
| * Cursor Position (CUP). |
| */ |
| hterm.VT.CSI['H'] = function(parseState) { |
| this.terminal.setCursorPosition(parseState.iarg(0, 1) - 1, |
| parseState.iarg(1, 1) - 1); |
| }; |
| |
| /** |
| * Cursor Forward Tabulation (CHT). |
| */ |
| hterm.VT.CSI['I'] = function(parseState) { |
| var count = parseState.iarg(0, 1); |
| count = lib.f.clamp(count, 1, this.terminal.screenSize.width); |
| for (var i = 0; i < count; i++) { |
| this.terminal.forwardTabStop(); |
| } |
| }; |
| |
| /** |
| * Erase in Display (ED, DECSED). |
| */ |
| hterm.VT.CSI['J'] = |
| hterm.VT.CSI['?J'] = function(parseState, code) { |
| var arg = parseState.args[0]; |
| |
| if (!arg || arg == 0) { |
| this.terminal.eraseBelow(); |
| } else if (arg == 1) { |
| this.terminal.eraseAbove(); |
| } else if (arg == 2) { |
| this.terminal.clear(); |
| } else if (arg == 3) { |
| if (this.enableCsiJ3) { |
| this.terminal.clearScrollback(); |
| } |
| } |
| }; |
| |
| /** |
| * Erase in line (EL, DECSEL). |
| */ |
| hterm.VT.CSI['K'] = |
| hterm.VT.CSI['?K'] = function(parseState, code) { |
| var arg = parseState.args[0]; |
| |
| if (!arg || arg == 0) { |
| this.terminal.eraseToRight(); |
| } else if (arg == 1) { |
| this.terminal.eraseToLeft(); |
| } else if (arg == 2) { |
| this.terminal.eraseLine(); |
| } |
| }; |
| |
| /** |
| * Insert Lines (IL). |
| */ |
| hterm.VT.CSI['L'] = function(parseState) { |
| this.terminal.insertLines(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Delete Lines (DL). |
| */ |
| hterm.VT.CSI['M'] = function(parseState) { |
| this.terminal.deleteLines(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Delete Characters (DCH). |
| * |
| * This command shifts the line contents left, starting at the cursor position. |
| */ |
| hterm.VT.CSI['P'] = function(parseState) { |
| this.terminal.deleteChars(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Scroll Up (SU). |
| */ |
| hterm.VT.CSI['S'] = function(parseState) { |
| this.terminal.vtScrollUp(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Scroll Down (SD). |
| * Also 'Initiate highlight mouse tracking'. Will not implement this part. |
| */ |
| hterm.VT.CSI['T'] = function(parseState) { |
| if (parseState.args.length <= 1) |
| this.terminal.vtScrollDown(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Reset one or more features of the title modes to the default value. |
| * |
| * ESC [ > Ps T |
| * |
| * Normally, "reset" disables the feature. It is possible to disable the |
| * ability to reset features by compiling a different default for the title |
| * modes into xterm. |
| * |
| * Ps values: |
| * 0 - Do not set window/icon labels using hexadecimal. |
| * 1 - Do not query window/icon labels using hexadecimal. |
| * 2 - Do not set window/icon labels using UTF-8. |
| * 3 - Do not query window/icon labels using UTF-8. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['>T'] = hterm.VT.ignore; |
| |
| /** |
| * Erase Characters (ECH). |
| */ |
| hterm.VT.CSI['X'] = function(parseState) { |
| this.terminal.eraseToRight(parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Cursor Backward Tabulation (CBT). |
| */ |
| hterm.VT.CSI['Z'] = function(parseState) { |
| var count = parseState.iarg(0, 1); |
| count = lib.f.clamp(count, 1, this.terminal.screenSize.width); |
| for (var i = 0; i < count; i++) { |
| this.terminal.backwardTabStop(); |
| } |
| }; |
| |
| /** |
| * Character Position Absolute (HPA). |
| * |
| * Same as Cursor Horizontal Absolute (CHA). |
| */ |
| hterm.VT.CSI['`'] = hterm.VT.CSI['G']; |
| |
| /** |
| * Character Position Relative (HPR). |
| */ |
| hterm.VT.CSI['a'] = function(parseState) { |
| this.terminal.setCursorColumn(this.terminal.getCursorColumn() + |
| parseState.iarg(0, 1)); |
| }; |
| |
| /** |
| * Repeat the preceding graphic character. |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['b'] = hterm.VT.ignore; |
| |
| /** |
| * Send Device Attributes (Primary DA). |
| * |
| * TODO(rginda): This is hardcoded to send back 'VT100 with Advanced Video |
| * Option', but it may be more correct to send a VT220 response once |
| * we fill out the 'Not currently implemented' parts. |
| */ |
| hterm.VT.CSI['c'] = function(parseState) { |
| if (!parseState.args[0] || parseState.args[0] == 0) { |
| this.terminal.io.sendString('\x1b[?1;2c'); |
| } |
| }; |
| |
| /** |
| * Send Device Attributes (Secondary DA). |
| * |
| * TODO(rginda): This is hardcoded to send back 'VT100' but it may be more |
| * correct to send a VT220 response once we fill out more 'Not currently |
| * implemented' parts. |
| */ |
| hterm.VT.CSI['>c'] = function(parseState) { |
| this.terminal.io.sendString('\x1b[>0;256;0c'); |
| }; |
| |
| /** |
| * Line Position Absolute (VPA). |
| */ |
| hterm.VT.CSI['d'] = function(parseState) { |
| this.terminal.setAbsoluteCursorRow(parseState.iarg(0, 1) - 1); |
| }; |
| |
| /** |
| * Horizontal and Vertical Position (HVP). |
| * |
| * Same as Cursor Position (CUP). |
| */ |
| hterm.VT.CSI['f'] = hterm.VT.CSI['H']; |
| |
| /** |
| * Tab Clear (TBC). |
| */ |
| hterm.VT.CSI['g'] = function(parseState) { |
| if (!parseState.args[0] || parseState.args[0] == 0) { |
| // Clear tab stop at cursor. |
| this.terminal.clearTabStopAtCursor(false); |
| } else if (parseState.args[0] == 3) { |
| // Clear all tab stops. |
| this.terminal.clearAllTabStops(); |
| } |
| }; |
| |
| /** |
| * Set Mode (SM). |
| */ |
| hterm.VT.CSI['h'] = function(parseState) { |
| for (var i = 0; i < parseState.args.length; i++) { |
| this.setANSIMode(parseState.args[i], true); |
| } |
| }; |
| |
| /** |
| * DEC Private Mode Set (DECSET). |
| */ |
| hterm.VT.CSI['?h'] = function(parseState) { |
| for (var i = 0; i < parseState.args.length; i++) { |
| this.setDECMode(parseState.args[i], true); |
| } |
| }; |
| |
| /** |
| * Media Copy (MC). |
| * Media Copy (MC, DEC Specific). |
| * |
| * These commands control the printer. Will not implement. |
| */ |
| hterm.VT.CSI['i'] = |
| hterm.VT.CSI['?i'] = hterm.VT.ignore; |
| |
| /** |
| * Reset Mode (RM). |
| */ |
| hterm.VT.CSI['l'] = function(parseState) { |
| for (var i = 0; i < parseState.args.length; i++) { |
| this.setANSIMode(parseState.args[i], false); |
| } |
| }; |
| |
| /** |
| * DEC Private Mode Reset (DECRST). |
| */ |
| hterm.VT.CSI['?l'] = function(parseState) { |
| for (var i = 0; i < parseState.args.length; i++) { |
| this.setDECMode(parseState.args[i], false); |
| } |
| }; |
| |
| /** |
| * Parse extended SGR 38/48 sequences. |
| * |
| * This deals with the various ISO 8613-6 forms, and with legacy xterm forms |
| * that are common in the wider application world. |
| * |
| * @param {hterm.VT.ParseState} parseState The current input state. |
| * @param {number} i The argument in parseState to start processing. |
| * @param {hterm.TextAttributes} attrs The current text attributes. |
| * @return {Object} The skipCount member defines how many arguments to skip |
| * (i.e. how many were processed), and the color member is the color that |
| * was successfully processed, or undefined if not. |
| */ |
| hterm.VT.prototype.parseSgrExtendedColors = function(parseState, i, attrs) { |
| let ary; |
| let usedSubargs; |
| |
| if (parseState.argHasSubargs(i)) { |
| // The ISO 8613-6 compliant form. |
| // e.g. 38:[color choice]:[arg1]:[arg2]:... |
| ary = parseState.args[i].split(':'); |
| ary.shift(); // Remove "38". |
| usedSubargs = true; |
| } else if (parseState.argHasSubargs(i + 1)) { |
| // The xterm form which isn't ISO 8613-6 compliant. Not many emulators |
| // support this, and others actively do not want to. We'll ignore it so |
| // at least the rest of the stream works correctly. e.g. 38;2:R:G:B |
| // We return 0 here so we only skip the "38" ... we can't be confident the |
| // next arg is actually supposed to be part of it vs a typo where the next |
| // arg is legit. |
| return {skipCount: 0}; |
| } else { |
| // The xterm form which isn't ISO 8613-6 compliant, but many emulators |
| // support, and many applications rely on. |
| // e.g. 38;2;R;G;B |
| ary = parseState.args.slice(i + 1); |
| usedSubargs = false; |
| } |
| |
| // Figure out which form to parse. |
| switch (parseInt(ary[0])) { |
| default: // Unknown. |
| case 0: // Implementation defined. We ignore it. |
| return {skipCount: 0}; |
| |
| case 1: { // Transparent color. |
| // Require ISO 8613-6 form. |
| if (!usedSubargs) |
| return {skipCount: 0}; |
| |
| return { |
| color: 'rgba(0, 0, 0, 0)', |
| skipCount: 0, |
| }; |
| } |
| |
| case 2: { // RGB color. |
| // Skip over the color space identifier, if it exists. |
| let start; |
| if (usedSubargs) { |
| // The ISO 8613-6 compliant form: |
| // 38:2:<color space id>:R:G:B[:...] |
| // The xterm form isn't ISO 8613-6 compliant. |
| // 38:2:R:G:B |
| // Since the ISO 8613-6 form requires at least 5 arguments, |
| // we can still support the xterm form unambiguously. |
| if (ary.length == 4) |
| start = 1; |
| else |
| start = 2; |
| } else { |
| // The legacy xterm form: 38;2;R;G;B |
| start = 1; |
| } |
| |
| // We need at least 3 args for RGB. If we don't have them, assume this |
| // sequence is corrupted, so don't eat anything more. |
| // We ignore more than 3 args on purpose since ISO 8613-6 defines some, |
| // and we don't care about them. |
| if (ary.length < start + 3) |
| return {skipCount: 0}; |
| |
| const r = parseState.parseInt(ary[start + 0]); |
| const g = parseState.parseInt(ary[start + 1]); |
| const b = parseState.parseInt(ary[start + 2]); |
| return { |
| color: `rgb(${r}, ${g}, ${b})`, |
| skipCount: usedSubargs ? 0 : 4, |
| }; |
| } |
| |
| case 3: { // CMY color. |
| // No need to support xterm/legacy forms as xterm doesn't support CMY. |
| if (!usedSubargs) |
| return {skipCount: 0}; |
| |
| // We need at least 4 args for CMY. If we don't have them, assume |
| // this sequence is corrupted. We ignore the color space identifier, |
| // tolerance, etc... |
| if (ary.length < 4) |
| return {skipCount: 0}; |
| |
| // TODO: See CMYK below. |
| const c = parseState.parseInt(ary[1]); |
| const m = parseState.parseInt(ary[2]); |
| const y = parseState.parseInt(ary[3]); |
| return {skipCount: 0}; |
| } |
| |
| case 4: { // CMYK color. |
| // No need to support xterm/legacy forms as xterm doesn't support CMYK. |
| if (!usedSubargs) |
| return {skipCount: 0}; |
| |
| // We need at least 5 args for CMYK. If we don't have them, assume |
| // this sequence is corrupted. We ignore the color space identifier, |
| // tolerance, etc... |
| if (ary.length < 5) |
| return {skipCount: 0}; |
| |
| // TODO: Implement this. |
| // Might wait until CSS4 is adopted for device-cmyk(): |
| // https://www.w3.org/TR/css-color-4/#cmyk-colors |
| // Or normalize it to RGB ourselves: |
| // https://www.w3.org/TR/css-color-4/#cmyk-rgb |
| const c = parseState.parseInt(ary[1]); |
| const m = parseState.parseInt(ary[2]); |
| const y = parseState.parseInt(ary[3]); |
| const k = parseState.parseInt(ary[4]); |
| return {skipCount: 0}; |
| } |
| |
| case 5: { // Color palette index. |
| // If we're short on args, assume this sequence is corrupted, so don't |
| // eat anything more. |
| if (ary.length < 2) |
| return {skipCount: 0}; |
| |
| // Support 38:5:P (ISO 8613-6) and 38;5;P (xterm/legacy). |
| // We also ignore extra args with 38:5:P:[...], but more for laziness. |
| const ret = { |
| skipCount: usedSubargs ? 0 : 2, |
| }; |
| const color = parseState.parseInt(ary[1]); |
| if (color < attrs.colorPalette.length) |
| ret.color = color; |
| return ret; |
| } |
| } |
| }; |
| |
| /** |
| * Character Attributes (SGR). |
| * |
| * Iterate through the list of arguments, applying the attribute changes based |
| * on the argument value... |
| */ |
| hterm.VT.CSI['m'] = function(parseState) { |
| var attrs = this.terminal.getTextAttributes(); |
| |
| if (!parseState.args.length) { |
| attrs.reset(); |
| return; |
| } |
| |
| for (var i = 0; i < parseState.args.length; i++) { |
| // If this argument has subargs (i.e. it has args followed by colons), |
| // the iarg logic will implicitly truncate that off for us. |
| var arg = parseState.iarg(i, 0); |
| |
| if (arg < 30) { |
| if (arg == 0) { // Normal (default). |
| attrs.reset(); |
| } else if (arg == 1) { // Bold. |
| attrs.bold = true; |
| } else if (arg == 2) { // Faint. |
| attrs.faint = true; |
| } else if (arg == 3) { // Italic. |
| attrs.italic = true; |
| } else if (arg == 4) { // Underline. |
| if (parseState.argHasSubargs(i)) { |
| const uarg = parseState.args[i].split(':')[1]; |
| if (uarg == 0) |
| attrs.underline = false; |
| else if (uarg == 1) |
| attrs.underline = 'solid'; |
| else if (uarg == 2) |
| attrs.underline = 'double'; |
| else if (uarg == 3) |
| attrs.underline = 'wavy'; |
| else if (uarg == 4) |
| attrs.underline = 'dotted'; |
| else if (uarg == 5) |
| attrs.underline = 'dashed'; |
| } else { |
| attrs.underline = 'solid'; |
| } |
| } else if (arg == 5) { // Blink. |
| attrs.blink = true; |
| } else if (arg == 7) { // Inverse. |
| attrs.inverse = true; |
| } else if (arg == 8) { // Invisible. |
| attrs.invisible = true; |
| } else if (arg == 9) { // Crossed out. |
| attrs.strikethrough = true; |
| } else if (arg == 21) { // Double underlined. |
| attrs.underline = 'double'; |
| } else if (arg == 22) { // Not bold & not faint. |
| attrs.bold = false; |
| attrs.faint = false; |
| } else if (arg == 23) { // Not italic. |
| attrs.italic = false; |
| } else if (arg == 24) { // Not underlined. |
| attrs.underline = false; |
| } else if (arg == 25) { // Not blink. |
| attrs.blink = false; |
| } else if (arg == 27) { // Steady. |
| attrs.inverse = false; |
| } else if (arg == 28) { // Visible. |
| attrs.invisible = false; |
| } else if (arg == 29) { // Not crossed out. |
| attrs.strikethrough = false; |
| } |
| |
| } else if (arg < 50) { |
| // Select fore/background color from bottom half of 16 color palette |
| // or from the 256 color palette or alternative specify color in fully |
| // qualified rgb(r, g, b) form. |
| if (arg < 38) { |
| attrs.foregroundSource = arg - 30; |
| |
| } else if (arg == 38) { |
| const result = this.parseSgrExtendedColors(parseState, i, attrs); |
| if (result.color !== undefined) |
| attrs.foregroundSource = result.color; |
| i += result.skipCount; |
| |
| } else if (arg == 39) { |
| attrs.foregroundSource = attrs.SRC_DEFAULT; |
| |
| } else if (arg < 48) { |
| attrs.backgroundSource = arg - 40; |
| |
| } else if (arg == 48) { |
| const result = this.parseSgrExtendedColors(parseState, i, attrs); |
| if (result.color !== undefined) |
| attrs.backgroundSource = result.color; |
| i += result.skipCount; |
| |
| } else { |
| attrs.backgroundSource = attrs.SRC_DEFAULT; |
| } |
| |
| } else if (arg == 58) { // Underline coloring. |
| const result = this.parseSgrExtendedColors(parseState, i, attrs); |
| if (result.color !== undefined) |
| attrs.underlineSource = result.color; |
| i += result.skipCount; |
| |
| } else if (arg == 59) { // Disable underline coloring. |
| attrs.underlineSource = attrs.SRC_DEFAULT; |
| |
| } else if (arg >= 90 && arg <= 97) { |
| attrs.foregroundSource = arg - 90 + 8; |
| |
| } else if (arg >= 100 && arg <= 107) { |
| attrs.backgroundSource = arg - 100 + 8; |
| } |
| } |
| |
| attrs.setDefaults(this.terminal.getForegroundColor(), |
| this.terminal.getBackgroundColor()); |
| }; |
| |
| // SGR calls can handle subargs. |
| hterm.VT.CSI['m'].supportsSubargs = true; |
| |
| /** |
| * Set xterm-specific keyboard modes. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['>m'] = hterm.VT.ignore; |
| |
| /** |
| * Device Status Report (DSR, DEC Specific). |
| * |
| * 5 - Status Report. Result (OK) is CSI 0 n |
| * 6 - Report Cursor Position (CPR) [row;column]. Result is CSI r ; c R |
| */ |
| hterm.VT.CSI['n'] = function(parseState) { |
| if (parseState.args[0] == 5) { |
| this.terminal.io.sendString('\x1b0n'); |
| } else if (parseState.args[0] == 6) { |
| var row = this.terminal.getCursorRow() + 1; |
| var col = this.terminal.getCursorColumn() + 1; |
| this.terminal.io.sendString('\x1b[' + row + ';' + col + 'R'); |
| } |
| }; |
| |
| /** |
| * Disable modifiers which may be enabled via CSI['>m']. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['>n'] = hterm.VT.ignore; |
| |
| /** |
| * Device Status Report (DSR, DEC Specific). |
| * |
| * 6 - Report Cursor Position (CPR) [row;column] as CSI ? r ; c R |
| * 15 - Report Printer status as CSI ? 1 0 n (ready) or |
| * CSI ? 1 1 n (not ready). |
| * 25 - Report UDK status as CSI ? 2 0 n (unlocked) or CSI ? 2 1 n (locked). |
| * 26 - Report Keyboard status as CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). |
| * The last two parameters apply to VT400 & up, and denote keyboard ready |
| * and LK01 respectively. |
| * 53 - Report Locator status as CSI ? 5 3 n Locator available, if compiled-in, |
| * or CSI ? 5 0 n No Locator, if not. |
| */ |
| hterm.VT.CSI['?n'] = function(parseState) { |
| if (parseState.args[0] == 6) { |
| var row = this.terminal.getCursorRow() + 1; |
| var col = this.terminal.getCursorColumn() + 1; |
| this.terminal.io.sendString('\x1b[' + row + ';' + col + 'R'); |
| } else if (parseState.args[0] == 15) { |
| this.terminal.io.sendString('\x1b[?11n'); |
| } else if (parseState.args[0] == 25) { |
| this.terminal.io.sendString('\x1b[?21n'); |
| } else if (parseState.args[0] == 26) { |
| this.terminal.io.sendString('\x1b[?12;1;0;0n'); |
| } else if (parseState.args[0] == 53) { |
| this.terminal.io.sendString('\x1b[?50n'); |
| } |
| }; |
| |
| /** |
| * This is used by xterm to decide whether to hide the pointer cursor as the |
| * user types. |
| * |
| * Valid values for the parameter: |
| * 0 - Never hide the pointer. |
| * 1 - Hide if the mouse tracking mode is not enabled. |
| * 2 - Always hide the pointer. |
| * |
| * If no parameter is given, xterm uses the default, which is 1. |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['>p'] = hterm.VT.ignore; |
| |
| /** |
| * Soft terminal reset (DECSTR). |
| */ |
| hterm.VT.CSI['!p'] = function() { |
| this.terminal.softReset(); |
| }; |
| |
| /** |
| * Request ANSI Mode (DECRQM). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['$p'] = hterm.VT.ignore; |
| hterm.VT.CSI['?$p'] = hterm.VT.ignore; |
| |
| /** |
| * Set conformance level (DECSCL). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['"p'] = hterm.VT.ignore; |
| |
| /** |
| * Load LEDs (DECLL). |
| * |
| * Not currently implemented. Could be implemented as virtual LEDs overlaying |
| * the terminal if anyone cares. |
| */ |
| hterm.VT.CSI['q'] = hterm.VT.ignore; |
| |
| /** |
| * Set cursor style (DECSCUSR, VT520). |
| */ |
| hterm.VT.CSI[' q'] = function(parseState) { |
| var arg = parseState.args[0]; |
| |
| if (arg == 0 || arg == 1) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK); |
| this.terminal.setCursorBlink(true); |
| } else if (arg == 2) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK); |
| this.terminal.setCursorBlink(false); |
| } else if (arg == 3) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE); |
| this.terminal.setCursorBlink(true); |
| } else if (arg == 4) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE); |
| this.terminal.setCursorBlink(false); |
| } else if (arg == 5) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM); |
| this.terminal.setCursorBlink(true); |
| } else if (arg == 6) { |
| this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM); |
| this.terminal.setCursorBlink(false); |
| } else { |
| console.warn('Unknown cursor style: ' + arg); |
| } |
| }; |
| |
| /** |
| * Select character protection attribute (DECSCA). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['"q'] = hterm.VT.ignore; |
| |
| /** |
| * Set Scrolling Region (DECSTBM). |
| */ |
| hterm.VT.CSI['r'] = function(parseState) { |
| var args = parseState.args; |
| var scrollTop = args[0] ? parseInt(args[0], 10) -1 : null; |
| var scrollBottom = args[1] ? parseInt(args[1], 10) - 1 : null; |
| this.terminal.setVTScrollRegion(scrollTop, scrollBottom); |
| this.terminal.setCursorPosition(0, 0); |
| }; |
| |
| /** |
| * Restore DEC Private Mode Values. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['?r'] = hterm.VT.ignore; |
| |
| /** |
| * Change Attributes in Rectangular Area (DECCARA) |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['$r'] = hterm.VT.ignore; |
| |
| /** |
| * Save cursor (ANSI.SYS) |
| */ |
| hterm.VT.CSI['s'] = function() { |
| this.terminal.saveCursorAndState(); |
| }; |
| |
| /** |
| * Save DEC Private Mode Values. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['?s'] = hterm.VT.ignore; |
| |
| /** |
| * Window manipulation (from dtterm, as well as extensions). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['t'] = hterm.VT.ignore; |
| |
| /** |
| * Reverse Attributes in Rectangular Area (DECRARA). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['$t'] = hterm.VT.ignore; |
| |
| /** |
| * Set one or more features of the title modes. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['>t'] = hterm.VT.ignore; |
| |
| /** |
| * Set warning-bell volume (DECSWBV, VT520). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI[' t'] = hterm.VT.ignore; |
| |
| /** |
| * Restore cursor (ANSI.SYS). |
| */ |
| hterm.VT.CSI['u'] = function() { |
| this.terminal.restoreCursorAndState(); |
| }; |
| |
| /** |
| * Set margin-bell volume (DECSMBV, VT520). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI[' u'] = hterm.VT.ignore; |
| |
| /** |
| * Copy Rectangular Area (DECCRA, VT400 and up). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['$v'] = hterm.VT.ignore; |
| |
| /** |
| * Enable Filter Rectangle (DECEFR). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['\'w'] = hterm.VT.ignore; |
| |
| /** |
| * Request Terminal Parameters (DECREQTPARM). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['x'] = hterm.VT.ignore; |
| |
| /** |
| * Select Attribute Change Extent (DECSACE). |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['*x'] = hterm.VT.ignore; |
| |
| /** |
| * Fill Rectangular Area (DECFRA), VT420 and up. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['$x'] = hterm.VT.ignore; |
| |
| /** |
| * vt_tiledata (as used by NAOhack and UnNetHack) |
| * (see https://nethackwiki.com/wiki/Vt_tiledata for more info) |
| * |
| * Implemented as far as we care (start a glyph and end a glyph). |
| */ |
| hterm.VT.CSI['z'] = function(parseState) { |
| if (parseState.args.length < 1) |
| return; |
| var arg = parseState.args[0]; |
| if (arg == 0) { |
| // Start a glyph (one parameter, the glyph number). |
| if (parseState.args.length < 2) |
| return; |
| this.terminal.getTextAttributes().tileData = parseState.args[1]; |
| } else if (arg == 1) { |
| // End a glyph. |
| this.terminal.getTextAttributes().tileData = null; |
| } |
| }; |
| |
| /** |
| * Enable Locator Reporting (DECELR). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['\'z'] = hterm.VT.ignore; |
| |
| /** |
| * Erase Rectangular Area (DECERA), VT400 and up. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['$z'] = hterm.VT.ignore; |
| |
| /** |
| * Select Locator Events (DECSLE). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['\'{'] = hterm.VT.ignore; |
| |
| /** |
| * Request Locator Position (DECRQLP). |
| * |
| * Not currently implemented. |
| */ |
| hterm.VT.CSI['\'|'] = hterm.VT.ignore; |
| |
| /** |
| * Insert Columns (DECIC), VT420 and up. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['\'}'] = hterm.VT.ignore; |
| |
| /** |
| * Delete P s Columns (DECDC), VT420 and up. |
| * |
| * Will not implement. |
| */ |
| hterm.VT.CSI['\'~'] = hterm.VT.ignore; |