Index: chrome/browser/resources/hterm/js/vt100.js |
diff --git a/chrome/browser/resources/hterm/js/vt100.js b/chrome/browser/resources/hterm/js/vt100.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8defea02aa9ad2c95d6185362c5077d175fa693f |
--- /dev/null |
+++ b/chrome/browser/resources/hterm/js/vt100.js |
@@ -0,0 +1,663 @@ |
+// Copyright (c) 2011 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+/** |
+ * @fileoverview This file implements the VT100 interpreter, which |
+ * operates in conjunction with a hterm.Terminal to provide |
+ * interpretation of VT100-style control sequences. |
+ * |
+ * Original code by Cory Maccarrone. |
+ */ |
+ |
+/** |
+ * Constructor for the VT100 Interpreter. |
+ * |
+ * The interpreter operates on a terminal object capable of performing cursor |
+ * move operations, painting characters, etc. |
+ * |
+ * @param {hterm.Terminal} terminal Terminal to use with |
+ * the interpreter. Direct commands are sent to it in the presence of |
+ * control characters -- otherwise, normal characters are passed straight |
+ * through to its render functions. |
+ * @constructor |
+ */ |
+hterm.VT100 = function(terminal) { |
+ this.terminal_ = terminal; |
+ |
+ // Sequence being processed -- that seen so far |
+ this.pendingSequence_ = []; |
+ |
+ // Response to be sent back to the guest |
+ this.pendingResponse_ = ''; |
+ |
+ /** |
+ * Enable/disable application keypad. |
+ * |
+ * This changes the way numeric keys are sent from the keyboard. |
+ */ |
+ this.applicationKeypad = false; |
+ |
+ /** |
+ * Enable/disable the application cursor mode. |
+ * |
+ * This changes the way cursor keys are sent from the keyboard. |
+ */ |
+ this.applicationCursor = false; |
+ |
+ /** |
+ * Whether backspace should send ^H or not. |
+ */ |
+ this.backspaceSendsBackspace = false; |
+ |
+ /** |
+ * Set whether the alt key sends a leading escape or not. |
+ */ |
+ this.altSendsEscape = true; |
+ |
+ /** |
+ * Set whether the meta key sends a leading escape or not. |
+ */ |
+ this.metaSendsEscape = true; |
+}; |
+ |
+/** |
+ * Interpret a sequence of characters. |
+ * |
+ * Incomplete escape sequences are buffered until the next call. |
+ * |
+ * @param {string} str Sequence of characters to interpret or pass through. |
+ */ |
+hterm.VT100.prototype.interpretString = function(str) { |
+ var i = 0; |
+ |
+ while (i < str.length) { |
+ while (this.pendingSequence_.length && i < str.length) { |
+ this.interpretCharacter(str.substr(i, 1)); |
+ i++; |
+ } |
+ |
+ if (i == str.length) |
+ break; |
+ |
+ var nextEscape = str.substr(i).search(/[\x1b\n\t]|$/); |
+ |
+ if (nextEscape == -1) |
+ nextEscape = str.length; |
+ |
+ if (nextEscape != 0) { |
+ var plainText = str.substr(i, nextEscape); |
+ this.terminal_.print(plainText); |
+ i += nextEscape; |
+ } |
+ |
+ if (i == str.length) |
+ break; |
+ |
+ this.interpretCharacter(str.substr(i, 1)); |
+ i++; |
+ } |
+}; |
+ |
+/** |
+ * Interpret a single character in a sequence. |
+ * |
+ * This function is called for each character in terminal input, and |
+ * accumulates characters until a recognized control sequence is read. If the |
+ * character is not part of a control sequence, it is queued up for rendering. |
+ * |
+ * @param {string} character Character to interpret or pass through. |
+ */ |
+hterm.VT100.prototype.interpretCharacter = function(character) { |
+ var interpret = false; |
+ |
+ if (character == '\n') { |
+ this.terminal_.newLine(); |
+ return; |
+ } |
+ |
+ if (character == '\t') { |
+ // TODO(rginda): I don't think this is the correct behavior. |
+ this.terminal_.cursorRight(4); |
+ return; |
+ } |
+ |
+ if (character == '\x1b') { |
+ this.pendingSequence_.length = 1; |
+ this.pendingSequence_[0] = character; |
+ return; |
+ } |
+ |
+ if (!this.pendingSequence_.length || |
+ (character < '\x20' && character != '\x07')) { |
+ // We don't have a pending escape, or this character is invalid in the |
+ // context of an escape sequence. The VT100 spec says to just print it. |
+ this.terminal_.print(character); |
+ return; |
+ } |
+ |
+ this.pendingSequence_.push(character); |
+ var sequence = this.pendingSequence_; |
+ |
+ var processed = true; |
+ switch (sequence[1]) { |
+ case '[': |
+ if (!this.interpretControlSequenceInducer_(sequence.slice(2))) { |
+ processed = false; |
+ } |
+ break; |
+ |
+ case ']': |
+ if (!this.interpretOperatingSystemCommand_(sequence.slice(2))) { |
+ processed = false; |
+ } |
+ break; |
+ |
+ case '=': // Application keypad |
+ this.applicationKeypad = true; |
+ break; |
+ |
+ case '>': // Normal keypad |
+ this.applicationKeypad = false; |
+ break; |
+ |
+ case '7': // Save cursor |
+ this.terminal_.saveCursor(); |
+ break; |
+ |
+ case '8': // Restore cursor |
+ this.terminal_.restoreCursor(); |
+ break; |
+ |
+ case 'D': // Index, like newline, only keep the X position |
+ this.terminal_.lineFeed(); |
+ break; |
+ |
+ case 'E': // Next line. Like newline, but doesn't add lines. |
+ this.terminal_.setCursorColumn(0); |
+ this.terminal_.cursorDown(1); |
+ break; |
+ |
+ case 'M': // Reverse index. |
+ // This is like newline, but in reverse. When we hit the top of the |
+ // terminal, lines are added at the top while swapping out the bottom |
+ // lines. |
+ this.terminal_.reverseLineFeed(); |
+ break; |
+ |
+ case 'c': // Full reset |
+ this.terminal_.reset(); |
+ break; |
+ |
+ case '#': // DEC commands |
+ if (sequence.length < 3) { |
+ processed = false; |
+ break; |
+ } |
+ switch (sequence[2]) { |
+ case '8': // DEC screen alignment test |
+ this.fill('E'); |
+ break; |
+ default: |
+ console.log('Unsupported DEC command: ' + sequence[2]); |
+ break; |
+ } |
+ break; |
+ |
+ case '(': // Designate G0 character set |
+ if (sequence.length < 3) { |
+ processed = false; |
+ break; |
+ } |
+ switch (sequence[2]) { |
+ case '0': // Line drawing |
+ this.terminal_.setSpecialCharsEnabled(true); |
+ break; |
+ default: |
+ this.terminal_.setSpecialCharsEnabled(false); |
+ break; |
+ } |
+ break; |
+ |
+ case ')': // Designate G1 character set |
+ case '*': // Designate G2 character set |
+ case '+': // Designate G3 character set |
+ if (sequence.length < 3) { |
+ processed = false; |
+ break; |
+ } |
+ console.log('Code ' + sequence[2]); |
+ break; |
+ |
+ case 'H': // Set a tab stop at the cursor position |
+ this.terminal_.setTabStopAtCursor(true); |
+ break; |
+ |
+ default: |
+ console.log('Unsupported escape sequence: ' + sequence[1]); |
+ break; |
+ } |
+ |
+ if (processed) { |
+ //console.log('Escape sequence: ' + sequence.slice(1)); |
+ this.pendingSequence_.length = 0; |
+ } |
+ |
+ return; |
+}; |
+ |
+/** |
+ * Return any pending response from the interpretation of control sequences. |
+ * |
+ * The response should be returned as if the user typed it, and the pending |
+ * response is cleared from the interpreter. |
+ * |
+ * @return {string} response to send. |
+ */ |
+hterm.VT100.prototype.getAndClearPendingResponse = function() { |
+ var response = this.pendingResponse_; |
+ this.pendingResponse_ = ''; |
+ return response; |
+}; |
+ |
+/** |
+ * Interpret an operating system command (OSC) sequence. |
+ * |
+ * @param {Array} sequence Sequence to interpret. |
+ * @return {boolean} Whether the sequence was interpreted or not. |
+ * @private |
+ */ |
+hterm.VT100.prototype.interpretOperatingSystemCommand_ = |
+ function(sequence) { |
+ // These commands tend to do things like change the window title and other |
+ // things. |
+ var processed = false; |
+ var length = sequence.length; |
+ var i = 0; |
+ var args = []; |
+ var currentArg = ''; |
+ var leadingZeroFilter = true; |
+ |
+ // Parse the command into a sequence command and series of numeric arguments. |
+ while (true) { |
+ if (i >= length) { |
+ // We ran out of characters interpreting the string |
+ break; |
+ } |
+ |
+ if (sequence[i] == ';') { |
+ // New argument |
+ args.push(currentArg); |
+ currentArg = ''; |
+ leadingZeroFilter = true; |
+ } else if (sequence[i] == '\x7' || |
+ (sequence[i] == '\x1b' && |
+ sequence[i + 1] == '\\')) { |
+ // Terminating character. This'll tell us how to interpret the control |
+ // sequence. |
+ if (currentArg != '') { |
+ args.push(currentArg); |
+ } |
+ processed = true; |
+ break; |
+ } else { |
+ // Part of the arg, just add it, filtering out leadining zeros. |
+ if (!(leadingZeroFilter && sequence[i] == '0')) { |
+ leadingZeroFilter = false; |
+ currentArg += sequence[i]; |
+ } |
+ } |
+ i++; |
+ } |
+ |
+ if (!processed) |
+ return processed; |
+ |
+ // Interpret the command |
+ if (args[0] == '') { |
+ // The leading-zero filter dropped our zero, so put it back. |
+ args[0] = 0; |
+ } |
+ |
+ switch (parseInt(args[0], 10)) { |
+ case 0: |
+ case 2: |
+ // Change the window title to args[1] |
+ // TODO(rginda): this. |
+ break; |
+ default: |
+ console.log('Unsupported OSC command: ' + sequence.slice(0, i + 1)); |
+ break; |
+ } |
+ |
+ return processed; |
+}; |
+ |
+/** |
+ * Interpret a control sequence inducer (CSI) command. |
+ * |
+ * @param {Array} sequence Sequence to interpret. |
+ * @return {boolean} Whether the sequence was interpreted succesfully or not. |
+ * @private |
+ */ |
+hterm.VT100.prototype.interpretControlSequenceInducer_ = |
+ function(sequence) { |
+ // These escape codes all end with a letter, and have arguments separated by |
+ // a semicolon. |
+ var processed = false; |
+ var args = []; |
+ var currentArg = ''; |
+ var terminator = /[A-Za-z@]/; |
+ var seqCommand = ''; |
+ var query = false; |
+ var leadingZeroFilter = true; |
+ |
+ // Parse the command into a sequence command and series of numeric arguments. |
+ for (var i = 0; i < sequence.length; ++i) { |
+ if (sequence[i] == '?') { |
+ // Some commands have different meaning with a leading '?'. We'll call |
+ // that the 'query' flag. |
+ query = true; |
+ leadingZeroFilter = true; |
+ } else if (sequence[i] == ';') { |
+ // New argument |
+ args.push(parseInt(currentArg, 10)); |
+ currentArg = ''; |
+ leadingZeroFilter = true; |
+ } else if (terminator.test(sequence[i])) { |
+ // Terminating character. This'll tell us how to interpret the control |
+ // sequence. |
+ seqCommand = sequence[i]; |
+ if (currentArg != '') { |
+ args.push(parseInt(currentArg, 10)); |
+ } |
+ processed = true; |
+ break; |
+ } else { |
+ // Part of the arg, just add it, filtering out leading zeros. |
+ if (!(leadingZeroFilter && sequence[i] == '0')) { |
+ leadingZeroFilter = false; |
+ currentArg += sequence[i]; |
+ } |
+ } |
+ } |
+ |
+ if (!processed) { |
+ return processed; |
+ } |
+ |
+ // Interpret the command |
+ switch (seqCommand) { |
+ case 'A': // Cursor up |
+ this.terminal_.cursorUp(args[0] || 1); |
+ break; |
+ |
+ case 'B': // Cursor down |
+ this.terminal_.cursorDown(args[0] || 1); |
+ break; |
+ |
+ case 'C': // Cursor right |
+ this.terminal_.cursorRight(args[0] || 1); |
+ break; |
+ |
+ case 'D': // Cursor left |
+ this.terminal_.cursorLeft(args[0] || 1); |
+ break; |
+ |
+ case 'E': // Next line |
+ // This is like Cursor Down, except the cursor moves to the beginning of |
+ // the line as well. |
+ this.terminal_.cursorDown(args[0] || 1); |
+ this.terminal_.setCursorColumn(0); |
+ break; |
+ |
+ case 'F': // Previous line |
+ // This is like Cursor Up, except the cursor moves to the beginning of the |
+ // line as well. |
+ this.terminal_.cursorUp(args[0] || 1); |
+ this.terminal_.setCursorColumn(0); |
+ break; |
+ |
+ case 'G': // Cursor absolute column |
+ var position = args[0] ? args[0] - 1 : 0; |
+ this.terminal_.setCursorColumn(position); |
+ break; |
+ |
+ case 'H': // Cursor absolute row;col |
+ case 'f': // Horizontal & Vertical Position |
+ var row = args[0] ? args[0] - 1 : 0; |
+ var col = args[1] ? args[1] - 1 : 0; |
+ this.terminal_.setCursorPosition(row, col); |
+ break; |
+ |
+ case 'K': // Erase in Line |
+ switch (args[0]) { |
+ case 1: // Erase to left |
+ this.terminal_.eraseToLeft(); |
+ break; |
+ case 2: // Erase the line |
+ this.terminal_.eraseLine(); |
+ break; |
+ case 0: // Erase to right |
+ default: |
+ // Erase to right |
+ this.terminal_.eraseToRight(); |
+ break; |
+ } |
+ break; |
+ |
+ case 'J': // Erase in display |
+ switch (args[0]) { |
+ case 1: // Erase above |
+ this.terminal_.eraseToLeft(); |
+ this.terminal_.eraseAbove(); |
+ break; |
+ case 2: // Erase all |
+ this.terminal_.clear(); |
+ break; |
+ case 0: // Erase below |
+ default: |
+ this.terminal_.eraseToRight(); |
+ this.terminal_.eraseBelow(); |
+ break; |
+ } |
+ break; |
+ |
+ case 'X': // Erase character |
+ this.terminal_.eraseToRight(args[0] || 1); |
+ break; |
+ |
+ case 'L': // Insert lines |
+ this.terminal_.insertLines(args[0] || 1); |
+ break; |
+ |
+ case 'M': // Delete lines |
+ this.terminal_.deleteLines(args[0] || 1); |
+ break; |
+ |
+ case '@': // Insert characters |
+ var amount = 1; |
+ if (args[0]) { |
+ amount = args[0]; |
+ } |
+ this.terminal_.insertSpace(amount); |
+ break; |
+ |
+ case 'P': // Delete characters |
+ // This command shifts the line contents left, starting at the cursor |
+ // position. |
+ this.terminal_.deleteChars(args[0] || 1); |
+ break; |
+ |
+ case 'S': // Scroll up an amount |
+ this.terminal_.vtScrollUp(args[0] || 1); |
+ break; |
+ |
+ case 'T': // Scroll down an amount |
+ this.terminal_.vtScrollDown(args[0] || 1); |
+ break; |
+ |
+ case 'c': // Send device attributes |
+ if (!args[0]) { |
+ this.pendingResponse_ += '\x1b[?1;2c'; |
+ } |
+ break; |
+ |
+ case 'd': // Line position absolute |
+ this.terminal_.setCursorRow((args[0] - 1) || 0); |
+ break; |
+ |
+ case 'g': // Clear tab stops |
+ switch (args[0] || 0) { |
+ case 0: |
+ this.terminal_.setTabStopAtCursor(false); |
+ break; |
+ case 3: // Clear all tab stops in the page |
+ this.terminal_.clearTabStops(); |
+ break; |
+ default: |
+ break; |
+ } |
+ break; |
+ |
+ case 'm': // Color change |
+ if (args.length == 0) { |
+ this.terminal_.clearColorAndAttributes(); |
+ } else { |
+ if (args.length == 3 && |
+ (args[0] == 38 || args[0] == 48) && args[1] == 5) { |
+ // This is code for the 256-color palette, skip the normal processing. |
+ if (args[0] == 38) { |
+ // Set the foreground color to the 3rd argument. |
+ this.terminal_.setForegroundColor256(args[2]); |
+ } else if (args[0] == 48) { |
+ // Set the background color to the 3rd argument. |
+ this.terminal_.setBackgroundColor256(args[2]); |
+ } |
+ } else { |
+ var numArgs = args.length; |
+ for (var argNum = 0; argNum < numArgs; ++argNum) { |
+ var arg = args[argNum]; |
+ if (isNaN(arg)) { |
+ // This is the same as an attribute of zero. |
+ this.terminal_.setAttributes(0); |
+ } else if (arg < 30) { |
+ // This is an attribute argument. |
+ this.terminal_.setAttributes(arg); |
+ } else if (arg < 40) { |
+ // This is a foreground color argument. |
+ this.terminal_.setForegroundColor(arg); |
+ } else if (arg < 50) { |
+ // This is a background color argument. |
+ this.terminal_.setBackgroundColor(arg); |
+ } |
+ } |
+ } |
+ } |
+ break; |
+ |
+ case 'n': // Device status report |
+ switch (args[0]) { |
+ case 5: |
+ if (!query) { |
+ var response = '\x1b0n'; |
+ this.pendingResponse_ += response; |
+ } |
+ break; |
+ |
+ case 6: |
+ var curX = this.terminal_.getCursorColumn() + 1; |
+ var curY = this.terminal_.getCursorRow() + 1; |
+ var response = '\x1b[' + curY + ';' + curX + 'R'; |
+ this.pendingResponse_ += response; |
+ break; |
+ } |
+ break; |
+ |
+ case 'l': // Reset mode |
+ case 'h': // Set mode |
+ var set = (seqCommand == 'h' ? true : false); |
+ if (query) { |
+ switch (args[0]) { |
+ case 1: // Normal (l) or application (h) cursor keys |
+ this.applicationCursor = set; |
+ break; |
+ case 3: // 80 (if l) or 132 (if h) column mode |
+ // Our size is always determined by the window size, so we ignore |
+ // attempts to resize from remote end. |
+ break; |
+ case 4: // Fast (l) or slow (h) scroll |
+ // This is meaningless to us. |
+ break; |
+ case 5: // Normal (l) or reverse (h) video mode |
+ this.terminal_.setReverseVideo(set); |
+ break; |
+ case 6: // Normal (l) or origin (h) cursor mode |
+ this.terminal_.setOriginMode(set); |
+ break; |
+ case 7: // No (l) wraparound mode or wraparound (h) mode |
+ this.terminal_.setWraparound(set); |
+ break; |
+ case 12: // Stop (l) or start (h) blinking cursor |
+ this.terminal_.setCursorBlink(set); |
+ break; |
+ case 25: // Hide (l) or show (h) cursor |
+ this.terminal_.setCursorVisible(set); |
+ break; |
+ case 45: // Disable (l) or enable (h) reverse wraparound |
+ this.terminal_.setReverseWraparound(set); |
+ break; |
+ case 67: // Backspace is delete (h) or backspace (l) |
+ this.backspaceSendsBackspace = set; |
+ break; |
+ case 1036: // Meta sends (h) or doesn't send (l) escape |
+ this.metaSendsEscape = set; |
+ break; |
+ case 1039: // Alt sends (h) or doesn't send (l) escape |
+ this.altSendsEscape = set; |
+ break; |
+ case 1049: // Switch to/from alternate, save/restore cursor |
+ this.terminal_.setAlternateMode(set); |
+ break; |
+ default: |
+ console.log('Unimplemented l/h command: ' + |
+ (query ? '?' : '') + args[0]); |
+ break; |
+ } |
+ |
+ } else { |
+ switch (args[0]) { |
+ case 4: // Replace (l) or insert (h) mode |
+ this.terminal_.setInsertMode(set); |
+ break; |
+ case 20: |
+ // Normal linefeed (l), \n means move down only |
+ // Automatic linefeed (h), \n means \n\r |
+ this.terminal_.setAutoLinefeed(set); |
+ break; |
+ default: |
+ console.log('Unimplemented l/h command: ' + |
+ (query ? '?' : '') + args[0]); |
+ break; |
+ } |
+ } |
+ break; |
+ |
+ case 'r': |
+ if (query) { |
+ // Restore DEC private mode values |
+ // TODO(maccarro): Implement this |
+ } else { |
+ // Set scroll region |
+ var scrollTop = args[0] || null; |
+ var scrollBottom = args[1] || null; |
+ this.terminal_.setScrollRegion(scrollTop, scrollBottom); |
+ } |
+ break; |
+ |
+ default: |
+ console.log('Unknown control: ' + seqCommand); |
+ break; |
+ } |
+ return processed; |
+}; |