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

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: Address review comments Created 9 years 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 /**
35 * Enable/disable application keypad.
36 *
37 * This changes the way numeric keys are sent from the keyboard.
38 */
39 this.applicationKeypad = false;
40
41 /**
42 * Enable/disable the application cursor mode.
43 *
44 * This changes the way cursor keys are sent from the keyboard.
45 */
46 this.applicationCursor = false;
47
48 /**
49 * Whether backspace should send ^H or not.
50 */
51 this.backspaceSendsBackspace = false;
52
53 /**
54 * Set whether the alt key sends a leading escape or not.
55 */
56 this.altSendsEscape = true;
57
58 /**
59 * Set whether the meta key sends a leading escape or not.
60 */
61 this.metaSendsEscape = true;
62 };
63
64 /**
65 * Interpret a sequence of characters.
66 *
67 * Incomplete escape sequences are buffered until the next call.
68 *
69 * @param {string} str Sequence of characters to interpret or pass through.
70 */
71 hterm.VT100.prototype.interpretString = function(str) {
72 var i = 0;
73
74 while (i < str.length) {
75 while (this.pendingSequence_.length && i < str.length) {
76 this.interpretCharacter(str.substr(i, 1));
77 i++;
78 }
79
80 if (i == str.length)
81 break;
82
83 var nextEscape = str.substr(i).search(/[\x1b\n\t]|$/);
84
85 if (nextEscape == -1)
86 nextEscape = str.length;
87
88 if (nextEscape != 0) {
89 var plainText = str.substr(i, nextEscape);
90 this.terminal_.print(plainText);
91 i += nextEscape;
92 }
93
94 if (i == str.length)
95 break;
96
97 this.interpretCharacter(str.substr(i, 1));
98 i++;
99 }
100 };
101
102 /**
103 * Interpret a single character in a sequence.
104 *
105 * This function is called for each character in terminal input, and
106 * accumulates characters until a recognized control sequence is read. If the
107 * character is not part of a control sequence, it is queued up for rendering.
108 *
109 * @param {string} character Character to interpret or pass through.
110 */
111 hterm.VT100.prototype.interpretCharacter = function(character) {
112 var interpret = false;
113
114 if (character == '\n') {
115 this.terminal_.newLine();
116 return;
117 }
118
119 if (character == '\t') {
120 // TODO(rginda): I don't think this is the correct behavior.
121 this.terminal_.cursorRight(4);
122 return;
123 }
124
125 if (character == '\x1b') {
126 this.pendingSequence_.length = 1;
127 this.pendingSequence_[0] = character;
128 return;
129 }
130
131 if (!this.pendingSequence_.length ||
132 (character < '\x20' && character != '\x07')) {
133 // We don't have a pending escape, or this character is invalid in the
134 // context of an escape sequence. The VT100 spec says to just print it.
135 this.terminal_.print(character);
136 return;
137 }
138
139 this.pendingSequence_.push(character);
140 var sequence = this.pendingSequence_;
141
142 var processed = true;
143 switch (sequence[1]) {
144 case '[':
145 if (!this.interpretControlSequenceInducer_(sequence.slice(2))) {
146 processed = false;
147 }
148 break;
149
150 case ']':
151 if (!this.interpretOperatingSystemCommand_(sequence.slice(2))) {
152 processed = false;
153 }
154 break;
155
156 case '=': // Application keypad
157 this.applicationKeypad = true;
158 break;
159
160 case '>': // Normal keypad
161 this.applicationKeypad = false;
162 break;
163
164 case '7': // Save cursor
165 this.terminal_.saveCursor();
166 break;
167
168 case '8': // Restore cursor
169 this.terminal_.restoreCursor();
170 break;
171
172 case 'D': // Index, like newline, only keep the X position
173 this.terminal_.lineFeed();
174 break;
175
176 case 'E': // Next line. Like newline, but doesn't add lines.
177 this.terminal_.setCursorColumn(0);
178 this.terminal_.cursorDown(1);
179 break;
180
181 case 'M': // Reverse index.
182 // This is like newline, but in reverse. When we hit the top of the
183 // terminal, lines are added at the top while swapping out the bottom
184 // lines.
185 this.terminal_.reverseLineFeed();
186 break;
187
188 case 'c': // Full reset
189 this.terminal_.reset();
190 break;
191
192 case '#': // DEC commands
193 if (sequence.length < 3) {
194 processed = false;
195 break;
196 }
197 switch (sequence[2]) {
198 case '8': // DEC screen alignment test
199 this.fill('E');
200 break;
201 default:
202 console.log('Unsupported DEC command: ' + sequence[2]);
203 break;
204 }
205 break;
206
207 case '(': // Designate G0 character set
208 if (sequence.length < 3) {
209 processed = false;
210 break;
211 }
212 switch (sequence[2]) {
213 case '0': // Line drawing
214 this.terminal_.setSpecialCharsEnabled(true);
215 break;
216 default:
217 this.terminal_.setSpecialCharsEnabled(false);
218 break;
219 }
220 break;
221
222 case ')': // Designate G1 character set
223 case '*': // Designate G2 character set
224 case '+': // Designate G3 character set
225 if (sequence.length < 3) {
226 processed = false;
227 break;
228 }
229 console.log('Code ' + sequence[2]);
230 break;
231
232 case 'H': // Set a tab stop at the cursor position
233 this.terminal_.setTabStopAtCursor(true);
234 break;
235
236 default:
237 console.log('Unsupported escape sequence: ' + sequence[1]);
238 break;
239 }
240
241 if (processed) {
242 //console.log('Escape sequence: ' + sequence.slice(1));
243 this.pendingSequence_.length = 0;
244 }
245
246 return;
247 };
248
249 /**
250 * Return any pending response from the interpretation of control sequences.
251 *
252 * The response should be returned as if the user typed it, and the pending
253 * response is cleared from the interpreter.
254 *
255 * @return {string} response to send.
256 */
257 hterm.VT100.prototype.getAndClearPendingResponse = function() {
258 var response = this.pendingResponse_;
259 this.pendingResponse_ = '';
260 return response;
261 };
262
263 /**
264 * Interpret an operating system command (OSC) sequence.
265 *
266 * @param {Array} sequence Sequence to interpret.
267 * @return {boolean} Whether the sequence was interpreted or not.
268 * @private
269 */
270 hterm.VT100.prototype.interpretOperatingSystemCommand_ =
271 function(sequence) {
272 // These commands tend to do things like change the window title and other
273 // things.
274 var processed = false;
275 var length = sequence.length;
276 var i = 0;
277 var args = [];
278 var currentArg = '';
279 var leadingZeroFilter = true;
280
281 // Parse the command into a sequence command and series of numeric arguments.
282 while (true) {
283 if (i >= length) {
284 // We ran out of characters interpreting the string
285 break;
286 }
287
288 if (sequence[i] == ';') {
289 // New argument
290 args.push(currentArg);
291 currentArg = '';
292 leadingZeroFilter = true;
293 } else if (sequence[i] == '\x7' ||
294 (sequence[i] == '\x1b' &&
295 sequence[i + 1] == '\\')) {
296 // Terminating character. This'll tell us how to interpret the control
297 // sequence.
298 if (currentArg != '') {
299 args.push(currentArg);
300 }
301 processed = true;
302 break;
303 } else {
304 // Part of the arg, just add it, filtering out leadining zeros.
305 if (!(leadingZeroFilter && sequence[i] == '0')) {
306 leadingZeroFilter = false;
307 currentArg += sequence[i];
308 }
309 }
310 i++;
311 }
312
313 if (!processed)
314 return processed;
315
316 // Interpret the command
317 if (args[0] == '') {
318 // The leading-zero filter dropped our zero, so put it back.
319 args[0] = 0;
320 }
321
322 switch (parseInt(args[0], 10)) {
323 case 0:
324 case 2:
325 // Change the window title to args[1]
326 // TODO(rginda): this.
327 break;
328 default:
329 console.log('Unsupported OSC command: ' + sequence.slice(0, i + 1));
330 break;
331 }
332
333 return processed;
334 };
335
336 /**
337 * Interpret a control sequence inducer (CSI) command.
338 *
339 * @param {Array} sequence Sequence to interpret.
340 * @return {boolean} Whether the sequence was interpreted succesfully or not.
341 * @private
342 */
343 hterm.VT100.prototype.interpretControlSequenceInducer_ =
344 function(sequence) {
345 // These escape codes all end with a letter, and have arguments separated by
346 // a semicolon.
347 var processed = false;
348 var args = [];
349 var currentArg = '';
350 var terminator = /[A-Za-z@]/;
351 var seqCommand = '';
352 var query = false;
353 var leadingZeroFilter = true;
354
355 // Parse the command into a sequence command and series of numeric arguments.
356 for (var i = 0; i < sequence.length; ++i) {
357 if (sequence[i] == '?') {
358 // Some commands have different meaning with a leading '?'. We'll call
359 // that the 'query' flag.
360 query = true;
361 leadingZeroFilter = true;
362 } else if (sequence[i] == ';') {
363 // New argument
364 args.push(parseInt(currentArg, 10));
365 currentArg = '';
366 leadingZeroFilter = true;
367 } else if (terminator.test(sequence[i])) {
368 // Terminating character. This'll tell us how to interpret the control
369 // sequence.
370 seqCommand = sequence[i];
371 if (currentArg != '') {
372 args.push(parseInt(currentArg, 10));
373 }
374 processed = true;
375 break;
376 } else {
377 // Part of the arg, just add it, filtering out leading zeros.
378 if (!(leadingZeroFilter && sequence[i] == '0')) {
379 leadingZeroFilter = false;
380 currentArg += sequence[i];
381 }
382 }
383 }
384
385 if (!processed) {
386 return processed;
387 }
388
389 // Interpret the command
390 switch (seqCommand) {
391 case 'A': // Cursor up
392 this.terminal_.cursorUp(args[0] || 1);
393 break;
394
395 case 'B': // Cursor down
396 this.terminal_.cursorDown(args[0] || 1);
397 break;
398
399 case 'C': // Cursor right
400 this.terminal_.cursorRight(args[0] || 1);
401 break;
402
403 case 'D': // Cursor left
404 this.terminal_.cursorLeft(args[0] || 1);
405 break;
406
407 case 'E': // Next line
408 // This is like Cursor Down, except the cursor moves to the beginning of
409 // the line as well.
410 this.terminal_.cursorDown(args[0] || 1);
411 this.terminal_.setCursorColumn(0);
412 break;
413
414 case 'F': // Previous line
415 // This is like Cursor Up, except the cursor moves to the beginning of the
416 // line as well.
417 this.terminal_.cursorUp(args[0] || 1);
418 this.terminal_.setCursorColumn(0);
419 break;
420
421 case 'G': // Cursor absolute column
422 var position = args[0] ? args[0] - 1 : 0;
423 this.terminal_.setCursorColumn(position);
424 break;
425
426 case 'H': // Cursor absolute row;col
427 case 'f': // Horizontal & Vertical Position
428 var row = args[0] ? args[0] - 1 : 0;
429 var col = args[1] ? args[1] - 1 : 0;
430 this.terminal_.setCursorPosition(row, col);
431 break;
432
433 case 'K': // Erase in Line
434 switch (args[0]) {
435 case 1: // Erase to left
436 this.terminal_.eraseToLeft();
437 break;
438 case 2: // Erase the line
439 this.terminal_.eraseLine();
440 break;
441 case 0: // Erase to right
442 default:
443 // Erase to right
444 this.terminal_.eraseToRight();
445 break;
446 }
447 break;
448
449 case 'J': // Erase in display
450 switch (args[0]) {
451 case 1: // Erase above
452 this.terminal_.eraseToLeft();
453 this.terminal_.eraseAbove();
454 break;
455 case 2: // Erase all
456 this.terminal_.clear();
457 break;
458 case 0: // Erase below
459 default:
460 this.terminal_.eraseToRight();
461 this.terminal_.eraseBelow();
462 break;
463 }
464 break;
465
466 case 'X': // Erase character
467 this.terminal_.eraseToRight(args[0] || 1);
468 break;
469
470 case 'L': // Insert lines
471 this.terminal_.insertLines(args[0] || 1);
472 break;
473
474 case 'M': // Delete lines
475 this.terminal_.deleteLines(args[0] || 1);
476 break;
477
478 case '@': // Insert characters
479 var amount = 1;
480 if (args[0]) {
481 amount = args[0];
482 }
483 this.terminal_.insertSpace(amount);
484 break;
485
486 case 'P': // Delete characters
487 // This command shifts the line contents left, starting at the cursor
488 // position.
489 this.terminal_.deleteChars(args[0] || 1);
490 break;
491
492 case 'S': // Scroll up an amount
493 this.terminal_.vtScrollUp(args[0] || 1);
494 break;
495
496 case 'T': // Scroll down an amount
497 this.terminal_.vtScrollDown(args[0] || 1);
498 break;
499
500 case 'c': // Send device attributes
501 if (!args[0]) {
502 this.pendingResponse_ += '\x1b[?1;2c';
503 }
504 break;
505
506 case 'd': // Line position absolute
507 this.terminal_.setCursorRow((args[0] - 1) || 0);
508 break;
509
510 case 'g': // Clear tab stops
511 switch (args[0] || 0) {
512 case 0:
513 this.terminal_.setTabStopAtCursor(false);
514 break;
515 case 3: // Clear all tab stops in the page
516 this.terminal_.clearTabStops();
517 break;
518 default:
519 break;
520 }
521 break;
522
523 case 'm': // Color change
524 if (args.length == 0) {
525 this.terminal_.clearColorAndAttributes();
526 } else {
527 if (args.length == 3 &&
528 (args[0] == 38 || args[0] == 48) && args[1] == 5) {
529 // This is code for the 256-color palette, skip the normal processing.
530 if (args[0] == 38) {
531 // Set the foreground color to the 3rd argument.
532 this.terminal_.setForegroundColor256(args[2]);
533 } else if (args[0] == 48) {
534 // Set the background color to the 3rd argument.
535 this.terminal_.setBackgroundColor256(args[2]);
536 }
537 } else {
538 var numArgs = args.length;
539 for (var argNum = 0; argNum < numArgs; ++argNum) {
540 var arg = args[argNum];
541 if (isNaN(arg)) {
542 // This is the same as an attribute of zero.
543 this.terminal_.setAttributes(0);
544 } else if (arg < 30) {
545 // This is an attribute argument.
546 this.terminal_.setAttributes(arg);
547 } else if (arg < 40) {
548 // This is a foreground color argument.
549 this.terminal_.setForegroundColor(arg);
550 } else if (arg < 50) {
551 // This is a background color argument.
552 this.terminal_.setBackgroundColor(arg);
553 }
554 }
555 }
556 }
557 break;
558
559 case 'n': // Device status report
560 switch (args[0]) {
561 case 5:
562 if (!query) {
563 var response = '\x1b0n';
564 this.pendingResponse_ += response;
565 }
566 break;
567
568 case 6:
569 var curX = this.terminal_.getCursorColumn() + 1;
570 var curY = this.terminal_.getCursorRow() + 1;
571 var response = '\x1b[' + curY + ';' + curX + 'R';
572 this.pendingResponse_ += response;
573 break;
574 }
575 break;
576
577 case 'l': // Reset mode
578 case 'h': // Set mode
579 var set = (seqCommand == 'h' ? true : false);
580 if (query) {
581 switch (args[0]) {
582 case 1: // Normal (l) or application (h) cursor keys
583 this.applicationCursor = set;
584 break;
585 case 3: // 80 (if l) or 132 (if h) column mode
586 // Our size is always determined by the window size, so we ignore
587 // attempts to resize from remote end.
588 break;
589 case 4: // Fast (l) or slow (h) scroll
590 // This is meaningless to us.
591 break;
592 case 5: // Normal (l) or reverse (h) video mode
593 this.terminal_.setReverseVideo(set);
594 break;
595 case 6: // Normal (l) or origin (h) cursor mode
596 this.terminal_.setOriginMode(set);
597 break;
598 case 7: // No (l) wraparound mode or wraparound (h) mode
599 this.terminal_.setWraparound(set);
600 break;
601 case 12: // Stop (l) or start (h) blinking cursor
602 this.terminal_.setCursorBlink(set);
603 break;
604 case 25: // Hide (l) or show (h) cursor
605 this.terminal_.setCursorVisible(set);
606 break;
607 case 45: // Disable (l) or enable (h) reverse wraparound
608 this.terminal_.setReverseWraparound(set);
609 break;
610 case 67: // Backspace is delete (h) or backspace (l)
611 this.backspaceSendsBackspace = set;
612 break;
613 case 1036: // Meta sends (h) or doesn't send (l) escape
614 this.metaSendsEscape = set;
615 break;
616 case 1039: // Alt sends (h) or doesn't send (l) escape
617 this.altSendsEscape = set;
618 break;
619 case 1049: // Switch to/from alternate, save/restore cursor
620 this.terminal_.setAlternateMode(set);
621 break;
622 default:
623 console.log('Unimplemented l/h command: ' +
624 (query ? '?' : '') + args[0]);
625 break;
626 }
627
628 } else {
629 switch (args[0]) {
630 case 4: // Replace (l) or insert (h) mode
631 this.terminal_.setInsertMode(set);
632 break;
633 case 20:
634 // Normal linefeed (l), \n means move down only
635 // Automatic linefeed (h), \n means \n\r
636 this.terminal_.setAutoLinefeed(set);
637 break;
638 default:
639 console.log('Unimplemented l/h command: ' +
640 (query ? '?' : '') + args[0]);
641 break;
642 }
643 }
644 break;
645
646 case 'r':
647 if (query) {
648 // Restore DEC private mode values
649 // TODO(maccarro): Implement this
650 } else {
651 // Set scroll region
652 var scrollTop = args[0] || null;
653 var scrollBottom = args[1] || null;
654 this.terminal_.setScrollRegion(scrollTop, scrollBottom);
655 }
656 break;
657
658 default:
659 console.log('Unknown control: ' + seqCommand);
660 break;
661 }
662 return processed;
663 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698