* @license MIT
*/
+import { BufferSet } from './BufferSet';
import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './EventEmitter';
import { Viewport } from './Viewport';
-import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard';
+import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
import { CircularList } from './utils/CircularList';
import { C0 } from './EscapeSequences';
import { InputHandler } from './InputHandler';
import { Parser } from './Parser';
+import { Renderer } from './Renderer';
+import { Linkifier } from './Linkifier';
+import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
-import * as Keyboard from './utils/Keyboard';
+import * as Mouse from './utils/Mouse';
import { CHARSETS } from './Charsets';
+import { getRawByteCoords } from './utils/Mouse';
+import { translateBufferLineToString } from './utils/BufferLine';
/**
* Terminal Emulation References:
var WRITE_BATCH_SIZE = 300;
/**
- * The maximum number of refresh frames to skip when the write buffer is non-
- * empty. Note that these frames may be intermingled with frames that are
- * skipped via requestAnimationFrame's mechanism.
+ * The time between cursor blinks. This is driven by JS rather than a CSS
+ * animation due to a bug in Chromium that causes it to use excessive CPU time.
+ * See https://github.com/Microsoft/vscode/issues/22900
*/
-var MAX_REFRESH_FRAME_SKIP = 5;
+var CURSOR_BLINK_INTERVAL = 600;
/**
* Terminal
this.on('data', options.handler);
}
- /**
- * The scroll position of the y cursor, ie. ybase + y = the y position within the entire
- * buffer
- */
- this.ybase = 0;
-
- /**
- * The scroll position of the viewport
- */
- this.ydisp = 0;
-
- /**
- * The cursor's x position after ybase
- */
- this.x = 0;
-
- /**
- * The cursor's y position after ybase
- */
- this.y = 0;
-
- /** A queue of the rows to be refreshed */
- this.refreshRowsQueue = [];
-
this.cursorState = 0;
this.cursorHidden = false;
this.convertEol;
this.queue = '';
- this.scrollTop = 0;
- this.scrollBottom = this.rows - 1;
- this.customKeydownHandler = null;
+ this.customKeyEventHandler = null;
+ this.cursorBlinkInterval = null;
// modes
this.applicationKeypad = false;
this.originMode = false;
this.insertMode = false;
this.wraparoundMode = true; // defaults: xterm - true, vt100 - false
- this.normal = null;
// charset
this.charset = null;
this.inputHandler = new InputHandler(this);
this.parser = new Parser(this.inputHandler, this);
+ // Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
+ this.renderer = this.renderer || null;
+ this.selectionManager = this.selectionManager || null;
+ this.linkifier = this.linkifier || new Linkifier();
// user input states
this.writeBuffer = [];
this.writeInProgress = false;
- this.refreshFramesSkipped = 0;
/**
* Whether _xterm.js_ sent XOFF in order to catch up with the pty process.
// leftover surrogate high from previous write invocation
this.surrogate_high = '';
- /**
- * An array of all lines in the entire buffer, including the prompt. The lines are array of
- * characters which are 2-length arrays where [0] is an attribute and [1] is the character.
- */
- this.lines = new CircularList(this.scrollback);
+ // Create the terminal's buffers and set the current buffer
+ this.buffers = new BufferSet(this);
+ this.buffer = this.buffers.active; // Convenience shortcut;
+ this.buffers.on('activate', function (buffer) {
+ this._terminal.buffer = buffer;
+ });
+
var i = this.rows;
+
while (i--) {
- this.lines.push(this.blankLine());
+ this.buffer.lines.push(this.blankLine());
+ }
+ // Ensure the selection manager has the correct buffer
+ if (this.selectionManager) {
+ this.selectionManager.setBuffer(this.buffer.lines);
}
- this.tabs;
this.setupStops();
// Store if user went browsing history in scrollback
debug: false,
cancelEvents: false,
disableStdin: false,
+ useFlowControl: false,
tabStopWidth: 8
// programFeatures: false,
// focusKeys: false,
* Retrieves an option's value from the terminal.
* @param {string} key The option key.
*/
-Terminal.prototype.getOption = function(key, value) {
+Terminal.prototype.getOption = function(key) {
if (!(key in Terminal.defaults)) {
throw new Error('No option with key "' + key + '"');
}
}
switch (key) {
case 'scrollback':
+ if (value < this.rows) {
+ let msg = 'Setting the scrollback value less than the number of rows ';
+
+ msg += `(${this.rows}) is not allowed.`;
+
+ console.warn(msg);
+ return false;
+ }
+
if (this.options[key] !== value) {
- if (this.lines.length > value) {
- const amountToTrim = this.lines.length - value;
- const needsRefresh = (this.ydisp - amountToTrim < 0);
- this.lines.trimStart(amountToTrim);
- this.ybase = Math.max(this.ybase - amountToTrim, 0);
- this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
+ if (this.buffer.lines.length > value) {
+ const amountToTrim = this.buffer.lines.length - value;
+ const needsRefresh = (this.buffer.ydisp - amountToTrim < 0);
+ this.buffer.lines.trimStart(amountToTrim);
+ this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0);
+ this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0);
if (needsRefresh) {
this.refresh(0, this.rows - 1);
}
}
- this.lines.maxLength = value;
+ this.buffer.lines.maxLength = value;
this.viewport.syncScrollArea();
}
break;
this[key] = value;
this.options[key] = value;
switch (key) {
- case 'cursorBlink': this.element.classList.toggle('xterm-cursor-blink', value); break;
+ case 'cursorBlink': this.setCursorBlinking(value); break;
case 'cursorStyle':
// Style 'block' applies with no class
this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline');
}
};
+Terminal.prototype.restartCursorBlinking = function () {
+ this.setCursorBlinking(this.options.cursorBlink);
+};
+
+Terminal.prototype.setCursorBlinking = function (enabled) {
+ this.element.classList.toggle('xterm-cursor-blink', enabled);
+ this.clearCursorBlinkingInterval();
+ if (enabled) {
+ var self = this;
+ this.cursorBlinkInterval = setInterval(function () {
+ self.element.classList.toggle('xterm-cursor-blink-on');
+ }, CURSOR_BLINK_INTERVAL);
+ }
+};
+
+Terminal.prototype.clearCursorBlinkingInterval = function () {
+ this.element.classList.remove('xterm-cursor-blink-on');
+ if (this.cursorBlinkInterval) {
+ clearInterval(this.cursorBlinkInterval);
+ this.cursorBlinkInterval = null;
+ }
+};
+
/**
* Binds the desired focus behavior on a given terminal object.
*
}
term.element.classList.add('focus');
term.showCursor();
+ term.restartCursorBlinking.apply(term);
Terminal.focus = term;
term.emit('focus', {terminal: term});
});
*/
Terminal.bindBlur = function (term) {
on(term.textarea, 'blur', function (ev) {
- term.queueRefresh(term.y, term.y);
+ term.refresh(term.buffer.y, term.buffer.y);
if (term.sendFocus) {
term.send(C0.ESC + '[O');
}
term.element.classList.remove('focus');
+ term.clearCursorBlinkingInterval.apply(term);
Terminal.focus = null;
term.emit('blur', {terminal: term});
});
Terminal.bindBlur(this);
// Bind clipboard functionality
- on(this.element, 'copy', function (ev) {
- copyHandler.call(this, ev, term);
- });
- on(this.textarea, 'paste', function (ev) {
- pasteHandler.call(this, ev, term);
- });
- on(this.element, 'paste', function (ev) {
- pasteHandler.call(this, ev, term);
+ on(this.element, 'copy', event => {
+ // If mouse events are active it means the selection manager is disabled and
+ // copy should be handled by the host program.
+ if (!term.hasSelection) {
+ return;
+ }
+ copyHandler(event, term, this.selectionManager);
});
+ const pasteHandlerWrapper = event => pasteHandler(event, term);
+ on(this.textarea, 'paste', pasteHandlerWrapper);
+ on(this.element, 'paste', pasteHandlerWrapper);
- function rightClickHandlerWrapper (ev) {
- rightClickHandler.call(this, ev, term);
- }
-
+ // Handle right click context menus
if (term.browser.isFirefox) {
- on(this.element, 'mousedown', function (ev) {
- if (ev.button == 2) {
- rightClickHandlerWrapper(ev);
+ // Firefox doesn't appear to fire the contextmenu event on right click
+ on(this.element, 'mousedown', event => {
+ if (event.button == 2) {
+ rightClickHandler(event, this.textarea, this.selectionManager);
}
});
} else {
- on(this.element, 'contextmenu', rightClickHandlerWrapper);
+ on(this.element, 'contextmenu', event => {
+ rightClickHandler(event, this.textarea, this.selectionManager);
+ });
+ }
+
+ // Move the textarea under the cursor when middle clicking on Linux to ensure
+ // middle click to paste selection works. This only appears to work in Chrome
+ // at the time is writing.
+ if (term.browser.isLinux) {
+ // Use auxclick event over mousedown the latter doesn't seem to work. Note
+ // that the regular click event doesn't fire for the middle mouse button.
+ on(this.element, 'auxclick', event => {
+ if (event.button === 1) {
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
+ }
+ });
}
};
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper));
term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
+ term.on('refresh', function (data) {
+ term.queueLinkification(data.start, data.end)
+ });
};
* Opens the terminal within an element.
*
* @param {HTMLElement} parent The element to create the terminal within.
+ * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM
*/
-Terminal.prototype.open = function(parent) {
+Terminal.prototype.open = function(parent, focus) {
var self=this, i=0, div;
this.parent = parent || this.parent;
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.classList.add('xterm-theme-' + this.theme);
- this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink);
+ this.setCursorBlinking(this.options.cursorBlink);
- this.element.style.height
this.element.setAttribute('tabindex', 0);
this.viewportElement = document.createElement('div');
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
+ // Create the selection container.
+ this.selectionContainer = document.createElement('div');
+ this.selectionContainer.classList.add('xterm-selection');
+ this.element.appendChild(this.selectionContainer);
+
// Create the container that will hold the lines of the terminal and then
// produce the lines the lines.
this.rowContainer = document.createElement('div');
this.rowContainer.classList.add('xterm-rows');
this.element.appendChild(this.rowContainer);
this.children = [];
+ this.linkifier.attachToDom(document, this.children);
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
}
this.parent.appendChild(this.element);
- this.charMeasure = new CharMeasure(this.helperContainer);
+ this.charMeasure = new CharMeasure(document, this.helperContainer);
this.charMeasure.on('charsizechanged', function () {
- self.updateCharSizeCSS();
+ self.updateCharSizeStyles();
});
this.charMeasure.measure();
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
+ this.renderer = new Renderer(this);
+ this.selectionManager = new SelectionManager(
+ this, this.buffer.lines, this.rowContainer, this.charMeasure
+ );
+ this.selectionManager.on('refresh', data => {
+ this.renderer.refreshSelection(data.start, data.end);
+ });
+ this.selectionManager.on('newselection', text => {
+ // If there's a new selection, put it into the textarea, focus and select it
+ // in order to register it as a selection on the OS. This event is fired
+ // only on Linux to enable middle click to paste selection.
+ this.textarea.value = text;
+ this.textarea.focus();
+ this.textarea.select();
+ });
+ this.on('scroll', () => this.selectionManager.refresh());
+ this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
// Setup loop that draws to screen
- this.queueRefresh(0, this.rows - 1);
- this.refreshLoop();
+ this.refresh(0, this.rows - 1);
// Initialize global actions that
// need to be taken on the document.
this.initGlobal();
- // Ensure there is a Terminal.focus.
- this.focus();
+ /**
+ * Automatic focus functionality.
+ * TODO: Default to `false` starting with xterm.js 3.0.
+ */
+ if (typeof focus == 'undefined') {
+ let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n';
+
+ message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 ';
+ message += 'it will default to `false`.';
+
+ console.warn(message);
+ focus = true;
+ }
+
+ if (focus) {
+ this.focus();
+ }
on(this.element, 'click', function() {
var selection = document.getSelection(),
// them into terminal mouse protocols.
this.bindMouse();
- // Figure out whether boldness affects
- // the character width of monospace fonts.
- if (Terminal.brokenBold == null) {
- Terminal.brokenBold = isBoldBroken(this.document);
- }
-
/**
* This event is emitted when terminal has completed opening.
*
* Updates the helper CSS class with any changes necessary after the terminal's
* character width has been changed.
*/
-Terminal.prototype.updateCharSizeCSS = function() {
- this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}';
+Terminal.prototype.updateCharSizeStyles = function() {
+ this.charSizeStyleElement.textContent =
+ `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
+ `.xterm-normal-char{width:${this.charMeasure.width}px;}` +
+ `.xterm-rows > div{height:${this.charMeasure.height}px;}`;
}
/**
button = getButton(ev);
// get mouse coordinates
- pos = getCoords(ev);
+ pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
var button = pressed
, pos;
- pos = getCoords(ev);
+ pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
return button;
}
- // mouse coordinates measured in cols/rows
- function getCoords(ev) {
- var x, y, w, h, el;
-
- // ignore browsers without pageX for now
- if (ev.pageX == null) return;
-
- x = ev.pageX;
- y = ev.pageY;
- el = self.element;
-
- // should probably check offsetParent
- // but this is more portable
- while (el && el !== self.document.documentElement) {
- x -= el.offsetLeft;
- y -= el.offsetTop;
- el = 'offsetParent' in el
- ? el.offsetParent
- : el.parentNode;
- }
-
- // convert to cols/rows
- w = self.element.clientWidth;
- h = self.element.clientHeight;
- x = Math.ceil((x / w) * self.cols);
- y = Math.ceil((y / h) * self.rows);
-
- // be sure to avoid sending
- // bad positions to the program
- if (x < 0) x = 0;
- if (x > self.cols) x = self.cols;
- if (y < 0) y = 0;
- if (y > self.rows) y = self.rows;
-
- // xterm sends raw bytes and
- // starts at 32 (SP) for each.
- x += 32;
- y += 32;
-
- return {
- x: x,
- y: y,
- type: 'wheel'
- };
- }
-
on(el, 'mousedown', function(ev) {
if (!self.mouseEvents) return;
self.viewport.onWheel(ev);
return self.cancel(ev);
});
+
+ on(el, 'touchstart', function(ev) {
+ if (self.mouseEvents) return;
+ self.viewport.onTouchStart(ev);
+ return self.cancel(ev);
+ });
+
+ on(el, 'touchmove', function(ev) {
+ if (self.mouseEvents) return;
+ self.viewport.onTouchMove(ev);
+ return self.cancel(ev);
+ });
};
/**
this._events = {};
this.handler = function() {};
this.write = function() {};
- if (this.element.parentNode) {
+ if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
//this.emit('close');
};
-
-/**
- * Flags used to render terminal text properly
- */
-Terminal.flags = {
- BOLD: 1,
- UNDERLINE: 2,
- BLINK: 4,
- INVERSE: 8,
- INVISIBLE: 16
-}
-
/**
- * Queues a refresh between two rows (inclusive), to be done on next animation
- * frame.
- * @param {number} start The start row.
- * @param {number} end The end row.
+ * Tells the renderer to refresh terminal content between two rows (inclusive) at the next
+ * opportunity.
+ * @param {number} start The row to start from (between 0 and this.rows - 1).
+ * @param {number} end The row to end at (between start and this.rows - 1).
*/
-Terminal.prototype.queueRefresh = function(start, end) {
- this.refreshRowsQueue.push({ start: start, end: end });
-}
-
-/**
- * Performs the refresh loop callback, calling refresh only if a refresh is
- * necessary before queueing up the next one.
- */
-Terminal.prototype.refreshLoop = function() {
- // Don't refresh if there were no row changes
- if (this.refreshRowsQueue.length > 0) {
- // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it
- // will need to be immediately refreshed anyway. This saves a lot of
- // rendering time as the viewport DOM does not need to be refreshed, no
- // scroll events, no layouts, etc.
- var skipFrame = this.writeBuffer.length > 0 && this.refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP;
-
- if (!skipFrame) {
- this.refreshFramesSkipped = 0;
- var start;
- var end;
- if (this.refreshRowsQueue.length > 4) {
- // Just do a full refresh when 5+ refreshes are queued
- start = 0;
- end = this.rows - 1;
- } else {
- // Get start and end rows that need refreshing
- start = this.refreshRowsQueue[0].start;
- end = this.refreshRowsQueue[0].end;
- for (var i = 1; i < this.refreshRowsQueue.length; i++) {
- if (this.refreshRowsQueue[i].start < start) {
- start = this.refreshRowsQueue[i].start;
- }
- if (this.refreshRowsQueue[i].end > end) {
- end = this.refreshRowsQueue[i].end;
- }
- }
- }
- this.refreshRowsQueue = [];
- this.refresh(start, end);
- }
+Terminal.prototype.refresh = function(start, end) {
+ if (this.renderer) {
+ this.renderer.queueRefresh(start, end);
}
- window.requestAnimationFrame(this.refreshLoop.bind(this));
-}
+};
/**
- * Refreshes (re-renders) terminal content within two rows (inclusive)
- *
- * Rendering Engine:
- *
- * In the screen buffer, each character is stored as a an array with a character
- * and a 32-bit integer:
- * - First value: a utf-16 character.
- * - Second value:
- * - Next 9 bits: background color (0-511).
- * - Next 9 bits: foreground color (0-511).
- * - Next 14 bits: a mask for misc. flags:
- * - 1=bold
- * - 2=underline
- * - 4=blink
- * - 8=inverse
- * - 16=invisible
- *
- * @param {number} start The row to start from (between 0 and terminal's height terminal - 1)
- * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
+ * Queues linkification for the specified rows.
+ * @param {number} start The row to start from (between 0 and this.rows - 1).
+ * @param {number} end The row to end at (between start and this.rows - 1).
*/
-Terminal.prototype.refresh = function(start, end) {
- var self = this;
-
- var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;
-
- // If this is a big refresh, remove the terminal rows from the DOM for faster calculations
- if (end - start >= this.rows / 2) {
- parent = this.element.parentNode;
- if (parent) {
- this.element.removeChild(this.rowContainer);
- }
- }
-
- width = this.cols;
- y = start;
-
- if (end >= this.rows.length) {
- this.log('`end` is too large. Most likely a bad CSR.');
- end = this.rows.length - 1;
- }
-
- for (; y <= end; y++) {
- row = y + this.ydisp;
-
- line = this.lines.get(row);
- if (!line || !this.children[y]) {
- // Continue if the line is not available, this means a resize is currently in progress
- continue;
- }
- out = '';
-
- if (this.y === y - (this.ybase - this.ydisp)
- && this.cursorState
- && !this.cursorHidden) {
- x = this.x;
- } else {
- x = -1;
- }
-
- attr = this.defAttr;
- i = 0;
-
- for (; i < width; i++) {
- if (!line[i]) {
- // Continue if the character is not available, this means a resize is currently in progress
- continue;
- }
- data = line[i][0];
- ch = line[i][1];
- ch_width = line[i][2];
- if (!ch_width)
- continue;
-
- if (i === x) data = -1;
-
- if (data !== attr) {
- if (attr !== this.defAttr) {
- out += '</span>';
- }
- if (data !== this.defAttr) {
- if (data === -1) {
- out += '<span class="reverse-video terminal-cursor">';
- } else {
- var classNames = [];
-
- bg = data & 0x1ff;
- fg = (data >> 9) & 0x1ff;
- flags = data >> 18;
-
- if (flags & Terminal.flags.BOLD) {
- if (!Terminal.brokenBold) {
- classNames.push('xterm-bold');
- }
- // See: XTerm*boldColors
- if (fg < 8) fg += 8;
- }
-
- if (flags & Terminal.flags.UNDERLINE) {
- classNames.push('xterm-underline');
- }
-
- if (flags & Terminal.flags.BLINK) {
- classNames.push('xterm-blink');
- }
-
- // If inverse flag is on, then swap the foreground and background variables.
- if (flags & Terminal.flags.INVERSE) {
- /* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */
- bg = [fg, fg = bg][0];
- // Should inverse just be before the
- // above boldColors effect instead?
- if ((flags & 1) && fg < 8) fg += 8;
- }
-
- if (flags & Terminal.flags.INVISIBLE) {
- classNames.push('xterm-hidden');
- }
-
- /**
- * Weird situation: Invert flag used black foreground and white background results
- * in invalid background color, positioned at the 256 index of the 256 terminal
- * color map. Pin the colors manually in such a case.
- *
- * Source: https://github.com/sourcelair/xterm.js/issues/57
- */
- if (flags & Terminal.flags.INVERSE) {
- if (bg == 257) {
- bg = 15;
- }
- if (fg == 256) {
- fg = 0;
- }
- }
-
- if (bg < 256) {
- classNames.push('xterm-bg-color-' + bg);
- }
-
- if (fg < 256) {
- classNames.push('xterm-color-' + fg);
- }
-
- out += '<span';
- if (classNames.length) {
- out += ' class="' + classNames.join(' ') + '"';
- }
- out += '>';
- }
- }
- }
-
- if (ch_width === 2) {
- out += '<span class="xterm-wide-char">';
- }
- switch (ch) {
- case '&':
- out += '&';
- break;
- case '<':
- out += '<';
- break;
- case '>':
- out += '>';
- break;
- default:
- if (ch <= ' ') {
- out += ' ';
- } else {
- out += ch;
- }
- break;
- }
- if (ch_width === 2) {
- out += '</span>';
- }
-
- attr = data;
+Terminal.prototype.queueLinkification = function(start, end) {
+ if (this.linkifier) {
+ for (let i = start; i <= end; i++) {
+ this.linkifier.linkifyRow(i);
}
-
- if (attr !== this.defAttr) {
- out += '</span>';
- }
-
- this.children[y].innerHTML = out;
}
-
- if (parent) {
- this.element.appendChild(this.rowContainer);
- }
-
- this.emit('refresh', {element: this.element, start: start, end: end});
};
/**
Terminal.prototype.showCursor = function() {
if (!this.cursorState) {
this.cursorState = 1;
- this.queueRefresh(this.y, this.y);
+ this.refresh(this.buffer.y, this.buffer.y);
}
};
/**
* Scroll the terminal down 1 row, creating a blank line.
+ * @param {boolean} isWrapped Whether the new line is wrapped from the previous
+ * line.
*/
-Terminal.prototype.scroll = function() {
+Terminal.prototype.scroll = function(isWrapped) {
var row;
// Make room for the new row in lines
- if (this.lines.length === this.lines.maxLength) {
- this.lines.trimStart(1);
- this.ybase--;
- if (this.ydisp !== 0) {
- this.ydisp--;
+ if (this.buffer.lines.length === this.buffer.lines.maxLength) {
+ this.buffer.lines.trimStart(1);
+ this.buffer.ybase--;
+ if (this.buffer.ydisp !== 0) {
+ this.buffer.ydisp--;
}
}
- this.ybase++;
+ this.buffer.ybase++;
// TODO: Why is this done twice?
if (!this.userScrolling) {
- this.ydisp = this.ybase;
+ this.buffer.ydisp = this.buffer.ybase;
}
// last line
- row = this.ybase + this.rows - 1;
+ row = this.buffer.ybase + this.rows - 1;
// subtract the bottom scroll region
- row -= this.rows - 1 - this.scrollBottom;
+ row -= this.rows - 1 - this.buffer.scrollBottom;
- if (row === this.lines.length) {
+ if (row === this.buffer.lines.length) {
// Optimization: pushing is faster than splicing when they amount to the same behavior
- this.lines.push(this.blankLine());
+ this.buffer.lines.push(this.blankLine(undefined, isWrapped));
} else {
// add our new line
- this.lines.splice(row, 0, this.blankLine());
+ this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped));
}
- if (this.scrollTop !== 0) {
- if (this.ybase !== 0) {
- this.ybase--;
+ if (this.buffer.scrollTop !== 0) {
+ if (this.buffer.ybase !== 0) {
+ this.buffer.ybase--;
if (!this.userScrolling) {
- this.ydisp = this.ybase;
+ this.buffer.ydisp = this.buffer.ybase;
}
}
- this.lines.splice(this.ybase + this.scrollTop, 1);
+ this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1);
}
// this.maxRange();
- this.updateRange(this.scrollTop);
- this.updateRange(this.scrollBottom);
+ this.updateRange(this.buffer.scrollTop);
+ this.updateRange(this.buffer.scrollBottom);
/**
* This event is emitted whenever the terminal is scrolled.
*
* @event scroll
*/
- this.emit('scroll', this.ydisp);
+ this.emit('scroll', this.buffer.ydisp);
};
/**
*/
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
if (disp < 0) {
+ if (this.buffer.ydisp === 0) {
+ return;
+ }
this.userScrolling = true;
- } else if (disp + this.ydisp >= this.ybase) {
+ } else if (disp + this.buffer.ydisp >= this.buffer.ybase) {
this.userScrolling = false;
}
- this.ydisp += disp;
+ const oldYdisp = this.buffer.ydisp;
+ this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0);
- if (this.ydisp > this.ybase) {
- this.ydisp = this.ybase;
- } else if (this.ydisp < 0) {
- this.ydisp = 0;
+ // No change occurred, don't trigger scroll/refresh
+ if (oldYdisp === this.buffer.ydisp) {
+ return;
}
if (!suppressScrollEvent) {
- this.emit('scroll', this.ydisp);
+ this.emit('scroll', this.buffer.ydisp);
}
- this.queueRefresh(0, this.rows - 1);
+ this.refresh(0, this.rows - 1);
};
/**
*/
Terminal.prototype.scrollPages = function(pageCount) {
this.scrollDisp(pageCount * (this.rows - 1));
-}
+};
/**
* Scrolls the display of the terminal to the top.
*/
Terminal.prototype.scrollToTop = function() {
- this.scrollDisp(-this.ydisp);
-}
+ this.scrollDisp(-this.buffer.ydisp);
+};
/**
* Scrolls the display of the terminal to the bottom.
*/
Terminal.prototype.scrollToBottom = function() {
- this.scrollDisp(this.ybase - this.ydisp);
-}
+ this.scrollDisp(this.buffer.ybase - this.buffer.ydisp);
+};
/**
* Writes text to the terminal.
- * @param {string} text The text to write to the terminal.
+ * @param {string} data The text to write to the terminal.
*/
Terminal.prototype.write = function(data) {
this.writeBuffer.push(data);
// Send XOFF to pause the pty process if the write buffer becomes too large so
// xterm.js can catch up before more data is sent. This is necessary in order
// to keep signals such as ^C responsive.
- if (!this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
+ if (this.options.useFlowControl && !this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk
this.send(C0.DC3);
self.innerWrite();
});
}
-}
+};
Terminal.prototype.innerWrite = function() {
var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
this.xoffSentToCatchUp = false;
}
- this.refreshStart = this.y;
- this.refreshEnd = this.y;
+ this.refreshStart = this.buffer.y;
+ this.refreshEnd = this.buffer.y;
- this.parser.parse(data);
+ // HACK: Set the parser state based on it's state at the time of return.
+ // This works around the bug #662 which saw the parser state reset in the
+ // middle of parsing escape sequence in two chunks. For some reason the
+ // state of the parser resets to 0 after exiting parser.parse. This change
+ // just sets the state back based on the correct return statement.
+ var state = this.parser.parse(data);
+ this.parser.setState(state);
- this.updateRange(this.y);
- this.queueRefresh(this.refreshStart, this.refreshEnd);
+ this.updateRange(this.buffer.y);
+ this.refresh(this.refreshStart, this.refreshEnd);
}
if (this.writeBuffer.length > 0) {
// Allow renderer to catch up before processing the next batch
/**
* Writes text to the terminal, followed by a break line character (\n).
- * @param {string} text The text to write to the terminal.
+ * @param {string} data The text to write to the terminal.
*/
Terminal.prototype.writeln = function(data) {
this.write(data + '\r\n');
};
/**
- * Attaches a custom keydown handler which is run before keys are processed, giving consumers of
- * xterm.js ultimate control as to what keys should be processed by the terminal and what keys
- * should not.
+ * DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead.
* @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a
* function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent
* the default action. The function returns whether the event should be processed by xterm.js.
*/
Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) {
- this.customKeydownHandler = customKeydownHandler;
-}
+ let message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.';
+ console.warn(message);
+ this.attachCustomKeyEventHandler(customKeydownHandler);
+};
+
+/**
+ * Attaches a custom key event handler which is run before keys are processed, giving consumers of
+ * xterm.js ultimate control as to what keys should be processed by the terminal and what keys
+ * should not.
+ * @param {function} customKeyEventHandler The custom KeyboardEvent handler to attach. This is a
+ * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent
+ * the default action. The function returns whether the event should be processed by xterm.js.
+ */
+Terminal.prototype.attachCustomKeyEventHandler = function(customKeyEventHandler) {
+ this.customKeyEventHandler = customKeyEventHandler;
+};
+
+/**
+ * Attaches a http(s) link handler, forcing web links to behave differently to
+ * regular <a> tags. This will trigger a refresh as links potentially need to be
+ * reconstructed. Calling this with null will remove the handler.
+ * @param {LinkMatcherHandler} handler The handler callback function.
+ */
+Terminal.prototype.setHypertextLinkHandler = function(handler) {
+ if (!this.linkifier) {
+ throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
+ }
+ this.linkifier.setHypertextLinkHandler(handler);
+ // Refresh to force links to refresh
+ this.refresh(0, this.rows - 1);
+};
+
+/**
+ * Attaches a validation callback for hypertext links. This is useful to use
+ * validation logic or to do something with the link's element and url.
+ * @param {LinkMatcherValidationCallback} callback The callback to use, this can
+ * be cleared with null.
+ */
+Terminal.prototype.setHypertextValidationCallback = function(callback) {
+ if (!this.linkifier) {
+ throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
+ }
+ this.linkifier.setHypertextValidationCallback(callback);
+ // Refresh to force links to refresh
+ this.refresh(0, this.rows - 1);
+};
+
+/**
+ * Registers a link matcher, allowing custom link patterns to be matched and
+ * handled.
+ * @param {RegExp} regex The regular expression to search for, specifically
+ * this searches the textContent of the rows. You will want to use \s to match
+ * a space ' ' character for example.
+ * @param {LinkMatcherHandler} handler The callback when the link is called.
+ * @param {LinkMatcherOptions} [options] Options for the link matcher.
+ * @return {number} The ID of the new matcher, this can be used to deregister.
+ */
+Terminal.prototype.registerLinkMatcher = function(regex, handler, options) {
+ if (this.linkifier) {
+ var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
+ this.refresh(0, this.rows - 1);
+ return matcherId;
+ }
+};
+
+/**
+ * Deregisters a link matcher if it has been registered.
+ * @param {number} matcherId The link matcher's ID (returned after register)
+ */
+Terminal.prototype.deregisterLinkMatcher = function(matcherId) {
+ if (this.linkifier) {
+ if (this.linkifier.deregisterLinkMatcher(matcherId)) {
+ this.refresh(0, this.rows - 1);
+ }
+ }
+};
+
+/**
+ * Gets whether the terminal has an active selection.
+ */
+Terminal.prototype.hasSelection = function() {
+ return this.selectionManager ? this.selectionManager.hasSelection : false;
+};
+
+/**
+ * Gets the terminal's current selection, this is useful for implementing copy
+ * behavior outside of xterm.js.
+ */
+Terminal.prototype.getSelection = function() {
+ return this.selectionManager ? this.selectionManager.selectionText : '';
+};
+
+/**
+ * Clears the current terminal selection.
+ */
+Terminal.prototype.clearSelection = function() {
+ if (this.selectionManager) {
+ this.selectionManager.clearSelection();
+ }
+};
+
+/**
+ * Selects all text within the terminal.
+ */
+Terminal.prototype.selectAll = function() {
+ if (this.selectionManager) {
+ this.selectionManager.selectAll();
+ }
+};
/**
* Handle a keydown event
* @param {KeyboardEvent} ev The keydown event to be handled.
*/
Terminal.prototype.keyDown = function(ev) {
- if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) {
+ if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) {
return false;
}
+ this.restartCursorBlinking();
+
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
- if (this.ybase !== this.ydisp) {
+ if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
}
return false;
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
result.key = C0.ESC + (ev.keyCode - 48);
}
+ } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
+ if (ev.keyCode === 65) { // cmd + a
+ this.selectAll();
+ }
}
break;
}
Terminal.prototype.keyPress = function(ev) {
var key;
+ if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) {
+ return false;
+ }
+
this.cancel(ev);
if (ev.charCode) {
this.showCursor();
this.handler(key);
- return false;
+ return true;
};
/**
* @param {number} y The number of rows to resize to.
*/
Terminal.prototype.resize = function(x, y) {
+ if (isNaN(x) || isNaN(y)) {
+ return;
+ }
+
+ if (y > this.getOption('scrollback')) {
+ this.setOption('scrollback', y)
+ }
+
var line
, el
, i
, addToY;
if (x === this.cols && y === this.rows) {
+ // Check if we still need to measure the char size (fixes #785).
+ if (!this.charMeasure.width || !this.charMeasure.height) {
+ this.charMeasure.measure();
+ }
return;
}
j = this.cols;
if (j < x) {
ch = [this.defAttr, ' ', 1]; // does xterm use the default attr?
- i = this.lines.length;
- while (i--) {
- while (this.lines.get(i).length < x) {
- this.lines.get(i).push(ch);
- }
- }
- } else { // (j > x)
- i = this.lines.length;
+ i = this.buffer.lines.length;
while (i--) {
- while (this.lines.get(i).length > x) {
- this.lines.get(i).pop();
+ while (this.buffer.lines.get(i).length < x) {
+ this.buffer.lines.get(i).push(ch);
}
}
}
+
this.cols = x;
this.setupStops(this.cols);
if (j < y) {
el = this.element;
while (j++ < y) {
- // y is rows, not this.y
- if (this.lines.length < y + this.ybase) {
- if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
+ // y is rows, not this.buffer.y
+ if (this.buffer.lines.length < y + this.buffer.ybase) {
+ if (this.buffer.ybase > 0 && this.buffer.lines.length <= this.buffer.ybase + this.buffer.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
- this.ybase--;
- addToY++
- if (this.ydisp > 0) {
+ this.buffer.ybase--;
+ addToY++;
+ if (this.buffer.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
- this.ydisp--;
+ this.buffer.ydisp--;
}
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
- this.lines.push(this.blankLine());
+ this.buffer.lines.push(this.blankLine());
}
}
if (this.children.length < y) {
}
} else { // (j > y)
while (j-- > y) {
- if (this.lines.length > y + this.ybase) {
- if (this.lines.length > this.ybase + this.y + 1) {
+ if (this.buffer.lines.length > y + this.buffer.ybase) {
+ if (this.buffer.lines.length > this.buffer.ybase + this.buffer.y + 1) {
// The line is a blank line below the cursor, remove it
- this.lines.pop();
+ this.buffer.lines.pop();
} else {
// The line is the cursor, scroll down
- this.ybase++;
- this.ydisp++;
+ this.buffer.ybase++;
+ this.buffer.ydisp++;
}
}
if (this.children.length > y) {
this.rows = y;
// Make sure that the cursor stays on screen
- if (this.y >= y) {
- this.y = y - 1;
+ if (this.buffer.y >= y) {
+ this.buffer.y = y - 1;
}
if (addToY) {
- this.y += addToY;
+ this.buffer.y += addToY;
}
- if (this.x >= x) {
- this.x = x - 1;
+ if (this.buffer.x >= x) {
+ this.buffer.x = x - 1;
}
- this.scrollTop = 0;
- this.scrollBottom = y - 1;
+ this.buffer.scrollTop = 0;
+ this.buffer.scrollBottom = y - 1;
this.charMeasure.measure();
- this.queueRefresh(0, this.rows - 1);
-
- this.normal = null;
+ this.refresh(0, this.rows - 1);
this.geometry = [this.cols, this.rows];
this.emit('resize', {terminal: this, cols: x, rows: y});
*/
Terminal.prototype.setupStops = function(i) {
if (i != null) {
- if (!this.tabs[i]) {
+ if (!this.buffer.tabs[i]) {
i = this.prevStop(i);
}
} else {
- this.tabs = {};
+ this.buffer.tabs = {};
i = 0;
}
for (; i < this.cols; i += this.getOption('tabStopWidth')) {
- this.tabs[i] = true;
+ this.buffer.tabs[i] = true;
}
};
* @param {number} x The position to move the cursor to the previous tab stop.
*/
Terminal.prototype.prevStop = function(x) {
- if (x == null) x = this.x;
- while (!this.tabs[--x] && x > 0);
+ if (x == null) x = this.buffer.x;
+ while (!this.buffer.tabs[--x] && x > 0);
return x >= this.cols
? this.cols - 1
: x < 0 ? 0 : x;
* @param {number} x The position to move the cursor one tab stop forward.
*/
Terminal.prototype.nextStop = function(x) {
- if (x == null) x = this.x;
- while (!this.tabs[++x] && x < this.cols);
+ if (x == null) x = this.buffer.x;
+ while (!this.buffer.tabs[++x] && x < this.cols);
return x >= this.cols
? this.cols - 1
: x < 0 ? 0 : x;
* @param {number} y The line in which to operate.
*/
Terminal.prototype.eraseRight = function(x, y) {
- var line = this.lines.get(this.ybase + y)
- , ch = [this.eraseAttr(), ' ', 1]; // xterm
-
-
+ var line = this.buffer.lines.get(this.buffer.ybase + y);
+ if (!line) {
+ return;
+ }
+ var ch = [this.eraseAttr(), ' ', 1]; // xterm
for (; x < this.cols; x++) {
line[x] = ch;
}
-
this.updateRange(y);
};
* @param {number} y The line in which to operate.
*/
Terminal.prototype.eraseLeft = function(x, y) {
- var line = this.lines.get(this.ybase + y)
- , ch = [this.eraseAttr(), ' ', 1]; // xterm
-
+ var line = this.buffer.lines.get(this.buffer.ybase + y);
+ if (!line) {
+ return;
+ }
+ var ch = [this.eraseAttr(), ' ', 1]; // xterm
x++;
- while (x--) line[x] = ch;
-
+ while (x--) {
+ line[x] = ch;
+ }
this.updateRange(y);
};
* Clears the entire buffer, making the prompt line the new first line.
*/
Terminal.prototype.clear = function() {
- if (this.ybase === 0 && this.y === 0) {
+ if (this.buffer.ybase === 0 && this.buffer.y === 0) {
// Don't clear if it's already clear
return;
}
- this.lines.set(0, this.lines.get(this.ybase + this.y));
- this.lines.length = 1;
- this.ydisp = 0;
- this.ybase = 0;
- this.y = 0;
+ this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y));
+ this.buffer.lines.length = 1;
+ this.buffer.ydisp = 0;
+ this.buffer.ybase = 0;
+ this.buffer.y = 0;
for (var i = 1; i < this.rows; i++) {
- this.lines.push(this.blankLine());
+ this.buffer.lines.push(this.blankLine());
}
- this.queueRefresh(0, this.rows - 1);
- this.emit('scroll', this.ydisp);
+ this.refresh(0, this.rows - 1);
+ this.emit('scroll', this.buffer.ydisp);
};
/**
/**
* Return the data array of a blank line
* @param {number} cur First bunch of data for each "blank" character.
+ * @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
*/
-Terminal.prototype.blankLine = function(cur) {
+Terminal.prototype.blankLine = function(cur, isWrapped) {
var attr = cur
? this.eraseAttr()
: this.defAttr;
, line = []
, i = 0;
+ // TODO: It is not ideal that this is a property on an array, a buffer line
+ // class should be added that will hold this data and other useful functions.
+ if (isWrapped) {
+ line.isWrapped = isWrapped;
+ }
+
for (; i < this.cols; i++) {
line[i] = ch;
}
return;
}
+ // Clear the selection if the selection manager is available and has an active selection
+ if (this.selectionManager && this.selectionManager.hasSelection) {
+ this.selectionManager.clearSelection();
+ }
+
// Input is being sent to the terminal, the terminal should focus the prompt.
- if (this.ybase !== this.ydisp) {
+ if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
}
this.emit('data', data);
* ESC D Index (IND is 0x84).
*/
Terminal.prototype.index = function() {
- this.y++;
- if (this.y > this.scrollBottom) {
- this.y--;
+ this.buffer.y++;
+ if (this.buffer.y > this.buffer.scrollBottom) {
+ this.buffer.y--;
this.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
- if (this.x >= this.cols) {
- this.x--;
+ if (this.buffer.x >= this.cols) {
+ this.buffer.x--;
}
};
*/
Terminal.prototype.reverseIndex = function() {
var j;
- if (this.y === this.scrollTop) {
+ if (this.buffer.y === this.buffer.scrollTop) {
// possibly move the code below to term.reverseScroll();
// test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
// blankLine(true) is xterm/linux behavior
- this.lines.shiftElements(this.y + this.ybase, this.rows - 1, 1);
- this.lines.set(this.y + this.ybase, this.blankLine(true));
- this.updateRange(this.scrollTop);
- this.updateRange(this.scrollBottom);
+ this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1);
+ this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
+ this.updateRange(this.buffer.scrollTop);
+ this.updateRange(this.buffer.scrollBottom);
} else {
- this.y--;
+ this.buffer.y--;
}
};
Terminal.prototype.reset = function() {
this.options.rows = this.rows;
this.options.cols = this.cols;
- var customKeydownHandler = this.customKeydownHandler;
+ var customKeyEventHandler = this.customKeyEventHandler;
+ var cursorBlinkInterval = this.cursorBlinkInterval;
+ var inputHandler = this.inputHandler;
+ var buffers = this.buffers;
Terminal.call(this, this.options);
- this.customKeydownHandler = customKeydownHandler;
- this.queueRefresh(0, this.rows - 1);
+ this.customKeyEventHandler = customKeyEventHandler;
+ this.cursorBlinkInterval = cursorBlinkInterval;
+ this.inputHandler = inputHandler;
+ this.buffers = buffers;
+ this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea();
};
* ESC H Tab Set (HTS is 0x88).
*/
Terminal.prototype.tabSet = function() {
- this.tabs[this.x] = true;
+ this.buffer.tabs[this.buffer.x] = true;
};
/**
child.prototype = new f;
}
-// if bold is broken, we can't
-// use it in the terminal.
-function isBoldBroken(document) {
- var body = document.getElementsByTagName('body')[0];
- var el = document.createElement('span');
- el.innerHTML = 'hello world';
- body.appendChild(el);
- var w1 = el.scrollWidth;
- el.style.fontWeight = 'bold';
- var w2 = el.scrollWidth;
- body.removeChild(el);
- return w1 !== w2;
-}
-
function indexOf(obj, el) {
var i = obj.length;
while (i--) {
* Expose
*/
+Terminal.translateBufferLineToString = translateBufferLineToString;
Terminal.EventEmitter = EventEmitter;
Terminal.inherits = inherits;