Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(127)

Side by Side Diff: chrome/browser/resources/hterm/js/vt100.js

Issue 8680034: Initial landing of Screen, Terminal, and VT100 classes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6 * @fileoverview This file implements the VT100 interpreter, which
7 * operates in conjunction with a hterm.Terminal to provide
8 * interpretation of VT100-style control sequences.
9 *
10 * Original code by Cory Maccarrone.
11 */
12
13 /**
14 * Constructor for the VT100 Interpreter.
15 *
16 * The interpreter operates on a terminal object capable of performing cursor
17 * move operations, painting characters, etc.
18 *
19 * @param {hterm.Terminal} terminal Terminal to use with
20 * the interpreter. Direct commands are sent to it in the presence of
21 * control characters -- otherwise, normal characters are passed straight
22 * through to its render functions.
23 * @constructor
24 */
25 hterm.VT100 = function(terminal) {
26 this.terminal_ = terminal;
27
28 // Sequence being processed -- that seen so far
29 this.pendingSequence_ = [];
30
31 // Response to be sent back to the guest
32 this.pendingResponse_ = '';
33
34 // Keyboard flags, stored here as we interpret the control sequences that
35 // change them.
36 this.applicationKeypad_ = false;
37 this.applicationCursor_ = false;
38 this.backspaceSendsBackspace_ = false;
39 this.altSendsEscape_ = true;
40 this.metaSendsEscape_ = true;
41 };
42
43 /**
44 * Interpret a sequence of characters.
45 *
46 * Incomplete escape sequences are buffered until the next call.
47 *
48 * @param {string} str Sequence of characters to interpret or pass through.
49 */
50 hterm.VT100.prototype.interpretString = function(str) {
51 var i = 0;
52
53 while (i < str.length) {
54 while (this.pendingSequence_.length && i < str.length) {
55 this.interpretCharacter(str.substr(i, 1));
56 i++;
57 }
58
59 if (i == str.length)
60 break;
61
62 var nextEscape = str.substr(i).search(/[\x1b\n\t]|$/);
63
64 if (nextEscape == -1)
65 nextEscape = str.length;
66
67 if (nextEscape != 0) {
68 var plainText = str.substr(i, nextEscape);
69 this.terminal_.print(plainText);
70 i += nextEscape;
71 }
72
73 if (i == str.length)
74 break;
75
76 this.interpretCharacter(str.substr(i, 1));
77 i++;
78 }
79 };
80
81 /**
82 * Interpret a single character in a sequence.
83 *
84 * This function is called for each character in terminal input, and
85 * accumulates characters until a recognized control sequence is read. If the
86 * character is not part of a control sequence, it is queued up for rendering.
87 *
88 * @param {string} character Character to interpret or pass through.
89 */
90 hterm.VT100.prototype.interpretCharacter = function(character) {
91 var interpret = false;
92
93 // Check if this is either an escape, a bell, or a printable character. These
94 // are all part of escape sequences. Others may be intermingled in escape
95 // sequences, and they'll be picked out and sent to the renderer. This is per
96 // VT100 spec.
97 if (character == '\x1b' ||
98 ((character >= '\x20' || character == '\x07') &&
99 this.pendingSequence_.length)) {
klimek 2011/11/24 19:13:51 Can't you save this whole 'interpret' variable by
rginda 2011/11/28 20:39:47 This wonkyness is a result of some changes I made
100 interpret = true;
101 }
102
103 if (!interpret) {
104 // Other characters are printed to the terminal.
105 if (character == '\n') {
106 this.terminal_.newLine();
107 return;
108 }
109
110 if (character == '\t') {
111 this.terminal_.cursorRight(4);
112 return;
113 }
114
115 this.terminal_.print(character);
116 return;
117 }
118
119 this.pendingSequence_.push(character);
120 var sequence = this.pendingSequence_;
121
122 if (sequence.length == 0 ||
klimek 2011/11/24 19:13:51 == 0 cannot happen after a push; or am I missing s
rginda 2011/11/28 20:39:47 Done.
123 sequence[0] != '\x1b' ||
klimek 2011/11/24 19:13:51 Also, I don't see how this code can every push som
rginda 2011/11/28 20:39:47 Done.
124 sequence.length < 2) {
klimek 2011/11/24 19:13:51 ... so this is basically if (sequence.length == 1)
rginda 2011/11/28 20:39:47 Done.
125 // This isn't a sequence, or it isn't log enough, return.
126 return;
127 }
128
129 var processed = true;
130 switch (sequence[1]) {
131 case '\x1b':
132 // Another escape, restart processing from this char
133 this.pendingSequence_.length = 1;
134 processed = false;
135 break;
136
137 case '[':
138 if (!this.interpretControlSequenceInducer_(sequence.slice(2))) {
139 processed = false;
140 }
141 break;
142
143 case ']':
144 if (!this.interpretOperatingSystemCommand_(sequence.slice(2))) {
145 processed = false;
146 }
147 break;
148
149 case '=': // Application keypad
150 this.setApplicationKeypad(true);
151 break;
152
153 case '>': // Normal keypad
154 this.setApplicationKeypad(false);
155 break;
156
157 case '7': // Save cursor
158 this.terminal_.saveCursor();
159 break;
160
161 case '8': // Restore cursor
162 this.terminal_.restoreCursor();
163 break;
164
165 case 'D': // Index, like newline, only keep the X position
166 this.terminal_.lineFeed();
167 break;
168
169 case 'E': // Next line. Like newline, but doesn't add lines.
170 this.terminal_.setCursorColumn(0);
171 this.terminal_.cursorDown(1);
172 break;
173
174 case 'M': // Reverse index.
175 // This is like newline, but in reverse. When we hit the top of the
176 // terminal, lines are added at the top while swapping out the bottom
177 // lines.
178 this.terminal_.reverseLineFeed();
179 break;
180
181 case 'c': // Full reset
182 this.terminal_.reset();
183 break;
184
185 case '#': // DEC commands
186 if (sequence.length < 3) {
187 processed = false;
188 break;
189 }
190 switch (sequence[2]) {
191 case '8': // DEC screen alignment test
192 this.fill('E');
193 break;
194 default:
195 console.log('Unsupported DEC command: ' + sequence[2]);
196 break;
197 }
198 break;
199
200 case '(': // Designate G0 character set
201 if (sequence.length < 3) {
202 processed = false;
203 break;
204 }
205 switch (sequence[2]) {
206 case '0': // Line drawing
207 this.terminal_.setSpecialCharsEnabled(true);
208 break;
209 default:
210 this.terminal_.setSpecialCharsEnabled(false);
211 break;
212 }
213 break;
214
215 case ')': // Designate G1 character set
216 case '*': // Designate G2 character set
217 case '+': // Designate G3 character set
218 if (sequence.length < 3) {
219 processed = false;
220 break;
221 }
222 console.log('Code ' + sequence[2]);
223 break;
224
225 case 'H': // Set a tab stop at the cursor position
226 this.terminal_.setTabStopAtCursor(true);
227 break;
228
229 default:
230 console.log('Unsupported escape sequence: ' + sequence[1]);
231 break;
232 }
233
234 if (processed) {
235 //console.log('Escape sequence: ' + sequence.slice(1));
236 this.pendingSequence_.length = 0;
237 }
238
239 return;
240 };
241
242 /**
243 * Return any pending response from the interpretation of control sequences.
244 *
245 * The response should be returned as if the user typed it, and the pending
246 * response is cleared from the interpreter.
247 *
248 * @return {string} response to send.
249 */
250 hterm.VT100.prototype.getAndClearPendingResponse = function() {
251 var response = this.pendingResponse_;
252 this.pendingResponse_ = '';
253 return response;
254 };
255
256 /**
257 * Set whether backspace should send ^H
258 *
259 * @param {boolean} enable Whether backspace should send ^H or not.
260 */
261 hterm.VT100.prototype.setBackspaceSendsBackspace =
262 function(enable) {
263 this.backspaceSendsBackspace_ = enable;
264 };
265
266 /**
267 * Return whether backspace should send ^H
268 *
269 * @return {boolean} Whether backspace should send ^H or not.
270 */
271 hterm.VT100.prototype.getBackspaceSendsBackspace =
272 function() {
273 return this.backspaceSendsBackspace_;
274 };
275
276 /**
277 * Set whether the meta key sends a leading escape or not.
278 *
279 * @param {boolean} enable Whether meta sends a leading escape or not.
280 */
281 hterm.VT100.prototype.setMetaSendsEscape = function(enable) {
282 this.metaSendsEscape_ = enable;
arv (Not doing code reviews) 2011/11/28 18:21:00 All these noop getters and setters are pretty sill
rginda 2011/11/28 20:39:47 I avoid real getters and setters like the plague (
283 };
284
285 /**
286 * Return whether the meta key sends a leading escape or not.
287 *
288 * @return {boolean} Whether meta sends a leading escape or not.
289 */
290 hterm.VT100.prototype.getMetaSendsEscape = function() {
291 return this.metaSendsEscape_;
292 };
293
294 /**
295 * Set whether the alt key sends a leading escape or not.
296 *
297 * @param {boolean} enable Whether alt sends a leading escape or not.
298 */
299 hterm.VT100.prototype.setAltSendsEscape = function(enable) {
300 this.altSendsEscape_ = enable;
301 };
302
303 /**
304 * Return whether the alt key sends a leading escape or not.
305 *
306 * @return {boolean} Whether alt sends a leading escape or not.
307 */
308 hterm.VT100.prototype.getAltSendsEscape = function() {
309 return this.altSendsEscape_;
310 };
311
312 /**
313 * Enable/disable application keypad. This changes the way numeric keys are
314 * sent from the keyboard.
315 *
316 * @param {boolean} enabled Whether the application keypad should be enabled or
317 * not.
318 */
319 hterm.VT100.prototype.setApplicationKeypad =
320 function(enabled) {
321 this.applicationKeypad_ = enabled;
322 };
323
324 /**
325 * Enable/disable the application cursor mode.
326 *
327 * This changes the way cursor keys are sent from the keyboard.
328 *
329 * @param {boolean} enabled Whether the application cursor keys are enabled or
330 * not.
331 */
332 hterm.VT100.prototype.setApplicationCursor =
333 function(enabled) {
334 this.applicationCursor_ = enabled;
335 };
336
337 /**
338 * Get the current application cursor mode.
339 *
340 * @return {boolean} Whether application cursor mode is enabled.
341 */
342 hterm.VT100.prototype.getApplicationCursor = function() {
343 return this.applicationCursor_;
344 };
345
346 /**
347 * Interpret an operating system command (OSC) sequence.
348 *
349 * @param {Array} sequence Sequence to interpret.
350 * @return {boolean} Whether the sequence was interpreted or not.
351 * @private
352 */
353 hterm.VT100.prototype.interpretOperatingSystemCommand_ =
354 function(sequence) {
355 // These commands tend to do things like change the window title and other
356 // things.
357 var processed = false;
358 var length = sequence.length;
359 var i = 0;
360 var args = [];
361 var currentArg = '';
362 var leadingZeroFilter = true;
363
364 // Parse the command into a sequence command and series of numeric arguments.
365 while (true) {
366 if (i >= length) {
367 // We ran out of characters interpreting the string
368 break;
369 }
370
371 if (sequence[i] == ';') {
372 // New argument
373 args.push(currentArg);
374 currentArg = '';
375 leadingZeroFilter = true;
376 } else if (sequence[i] == '\x7' ||
377 (sequence[i] == '\x1b' &&
378 sequence[i + 1] == '\\')) {
379 // Terminating character. This'll tell us how to interpret the control
380 // sequence.
381 if (currentArg != '') {
382 args.push(currentArg);
383 }
384 processed = true;
385 break;
386 } else {
387 // Part of the arg, just add it, filtering out leadining zeros.
388 if (!(leadingZeroFilter && sequence[i] == '0')) {
389 leadingZeroFilter = false;
390 currentArg += sequence[i];
391 }
392 }
393 i++;
394 }
395
396 if (!processed)
397 return processed;
398
399 // Interpret the command
400 if (args[0] == '') {
401 // The leading-zero filter dropped our zero, so put it back.
402 args[0] = 0;
403 }
404
405 switch (parseInt(args[0], 10)) {
406 case 0:
407 case 2:
408 // Change the window title to args[1]
409 // TODO(rginda): this.
410 break;
411 default:
412 console.log('Unsupported OSC command: ' + sequence.slice(0, i + 1));
413 break;
414 }
415
416 return processed;
417 };
418
419 /**
420 * Interpret a control sequence inducer (CSI) command.
421 *
422 * @param {Array} sequence Sequence to interpret.
423 * @return {boolean} Whether the sequence was interpreted succesfully or not.
424 * @private
425 */
426 hterm.VT100.prototype.interpretControlSequenceInducer_ =
427 function(sequence) {
428 // These escape codes all end with a letter, and have arguments separated by
429 // a semicolon.
430 var processed = false;
431 var args = [];
432 var currentArg = '';
433 var terminator = /[A-Za-z@]/;
434 var seqCommand = '';
435 var query = false;
436 var leadingZeroFilter = true;
437
438 // Parse the command into a sequence command and series of numeric arguments.
439 for (var i = 0; i < sequence.length; ++i) {
440 if (sequence[i] == '?') {
441 // Some commands have different meaning with a leading '?'. We'll call
442 // that the 'query' flag.
443 query = true;
444 leadingZeroFilter = true;
445 } else if (sequence[i] == ';') {
446 // New argument
447 args.push(parseInt(currentArg, 10));
448 currentArg = '';
449 leadingZeroFilter = true;
450 } else if (terminator.test(sequence[i])) {
451 // Terminating character. This'll tell us how to interpret the control
452 // sequence.
453 seqCommand = sequence[i];
454 if (currentArg != '') {
455 args.push(parseInt(currentArg, 10));
456 }
457 processed = true;
458 break;
459 } else {
460 // Part of the arg, just add it, filtering out leading zeros.
461 if (!(leadingZeroFilter && sequence[i] == '0')) {
462 leadingZeroFilter = false;
463 currentArg += sequence[i];
464 }
465 }
466 }
467
468 if (!processed) {
469 return processed;
470 }
471
472 // Interpret the command
473 switch (seqCommand) {
474 case 'A': // Cursor up
475 this.terminal_.cursorUp(args[0] || 1);
476 break;
477
478 case 'B': // Cursor down
479 this.terminal_.cursorDown(args[0] || 1);
480 break;
481
482 case 'C': // Cursor right
483 this.terminal_.cursorRight(args[0] || 1);
484 break;
485
486 case 'D': // Cursor left
487 this.terminal_.cursorLeft(args[0] || 1);
488 break;
489
490 case 'E': // Next line
491 // This is like Cursor Down, except the cursor moves to the beginning of
492 // the line as well.
493 this.terminal_.cursorDown(args[0] || 1);
494 this.terminal_.setCursorColumn(0);
495 break;
496
497 case 'F': // Previous line
498 // This is like Cursor Up, except the cursor moves to the beginning of the
499 // line as well.
500 this.terminal_.cursorUp(args[0] || 1);
501 this.terminal_.setCursorColumn(0);
502 break;
503
504 case 'G': // Cursor absolute column
505 var position = args[0] ? args[0] - 1 : 0;
506 this.terminal_.setCursorColumn(position);
507 break;
508
509 case 'H': // Cursor absolute row;col
510 case 'f': // Horizontal & Vertical Position
511 var row = args[0] ? args[0] - 1 : 0;
512 var col = args[1] ? args[1] - 1 : 0;
513 this.terminal_.setCursorPosition(row, col);
514 break;
515
516 case 'K': // Erase in Line
517 switch (args[0]) {
518 case 1: // Erase to left
519 this.terminal_.eraseToLeft();
520 break;
521 case 2: // Erase the line
522 this.terminal_.eraseLine();
523 break;
524 case 0: // Erase to right
525 default:
526 // Erase to right
527 this.terminal_.eraseToRight();
528 break;
529 }
530 break;
531
532 case 'J': // Erase in display
533 switch (args[0]) {
534 case 1: // Erase above
535 this.terminal_.eraseToLeft();
536 this.terminal_.eraseAbove();
537 break;
538 case 2: // Erase all
539 this.terminal_.clear();
540 break;
541 case 0: // Erase below
542 default:
543 this.terminal_.eraseToRight();
544 this.terminal_.eraseBelow();
545 break;
546 }
547 break;
548
549 case 'X': // Erase character
550 this.terminal_.eraseToRight(args[0] || 1);
551 break;
552
553 case 'L': // Insert lines
554 this.terminal_.insertLines(args[0] || 1);
555 break;
556
557 case 'M': // Delete lines
558 this.terminal_.deleteLines(args[0] || 1);
559 break;
560
561 case '@': // Insert characters
562 var amount = 1;
563 if (args[0]) {
564 amount = args[0];
565 }
566 this.terminal_.insertSpace(amount);
567 break;
568
569 case 'P': // Delete characters
570 // This command shifts the line contents left, starting at the cursor
571 // position.
572 this.terminal_.deleteChars(args[0] || 1);
573 break;
574
575 case 'S': // Scroll up an amount
576 this.terminal_.vtScrollUp(args[0] || 1);
577 break;
578
579 case 'T': // Scroll down an amount
580 this.terminal_.vtScrollDown(args[0] || 1);
581 break;
582
583 case 'c': // Send device attributes
584 if (!args[0]) {
585 this.pendingResponse_ += '\x1b[?1;2c';
586 }
587 break;
588
589 case 'd': // Line position absolute
590 this.terminal_.setCursorRow((args[0] - 1) || 0);
591 break;
592
593 case 'g': // Clear tab stops
594 switch (args[0] || 0) {
595 case 0:
596 this.terminal_.setTabStopAtCursor(false);
597 break;
598 case 3: // Clear all tab stops in the page
599 this.terminal_.clearTabStops();
600 break;
601 default:
602 break;
603 }
604 break;
605
606 case 'm': // Color change
607 if (args.length == 0) {
608 this.terminal_.clearColorAndAttributes();
609 } else {
610 if (args.length == 3 &&
611 (args[0] == 38 || args[0] == 48) && args[1] == 5) {
612 // This is code for the 256-color palette, skip the normal processing.
613 if (args[0] == 38) {
614 // Set the foreground color to the 3rd argument.
615 this.terminal_.setForegroundColor256(args[2]);
616 } else if (args[0] == 48) {
617 // Set the background color to the 3rd argument.
618 this.terminal_.setBackgroundColor256(args[2]);
619 }
620 } else {
621 var numArgs = args.length;
622 for (var argNum = 0; argNum < numArgs; ++argNum) {
623 var arg = args[argNum];
624 if (isNaN(arg)) {
625 // This is the same as an attribute of zero.
626 this.terminal_.setAttributes(0);
627 } else if (arg < 30) {
628 // This is an attribute argument.
629 this.terminal_.setAttributes(arg);
630 } else if (arg < 40) {
631 // This is a foreground color argument.
632 this.terminal_.setForegroundColor(arg);
633 } else if (arg < 50) {
634 // This is a background color argument.
635 this.terminal_.setBackgroundColor(arg);
636 }
637 }
638 }
639 }
640 break;
641
642 case 'n': // Device status report
643 switch (args[0]) {
644 case 5:
645 if (!query) {
646 var response = '\x1b0n';
647 this.pendingResponse_ += response;
648 }
649 break;
650
651 case 6:
652 var curX = this.terminal_.getCursorColumn() + 1;
653 var curY = this.terminal_.getCursorRow() + 1;
654 var response = '\x1b[' + curY + ';' + curX + 'R';
655 this.pendingResponse_ += response;
656 break;
657 }
658 break;
659
660 case 'l': // Reset mode
661 case 'h': // Set mode
662 var set = (seqCommand == 'h' ? true : false);
663 if (query) {
664 switch (args[0]) {
665 case 1: // Normal (l) or application (h) cursor keys
666 this.setApplicationCursor(set);
667 break;
668 case 3: // 80 (if l) or 132 (if h) column mode
669 // Our size is always determined by the window size, so we ignore
670 // attempts to resize from remote end.
671 break;
672 case 4: // Fast (l) or slow (h) scroll
673 // This is meaningless to us.
674 break;
675 case 5: // Normal (l) or reverse (h) video mode
676 this.terminal_.setReverseVideo(set);
677 break;
678 case 6: // Normal (l) or origin (h) cursor mode
679 this.terminal_.setOriginMode(set);
680 break;
681 case 7: // No (l) wraparound mode or wraparound (h) mode
682 this.terminal_.setWraparound(set);
683 break;
684 case 12: // Stop (l) or start (h) blinking cursor
685 this.terminal_.setCursorBlink(set);
686 break;
687 case 25: // Hide (l) or show (h) cursor
688 this.terminal_.setCursorVisible(set);
689 break;
690 case 45: // Disable (l) or enable (h) reverse wraparound
691 this.terminal_.setReverseWraparound(set);
692 break;
693 case 67: // Backspace is delete (h) or backspace (l)
694 this.setBackspaceSendsBackspace(set);
695 break;
696 case 1036: // Meta sends (h) or doesn't send (l) escape
697 this.setMetaSendsEscape(set);
698 break;
699 case 1039: // Alt sends (h) or doesn't send (l) escape
700 this.setAltSendsEscape(set);
701 break;
702 case 1049: // Switch to/from alternate, save/restore cursor
703 this.terminal_.setAlternateMode(set);
704 break;
705 default:
706 console.log('Unimplemented l/h command: ' +
707 (query ? '?' : '') + args[0]);
708 break;
709 }
710
711 } else {
712 switch (args[0]) {
713 case 4: // Replace (l) or insert (h) mode
714 this.terminal_.setInsertMode(set);
715 break;
716 case 20:
717 // Normal linefeed (l), \n means move down only
718 // Automatic linefeed (h), \n means \n\r
719 this.terminal_.setAutoLinefeed(set);
720 break;
721 default:
722 console.log('Unimplemented l/h command: ' +
723 (query ? '?' : '') + args[0]);
724 break;
725 }
726 }
727 break;
728
729 case 'r':
730 if (query) {
731 // Restore DEC private mode values
732 // TODO(maccarro): Implement this
733 } else {
734 // Set scroll region
735 var scrollTop = args[0] || null;
736 var scrollBottom = args[1] || null;
737 this.terminal_.setScrollRegion(scrollTop, scrollBottom);
738 }
739 break;
740
741 default:
742 console.log('Unknown control: ' + seqCommand);
743 break;
744 }
745 return processed;
746 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698