blob: 5fe3536cb5d315a787b25bc539385b9e3fe9ee82 [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Constructor for the Terminal class.
7 *
8 * A Terminal pulls together the hterm.ScrollPort, hterm.Screen and hterm.VT100
9 * classes to provide the complete terminal functionality.
10 *
11 * There are a number of lower-level Terminal methods that can be called
12 * directly to manipulate the cursor, text, scroll region, and other terminal
13 * attributes. However, the primary method is interpret(), which parses VT
14 * escape sequences and invokes the appropriate Terminal methods.
15 *
16 * This class was heavily influenced by Cory Maccarrone's Framebuffer class.
17 *
18 * TODO(rginda): Eventually we're going to need to support characters which are
19 * displayed twice as wide as standard latin characters. This is to support
20 * CJK (and possibly other character sets).
21 */
rginda87b86462011-12-14 13:48:03 -080022hterm.Terminal = function(fontSize, opt_lineHeight) {
rginda8ba33642011-12-14 12:31:31 -080023 // Two screen instances.
24 this.primaryScreen_ = new hterm.Screen();
25 this.alternateScreen_ = new hterm.Screen();
26
27 // The "current" screen.
28 this.screen_ = this.primaryScreen_;
29
rginda8ba33642011-12-14 12:31:31 -080030 // The local notion of the screen size. ScreenBuffers also have a size which
31 // indicates their present size. During size changes, the two may disagree.
32 // Also, the inactive screen's size is not altered until it is made the active
33 // screen.
34 this.screenSize = new hterm.Size(0, 0);
35
36 // The pixel dimensions of a single character on the screen.
37 this.characterSize_ = new hterm.Size(0, 0);
38
39 // The scroll port we'll be using to display the visible rows.
rginda87b86462011-12-14 13:48:03 -080040 this.scrollPort_ = new hterm.ScrollPort(this, fontSize, opt_lineHeight);
rginda8ba33642011-12-14 12:31:31 -080041 this.scrollPort_.subscribe('resize', this.onResize_.bind(this));
42 this.scrollPort_.subscribe('scroll', this.onScroll_.bind(this));
43
rginda87b86462011-12-14 13:48:03 -080044 // The div that contains this terminal.
45 this.div_ = null;
46
rgindac9bc5502012-01-18 11:48:44 -080047 // The document that contains the scrollPort. Defaulted to the global
48 // document here so that the terminal is functional even if it hasn't been
49 // inserted into a document yet, but re-set in decorate().
50 this.document_ = window.document;
rginda87b86462011-12-14 13:48:03 -080051
rginda8ba33642011-12-14 12:31:31 -080052 // The rows that have scrolled off screen and are no longer addressable.
53 this.scrollbackRows_ = [];
54
rgindac9bc5502012-01-18 11:48:44 -080055 // Saved tab stops.
56 this.tabStops_ = [];
57
rginda8ba33642011-12-14 12:31:31 -080058 // The VT's notion of the top and bottom rows. Used during some VT
59 // cursor positioning and scrolling commands.
60 this.vtScrollTop_ = null;
61 this.vtScrollBottom_ = null;
62
63 // The DIV element for the visible cursor.
64 this.cursorNode_ = null;
65
66 // The default colors for text with no other color attributes.
67 this.backgroundColor = 'black';
68 this.foregroundColor = 'white';
69
rgindac9bc5502012-01-18 11:48:44 -080070 // Default tab with of 8 to match xterm.
71 this.tabWidth = 8;
72
rginda8ba33642011-12-14 12:31:31 -080073 // The color of the cursor.
74 this.cursorColor = 'rgba(255,0,0,0.5)';
75
rginda87b86462011-12-14 13:48:03 -080076 // If true, scroll to the bottom on any keystroke.
77 this.scrollOnKeystroke = true;
78
rginda0f5c0292012-01-13 11:00:13 -080079 // If true, scroll to the bottom on terminal output.
80 this.scrollOnOutput = false;
81
rginda6d397402012-01-17 10:58:29 -080082 // Cursor position and attributes saved with DECSC.
83 this.savedOptions_ = {};
84
rginda8ba33642011-12-14 12:31:31 -080085 // The current mode bits for the terminal.
86 this.options_ = new hterm.Options();
87
88 // Timeouts we might need to clear.
89 this.timeouts_ = {};
rginda87b86462011-12-14 13:48:03 -080090
91 // The VT escape sequence interpreter.
rginda0f5c0292012-01-13 11:00:13 -080092 this.vt = new hterm.VT(this);
rginda87b86462011-12-14 13:48:03 -080093
94 // General IO interface that can be given to third parties without exposing
95 // the entire terminal object.
96 this.io = new hterm.Terminal.IO(this);
rgindac9bc5502012-01-18 11:48:44 -080097
98 this.realizeWidth_(80);
99 this.realizeHeight_(24);
100 this.setDefaultTabStops();
rginda87b86462011-12-14 13:48:03 -0800101};
102
103/**
104 * Create a new instance of a terminal command and run it with a given
105 * argument string.
106 *
107 * @param {function} commandClass The constructor for a terminal command.
108 * @param {string} argString The argument string to pass to the command.
109 */
110hterm.Terminal.prototype.runCommandClass = function(commandClass, argString) {
111 var self = this;
112 this.command = new commandClass(
113 { argString: argString || '',
114 io: this.io.push(),
115 onExit: function(code) {
116 self.io.pop();
117 self.io.println(hterm.msg('COMMAND_COMPLETE',
118 [self.command.commandName, code]));
119 }
120 });
121
122 this.command.run();
123};
124
125/**
126 * Return a copy of the current cursor position.
127 *
128 * @return {hterm.RowCol} The RowCol object representing the current position.
129 */
130hterm.Terminal.prototype.saveCursor = function() {
131 return this.screen_.cursorPosition.clone();
132};
133
134/**
135 * Restore a previously saved cursor position.
136 *
137 * @param {hterm.RowCol} cursor The position to restore.
138 */
139hterm.Terminal.prototype.restoreCursor = function(cursor) {
140 this.screen_.setCursorPosition(cursor.row, cursor.column);
rginda2312fff2012-01-05 16:20:52 -0800141 this.screen_.cursorPosition.overflow = cursor.overflow;
rginda87b86462011-12-14 13:48:03 -0800142};
143
144/**
145 * Set the width of the terminal, resizing the UI to match.
146 */
147hterm.Terminal.prototype.setWidth = function(columnCount) {
148 this.div_.style.width = this.characterSize_.width * columnCount + 16 + 'px'
rgindac9bc5502012-01-18 11:48:44 -0800149 this.realizeWidth_(columnCount);
150 this.scheduleSyncCursorPosition_();
151};
rginda87b86462011-12-14 13:48:03 -0800152
rgindac9bc5502012-01-18 11:48:44 -0800153/**
154 * Deal with terminal width changes.
155 *
156 * This function does what needs to be done when the terminal width changes
157 * out from under us. It happens here rather than in onResize_() because this
158 * code may need to run synchronously to handle programmatic changes of
159 * terminal width.
160 *
161 * Relying on the browser to send us an async resize event means we may not be
162 * in the correct state yet when the next escape sequence hits.
163 */
164hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
165 var deltaColumns = columnCount - this.screen_.getWidth();
166
rginda87b86462011-12-14 13:48:03 -0800167 this.screenSize.width = columnCount;
168 this.screen_.setColumnCount(columnCount);
rgindac9bc5502012-01-18 11:48:44 -0800169
170 if (deltaColumns > 0) {
171 this.setDefaultTabStops(this.screenSize.width - deltaColumns);
172 } else {
173 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
174 if (this.tabStops_[i] <= columnCount)
175 break;
176
177 this.tabStops_.pop();
178 }
179 }
180
181 this.screen_.setColumnCount(this.screenSize.width);
182};
183
184/**
185 * Deal with terminal height changes.
186 *
187 * This function does what needs to be done when the terminal height changes
188 * out from under us. It happens here rather than in onResize_() because this
189 * code may need to run synchronously to handle programmatic changes of
190 * terminal height.
191 *
192 * Relying on the browser to send us an async resize event means we may not be
193 * in the correct state yet when the next escape sequence hits.
194 */
195hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
196 var deltaRows = rowCount - this.screen_.getHeight();
197
198 this.screenSize.height = rowCount;
199
200 var cursor = this.saveCursor();
201
202 if (deltaRows < 0) {
203 // Screen got smaller.
204 deltaRows *= -1;
205 while (deltaRows) {
206 var lastRow = this.getRowCount() - 1;
207 if (lastRow - this.scrollbackRows_.length == cursor.row)
208 break;
209
210 if (this.getRowText(lastRow))
211 break;
212
213 this.screen_.popRow();
214 deltaRows--;
215 }
216
217 var ary = this.screen_.shiftRows(deltaRows);
218 this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
219
220 // We just removed rows from the top of the screen, we need to update
221 // the cursor to match.
222 cursor.row -= deltaRows;
223
224 } else if (deltaRows > 0) {
225 // Screen got larger.
226
227 if (deltaRows <= this.scrollbackRows_.length) {
228 var scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
229 var rows = this.scrollbackRows_.splice(
230 this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
231 this.screen_.unshiftRows(rows);
232 deltaRows -= scrollbackCount;
233 cursor.row += scrollbackCount;
234 }
235
236 if (deltaRows)
237 this.appendRows_(deltaRows);
238 }
239
240 this.restoreCursor(cursor);
rginda87b86462011-12-14 13:48:03 -0800241};
242
243/**
244 * Scroll the terminal to the top of the scrollback buffer.
245 */
246hterm.Terminal.prototype.scrollHome = function() {
247 this.scrollPort_.scrollRowToTop(0);
248};
249
250/**
251 * Scroll the terminal to the end.
252 */
253hterm.Terminal.prototype.scrollEnd = function() {
254 this.scrollPort_.scrollRowToBottom(this.getRowCount());
255};
256
257/**
258 * Scroll the terminal one page up (minus one line) relative to the current
259 * position.
260 */
261hterm.Terminal.prototype.scrollPageUp = function() {
262 var i = this.scrollPort_.getTopRowIndex();
263 this.scrollPort_.scrollRowToTop(i - this.screenSize.height + 1);
264};
265
266/**
267 * Scroll the terminal one page down (minus one line) relative to the current
268 * position.
269 */
270hterm.Terminal.prototype.scrollPageDown = function() {
271 var i = this.scrollPort_.getTopRowIndex();
272 this.scrollPort_.scrollRowToTop(i + this.screenSize.height - 1);
rginda8ba33642011-12-14 12:31:31 -0800273};
274
rgindac9bc5502012-01-18 11:48:44 -0800275/**
276 * Full terminal reset.
277 */
rginda87b86462011-12-14 13:48:03 -0800278hterm.Terminal.prototype.reset = function() {
rgindac9bc5502012-01-18 11:48:44 -0800279 this.clearAllTabStops();
280 this.setDefaultTabStops();
281 this.clearColorAndAttributes();
282 this.setVTScrollRegion(null, null);
283 this.clear();
284 this.setAbsoluteCursorPosition(0, 0);
285 this.softReset();
rginda87b86462011-12-14 13:48:03 -0800286};
287
rgindac9bc5502012-01-18 11:48:44 -0800288/**
289 * Soft terminal reset.
290 */
rginda0f5c0292012-01-13 11:00:13 -0800291hterm.Terminal.prototype.softReset = function() {
rgindac9bc5502012-01-18 11:48:44 -0800292 this.options_ = new hterm.Options();
rginda0f5c0292012-01-13 11:00:13 -0800293};
294
rginda87b86462011-12-14 13:48:03 -0800295hterm.Terminal.prototype.clearColorAndAttributes = function() {
296 //console.log('clearColorAndAttributes');
297};
298
299hterm.Terminal.prototype.setForegroundColor256 = function() {
300 console.log('setForegroundColor256');
301};
302
303hterm.Terminal.prototype.setBackgroundColor256 = function() {
304 console.log('setBackgroundColor256');
305};
306
307hterm.Terminal.prototype.setForegroundColor = function() {
308 //console.log('setForegroundColor');
309};
310
311hterm.Terminal.prototype.setBackgroundColor = function() {
312 //console.log('setBackgroundColor');
313};
314
315hterm.Terminal.prototype.setAttributes = function() {
316 //console.log('setAttributes');
317};
318
319hterm.Terminal.prototype.resize = function() {
320 console.log('resize');
321};
322
323hterm.Terminal.prototype.setSpecialCharsEnabled = function() {
324 //console.log('setSpecialCharactersEnabled');
325};
326
rgindac9bc5502012-01-18 11:48:44 -0800327/**
328 * Move the cursor forward to the next tab stop, or to the last column
329 * if no more tab stops are set.
330 */
331hterm.Terminal.prototype.forwardTabStop = function() {
332 var column = this.screen_.cursorPosition.column;
333
334 for (var i = 0; i < this.tabStops_.length; i++) {
335 if (this.tabStops_[i] > column) {
336 this.setCursorColumn(this.tabStops_[i]);
337 return;
338 }
339 }
340
341 this.setCursorColumn(this.screenSize.width - 1);
rginda0f5c0292012-01-13 11:00:13 -0800342};
343
rgindac9bc5502012-01-18 11:48:44 -0800344/**
345 * Move the cursor backward to the previous tab stop, or to the first column
346 * if no previous tab stops are set.
347 */
348hterm.Terminal.prototype.backwardTabStop = function() {
349 var column = this.screen_.cursorPosition.column;
350
351 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
352 if (this.tabStops_[i] < column) {
353 this.setCursorColumn(this.tabStops_[i]);
354 return;
355 }
356 }
357
358 this.setCursorColumn(1);
rginda0f5c0292012-01-13 11:00:13 -0800359};
360
rgindac9bc5502012-01-18 11:48:44 -0800361/**
362 * Set a tab stop at the given column.
363 *
364 * @param {int} column Zero based column.
365 */
366hterm.Terminal.prototype.setTabStop = function(column) {
367 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
368 if (this.tabStops_[i] == column)
369 return;
370
371 if (this.tabStops_[i] < column) {
372 this.tabStops_.splice(i + 1, 0, column);
373 return;
374 }
375 }
376
377 this.tabStops_.splice(0, 0, column);
rginda87b86462011-12-14 13:48:03 -0800378};
379
rgindac9bc5502012-01-18 11:48:44 -0800380/**
381 * Clear the tab stop at the current cursor position.
382 *
383 * No effect if there is no tab stop at the current cursor position.
384 */
385hterm.Terminal.prototype.clearTabStopAtCursor = function() {
386 var column = this.screen_.cursorPosition.column;
387
388 var i = this.tabStops_.indexOf(column);
389 if (i == -1)
390 return;
391
392 this.tabStops_.splice(i, 1);
393};
394
395/**
396 * Clear all tab stops.
397 */
398hterm.Terminal.prototype.clearAllTabStops = function() {
399 this.tabStops_.length = 0;
400};
401
402/**
403 * Set up the default tab stops, starting from a given column.
404 *
405 * This sets a tabstop every (column % this.tabWidth) column, starting
406 * from the specified column, or 0 if no column is provided.
407 *
408 * This does not clear the existing tab stops first, use clearAllTabStops
409 * for that.
410 *
411 * @param {int} opt_start Optional starting zero based starting column, useful
412 * for filling out missing tab stops when the terminal is resized.
413 */
414hterm.Terminal.prototype.setDefaultTabStops = function(opt_start) {
415 var start = opt_start || 0;
416 var w = this.tabWidth;
417 var stopCount = Math.floor((this.screenSize.width - start) / this.tabWidth)
418 for (var i = 0; i < stopCount; i++) {
419 this.setTabStop(Math.floor((start + i * w) / w) * w + w);
420 }
rginda87b86462011-12-14 13:48:03 -0800421};
422
rginda6d397402012-01-17 10:58:29 -0800423/**
424 * Save cursor position and attributes.
425 *
426 * TODO(rginda): Save attributes once we support them.
427 */
rginda87b86462011-12-14 13:48:03 -0800428hterm.Terminal.prototype.saveOptions = function() {
rginda6d397402012-01-17 10:58:29 -0800429 this.savedOptions_.cursor = this.saveCursor();
rginda87b86462011-12-14 13:48:03 -0800430};
431
rginda6d397402012-01-17 10:58:29 -0800432/**
433 * Restore cursor position and attributes.
434 *
435 * TODO(rginda): Restore attributes once we support them.
436 */
rginda87b86462011-12-14 13:48:03 -0800437hterm.Terminal.prototype.restoreOptions = function() {
rginda6d397402012-01-17 10:58:29 -0800438 if (this.savedOptions_.cursor)
439 this.restoreCursor(this.savedOptions_.cursor);
rginda8ba33642011-12-14 12:31:31 -0800440};
441
442/**
443 * Interpret a sequence of characters.
444 *
445 * Incomplete escape sequences are buffered until the next call.
446 *
447 * @param {string} str Sequence of characters to interpret or pass through.
448 */
449hterm.Terminal.prototype.interpret = function(str) {
rginda0f5c0292012-01-13 11:00:13 -0800450 this.vt.interpret(str);
rginda8ba33642011-12-14 12:31:31 -0800451 this.scheduleSyncCursorPosition_();
452};
453
454/**
455 * Take over the given DIV for use as the terminal display.
456 *
457 * @param {HTMLDivElement} div The div to use as the terminal display.
458 */
459hterm.Terminal.prototype.decorate = function(div) {
rginda87b86462011-12-14 13:48:03 -0800460 this.div_ = div;
461
rginda8ba33642011-12-14 12:31:31 -0800462 this.scrollPort_.decorate(div);
463 this.document_ = this.scrollPort_.getDocument();
464
465 // Get character dimensions from the scrollPort.
466 this.characterSize_.height = this.scrollPort_.getRowHeight();
467 this.characterSize_.width = this.scrollPort_.getCharacterWidth();
468
469 this.cursorNode_ = this.document_.createElement('div');
470 this.cursorNode_.style.cssText =
471 ('position: absolute;' +
rginda87b86462011-12-14 13:48:03 -0800472 'top: -99px;' +
473 'display: block;' +
rginda8ba33642011-12-14 12:31:31 -0800474 'width: ' + this.characterSize_.width + 'px;' +
475 'height: ' + this.characterSize_.height + 'px;' +
rginda6d397402012-01-17 10:58:29 -0800476 '-webkit-transition: opacity, background-color 100ms linear;' +
rginda8ba33642011-12-14 12:31:31 -0800477 'background-color: ' + this.cursorColor);
478 this.document_.body.appendChild(this.cursorNode_);
479
480 this.setReverseVideo(false);
rginda87b86462011-12-14 13:48:03 -0800481
482 this.vt.keyboard.installKeyboard(this.document_.body.firstChild);
483
484 var link = this.document_.createElement('link');
485 link.setAttribute('href', '../css/dialogs.css');
486 link.setAttribute('rel', 'stylesheet');
487 this.document_.head.appendChild(link);
488
489 this.alertDialog = new AlertDialog(this.document_.body);
490 this.promptDialog = new PromptDialog(this.document_.body);
491 this.confirmDialog = new ConfirmDialog(this.document_.body);
492
493 this.scrollPort_.focus();
rginda6d397402012-01-17 10:58:29 -0800494 this.scrollPort_.scheduleRedraw();
rginda87b86462011-12-14 13:48:03 -0800495};
496
497hterm.Terminal.prototype.getDocument = function() {
498 return this.document_;
rginda8ba33642011-12-14 12:31:31 -0800499};
500
501/**
502 * Return the HTML Element for a given row index.
503 *
504 * This is a method from the RowProvider interface. The ScrollPort uses
505 * it to fetch rows on demand as they are scrolled into view.
506 *
507 * TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
508 * pairs to conserve memory.
509 *
510 * @param {integer} index The zero-based row index, measured relative to the
511 * start of the scrollback buffer. On-screen rows will always have the
512 * largest indicies.
513 * @return {HTMLElement} The 'x-row' element containing for the requested row.
514 */
515hterm.Terminal.prototype.getRowNode = function(index) {
516 if (index < this.scrollbackRows_.length)
517 return this.scrollbackRows_[index];
518
519 var screenIndex = index - this.scrollbackRows_.length;
520 return this.screen_.rowsArray[screenIndex];
521};
522
523/**
524 * Return the text content for a given range of rows.
525 *
526 * This is a method from the RowProvider interface. The ScrollPort uses
527 * it to fetch text content on demand when the user attempts to copy their
528 * selection to the clipboard.
529 *
530 * @param {integer} start The zero-based row index to start from, measured
531 * relative to the start of the scrollback buffer. On-screen rows will
532 * always have the largest indicies.
533 * @param {integer} end The zero-based row index to end on, measured
534 * relative to the start of the scrollback buffer.
535 * @return {string} A single string containing the text value of the range of
536 * rows. Lines will be newline delimited, with no trailing newline.
537 */
538hterm.Terminal.prototype.getRowsText = function(start, end) {
539 var ary = [];
540 for (var i = start; i < end; i++) {
541 var node = this.getRowNode(i);
542 ary.push(node.textContent);
543 }
544
545 return ary.join('\n');
546};
547
548/**
549 * Return the text content for a given row.
550 *
551 * This is a method from the RowProvider interface. The ScrollPort uses
552 * it to fetch text content on demand when the user attempts to copy their
553 * selection to the clipboard.
554 *
555 * @param {integer} index The zero-based row index to return, measured
556 * relative to the start of the scrollback buffer. On-screen rows will
557 * always have the largest indicies.
558 * @return {string} A string containing the text value of the selected row.
559 */
560hterm.Terminal.prototype.getRowText = function(index) {
561 var node = this.getRowNode(index);
rginda87b86462011-12-14 13:48:03 -0800562 return node.textContent;
rginda8ba33642011-12-14 12:31:31 -0800563};
564
565/**
566 * Return the total number of rows in the addressable screen and in the
567 * scrollback buffer of this terminal.
568 *
569 * This is a method from the RowProvider interface. The ScrollPort uses
570 * it to compute the size of the scrollbar.
571 *
572 * @return {integer} The number of rows in this terminal.
573 */
574hterm.Terminal.prototype.getRowCount = function() {
575 return this.scrollbackRows_.length + this.screen_.rowsArray.length;
576};
577
578/**
579 * Create DOM nodes for new rows and append them to the end of the terminal.
580 *
581 * This is the only correct way to add a new DOM node for a row. Notice that
582 * the new row is appended to the bottom of the list of rows, and does not
583 * require renumbering (of the rowIndex property) of previous rows.
584 *
585 * If you think you want a new blank row somewhere in the middle of the
586 * terminal, look into moveRows_().
587 *
588 * This method does not pay attention to vtScrollTop/Bottom, since you should
589 * be using moveRows() in cases where they would matter.
590 *
591 * The cursor will be positioned at column 0 of the first inserted line.
592 */
593hterm.Terminal.prototype.appendRows_ = function(count) {
594 var cursorRow = this.screen_.rowsArray.length;
595 var offset = this.scrollbackRows_.length + cursorRow;
596 for (var i = 0; i < count; i++) {
597 var row = this.document_.createElement('x-row');
598 row.appendChild(this.document_.createTextNode(''));
599 row.rowIndex = offset + i;
600 this.screen_.pushRow(row);
601 }
602
603 var extraRows = this.screen_.rowsArray.length - this.screenSize.height;
604 if (extraRows > 0) {
605 var ary = this.screen_.shiftRows(extraRows);
606 Array.prototype.push.apply(this.scrollbackRows_, ary);
607 this.scheduleScrollDown_();
608 }
609
610 if (cursorRow >= this.screen_.rowsArray.length)
611 cursorRow = this.screen_.rowsArray.length - 1;
612
rginda87b86462011-12-14 13:48:03 -0800613 this.setAbsoluteCursorPosition(cursorRow, 0);
rginda8ba33642011-12-14 12:31:31 -0800614};
615
616/**
617 * Relocate rows from one part of the addressable screen to another.
618 *
619 * This is used to recycle rows during VT scrolls (those which are driven
620 * by VT commands, rather than by the user manipulating the scrollbar.)
621 *
622 * In this case, the blank lines scrolled into the scroll region are made of
623 * the nodes we scrolled off. These have their rowIndex properties carefully
624 * renumbered so as not to confuse the ScrollPort.
625 *
626 * TODO(rginda): I'm not sure why this doesn't require a scrollport repaint.
627 * It may just be luck. I wouldn't be surprised if we actually needed to call
628 * scrollPort_.invalidateRowRange, but I'm going to wait for evidence before
629 * adding it.
630 */
631hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
632 var ary = this.screen_.removeRows(fromIndex, count);
633 this.screen_.insertRows(toIndex, ary);
634
635 var start, end;
636 if (fromIndex < toIndex) {
637 start = fromIndex;
rginda87b86462011-12-14 13:48:03 -0800638 end = toIndex + count;
rginda8ba33642011-12-14 12:31:31 -0800639 } else {
640 start = toIndex;
rginda87b86462011-12-14 13:48:03 -0800641 end = fromIndex + count;
rginda8ba33642011-12-14 12:31:31 -0800642 }
643
644 this.renumberRows_(start, end);
rginda2312fff2012-01-05 16:20:52 -0800645 this.scrollPort_.scheduleInvalidate();
rginda8ba33642011-12-14 12:31:31 -0800646};
647
648/**
649 * Renumber the rowIndex property of the given range of rows.
650 *
651 * The start and end indicies are relative to the screen, not the scrollback.
652 * Rows in the scrollback buffer cannot be renumbered. Since they are not
rginda2312fff2012-01-05 16:20:52 -0800653 * addressable (you can't delete them, scroll them, etc), you should have
rginda8ba33642011-12-14 12:31:31 -0800654 * no need to renumber scrollback rows.
655 */
656hterm.Terminal.prototype.renumberRows_ = function(start, end) {
657 var offset = this.scrollbackRows_.length;
658 for (var i = start; i < end; i++) {
659 this.screen_.rowsArray[i].rowIndex = offset + i;
660 }
661};
662
663/**
664 * Print a string to the terminal.
665 *
666 * This respects the current insert and wraparound modes. It will add new lines
667 * to the end of the terminal, scrolling off the top into the scrollback buffer
668 * if necessary.
669 *
670 * The string is *not* parsed for escape codes. Use the interpret() method if
671 * that's what you're after.
672 *
673 * @param{string} str The string to print.
674 */
675hterm.Terminal.prototype.print = function(str) {
676 do {
rginda2312fff2012-01-05 16:20:52 -0800677 if (this.options_.wraparound && this.screen_.cursorPosition.overflow)
678 this.newLine();
679
rginda8ba33642011-12-14 12:31:31 -0800680 if (this.options_.insertMode) {
681 str = this.screen_.insertString(str);
682 } else {
683 str = this.screen_.overwriteString(str);
684 }
rginda2312fff2012-01-05 16:20:52 -0800685 } while (this.options_.wraparound && str);
rginda8ba33642011-12-14 12:31:31 -0800686
687 this.scheduleSyncCursorPosition_();
rginda0f5c0292012-01-13 11:00:13 -0800688
689 if (this.scrollOnOutput)
690 this.scrollPort_.scrollRowToBottom(this.getRowCount());
rginda8ba33642011-12-14 12:31:31 -0800691};
692
693/**
rginda87b86462011-12-14 13:48:03 -0800694 * Set the VT scroll region.
695 *
rginda87b86462011-12-14 13:48:03 -0800696 * This also resets the cursor position to the absolute (0, 0) position, since
697 * that's what xterm appears to do.
698 *
699 * @param {integer} scrollTop The zero-based top of the scroll region.
700 * @param {integer} scrollBottom The zero-based bottom of the scroll region,
701 * inclusive.
702 */
703hterm.Terminal.prototype.setVTScrollRegion = function(scrollTop, scrollBottom) {
704 this.vtScrollTop_ = scrollTop;
705 this.vtScrollBottom_ = scrollBottom;
706 this.setAbsoluteCursorPosition(0, 0);
707};
708
709/**
rginda8ba33642011-12-14 12:31:31 -0800710 * Return the top row index according to the VT.
711 *
712 * This will return 0 unless the terminal has been told to restrict scrolling
713 * to some lower row. It is used for some VT cursor positioning and scrolling
714 * commands.
715 *
716 * @return {integer} The topmost row in the terminal's scroll region.
717 */
718hterm.Terminal.prototype.getVTScrollTop = function() {
719 if (this.vtScrollTop_ != null)
720 return this.vtScrollTop_;
721
722 return 0;
rginda87b86462011-12-14 13:48:03 -0800723};
rginda8ba33642011-12-14 12:31:31 -0800724
725/**
726 * Return the bottom row index according to the VT.
727 *
728 * This will return the height of the terminal unless the it has been told to
729 * restrict scrolling to some higher row. It is used for some VT cursor
730 * positioning and scrolling commands.
731 *
732 * @return {integer} The bottommost row in the terminal's scroll region.
733 */
734hterm.Terminal.prototype.getVTScrollBottom = function() {
735 if (this.vtScrollBottom_ != null)
736 return this.vtScrollBottom_;
737
rginda87b86462011-12-14 13:48:03 -0800738 return this.screenSize.height - 1;
rginda8ba33642011-12-14 12:31:31 -0800739}
740
741/**
742 * Process a '\n' character.
743 *
744 * If the cursor is on the final row of the terminal this will append a new
745 * blank row to the screen and scroll the topmost row into the scrollback
746 * buffer.
747 *
748 * Otherwise, this moves the cursor to column zero of the next row.
749 */
750hterm.Terminal.prototype.newLine = function() {
751 if (this.screen_.cursorPosition.row == this.screen_.rowsArray.length - 1) {
rginda87b86462011-12-14 13:48:03 -0800752 // If we're at the end of the screen we need to append a new line and
753 // scroll the top line into the scrollback buffer.
rginda8ba33642011-12-14 12:31:31 -0800754 this.appendRows_(1);
rginda87b86462011-12-14 13:48:03 -0800755 } else if (this.screen_.cursorPosition.row == this.getVTScrollBottom()) {
756 // End of the scroll region does not affect the scrollback buffer.
757 this.vtScrollUp(1);
758 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
rginda8ba33642011-12-14 12:31:31 -0800759 } else {
rginda87b86462011-12-14 13:48:03 -0800760 // Anywhere else in the screen just moves the cursor.
761 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
rginda8ba33642011-12-14 12:31:31 -0800762 }
763};
764
765/**
766 * Like newLine(), except maintain the cursor column.
767 */
768hterm.Terminal.prototype.lineFeed = function() {
769 var column = this.screen_.cursorPosition.column;
770 this.newLine();
771 this.setCursorColumn(column);
772};
773
774/**
rginda87b86462011-12-14 13:48:03 -0800775 * If autoCarriageReturn is set then newLine(), else lineFeed().
776 */
777hterm.Terminal.prototype.formFeed = function() {
778 if (this.options_.autoCarriageReturn) {
779 this.newLine();
780 } else {
781 this.lineFeed();
782 }
783};
784
785/**
786 * Move the cursor up one row, possibly inserting a blank line.
787 *
788 * The cursor column is not changed.
789 */
790hterm.Terminal.prototype.reverseLineFeed = function() {
791 var scrollTop = this.getVTScrollTop();
792 var currentRow = this.screen_.cursorPosition.row;
793
794 if (currentRow == scrollTop) {
795 this.insertLines(1);
796 } else {
797 this.setAbsoluteCursorRow(currentRow - 1);
798 }
799};
800
801/**
rginda8ba33642011-12-14 12:31:31 -0800802 * Replace all characters to the left of the current cursor with the space
803 * character.
804 *
805 * TODO(rginda): This should probably *remove* the characters (not just replace
806 * with a space) if there are no characters at or beyond the current cursor
807 * position. Once it does that, it'll have the same text-attribute related
808 * issues as hterm.Screen.prototype.clearCursorRow :/
809 */
810hterm.Terminal.prototype.eraseToLeft = function() {
rginda87b86462011-12-14 13:48:03 -0800811 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800812 this.setCursorColumn(0);
rginda87b86462011-12-14 13:48:03 -0800813 this.screen_.overwriteString(hterm.getWhitespace(cursor.column + 1));
814 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800815};
816
817/**
818 * Erase a given number of characters to the right of the cursor, shifting
819 * remaining characters to the left.
820 *
821 * The cursor position is unchanged.
822 *
823 * TODO(rginda): Test that this works even when the cursor is positioned beyond
824 * the end of the text.
825 *
826 * TODO(rginda): This likely has text-attribute related troubles similar to the
827 * todo on hterm.Screen.prototype.clearCursorRow.
828 */
829hterm.Terminal.prototype.eraseToRight = function(opt_count) {
rginda87b86462011-12-14 13:48:03 -0800830 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800831
rginda87b86462011-12-14 13:48:03 -0800832 var maxCount = this.screenSize.width - cursor.column;
rginda8ba33642011-12-14 12:31:31 -0800833 var count = (opt_count && opt_count < maxCount) ? opt_count : maxCount;
834 this.screen_.deleteChars(count);
rginda87b86462011-12-14 13:48:03 -0800835 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800836};
837
838/**
839 * Erase the current line.
840 *
841 * The cursor position is unchanged.
842 *
843 * TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
844 * has a text-attribute related TODO.
845 */
846hterm.Terminal.prototype.eraseLine = function() {
rginda87b86462011-12-14 13:48:03 -0800847 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800848 this.screen_.clearCursorRow();
rginda87b86462011-12-14 13:48:03 -0800849 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800850};
851
852/**
853 * Erase all characters from the start of the scroll region to the current
854 * cursor position.
855 *
856 * The cursor position is unchanged.
857 *
858 * TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
859 * has a text-attribute related TODO.
860 */
861hterm.Terminal.prototype.eraseAbove = function() {
rginda87b86462011-12-14 13:48:03 -0800862 var cursor = this.saveCursor();
863
864 this.eraseToLeft();
rginda8ba33642011-12-14 12:31:31 -0800865
866 var top = this.getVTScrollTop();
rginda87b86462011-12-14 13:48:03 -0800867 for (var i = top; i < cursor.row; i++) {
868 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -0800869 this.screen_.clearCursorRow();
870 }
871
rginda87b86462011-12-14 13:48:03 -0800872 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800873};
874
875/**
876 * Erase all characters from the current cursor position to the end of the
877 * scroll region.
878 *
879 * The cursor position is unchanged.
880 *
881 * TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
882 * has a text-attribute related TODO.
883 */
884hterm.Terminal.prototype.eraseBelow = function() {
rginda87b86462011-12-14 13:48:03 -0800885 var cursor = this.saveCursor();
886
887 this.eraseToRight();
rginda8ba33642011-12-14 12:31:31 -0800888
889 var bottom = this.getVTScrollBottom();
rginda87b86462011-12-14 13:48:03 -0800890 for (var i = cursor.row + 1; i <= bottom; i++) {
891 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -0800892 this.screen_.clearCursorRow();
893 }
894
rginda87b86462011-12-14 13:48:03 -0800895 this.restoreCursor(cursor);
896};
897
898/**
899 * Fill the terminal with a given character.
900 *
901 * This methods does not respect the VT scroll region.
902 *
903 * @param {string} ch The character to use for the fill.
904 */
905hterm.Terminal.prototype.fill = function(ch) {
906 var cursor = this.saveCursor();
907
908 this.setAbsoluteCursorPosition(0, 0);
909 for (var row = 0; row < this.screenSize.height; row++) {
910 for (var col = 0; col < this.screenSize.width; col++) {
911 this.setAbsoluteCursorPosition(row, col);
912 this.screen_.overwriteString(ch);
913 }
914 }
915
916 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800917};
918
919/**
920 * Erase the entire scroll region.
921 *
922 * The cursor position is unchanged.
923 *
924 * TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
925 * has a text-attribute related TODO.
926 */
927hterm.Terminal.prototype.clear = function() {
rginda87b86462011-12-14 13:48:03 -0800928 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800929
930 var top = this.getVTScrollTop();
931 var bottom = this.getVTScrollBottom();
932
933 for (var i = top; i < bottom; i++) {
rginda87b86462011-12-14 13:48:03 -0800934 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -0800935 this.screen_.clearCursorRow();
936 }
937
rginda87b86462011-12-14 13:48:03 -0800938 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800939};
940
941/**
942 * VT command to insert lines at the current cursor row.
943 *
944 * This respects the current scroll region. Rows pushed off the bottom are
945 * lost (they won't show up in the scrollback buffer).
946 *
947 * TODO(rginda): This relies on hterm.Screen.prototype.clearCursorRow, which
948 * has a text-attribute related TODO.
949 *
950 * @param {integer} count The number of lines to insert.
951 */
952hterm.Terminal.prototype.insertLines = function(count) {
rginda87b86462011-12-14 13:48:03 -0800953 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800954
955 var bottom = this.getVTScrollBottom();
rginda87b86462011-12-14 13:48:03 -0800956 count = Math.min(count, bottom - cursor.row);
rginda8ba33642011-12-14 12:31:31 -0800957
958 var start = bottom - count;
rginda87b86462011-12-14 13:48:03 -0800959 if (start != cursor.row)
960 this.moveRows_(start, count, cursor.row);
rginda8ba33642011-12-14 12:31:31 -0800961
962 for (var i = 0; i < count; i++) {
rginda87b86462011-12-14 13:48:03 -0800963 this.setAbsoluteCursorPosition(cursor.row + i, 0);
rginda8ba33642011-12-14 12:31:31 -0800964 this.screen_.clearCursorRow();
965 }
966
rginda87b86462011-12-14 13:48:03 -0800967 cursor.column = 0;
968 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800969};
970
971/**
972 * VT command to delete lines at the current cursor row.
973 *
974 * New rows are added to the bottom of scroll region to take their place. New
975 * rows are strictly there to take up space and have no content or style.
976 */
977hterm.Terminal.prototype.deleteLines = function(count) {
rginda87b86462011-12-14 13:48:03 -0800978 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -0800979
rginda87b86462011-12-14 13:48:03 -0800980 var top = cursor.row;
rginda8ba33642011-12-14 12:31:31 -0800981 var bottom = this.getVTScrollBottom();
982
rginda87b86462011-12-14 13:48:03 -0800983 var maxCount = bottom - top + 1;
rginda8ba33642011-12-14 12:31:31 -0800984 count = Math.min(count, maxCount);
985
rginda87b86462011-12-14 13:48:03 -0800986 var moveStart = bottom - count + 1;
rginda8ba33642011-12-14 12:31:31 -0800987 if (count != maxCount)
988 this.moveRows_(top, count, moveStart);
989
990 for (var i = 0; i < count; i++) {
rginda87b86462011-12-14 13:48:03 -0800991 this.setAbsoluteCursorPosition(moveStart + i, 0);
rginda8ba33642011-12-14 12:31:31 -0800992 this.screen_.clearCursorRow();
993 }
994
rginda87b86462011-12-14 13:48:03 -0800995 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -0800996};
997
998/**
999 * Inserts the given number of spaces at the current cursor position.
1000 *
rginda87b86462011-12-14 13:48:03 -08001001 * The cursor position is not changed.
rginda8ba33642011-12-14 12:31:31 -08001002 */
1003hterm.Terminal.prototype.insertSpace = function(count) {
rginda87b86462011-12-14 13:48:03 -08001004 var cursor = this.saveCursor();
1005
rginda0f5c0292012-01-13 11:00:13 -08001006 var ws = hterm.getWhitespace(count || 1);
rginda8ba33642011-12-14 12:31:31 -08001007 this.screen_.insertString(ws);
rginda87b86462011-12-14 13:48:03 -08001008
1009 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08001010};
1011
1012/**
1013 * Forward-delete the specified number of characters starting at the cursor
1014 * position.
1015 *
1016 * @param {integer} count The number of characters to delete.
1017 */
1018hterm.Terminal.prototype.deleteChars = function(count) {
1019 this.screen_.deleteChars(count);
1020};
1021
1022/**
1023 * Shift rows in the scroll region upwards by a given number of lines.
1024 *
1025 * New rows are inserted at the bottom of the scroll region to fill the
1026 * vacated rows. The new rows not filled out with the current text attributes.
1027 *
1028 * This function does not affect the scrollback rows at all. Rows shifted
1029 * off the top are lost.
1030 *
rginda87b86462011-12-14 13:48:03 -08001031 * The cursor position is not altered.
1032 *
rginda8ba33642011-12-14 12:31:31 -08001033 * @param {integer} count The number of rows to scroll.
1034 */
1035hterm.Terminal.prototype.vtScrollUp = function(count) {
rginda87b86462011-12-14 13:48:03 -08001036 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08001037
rginda87b86462011-12-14 13:48:03 -08001038 this.setAbsoluteCursorRow(this.getVTScrollTop());
rginda8ba33642011-12-14 12:31:31 -08001039 this.deleteLines(count);
1040
rginda87b86462011-12-14 13:48:03 -08001041 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08001042};
1043
1044/**
1045 * Shift rows below the cursor down by a given number of lines.
1046 *
1047 * This function respects the current scroll region.
1048 *
1049 * New rows are inserted at the top of the scroll region to fill the
1050 * vacated rows. The new rows not filled out with the current text attributes.
1051 *
1052 * This function does not affect the scrollback rows at all. Rows shifted
1053 * off the bottom are lost.
1054 *
1055 * @param {integer} count The number of rows to scroll.
1056 */
1057hterm.Terminal.prototype.vtScrollDown = function(opt_count) {
rginda87b86462011-12-14 13:48:03 -08001058 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08001059
rginda87b86462011-12-14 13:48:03 -08001060 this.setAbsoluteCursorPosition(this.getVTScrollTop(), 0);
rginda8ba33642011-12-14 12:31:31 -08001061 this.insertLines(opt_count);
1062
rginda87b86462011-12-14 13:48:03 -08001063 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08001064};
1065
rginda87b86462011-12-14 13:48:03 -08001066
rginda8ba33642011-12-14 12:31:31 -08001067/**
1068 * Set the cursor position.
1069 *
1070 * The cursor row is relative to the scroll region if the terminal has
1071 * 'origin mode' enabled, or relative to the addressable screen otherwise.
1072 *
1073 * @param {integer} row The new zero-based cursor row.
1074 * @param {integer} row The new zero-based cursor column.
1075 */
1076hterm.Terminal.prototype.setCursorPosition = function(row, column) {
1077 if (this.options_.originMode) {
rginda87b86462011-12-14 13:48:03 -08001078 this.setRelativeCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08001079 } else {
rginda87b86462011-12-14 13:48:03 -08001080 this.setAbsoluteCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08001081 }
rginda87b86462011-12-14 13:48:03 -08001082};
rginda8ba33642011-12-14 12:31:31 -08001083
rginda87b86462011-12-14 13:48:03 -08001084hterm.Terminal.prototype.setRelativeCursorPosition = function(row, column) {
1085 var scrollTop = this.getVTScrollTop();
1086 row = hterm.clamp(row + scrollTop, scrollTop, this.getVTScrollBottom());
rginda2312fff2012-01-05 16:20:52 -08001087 column = hterm.clamp(column, 0, this.screenSize.width - 1);
rginda87b86462011-12-14 13:48:03 -08001088 this.screen_.setCursorPosition(row, column);
1089};
1090
1091hterm.Terminal.prototype.setAbsoluteCursorPosition = function(row, column) {
rginda2312fff2012-01-05 16:20:52 -08001092 row = hterm.clamp(row, 0, this.screenSize.height - 1);
1093 column = hterm.clamp(column, 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08001094 this.screen_.setCursorPosition(row, column);
1095};
1096
1097/**
1098 * Set the cursor column.
1099 *
1100 * @param {integer} column The new zero-based cursor column.
1101 */
1102hterm.Terminal.prototype.setCursorColumn = function(column) {
rginda87b86462011-12-14 13:48:03 -08001103 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, column);
rginda8ba33642011-12-14 12:31:31 -08001104};
1105
1106/**
1107 * Return the cursor column.
1108 *
1109 * @return {integer} The zero-based cursor column.
1110 */
1111hterm.Terminal.prototype.getCursorColumn = function() {
1112 return this.screen_.cursorPosition.column;
1113};
1114
1115/**
1116 * Set the cursor row.
1117 *
1118 * The cursor row is relative to the scroll region if the terminal has
1119 * 'origin mode' enabled, or relative to the addressable screen otherwise.
1120 *
1121 * @param {integer} row The new cursor row.
1122 */
rginda87b86462011-12-14 13:48:03 -08001123hterm.Terminal.prototype.setAbsoluteCursorRow = function(row) {
1124 this.setAbsoluteCursorPosition(row, this.screen_.cursorPosition.column);
rginda8ba33642011-12-14 12:31:31 -08001125};
1126
1127/**
1128 * Return the cursor row.
1129 *
1130 * @return {integer} The zero-based cursor row.
1131 */
1132hterm.Terminal.prototype.getCursorRow = function(row) {
1133 return this.screen_.cursorPosition.row;
1134};
1135
1136/**
1137 * Request that the ScrollPort redraw itself soon.
1138 *
1139 * The redraw will happen asynchronously, soon after the call stack winds down.
1140 * Multiple calls will be coalesced into a single redraw.
1141 */
1142hterm.Terminal.prototype.scheduleRedraw_ = function() {
rginda87b86462011-12-14 13:48:03 -08001143 if (this.timeouts_.redraw)
1144 return;
rginda8ba33642011-12-14 12:31:31 -08001145
1146 var self = this;
rginda87b86462011-12-14 13:48:03 -08001147 this.timeouts_.redraw = setTimeout(function() {
1148 delete self.timeouts_.redraw;
rginda8ba33642011-12-14 12:31:31 -08001149 self.scrollPort_.redraw_();
1150 }, 0);
1151};
1152
1153/**
1154 * Request that the ScrollPort be scrolled to the bottom.
1155 *
1156 * The scroll will happen asynchronously, soon after the call stack winds down.
1157 * Multiple calls will be coalesced into a single scroll.
1158 *
1159 * This affects the scrollbar position of the ScrollPort, and has nothing to
1160 * do with the VT scroll commands.
1161 */
1162hterm.Terminal.prototype.scheduleScrollDown_ = function() {
1163 if (this.timeouts_.scrollDown)
rginda87b86462011-12-14 13:48:03 -08001164 return;
rginda8ba33642011-12-14 12:31:31 -08001165
1166 var self = this;
1167 this.timeouts_.scrollDown = setTimeout(function() {
1168 delete self.timeouts_.scrollDown;
1169 self.scrollPort_.scrollRowToBottom(self.getRowCount());
1170 }, 10);
1171};
1172
1173/**
1174 * Move the cursor up a specified number of rows.
1175 *
1176 * @param {integer} count The number of rows to move the cursor.
1177 */
1178hterm.Terminal.prototype.cursorUp = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08001179 return this.cursorDown(-(count || 1));
rginda8ba33642011-12-14 12:31:31 -08001180};
1181
1182/**
1183 * Move the cursor down a specified number of rows.
1184 *
1185 * @param {integer} count The number of rows to move the cursor.
1186 */
1187hterm.Terminal.prototype.cursorDown = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08001188 count = count || 1;
rginda8ba33642011-12-14 12:31:31 -08001189 var minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
1190 var maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
1191 this.screenSize.height - 1);
1192
1193 var row = hterm.clamp(this.screen_.cursorPosition.row + count,
1194 minHeight, maxHeight);
rginda87b86462011-12-14 13:48:03 -08001195 this.setAbsoluteCursorRow(row);
rginda8ba33642011-12-14 12:31:31 -08001196};
1197
1198/**
1199 * Move the cursor left a specified number of columns.
1200 *
1201 * @param {integer} count The number of columns to move the cursor.
1202 */
1203hterm.Terminal.prototype.cursorLeft = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08001204 return this.cursorRight(-(count || 1));
rginda8ba33642011-12-14 12:31:31 -08001205};
1206
1207/**
1208 * Move the cursor right a specified number of columns.
1209 *
1210 * @param {integer} count The number of columns to move the cursor.
1211 */
1212hterm.Terminal.prototype.cursorRight = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08001213 count = count || 1;
rginda8ba33642011-12-14 12:31:31 -08001214 var column = hterm.clamp(this.screen_.cursorPosition.column + count,
rginda87b86462011-12-14 13:48:03 -08001215 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08001216 this.setCursorColumn(column);
1217};
1218
1219/**
1220 * Reverse the foreground and background colors of the terminal.
1221 *
1222 * This only affects text that was drawn with no attributes.
1223 *
1224 * TODO(rginda): Test xterm to see if reverse is respected for text that has
1225 * been drawn with attributes that happen to coincide with the default
1226 * 'no-attribute' colors. My guess is probably not.
1227 */
1228hterm.Terminal.prototype.setReverseVideo = function(state) {
rginda87b86462011-12-14 13:48:03 -08001229 this.options_.reverseVideo = state;
rginda8ba33642011-12-14 12:31:31 -08001230 if (state) {
1231 this.scrollPort_.setForegroundColor(this.backgroundColor);
1232 this.scrollPort_.setBackgroundColor(this.foregroundColor);
1233 } else {
1234 this.scrollPort_.setForegroundColor(this.foregroundColor);
1235 this.scrollPort_.setBackgroundColor(this.backgroundColor);
1236 }
1237};
1238
1239/**
rginda87b86462011-12-14 13:48:03 -08001240 * Ring the terminal bell.
1241 *
1242 * We only have a visual bell, which quickly toggles inverse video in the
1243 * terminal.
1244 */
1245hterm.Terminal.prototype.ringBell = function() {
rginda6d397402012-01-17 10:58:29 -08001246 this.cursorNode_.style.backgroundColor =
1247 this.scrollPort_.getForegroundColor();
rginda87b86462011-12-14 13:48:03 -08001248
1249 var self = this;
1250 setTimeout(function() {
rginda6d397402012-01-17 10:58:29 -08001251 self.cursorNode_.style.backgroundColor = self.cursorColor;
1252 }, 200);
rginda87b86462011-12-14 13:48:03 -08001253};
1254
1255/**
rginda8ba33642011-12-14 12:31:31 -08001256 * Set the origin mode bit.
1257 *
1258 * If origin mode is on, certain VT cursor and scrolling commands measure their
1259 * row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
1260 * to the top of the addressable screen.
1261 *
1262 * Defaults to off.
1263 *
1264 * @param {boolean} state True to set origin mode, false to unset.
1265 */
1266hterm.Terminal.prototype.setOriginMode = function(state) {
1267 this.options_.originMode = state;
1268};
1269
1270/**
1271 * Set the insert mode bit.
1272 *
1273 * If insert mode is on, existing text beyond the cursor position will be
1274 * shifted right to make room for new text. Otherwise, new text overwrites
1275 * any existing text.
1276 *
1277 * Defaults to off.
1278 *
1279 * @param {boolean} state True to set insert mode, false to unset.
1280 */
1281hterm.Terminal.prototype.setInsertMode = function(state) {
1282 this.options_.insertMode = state;
1283};
1284
1285/**
rginda87b86462011-12-14 13:48:03 -08001286 * Set the auto carriage return bit.
1287 *
1288 * If auto carriage return is on then a formfeed character is interpreted
1289 * as a newline, otherwise it's the same as a linefeed. The difference boils
1290 * down to whether or not the cursor column is reset.
1291 */
1292hterm.Terminal.prototype.setAutoCarriageReturn = function(state) {
1293 this.options_.autoCarriageReturn = state;
1294};
1295
1296/**
rginda8ba33642011-12-14 12:31:31 -08001297 * Set the wraparound mode bit.
1298 *
1299 * If wraparound mode is on, certain VT commands will allow the cursor to wrap
1300 * to the start of the following row. Otherwise, the cursor is clamped to the
1301 * end of the screen and attempts to write past it are ignored.
1302 *
1303 * Defaults to on.
1304 *
1305 * @param {boolean} state True to set wraparound mode, false to unset.
1306 */
1307hterm.Terminal.prototype.setWraparound = function(state) {
1308 this.options_.wraparound = state;
1309};
1310
1311/**
1312 * Set the reverse-wraparound mode bit.
1313 *
1314 * If wraparound mode is off, certain VT commands will allow the cursor to wrap
1315 * to the end of the previous row. Otherwise, the cursor is clamped to column
1316 * 0.
1317 *
1318 * Defaults to off.
1319 *
1320 * @param {boolean} state True to set reverse-wraparound mode, false to unset.
1321 */
1322hterm.Terminal.prototype.setReverseWraparound = function(state) {
1323 this.options_.reverseWraparound = state;
1324};
1325
1326/**
1327 * Selects between the primary and alternate screens.
1328 *
1329 * If alternate mode is on, the alternate screen is active. Otherwise the
1330 * primary screen is active.
1331 *
1332 * Swapping screens has no effect on the scrollback buffer.
1333 *
1334 * Each screen maintains its own cursor position.
1335 *
1336 * Defaults to off.
1337 *
1338 * @param {boolean} state True to set alternate mode, false to unset.
1339 */
1340hterm.Terminal.prototype.setAlternateMode = function(state) {
rginda6d397402012-01-17 10:58:29 -08001341 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08001342 this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
1343
1344 this.screen_.setColumnCount(this.screenSize.width);
1345
1346 var rowDelta = this.screenSize.height - this.screen_.getHeight();
1347 if (rowDelta > 0)
1348 this.appendRows_(rowDelta);
1349
rginda6d397402012-01-17 10:58:29 -08001350 this.restoreCursor(cursor);
1351
rginda2312fff2012-01-05 16:20:52 -08001352 this.scrollPort_.invalidate();
rginda8ba33642011-12-14 12:31:31 -08001353 this.syncCursorPosition_();
1354};
1355
1356/**
1357 * Set the cursor-blink mode bit.
1358 *
1359 * If cursor-blink is on, the cursor will blink when it is visible. Otherwise
1360 * a visible cursor does not blink.
1361 *
1362 * You should make sure to turn blinking off if you're going to dispose of a
1363 * terminal, otherwise you'll leak a timeout.
1364 *
1365 * Defaults to on.
1366 *
1367 * @param {boolean} state True to set cursor-blink mode, false to unset.
1368 */
1369hterm.Terminal.prototype.setCursorBlink = function(state) {
1370 this.options_.cursorBlink = state;
1371
1372 if (!state && this.timeouts_.cursorBlink) {
1373 clearTimeout(this.timeouts_.cursorBlink);
1374 delete this.timeouts_.cursorBlink;
1375 }
1376
1377 if (this.options_.cursorVisible)
1378 this.setCursorVisible(true);
1379};
1380
1381/**
1382 * Set the cursor-visible mode bit.
1383 *
1384 * If cursor-visible is on, the cursor will be visible. Otherwise it will not.
1385 *
1386 * Defaults to on.
1387 *
1388 * @param {boolean} state True to set cursor-visible mode, false to unset.
1389 */
1390hterm.Terminal.prototype.setCursorVisible = function(state) {
1391 this.options_.cursorVisible = state;
1392
1393 if (!state) {
rginda87b86462011-12-14 13:48:03 -08001394 this.cursorNode_.style.opacity = '0';
rginda8ba33642011-12-14 12:31:31 -08001395 return;
1396 }
1397
rginda87b86462011-12-14 13:48:03 -08001398 this.syncCursorPosition_();
1399
1400 this.cursorNode_.style.opacity = '1';
rginda8ba33642011-12-14 12:31:31 -08001401
1402 if (this.options_.cursorBlink) {
1403 if (this.timeouts_.cursorBlink)
1404 return;
1405
1406 this.timeouts_.cursorBlink = setInterval(this.onCursorBlink_.bind(this),
1407 500);
1408 } else {
1409 if (this.timeouts_.cursorBlink) {
1410 clearTimeout(this.timeouts_.cursorBlink);
1411 delete this.timeouts_.cursorBlink;
1412 }
1413 }
1414};
1415
1416/**
rginda87b86462011-12-14 13:48:03 -08001417 * Synchronizes the visible cursor and document selection with the current
1418 * cursor coordinates.
rginda8ba33642011-12-14 12:31:31 -08001419 */
1420hterm.Terminal.prototype.syncCursorPosition_ = function() {
1421 var topRowIndex = this.scrollPort_.getTopRowIndex();
1422 var bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
1423 var cursorRowIndex = this.scrollbackRows_.length +
1424 this.screen_.cursorPosition.row;
1425
1426 if (cursorRowIndex > bottomRowIndex) {
1427 // Cursor is scrolled off screen, move it outside of the visible area.
1428 this.cursorNode_.style.top = -this.characterSize_.height;
1429 return;
1430 }
1431
1432 this.cursorNode_.style.top = this.scrollPort_.visibleRowTopMargin +
1433 this.characterSize_.height * (cursorRowIndex - topRowIndex);
1434 this.cursorNode_.style.left = this.characterSize_.width *
1435 this.screen_.cursorPosition.column;
rginda87b86462011-12-14 13:48:03 -08001436
1437 this.cursorNode_.setAttribute('title',
1438 '(' + this.screen_.cursorPosition.row +
1439 ', ' + this.screen_.cursorPosition.column +
1440 ')');
1441
1442 // Update the caret for a11y purposes.
1443 var selection = this.document_.getSelection();
1444 if (selection && selection.isCollapsed)
1445 this.screen_.syncSelectionCaret(selection);
rginda8ba33642011-12-14 12:31:31 -08001446};
1447
1448/**
1449 * Synchronizes the visible cursor with the current cursor coordinates.
1450 *
1451 * The sync will happen asynchronously, soon after the call stack winds down.
1452 * Multiple calls will be coalesced into a single sync.
1453 */
1454hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
1455 if (this.timeouts_.syncCursor)
rginda87b86462011-12-14 13:48:03 -08001456 return;
rginda8ba33642011-12-14 12:31:31 -08001457
1458 var self = this;
1459 this.timeouts_.syncCursor = setTimeout(function() {
1460 self.syncCursorPosition_();
1461 delete self.timeouts_.syncCursor;
rginda87b86462011-12-14 13:48:03 -08001462 }, 0);
1463};
1464
1465/**
1466 * Invoked by hterm.Terminal.Keyboard when a VT keystroke is detected.
1467 *
1468 * @param {string} string The VT string representing the keystroke.
1469 */
1470hterm.Terminal.prototype.onVTKeystroke = function(string) {
1471 if (this.scrollOnKeystroke)
1472 this.scrollPort_.scrollRowToBottom(this.getRowCount());
1473
1474 this.io.onVTKeystroke(string);
rginda8ba33642011-12-14 12:31:31 -08001475};
1476
1477/**
1478 * React when the ScrollPort is scrolled.
1479 */
1480hterm.Terminal.prototype.onScroll_ = function() {
1481 this.scheduleSyncCursorPosition_();
1482};
1483
1484/**
1485 * React when the ScrollPort is resized.
rgindac9bc5502012-01-18 11:48:44 -08001486 *
1487 * Note: This function should not directly contain code that alters the internal
1488 * state of the terminal. That kind of code belongs in realizeWidth or
1489 * realizeHeight, so that it can be executed synchronously in the case of a
1490 * programmatic width change.
rginda8ba33642011-12-14 12:31:31 -08001491 */
1492hterm.Terminal.prototype.onResize_ = function() {
rgindac9bc5502012-01-18 11:48:44 -08001493 var columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
1494 this.characterSize_.width);
rginda8ba33642011-12-14 12:31:31 -08001495
rgindac9bc5502012-01-18 11:48:44 -08001496 if (columnCount != this.screenSize.width)
1497 this.realizeWidth_(columnCount);
rginda8ba33642011-12-14 12:31:31 -08001498
rgindac9bc5502012-01-18 11:48:44 -08001499 var rowCount = this.scrollPort_.visibleRowCount;
rginda8ba33642011-12-14 12:31:31 -08001500
rgindac9bc5502012-01-18 11:48:44 -08001501 if (rowCount != this.screenSize.height)
1502 this.realizeHeight_(rowCount);
rginda8ba33642011-12-14 12:31:31 -08001503
rgindac9bc5502012-01-18 11:48:44 -08001504 this.scheduleSyncCursorPosition_();
rginda8ba33642011-12-14 12:31:31 -08001505};
1506
1507/**
1508 * Service the cursor blink timeout.
1509 */
1510hterm.Terminal.prototype.onCursorBlink_ = function() {
rginda87b86462011-12-14 13:48:03 -08001511 if (this.cursorNode_.style.opacity == '0') {
1512 this.cursorNode_.style.opacity = '1';
rginda8ba33642011-12-14 12:31:31 -08001513 } else {
rginda87b86462011-12-14 13:48:03 -08001514 this.cursorNode_.style.opacity = '0';
rginda8ba33642011-12-14 12:31:31 -08001515 }
1516};