import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './EventEmitter';
import { Viewport } from './Viewport';
-import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard';
+import { 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';
/**
* Terminal Emulation References:
*/
var WRITE_BATCH_SIZE = 300;
+/**
+ * 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 CURSOR_BLINK_INTERVAL = 600;
+
/**
* Terminal
*/
this.scrollTop = 0;
this.scrollBottom = this.rows - 1;
this.customKeydownHandler = null;
+ this.cursorBlinkInterval = null;
// modes
this.applicationKeypad = false;
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.linkifier = this.linkifier || null;;
+ this.selectionManager = this.selectionManager || null;
+ this.linkifier = this.linkifier || new Linkifier();
// user input states
this.writeBuffer = [];
while (i--) {
this.lines.push(this.blankLine());
}
+ // Ensure the selection manager has the correct buffer
+ if (this.selectionManager) {
+ this.selectionManager.setBuffer(this.lines);
+ }
this.tabs;
this.setupStops();
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});
});
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 (this.mouseEvents) {
+ 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) {
+ on(this.element, 'mousedown', event => {
if (ev.button == 2) {
- rightClickHandlerWrapper(ev);
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
}
});
} else {
- on(this.element, 'contextmenu', rightClickHandlerWrapper);
+ on(this.element, 'contextmenu', event => {
+ moveTextAreaUnderMouseCursor(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, 'click', event => {
+ if (event.button === 1) {
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
+ }
+ });
}
};
* 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.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 needs to be added before the
+ // rowContainer as the selection must be below the text.
+ 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 = new Linkifier(document, 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.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.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.refresh(0, this.rows - 1);
// 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(),
* 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
- x = Math.ceil(x / self.charMeasure.width);
- y = Math.ceil(y / self.charMeasure.height);
-
- // 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;
/**
* 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 (row === this.lines.length) {
// Optimization: pushing is faster than splicing when they amount to the same behavior
- this.lines.push(this.blankLine());
+ this.lines.push(this.blankLine(undefined, isWrapped));
} else {
// add our new line
- this.lines.splice(row, 0, this.blankLine());
+ this.lines.splice(row, 0, this.blankLine(undefined, isWrapped));
}
if (this.scrollTop !== 0) {
*/
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
if (disp < 0) {
+ if (this.ydisp === 0) {
+ return;
+ }
this.userScrolling = true;
} else if (disp + this.ydisp >= this.ybase) {
this.userScrolling = false;
this.refreshStart = this.y;
this.refreshEnd = this.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.refresh(this.refreshStart, this.refreshEnd);
* reconstructed. Calling this with null will remove the handler.
* @param {LinkHandler} handler The handler callback function.
*/
-Terminal.prototype.attachHypertextLinkHandler = function(handler) {
+Terminal.prototype.setHypertextLinkHandler = function(handler) {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
}
- this.linkifier.attachHypertextLinkHandler(handler);
+ 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(handler) {
+ if (!this.linkifier) {
+ throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
+ }
+ this.linkifier.setHypertextValidationCallback(handler);
+ // Refresh to force links to refresh
+ this.refresh(0, this.rows - 1);
+}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
}
}
+/**
+ * Gets whether the terminal has an active selection.
+ */
+Terminal.prototype.hasSelection = function() {
+ return this.selectionManager.hasSelection;
+}
+
+/**
+ * Gets the terminal's current selection, this is useful for implementing copy
+ * behavior outside of xterm.js.
+ */
+Terminal.prototype.getSelection = function() {
+ return this.selectionManager.selectionText;
+}
+
+/**
+ * Clears the current terminal selection.
+ */
+Terminal.prototype.clearSelection = function() {
+ this.selectionManager.clearSelection();
+}
+
+/**
+ * Selects all text within the terminal.
+ */
+Terminal.prototype.selectAll = function() {
+ this.selectionManager.selectAll();
+}
+
/**
* Handle a keydown event
* Key Resources:
return false;
}
+ this.restartCursorBlinking();
+
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
if (this.ybase !== this.ydisp) {
this.scrollToBottom();
} 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;
}
this.lines.get(i).push(ch);
}
}
- } else { // (j > x)
- i = this.lines.length;
- while (i--) {
- while (this.lines.get(i).length > x) {
- this.lines.get(i).pop();
- }
- }
}
+
this.cols = x;
this.setupStops(this.cols);
/**
* 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;
}
this.options.rows = this.rows;
this.options.cols = this.cols;
var customKeydownHandler = this.customKeydownHandler;
+ var cursorBlinkInterval = this.cursorBlinkInterval;
Terminal.call(this, this.options);
this.customKeydownHandler = customKeydownHandler;
+ this.cursorBlinkInterval = cursorBlinkInterval;
this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea();
};
Terminal.EventEmitter = EventEmitter;
Terminal.inherits = inherits;
-// Expose for Phantom.JS tests
-Terminal.CharMeasure = CharMeasure;
-Terminal.Linkifier = Linkifier;
-
/**
* Adds an event listener to the terminal.
*