Merge remote-tracking branch 'ups/v3' into v3

This commit is contained in:
Daniel Imms
2017-10-02 11:00:24 -07:00
72 changed files with 3687 additions and 3838 deletions
+6 -6
View File
@@ -97,12 +97,10 @@ xterm.fit();
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
- Chrome 48+
- Edge 13+
- Firefox 44+
- Internet Explorer 11+
- Opera 35+
- Safari 8+
- Chrome latest
- Edge latest
- Firefox latest
- Safari latest
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
@@ -130,6 +128,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
+24 -25
View File
@@ -2,9 +2,7 @@ var term,
protocol,
socketURL,
socket,
pid,
charWidth,
charHeight;
pid;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
@@ -21,11 +19,13 @@ var terminalContainer = document.getElementById('terminal-container'),
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
function setTerminalSize () {
var cols = parseInt(colsElement.value, 10),
rows = parseInt(rowsElement.value, 10),
width = (cols * charWidth).toString() + 'px',
height = (rows * charHeight).toString() + 'px';
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
var height = (rows * term.charMeasure.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
@@ -92,27 +92,26 @@ function createTerminal() {
term.open(terminalContainer);
term.fit();
var initialGeometry = term.proposeGeometry(),
cols = initialGeometry.cols,
rows = initialGeometry.rows;
// fit is called within a setTimeout, cols and rows need this.
setTimeout(() => {
colsElement.value = term.cols;
rowsElement.value = term.rows;
colsElement.value = cols;
rowsElement.value = rows;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) {
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
charWidth = Math.ceil(term.element.offsetWidth / cols);
charHeight = Math.ceil(term.element.offsetHeight / rows);
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
});
});
});
}, 0);
}
function runRealTerminal() {
-6
View File
@@ -14,9 +14,3 @@ h1 {
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #111;
color: #fafafa;
padding: 2px;
}
+11 -3
View File
@@ -168,6 +168,10 @@ namespace methods_core {
t.setOption('bellStyle', 'visual');
t.setOption('bellStyle', 'sound');
t.setOption('bellStyle', 'both');
t.setOption('fontSize', 1);
t.setOption('lineHeight', 1);
t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'});
}
}
namespace scrolling {
@@ -210,9 +214,13 @@ namespace methods_experimental {
t.registerLinkMatcher(/foo/, () => true, {
matchIndex: 1,
priority: 1,
validationCallback: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => {
console.log(uri, element, callback);
}
validationCallback: (uri: string, callback: (isValid: boolean) => void) => {
console.log(uri, callback);
},
tooltipCallback: (e: MouseEvent, uri: string) => {
console.log(e, uri);
},
leaveCallback: () => {}
});
t.deregisterLinkMatcher(1);
}
+2
View File
@@ -7,6 +7,7 @@ const buffer = require('vinyl-buffer');
const coveralls = require('gulp-coveralls');
const fs = require('fs-extra');
const gulp = require('gulp');
const path = require('path');
const istanbul = require('gulp-istanbul');
const merge = require('merge-stream');
const mocha = require('gulp-mocha');
@@ -96,6 +97,7 @@ gulp.task('browserify-addons', ['tsc'], function() {
packageCache: {}
};
let searchBundle = browserify(searchOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
+1 -1
View File
@@ -64,7 +64,7 @@
"nodemon": "1.10.2",
"sorcery": "^0.10.0",
"tslint": "^4.0.2",
"typescript": "~2.2.0",
"typescript": "~2.4.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+132 -74
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -6,8 +7,10 @@ import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList';
import { LineData, CharData } from './Types';
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
@@ -32,8 +35,8 @@ export class Buffer implements IBuffer {
/**
* Create a new Buffer.
* @param _terminal The terminal the Buffer will belong to.
* @param _hasScrollback Whether the buffer should respecr the scrollback of
* the terminal..
* @param _hasScrollback Whether the buffer should respect the scrollback of
* the terminal.
*/
constructor(
private _terminal: ITerminal,
@@ -46,6 +49,16 @@ export class Buffer implements IBuffer {
return this._lines;
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
public get isCursorInViewport(): boolean {
const absoluteY = this.ybase + this.y;
const relativeY = absoluteY - this.ydisp;
return (relativeY >= 0 && relativeY < this._terminal.rows);
}
/**
* Gets the correct buffer length based on the rows provided, the terminal's
* scrollback and whether this buffer is flagged to have scrollback or not.
@@ -78,11 +91,10 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this.scrollBottom = 0;
this.scrollTop = 0;
this.tabs = {};
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
}
/**
@@ -91,11 +103,6 @@ export class Buffer implements IBuffer {
* @param newRows The new number of rows.
*/
public resize(newCols: number, newRows: number): void {
// Don't resize the buffer if it's empty and hasn't been used yet.
if (this._lines.length === 0) {
return;
}
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
@@ -103,83 +110,88 @@ export class Buffer implements IBuffer {
this._lines.maxLength = newMaxLength;
}
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
// The following adjustments should only happen if the buffer has been
// initialized/filled.
if (this._lines.length > 0) {
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
}
}
}
}
// Resize rows in both directions as needed
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.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) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
// Resize rows in both directions as needed
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.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) {
// Viewport is at the top of the buffer, must increase downwards
this.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._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
this.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._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
this.ydisp++;
}
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
}
this._lines.maxLength = newMaxLength;
}
}
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
this._lines.maxLength = newMaxLength;
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.scrollTop = 0;
}
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.scrollTop = 0;
this.scrollBottom = newRows - 1;
}
@@ -199,6 +211,9 @@ export class Buffer implements IBuffer {
let widthAdjustedStartCol = startCol;
let widthAdjustedEndCol = endCol;
const line = this.lines.get(lineIndex);
if (!line) {
return '';
}
for (let i = 0; i < line.length; i++) {
const char = line[i];
lineString += char[CHAR_DATA_CHAR_INDEX];
@@ -230,4 +245,47 @@ export class Buffer implements IBuffer {
return lineString.substring(widthAdjustedStartCol, finalEndCol);
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
*/
public setupTabStops(i?: number): void {
if (i != null) {
if (!this.tabs[i]) {
i = this.prevStop(i);
}
} else {
this.tabs = {};
i = 0;
}
for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) {
this.tabs[i] = true;
}
}
/**
* Move the cursor to the previous tab stop from the given position (default is current).
* @param x The position to move the cursor to the previous tab stop.
*/
public prevStop(x?: number): number {
if (x == null) {
x = this.x;
}
while (!this.tabs[--x] && x > 0);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
/**
* Move the cursor one tab stop forward from the given position (default is current).
* @param x The position to move the cursor one tab stop forward.
*/
public nextStop(x?: number): number {
if (x == null) {
x = this.x;
}
while (!this.tabs[++x] && x < this._terminal.cols);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+12
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -28,6 +29,8 @@ export class BufferSet extends EventEmitter implements IBufferSet {
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
this._alt = new Buffer(this._terminal, false);
this._activeBuffer = this._normal;
this.setupTabStops();
}
/**
@@ -87,4 +90,13 @@ export class BufferSet extends EventEmitter implements IBufferSet {
this._normal.resize(newCols, newRows);
this._alt.resize(newCols, newRows);
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
*/
public setupTabStops(i?: number): void {
this._normal.setupTabStops(i);
this._alt.setupTabStops(i);
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
+11
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -42,6 +43,16 @@ describe('CompositionHelper', () => {
},
handler: (text: string) => {
handledText += text;
},
buffer: {
isCursorInViewport: true
},
charMeasure: {
height: 10,
width: 10
},
options: {
lineHeight: 1
}
};
handledText = '';
+11 -10
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -193,26 +194,26 @@ export class CompositionHelper {
if (!this.isComposing) {
return;
}
const cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
if (cursor) {
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
// the .xterm element.
const xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
const cursorTop = xtermRows.offsetTop + cursor.offsetTop;
this.compositionView.style.left = cursor.offsetLeft + 'px';
if (this.terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
const cursorTop = this.terminal.buffer.y * cellHeight;
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
this.compositionView.style.left = cursorLeft + 'px';
this.compositionView.style.top = cursorTop + 'px';
this.compositionView.style.height = cursor.offsetHeight + 'px';
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
this.compositionView.style.height = cellHeight + 'px';
this.compositionView.style.lineHeight = cellHeight + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this.compositionView.getBoundingClientRect();
this.textarea.style.left = cursor.offsetLeft + 'px';
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = compositionViewBounds.width + 'px';
this.textarea.style.height = compositionViewBounds.height + 'px';
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+18 -15
View File
@@ -1,4 +1,6 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
@@ -36,13 +38,14 @@ export class InputHandler implements IInputHandler {
// dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0);
}
} else {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0);
}
this._terminal.updateRange(this._terminal.buffer.y);
}
@@ -81,21 +84,21 @@ export class InputHandler implements IInputHandler {
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
}
// insert empty cell at cursor
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (ch_width === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
}
}
@@ -157,7 +160,7 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
this._terminal.buffer.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.buffer.nextStop();
}
/**
@@ -188,7 +191,7 @@ export class InputHandler implements IInputHandler {
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
@@ -349,7 +352,7 @@ export class InputHandler implements IInputHandler {
public cursorForwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.buffer.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.buffer.nextStop();
}
}
@@ -467,7 +470,7 @@ export class InputHandler implements IInputHandler {
while (param--) {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(row - 1, 1);
this._terminal.buffer.lines.splice(row, 1);
this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true));
}
@@ -487,7 +490,7 @@ export class InputHandler implements IInputHandler {
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param--) {
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
@@ -535,7 +538,7 @@ export class InputHandler implements IInputHandler {
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row)[j++] = ch;
@@ -548,7 +551,7 @@ export class InputHandler implements IInputHandler {
public cursorBackwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.buffer.x = this._terminal.prevStop();
this._terminal.buffer.x = this._terminal.buffer.prevStop();
}
}
@@ -589,7 +592,7 @@ export class InputHandler implements IInputHandler {
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1;
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y);
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
while (param--) {
line[this._terminal.buffer.x++] = ch;
+70 -12
View File
@@ -1,9 +1,12 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
isNode: boolean;
@@ -17,24 +20,34 @@ export interface IBrowser {
isMSWindows: boolean;
}
export interface ITerminal extends IEventEmitter {
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
writeBuffer: string[];
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffers: IBufferSet;
buffer: IBuffer;
isFocused: boolean;
mouseHelper: IMouseHelper;
/**
* Emit the 'data' event and populate the given data.
@@ -47,6 +60,7 @@ export interface ITerminal extends IEventEmitter {
reset(): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
refresh(start: number, end: number): void;
}
/**
@@ -92,14 +106,12 @@ export interface IInputHandlingTerminal extends IEventEmitter {
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
nextStop(x?: number): number;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
prevStop(x?: number): number;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
@@ -117,20 +129,23 @@ export interface ITerminalOptions {
bellSound?: string;
bellStyle?: string;
cancelEvents?: boolean;
colors?: string[];
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
fontSize?: number;
fontFamily?: string;
geometry?: [number, number];
handler?: (data: string) => void;
lineHeight?: number;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
tabStopWidth?: number;
termName?: string;
theme?: ITheme;
useFlowControl?: boolean;
}
@@ -145,7 +160,10 @@ export interface IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet {
@@ -157,11 +175,17 @@ export interface IBufferSet {
activateAltBuffer(): void;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface IViewport {
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ISelectionManager {
@@ -186,12 +210,14 @@ export interface ICompositionHelper {
export interface ICharMeasure {
width: number;
height: number;
measure(): void;
measure(options: ITerminalOptions): void;
}
export interface ILinkifier {
linkifyRow(rowIndex: number): void;
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
@@ -232,6 +258,14 @@ export interface ILinkMatcherOptions {
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
@@ -291,3 +325,27 @@ export interface IInputHandler {
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
cursor?: string;
cursorAccent?: string;
selection?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+106 -79
View File
@@ -1,43 +1,90 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ILinkifier } from './Interfaces';
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { Linkifier } from './Linkifier';
import { LinkMatcher } from './Types';
import { LinkMatcher, LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
class TestLinkifier extends Linkifier {
constructor() {
constructor(_terminal: IBufferAccessor & IElementAccessor) {
super(_terminal);
Linkifier.TIME_BEFORE_LINKIFY = 0;
super();
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
}
class TestMouseZoneManager implements IMouseZoneManager {
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
this.zones.push(zone);
}
clearAll(): void {
this.clears++;
}
}
describe('Linkifier', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let container: HTMLElement;
let rows: HTMLElement[];
let terminal: IBufferAccessor & IElementAccessor;
let linkifier: TestLinkifier;
let mouseZoneManager: TestMouseZoneManager;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
linkifier = new TestLinkifier();
terminal = {
buffer: new MockBuffer(),
element: <HTMLElement>{}
};
terminal.buffer.lines = new CircularList<LineData>(20);
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
});
function addRow(html: string): void {
const element = document.createElement('div');
element.innerHTML = html;
container.appendChild(element);
rows.push(element);
function stringToRow(text: string): LineData {
let result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
function addRow(text: string): void {
terminal.buffer.lines.push(stringToRow(text));
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRows();
setTimeout(() => {
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
done();
}, 0);
}
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
});
done();
}, 0);
}
describe('before attachToDom', () => {
@@ -52,78 +99,42 @@ describe('Linkifier', () => {
describe('after attachToDom', () => {
beforeEach(() => {
rows = [];
linkifier.attachToDom(document, rows);
container = document.createElement('div');
document.body.appendChild(container);
linkifier.attachToDom(mouseZoneManager);
});
function clickElement(element: Node): void {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
describe('http links', () => {
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRow(0);
// Allow linkify to happen
setTimeout(() => {
assert.equal(rows[0].innerHTML, expectedHtml);
done();
}, 0);
}
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
});
it('should match a single link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], done);
});
it('should match a single link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done);
});
it('should match a single link at the end of a text node', done => {
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], done);
});
it('should match a link after a link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done);
});
it('should match a link after a link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done);
});
it('should match a link immediately after a link at the end of a text node', done => {
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done);
});
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
// This is a regression test for an issue that came about when using
// an oh-my-zsh theme that added the large blue diamond unicode
// character (U+1F537) which caused the path to be duplicated. See #642.
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
});
});
@@ -131,26 +142,42 @@ describe('Linkifier', () => {
it('should enable link if true', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
assert.equal(mouseZoneManager.zones.length, 0);
cb(true);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
assert.equal(mouseZoneManager.zones.length, 1);
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, 5);
assert.equal(mouseZoneManager.zones[0].y, 1);
// Fires done()
mouseZoneManager.zones[0].clickCallback(<any>{});
}
});
linkifier.linkifyRow(0);
linkifier.linkifyRows();
});
it('should validate the uri, not the row', done => {
addRow('abc test abc');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (uri, cb) => {
assert.equal(uri, 'test');
done();
}
});
linkifier.linkifyRows();
});
it('should disable link if false', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
assert.equal(mouseZoneManager.zones.length, 0);
cb(false);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
assert.equal(mouseZoneManager.zones.length, 0);
}
});
linkifier.linkifyRow(0);
// Allow time for the click to be performed
linkifier.linkifyRows();
// Allow time for the validation callback to be performed
setTimeout(() => done(), 10);
});
@@ -158,7 +185,7 @@ describe('Linkifier', () => {
addRow('test test');
let count = 0;
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
count += 1;
if (count === 2) {
done();
@@ -166,7 +193,7 @@ describe('Linkifier', () => {
cb(false);
}
});
linkifier.linkifyRow(0);
linkifier.linkifyRows();
});
});
+139 -205
View File
@@ -1,11 +1,13 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
const INVALID_LINK_CLASS = 'xterm-invalid-link';
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
@@ -34,7 +36,7 @@ const HYPERTEXT_LINK_MATCHER_ID = 0;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
export class Linkifier {
export class Linkifier extends EventEmitter implements ILinkifier {
/**
* The time to wait after a row is changed before it is linkified. This prevents
* the costly operation of searching every row multiple times, potentially a
@@ -42,51 +44,78 @@ export class Linkifier {
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: LinkMatcher[];
protected _linkMatchers: LinkMatcher[] = [];
private _document: Document;
private _rows: HTMLElement[];
private _rowTimeoutIds: number[];
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _rowsToLinkify: {start: number, end: number};
constructor() {
this._rowTimeoutIds = [];
this._linkMatchers = [];
constructor(
protected _terminal: IBufferAccessor & IElementAccessor
) {
super();
this._rowsToLinkify = {
start: null,
end: null
};
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
* Attaches the linkifier to the DOM, enabling linkification.
* @param document The document object.
* @param rows The array of rows to apply links to.
* @param mouseZoneManager The mouse zone manager to register link zones with.
*/
public attachToDom(document: Document, rows: HTMLElement[]): void {
this._document = document;
this._rows = rows;
public attachToDom(mouseZoneManager: IMouseZoneManager): void {
this._mouseZoneManager = mouseZoneManager;
}
/**
* Queues a row for linkification.
* @param {number} rowIndex The index of the row to linkify.
* Queue linkification on a set of rows.
* @param start The row to linkify from (inclusive).
* @param end The row to linkify to (inclusive).
*/
public linkifyRow(rowIndex: number): void {
public linkifyRows(start: number, end: number): void {
// Don't attempt linkify if not yet attached to DOM
if (!this._document) {
if (!this._mouseZoneManager) {
return;
}
const timeoutId = this._rowTimeoutIds[rowIndex];
if (timeoutId) {
clearTimeout(timeoutId);
// Increase range to linkify
if (!this._rowsToLinkify.start) {
this._rowsToLinkify.start = start;
this._rowsToLinkify.end = end;
} else {
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
this._rowsToLinkify.end = this._rowsToLinkify.end < end ? this._rowsToLinkify.end : end;
}
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
// Clear out any existing links on this row range
this._mouseZoneManager.clearAll(start, end);
// Restart timer
if (this._rowsTimeoutId) {
clearTimeout(this._rowsTimeoutId);
}
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier.TIME_BEFORE_LINKIFY);
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior
* for standard http(s) links.
* @param {LinkHandler} handler The handler to use, this can be cleared with
* null.
* Linkifies the rows requested.
*/
private _linkifyRows(): void {
this._rowsTimeoutId = null;
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
this._linkifyRow(i);
}
this._rowsToLinkify.start = null;
this._rowsToLinkify.end = null;
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior for
* tandard http(s) links.
* @param handler The handler to use, this can be cleared with null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
@@ -94,8 +123,7 @@ export class Linkifier {
/**
* Attaches a validation callback for hypertext links.
* @param {LinkMatcherValidationCallback} callback The callback to use, this
* can be cleared with null.
* @param callback The callback to use, this can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
@@ -104,12 +132,12 @@ export class Linkifier {
/**
* 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 {LinkHandler} handler The callback when the link is called.
* @param {ILinkMatcherOptions} [options] Options for the link matcher.
* @return {number} The ID of the new matcher, this can be used to deregister.
* @param 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 handler The callback when the link is called.
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
@@ -121,6 +149,8 @@ export class Linkifier {
handler,
matchIndex: options.matchIndex,
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
@@ -151,8 +181,8 @@ export class Linkifier {
/**
* Deregisters a link matcher if it has been registered.
* @param {number} matcherId The link matcher's ID (returned after register)
* @return {boolean} Whether a link matcher was found and deregistered.
* @param matcherId The link matcher's ID (returned after register)
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
@@ -167,197 +197,101 @@ export class Linkifier {
/**
* Linkifies a row.
* @param {number} rowIndex The index of the row to linkify.
* @param rowIndex The index of the row to linkify.
*/
private _linkifyRow(rowIndex: number): void {
const row = this._rows[rowIndex];
if (!row) {
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
return;
}
const text = row.textContent;
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
for (let i = 0; i < this._linkMatchers.length; i++) {
const matcher = this._linkMatchers[i];
const linkElements = this._doLinkifyRow(row, matcher);
if (linkElements.length > 0) {
// Fire validation callback
if (matcher.validationCallback) {
for (let j = 0; j < linkElements.length; j++) {
const element = linkElements[j];
matcher.validationCallback(element.textContent, element, isValid => {
if (!isValid) {
element.classList.add(INVALID_LINK_CLASS);
}
});
}
}
// Only allow a single LinkMatcher to trigger on any given row.
return;
}
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
}
}
/**
* Linkifies a row given a specific handler.
* @param {HTMLElement} row The row to linkify.
* @param {LinkMatcher} matcher The link matcher for this line.
* @param rowIndex The row index to linkify.
* @param text The text of the row (excludes text in the row that's already
* linkified).
* @param matcher The link matcher for this line.
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
const nodes = row.childNodes;
// Find the first match
let match = row.textContent.match(matcher.regex);
let match = text.match(matcher.regex);
if (!match || match.length === 0) {
return result;
return;
}
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Set the next searches start index
let rowStartIndex = match.index + uri.length;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const searchIndex = node.textContent.indexOf(uri);
if (searchIndex >= 0) {
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher);
if (node.textContent.length === uri.length) {
// Matches entire string
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
this._replaceNode(node, linkElement);
} else {
const element = (<HTMLElement>node);
if (element.nodeName === 'A') {
// This row has already been linkified
return result;
}
element.innerHTML = '';
element.appendChild(linkElement);
}
} else if (node.childNodes.length > 1) {
// Matches part of string in an element with multiple child nodes
for (let j = 0; j < node.childNodes.length; j++) {
const childNode = node.childNodes[j];
const childSearchIndex = childNode.textContent.indexOf(uri);
if (childSearchIndex !== -1) {
// Match found in currentNode
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
// Don't need to count nodesAdded by replacing the node as this
// is a child node, not a top-level node.
break;
}
}
} else {
// Matches part of string in a single text node
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
// No need to consider the new nodes
i += nodesAdded;
// Get index, match.index is for the outer match which includes negated chars
const index = text.indexOf(uri);
// Ensure the link is valid before registering
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
}
result.push(linkElement);
// Find the next match
match = row.textContent.substring(rowStartIndex).match(matcher.regex);
if (!match || match.length === 0) {
return result;
}
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
rowStartIndex += match.index + uri.length;
}
}
return result;
}
/**
* Creates a link anchor element.
* @param {string} uri The uri of the link.
* @return {HTMLAnchorElement} The link.
*/
private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
const element = this._document.createElement('a');
element.textContent = uri;
element.draggable = false;
if (isHypertextLinkHandler) {
element.href = uri;
// Force link on another tab so work is not lost
element.target = '_blank';
element.addEventListener('click', (event: MouseEvent) => {
if (handler) {
return handler(event, uri);
if (isValid) {
this._addLink(offset + index, rowIndex, uri, matcher);
}
});
} else {
element.addEventListener('click', (event: MouseEvent) => {
// Don't execute the handler if the link is flagged as invalid
if (element.classList.contains(INVALID_LINK_CLASS)) {
return;
this._addLink(offset + index, rowIndex, uri, matcher);
}
// Recursively check for links in the rest of the text
const remainingStartIndex = index + uri.length;
const remainingText = text.substr(remainingStartIndex);
if (remainingText.length > 0) {
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
}
}
/**
* Registers a link to the mouse zone manager.
* @param x The column the link starts.
* @param y The row the link is on.
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
*/
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
this._mouseZoneManager.add(new MouseZone(
x + 1,
x + 1 + uri.length,
y + 1,
e => {
if (matcher.handler) {
return matcher.handler(e, uri);
}
return handler(event, uri);
});
}
return element;
}
/**
* Replace a node with 1 or more other nodes.
* @param {Node} oldNode The node to replace.
* @param {Node[]} newNodes The new nodes to insert in order.
*/
private _replaceNode(oldNode: Node, ...newNodes: Node[]): void {
const parent = oldNode.parentNode;
for (let i = 0; i < newNodes.length; i++) {
parent.insertBefore(newNodes[i], oldNode);
}
parent.removeChild(oldNode);
}
/**
* Replace a substring within a node with a new node.
* @param {Node} targetNode The target node; either a text node or a <span>
* containing a single text node.
* @param {Node} newNode The new node to insert.
* @param {string} substring The substring to replace.
* @param {number} substringIndex The index of the substring within the string.
* @return The number of nodes to skip when searching for the next uri.
*/
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
// If the targetNode is a non-text node with a single child, make the child
// the new targetNode.
if (targetNode.childNodes.length === 1) {
targetNode = targetNode.childNodes[0];
}
// The targetNode will be either a text node or a <span>. The text node
// (targetNode or its only-child) needs to be replaced with newNode plus new
// text nodes potentially on either side.
if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
throw new Error('targetNode must be a text node or only contain a single text node');
}
const fullText = targetNode.textContent;
if (substringIndex === 0) {
// Replace with <newNode><textnode>
const rightText = fullText.substring(substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(targetNode, newNode, rightTextNode);
return 0;
}
if (substringIndex === targetNode.textContent.length - substring.length) {
// Replace with <textnode><newNode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
this._replaceNode(targetNode, leftTextNode, newNode);
return 0;
}
// Replace with <textnode><newNode><textnode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
const rightText = fullText.substring(substringIndex + substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode);
return 1;
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer';
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = '';
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
}
));
}
}
+12
View File
@@ -1,4 +1,6 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
@@ -189,6 +191,9 @@ export class Parser {
let code;
let low;
const cursorStartX = this._terminal.buffer.x;
const cursorStartY = this._terminal.buffer.y;
if (this._terminal.debug) {
this._terminal.log('data: ' + data);
}
@@ -580,6 +585,13 @@ export class Parser {
break;
}
}
// Fire the cursormove event if it's moved. This is done inside the parser
// as a render cannot happen in the middle of a parsing round.
if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) {
this._terminal.emit('cursormove');
}
return this._state;
}
-401
View File
@@ -1,401 +0,0 @@
/**
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { DomElementObjectPool } from './utils/DomElementObjectPool';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
/**
* 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.
*/
const MAX_REFRESH_FRAME_SKIP = 5;
/**
* Flags used to render terminal text properly.
*/
enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16
};
let brokenBold: boolean = null;
export class Renderer {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshFramesSkipped = 0;
private _refreshAnimationFrame = null;
private _spanElementObjectPool = new DomElementObjectPool('span');
constructor(private _terminal: ITerminal) {
// Figure out whether boldness affects
// the character width of monospace fonts.
if (brokenBold === null) {
brokenBold = checkBoldBroken(this._terminal.element);
}
this._spanElementObjectPool = new DomElementObjectPool('span');
// TODO: Pull more DOM interactions into Renderer.constructor, element for
// example should be owned by Renderer (and also exposed by Terminal due to
// to established public API).
}
/**
* 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.
*/
public queueRefresh(start: number, end: number): void {
this._refreshRowsQueue.push({ start: start, end: end });
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
}
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
private _refreshLoop(): void {
// 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.
const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP;
if (skipFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
return;
}
this._refreshFramesSkipped = 0;
let start;
let end;
if (this._refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this._terminal.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this._refreshRowsQueue[0].start;
end = this._refreshRowsQueue[0].end;
for (let 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._refreshAnimationFrame = null;
this._refresh(start, end);
}
/**
* 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)
*/
private _refresh(start: number, end: number): void {
// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
let parent;
if (end - start >= this._terminal.rows / 2) {
parent = this._terminal.element.parentNode;
if (parent) {
this._terminal.element.removeChild(this._terminal.rowContainer);
}
}
let width = this._terminal.cols;
let y = start;
if (end >= this._terminal.rows) {
this._terminal.log('`end` is too large. Most likely a bad CSR.');
end = this._terminal.rows - 1;
}
for (; y <= end; y++) {
let row = y + this._terminal.buffer.ydisp;
let line = this._terminal.buffer.lines.get(row);
let x;
if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) &&
this._terminal.cursorState &&
!this._terminal.cursorHidden) {
x = this._terminal.buffer.x;
} else {
x = -1;
}
let attr = this._terminal.defAttr;
const documentFragment = document.createDocumentFragment();
let innerHTML = '';
let currentElement;
// Return the row's spans to the pool
while (this._terminal.children[y].children.length) {
const child = this._terminal.children[y].children[0];
this._terminal.children[y].removeChild(child);
this._spanElementObjectPool.release(<HTMLElement>child);
}
for (let i = 0; i < width; i++) {
// TODO: Could data be a more specific type?
let data: any = line[i][0];
const ch = line[i][CHAR_DATA_CHAR_INDEX];
const ch_width: any = line[i][CHAR_DATA_WIDTH_INDEX];
const isCursor: boolean = i === x;
if (!ch_width) {
continue;
}
if (data !== attr || isCursor) {
if (attr !== this._terminal.defAttr && !isCursor) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
if (data !== this._terminal.defAttr || isCursor) {
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
}
currentElement = this._spanElementObjectPool.acquire();
let bg = data & 0x1ff;
let fg = (data >> 9) & 0x1ff;
let flags = data >> 18;
if (isCursor) {
currentElement.classList.add('reverse-video');
currentElement.classList.add('terminal-cursor');
}
if (flags & FLAGS.BOLD) {
if (!brokenBold) {
currentElement.classList.add('xterm-bold');
}
// See: XTerm*boldColors
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
currentElement.classList.add('xterm-underline');
}
if (flags & FLAGS.BLINK) {
currentElement.classList.add('xterm-blink');
}
// If inverse flag is on, then swap the foreground and background variables.
if (flags & FLAGS.INVERSE) {
let temp = bg;
bg = fg;
fg = temp;
// Should inverse just be before the above boldColors effect instead?
if ((flags & 1) && fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.INVISIBLE && !isCursor) {
currentElement.classList.add('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 & FLAGS.INVERSE) {
if (bg === 257) {
bg = 15;
}
if (fg === 256) {
fg = 0;
}
}
if (bg < 256) {
currentElement.classList.add(`xterm-bg-color-${bg}`);
}
if (fg < 256) {
currentElement.classList.add(`xterm-color-${fg}`);
}
}
}
if (ch_width === 2) {
// Wrap wide characters so they're sized correctly. It's more difficult to release these
// from the object pool so just create new ones via innerHTML.
innerHTML += `<span class="xterm-wide-char">${ch}</span>`;
} else if (ch.charCodeAt(0) > 255) {
// Wrap any non-wide unicode character as some fonts size them badly
innerHTML += `<span class="xterm-normal-char">${ch}</span>`;
} else {
switch (ch) {
case '&':
innerHTML += '&amp;';
break;
case '<':
innerHTML += '&lt;';
break;
case '>':
innerHTML += '&gt;';
break;
default:
if (ch <= ' ') {
innerHTML += '&nbsp;';
} else {
innerHTML += ch;
}
break;
}
}
// The cursor needs its own element, therefore we set attr to -1
// which will cause the next character to be rendered in a new element
attr = isCursor ? -1 : data;
}
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
this._terminal.children[y].appendChild(documentFragment);
}
if (parent) {
this._terminal.element.appendChild(this._terminal.rowContainer);
}
this._terminal.emit('refresh', {start, end});
};
/**
* Refreshes the selection in the DOM.
* @param start The selection start.
* @param end The selection end.
*/
public refreshSelection(start: [number, number], end: [number, number]): void {
// Remove all selections
while (this._terminal.selectionContainer.children.length) {
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
}
// Selection does not exist
if (!start || !end) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = document.createDocumentFragment();
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
this._terminal.selectionContainer.appendChild(documentFragment);
}
/**
* Creates a selection element at the specified position.
* @param row The row of the selection.
* @param colStart The start column.
* @param colEnd The end columns.
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
return element;
}
}
// If bold is broken, we can't use it in the terminal.
function checkBoldBroken(terminalElement: HTMLElement): boolean {
const document = terminalElement.ownerDocument;
const el = document.createElement('span');
el.innerHTML = 'hello world';
terminalElement.appendChild(el);
const w1 = el.offsetWidth;
const h1 = el.offsetHeight;
el.style.fontWeight = 'bold';
const w2 = el.offsetWidth;
const h2 = el.offsetHeight;
terminalElement.removeChild(el);
return w1 !== w2 || h1 !== h2;
}
+19 -20
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -17,10 +18,9 @@ class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: IBuffer,
rowContainer: HTMLElement,
charMeasure: CharMeasure
) {
super(terminal, buffer, rowContainer, charMeasure);
super(terminal, buffer, charMeasure);
}
public get model(): SelectionModel { return this._model; }
@@ -48,7 +48,6 @@ describe('SelectionManager', () => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
rowContainer = document.createElement('div');
terminal = new MockTerminal();
terminal.cols = 80;
terminal.rows = 2;
@@ -56,13 +55,13 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
selectionManager = new TestSelectionManager(terminal, buffer, null);
});
function stringToRow(text: string): LineData {
let result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1]);
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
@@ -101,21 +100,21 @@ describe('SelectionManager', () => {
it('should expand selection for wide characters', () => {
// Wide characters use a special format
buffer.lines.set(0, [
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, ' ', 1],
[null, 'a', 1],
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, 'b', 1],
[null, ' ', 1],
[null, 'f', 1],
[null, 'o', 1],
[null, 'o', 1]
[null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null],
[null, '文', 2, '文'.charCodeAt(0)],
[null, '', 0, null],
[null, ' ', 1, ' '.charCodeAt(0)],
[null, 'a', 1, 'a'.charCodeAt(0)],
[null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null],
[null, '文', 2, '文'.charCodeAt(0)],
[null, '', 0, ''.charCodeAt(0)],
[null, 'b', 1, 'b'.charCodeAt(0)],
[null, ' ', 1, ' '.charCodeAt(0)],
[null, 'f', 1, 'f'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)]
]);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
+20 -13
View File
@@ -1,8 +1,9 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Mouse from './utils/Mouse';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
@@ -98,7 +99,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
constructor(
private _terminal: ITerminal,
private _buffer: IBuffer,
private _rowContainer: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
@@ -116,8 +116,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(<MouseEvent>event));
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
@@ -275,7 +273,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
@@ -294,8 +292,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
const terminalHeight = this._terminal.rows * this._charMeasure.height;
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
}
@@ -312,11 +310,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Handles te mousedown event, setting up for a new selection.
* @param event The mousedown event.
*/
private _onMouseDown(event: MouseEvent): void {
public onMouseDown(event: MouseEvent): void {
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
if (event.button === 2 && this.hasSelection) {
event.stopPropagation();
return;
}
@@ -364,8 +361,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _addMouseDownListeners(): void {
// Listen on the document so that dragging outside of viewport works
this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
this._terminal.element.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
}
@@ -373,8 +370,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Removes the listeners that are registered when mousedown is triggered.
*/
private _removeMouseDownListeners(): void {
this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
clearInterval(this._dragScrollIntervalTimer);
this._dragScrollIntervalTimer = null;
}
@@ -413,6 +410,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return;
}
// Return early if the click event is not in the buffer (eg. in scroll bar)
if (line.length >= this._model.selectionStart[0]) {
return;
}
// If the mouse is over the second half of a wide character, adjust the
// selection to cover the whole character
const char = line[this._model.selectionStart[0]];
@@ -452,6 +454,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mousemove event.
*/
private _onMouseMove(event: MouseEvent): void {
// If the mousemove listener is active it means that a selection is
// currently being made, we should stop propogation to prevent mouse events
// to be sent to the pty.
event.stopImmediatePropagation();
// Record the previous position so we know whether to redraw the selection
// at the end.
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file contains integration tests for xterm.js.
+147 -3
View File
@@ -1,8 +1,16 @@
import { assert, expect } from 'chai';
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper } from './utils/TestUtils.test';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
const INIT_COLS = 80;
const INIT_ROWS = 24;
class TestTerminal extends Terminal {
public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollDisp: number} { return this._evaluateKeyEscapeSequence(<KeyboardEvent>ev); }
public keyDown(ev: any): boolean { return this._keyDown(ev); }
@@ -13,8 +21,12 @@ describe('term.js addons', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal();
term = new TestTerminal({
cols: INIT_COLS,
rows: INIT_ROWS
});
term.refresh = () => {};
(<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(<any>term).compositionHelper = new MockCompositionHelper();
// Force synchronous writes
@@ -283,6 +295,138 @@ describe('term.js addons', () => {
assert.equal(term.buffer.ydisp, startYDisp - 1);
});
});
describe('scroll() function', () => {
describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
});
});
describe('when scrollback === 0', () => {
beforeEach(() => {
term.setOption('scrollback', 0);
assert.equal(term.buffer.lines.maxLength, INIT_ROWS);
});
it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
// 'a' gets pushed out of buffer
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' ');
assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
});
});
});
});
describe('evaluateKeyEscapeSequence', () => {
+268 -385
View File
@@ -1,5 +1,8 @@
/**
* xterm.js: xterm, in the browser
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
@@ -7,7 +10,6 @@
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
* @license MIT
*
* Terminal Emulation References:
* http://vt100.net/
@@ -29,20 +31,27 @@ import { CircularList } from './utils/CircularList';
import { C0 } from './EscapeSequences';
import { InputHandler } from './InputHandler';
import { Parser } from './Parser';
import { Renderer } from './Renderer';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
import * as Mouse from './utils/Mouse';
import { MouseHelper } from './utils/MouseHelper';
import { CHARSETS } from './Charsets';
import { getRawByteCoords } from './utils/Mouse';
import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces';
import { BellSound } from './utils/Sounds';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZoneManager } from './input/MouseZoneManager';
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
import { IRenderer } from './renderer/Interfaces';
// Declare for RequireJS in loadAddon
// Declares required for loadAddon
declare var exports: any;
declare var module: any;
declare var define: any;
declare var require: any;
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -60,89 +69,7 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
*/
const 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
*/
const CURSOR_BLINK_INTERVAL = 600;
// TODO: Most of the color code should be removed after truecolor is implemented
// Colors 0-15
const tangoColors: string[] = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
// Colors 0-15 + 16-255
// Much thanks to TooTallNate for writing this.
const defaultColors: string[] = (function(): string[] {
let colors = tangoColors.slice();
let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
let i;
// 16-231
i = 0;
for (; i < 216; i++) {
out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]);
}
// 232-255 (grey)
i = 0;
let c: number;
for (; i < 24; i++) {
c = 8 + i * 10;
out(c, c, c);
}
function out(r: number, g: number, b: number): void {
colors.push('#' + hex(r) + hex(g) + hex(b));
}
function hex(c: number): string {
let s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
return colors;
})();
const _colors: string[] = defaultColors.slice();
const vcolors: number[][] = (function(): number[][] {
const out: number[][] = [];
let color;
for (let i = 0; i < 256; i++) {
color = parseInt(defaultColors[i].substring(1), 16);
out.push([
(color >> 16) & 0xff,
(color >> 8) & 0xff,
color & 0xff
]);
}
return out;
})();
const DEFAULT_OPTIONS: ITerminalOptions = {
colors: defaultColors,
convertEol: false,
termName: 'xterm',
geometry: [80, 24],
@@ -150,13 +77,17 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
cursorStyle: 'block',
bellSound: BellSound,
bellStyle: 'none',
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
lineHeight: 1.0,
scrollback: 1000,
screenKeys: false,
debug: false,
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
tabStopWidth: 8
tabStopWidth: 8,
theme: null
// programFeatures: false,
// focusKeys: false,
};
@@ -164,7 +95,6 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement;
public element: HTMLElement;
public rowContainer: HTMLElement;
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
@@ -175,7 +105,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private body: HTMLBodyElement;
private viewportScrollArea: HTMLElement;
private viewportElement: HTMLElement;
public selectionContainer: HTMLElement;
private helperContainer: HTMLElement;
private compositionView: HTMLElement;
private charSizeStyleElement: HTMLStyleElement;
@@ -194,10 +123,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private sendDataQueue: string;
private customKeyEventHandler: CustomKeyEventHandler;
// The ID from a setInterval that tracks the blink animation. This animation
// is done in JS due to a Chromium bug with CSS animations that thrashed the
// CPU.
private cursorBlinkInterval: NodeJS.Timer;
// modes
public applicationKeypad: boolean;
@@ -226,7 +151,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public urxvtMouse: boolean;
// misc
public children: HTMLElement[];
private refreshStart: number;
private refreshEnd: number;
public savedCols: number;
@@ -266,14 +190,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private inputHandler: InputHandler;
private parser: Parser;
private renderer: Renderer;
public renderer: IRenderer;
public selectionManager: SelectionManager;
private linkifier: Linkifier;
public linkifier: ILinkifier;
public buffers: BufferSet;
public buffer: Buffer;
public viewport: IViewport;
private compositionHelper: ICompositionHelper;
public charMeasure: CharMeasure;
private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper;
public cols: number;
public rows: number;
@@ -308,19 +234,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this[key] = this.options[key];
});
if (this.options.colors.length === 8) {
this.options.colors = this.options.colors.concat(_colors.slice(8));
} else if (this.options.colors.length === 16) {
this.options.colors = this.options.colors.concat(_colors.slice(16));
} else if (this.options.colors.length === 10) {
this.options.colors = this.options.colors.slice(0, -2).concat(
_colors.slice(8, -2), this.options.colors.slice(-2));
} else if (this.options.colors.length === 18) {
this.options.colors = this.options.colors.concat(
_colors.slice(16, -2), this.options.colors.slice(-2));
}
this.colors = this.options.colors;
// this.context = options.context || window;
// this.document = options.document || document;
// TODO: WHy not document.body?
@@ -338,7 +251,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.cursorHidden = false;
this.sendDataQueue = '';
this.customKeyEventHandler = null;
this.cursorBlinkInterval = null;
// modes
this.applicationKeypad = false;
@@ -379,7 +291,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Reuse renderer if the Terminal is being recreated via a reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier();
this.linkifier = this.linkifier || new Linkifier(this);
this._mouseZoneManager = this._mouseZoneManager || null;
// Create the terminal's buffers and set the current buffer
this.buffers = new BufferSet(this);
@@ -392,8 +305,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (this.selectionManager) {
this.selectionManager.setBuffer(this.buffer);
}
this.setupStops();
}
/**
@@ -411,6 +322,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea.focus();
}
public get isFocused(): boolean {
return document.activeElement === this.textarea;
}
/**
* Retrieves an option's value from the terminal.
* @param {string} key The option key.
@@ -443,17 +358,34 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
break;
case 'cursorStyle':
if (!value) {
value = 'block';
}
break;
if (!value) {
value = 'block';
}
break;
case 'lineHeight':
if (value < 1) {
console.warn(`${key} cannot be less than 1, value: ${value}`);
return;
}
case 'tabStopWidth':
if (value < 1) {
console.warn(`tabStopWidth cannot be less than 1, value: ${value}`);
console.warn(`${key} cannot be less than 1, value: ${value}`);
return;
}
break;
case 'theme':
// If open has been called we do not want to set options.theme as the
// source of truth is owned by the renderer.
if (this.renderer) {
this._setTheme(<ITheme>value);
return;
}
break;
case 'scrollback':
if (value < 0) {
console.warn(`${key} cannot be less than 0, value: ${value}`);
return;
}
if (this.options[key] !== value) {
const newBufferLength = this.rows + value;
if (this.buffer.lines.length > newBufferLength) {
@@ -472,58 +404,42 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this[key] = value;
this.options[key] = value;
switch (key) {
case 'cursorBlink': this.setCursorBlinking(value); break;
case 'cursorStyle':
this.element.classList.toggle(`xterm-cursor-style-block`, value === 'block');
this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline');
this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar');
case 'fontFamily':
case 'fontSize':
// When the font changes the size of the cells may change which requires a renderer clear
this.renderer.clear();
this.charMeasure.measure(this.options);
break;
case 'lineHeight':
// When the font changes the size of the cells may change which requires a renderer clear
this.renderer.clear();
this.renderer.onResize(this.cols, this.rows, false);
this.refresh(0, this.rows - 1);
// this.charMeasure.measure(this.options);
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
this.viewport.syncScrollArea();
break;
case 'tabStopWidth': this.setupStops(); break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'bellSound':
case 'bellStyle': this.syncBellSound(); break;
}
}
private restartCursorBlinking(): void {
this.setCursorBlinking(this.options.cursorBlink);
}
private setCursorBlinking(enabled: boolean): void {
this.element.classList.toggle('xterm-cursor-blink', enabled);
this.clearCursorBlinkingInterval();
if (enabled) {
this.cursorBlinkInterval = setInterval(() => {
this.element.classList.toggle('xterm-cursor-blink-on');
}, CURSOR_BLINK_INTERVAL);
}
}
private clearCursorBlinkingInterval(): void {
this.element.classList.remove('xterm-cursor-blink-on');
if (this.cursorBlinkInterval) {
clearInterval(this.cursorBlinkInterval);
this.cursorBlinkInterval = null;
// Inform renderer of changes
if (this.renderer) {
this.renderer.onOptionsChanged();
}
}
/**
* Binds the desired focus behavior on a given terminal object.
*/
private bindFocus(): void {
globalOn(this.textarea, 'focus', (ev) => {
if (this.sendFocus) {
this.send(C0.ESC + '[I');
}
this.element.classList.add('focus');
this.showCursor();
this.restartCursorBlinking.apply(this);
// TODO: Why pass terminal here?
this.emit('focus');
});
private _onTextAreaFocus(): void {
if (this.sendFocus) {
this.send(C0.ESC + '[I');
}
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
};
/**
@@ -537,17 +453,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
/**
* Binds the desired blur behavior on a given terminal object.
*/
private bindBlur(): void {
on(this.textarea, 'blur', (ev) => {
this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) {
this.send(C0.ESC + '[O');
}
this.element.classList.remove('focus');
this.clearCursorBlinkingInterval.apply(this);
// TODO: Why pass terminal here?
this.emit('blur');
});
private _onTextAreaBlur(): void {
this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) {
this.send(C0.ESC + '[O');
}
this.element.classList.remove('focus');
this.emit('blur');
}
/**
@@ -555,8 +467,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/
private initGlobal(): void {
this.bindKeys();
this.bindFocus();
this.bindBlur();
// Bind clipboard functionality
on(this.element, 'copy', (event: ClipboardEvent) => {
@@ -641,22 +551,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('refresh', (data) => this.queueLinkification(data.start, data.end));
}
/**
* Insert the given row to the terminal or produce a new one
* if no row argument is passed. Return the inserted row.
* @param {HTMLElement} row (optional) The row to append to the terminal.
*/
private insertRow(row?: HTMLElement): HTMLElement {
if (typeof row !== 'object') {
row = document.createElement('div');
}
this.rowContainer.appendChild(row);
this.children.push(row);
return row;
};
/**
* Opens the terminal within an element.
*
@@ -677,12 +571,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.document = this.parent.ownerDocument;
this.body = <HTMLBodyElement>this.document.body;
initializeCharAtlas(this.document);
// Create main element container
this.element = this.document.createElement('div');
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.classList.add(`xterm-cursor-style-${this.options.cursorStyle}`);
this.setCursorBlinking(this.options.cursorBlink);
this.element.setAttribute('tabindex', '0');
@@ -696,18 +590,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// preload audio
this.syncBellSound();
// 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);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
@@ -721,8 +606,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.textarea.addEventListener('focus', () => this.emit('focus'));
this.textarea.addEventListener('blur', () => this.emit('blur'));
this.textarea.addEventListener('focus', () => this._onTextAreaFocus());
this.textarea.addEventListener('blur', () => this._onTextAreaBlur());
this.helperContainer.appendChild(this.textarea);
this.compositionView = document.createElement('div');
@@ -733,23 +618,26 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.charSizeStyleElement = document.createElement('style');
this.helperContainer.appendChild(this.charSizeStyleElement);
for (; i < this.rows; i++) {
this.insertRow();
}
this.parent.appendChild(this.element);
this.charMeasure = new CharMeasure(document, this.helperContainer);
this.charMeasure.on('charsizechanged', () => {
this.updateCharSizeStyles();
});
this.charMeasure.measure();
this.renderer = new Renderer(this, this.options.theme);
this.options.theme = null;
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
this.renderer = new Renderer(this);
this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure);
this.selectionManager.on('refresh', data => {
this.renderer.refreshSelection(data.start, data.end);
});
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(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
@@ -758,9 +646,17 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea.focus();
this.textarea.select();
});
this.on('scroll', () => this.selectionManager.refresh());
this.on('scroll', () => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
});
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
this.mouseHelper = new MouseHelper(this.renderer);
// Measure the character size
this.charMeasure.measure(this.options);
// Setup loop that draws to screen
this.refresh(0, this.rows - 1);
@@ -772,6 +668,17 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.bindMouse();
}
/**
* Sets the theme on the renderer. The renderer must have been initialized.
* @param theme The theme to ste.
*/
private _setTheme(theme: ITheme): void {
const colors = this.renderer.setTheme(theme);
if (this.viewport) {
this.viewport.onThemeChanged(colors);
}
}
/**
* Attempts to load an add-on using CommonJS or RequireJS (whichever is available).
* @param {string} addon The name of the addon to load
@@ -791,17 +698,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
}
/**
* Updates the helper CSS class with any changes necessary after the terminal's
* character width has been changed.
*/
public updateCharSizeStyles(): void {
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;}`;
}
/**
* XTerm mouse events
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
@@ -828,7 +724,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
button = getButton(ev);
// get mouse coordinates
pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
@@ -854,7 +750,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
@@ -1141,9 +1037,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/
private queueLinkification(start: number, end: number): void {
if (this.linkifier) {
for (let i = start; i <= end; i++) {
this.linkifier.linkifyRow(i);
}
this.linkifier.linkifyRows(start, end);
}
}
@@ -1159,52 +1053,53 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
/**
* Scroll the terminal down 1 row, creating a blank line.
* @param {boolean} isWrapped Whether the new line is wrapped from the previous
* line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(isWrapped?: boolean): void {
let row;
const newLine = this.blankLine(undefined, isWrapped);
const topRow = this.buffer.ybase + this.buffer.scrollTop;
let bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
// Make room for the new row in lines
const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength;
if (bufferNeedsTrimming) {
this.buffer.lines.trimStart(1);
this.buffer.ybase--;
this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0);
if (this.buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength;
// Insert the line using the fastest method
if (bottomRow === this.buffer.lines.length - 1) {
this.buffer.lines.push(newLine);
} else {
this.buffer.lines.splice(bottomRow + 1, 0, newLine);
}
// Only adjust ybase and ydisp when the buffer is not trimmed
if (!willBufferBeTrimmed) {
this.buffer.ybase++;
// Only scroll the ydisp with ybase if the user has not scrolled up
if (!this.userScrolling) {
this.buffer.ydisp++;
}
} else {
// When the buffer is full and the user has scrolled up, keep the text
// stable unless ydisp is right at the top
if (this.userScrolling) {
this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0);
}
}
} else {
// scrollTop is non-zero which means no line will be going to the
// scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/;
this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
this.buffer.lines.set(bottomRow, newLine);
}
this.buffer.ybase++;
// Scroll the viewport down to the bottom if the user is not scrolling
// Move the viewport to the bottom of the buffer unless the user is
// scrolling.
if (!this.userScrolling) {
this.buffer.ydisp = this.buffer.ybase;
}
// last line
row = this.buffer.ybase + this.rows - 1;
// subtract the bottom scroll region
row -= this.rows - 1 - this.buffer.scrollBottom;
if (row === this.buffer.lines.length) {
// Optimization: pushing is faster than splicing when they amount to the same behavior
this.buffer.lines.push(this.blankLine(undefined, isWrapped));
} else {
// add our new line
this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped));
}
if (this.buffer.scrollTop !== 0) {
if (this.buffer.ybase !== 0) {
this.buffer.ybase--;
if (!this.userScrolling) {
this.buffer.ydisp = this.buffer.ybase;
}
}
this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1);
}
// this.maxRange();
// Flag rows that need updating
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
@@ -1379,7 +1274,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
}
this.linkifier.setHypertextValidationCallback(callback);
// Refresh to force links to refresh
// // Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
@@ -1399,6 +1294,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.refresh(0, this.rows - 1);
return matcherId;
}
return 0;
}
/**
@@ -1457,8 +1353,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return false;
}
this.restartCursorBlinking();
if (!this.compositionHelper.keydown(ev)) {
if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
@@ -1910,17 +1804,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return;
}
let line;
let el;
let i;
let j;
let ch;
let 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();
this.charMeasure.measure(this.options);
}
return;
}
@@ -1930,21 +1817,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.buffers.resize(x, y);
// Adjust rows in the DOM to accurately reflect the new dimensions
while (this.children.length < y) {
this.insertRow();
}
while (this.children.length > y) {
el = this.children.shift();
if (!el) continue;
el.parentNode.removeChild(el);
}
this.cols = x;
this.rows = y;
this.setupStops(this.cols);
this.buffers.setupTabStops(this.cols);
this.charMeasure.measure();
this.charMeasure.measure(this.options);
this.refresh(0, this.rows - 1);
@@ -1975,45 +1852,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.refreshEnd = this.rows - 1;
}
/**
* Setup the tab stops.
* @param {number} i
*/
public setupStops(i?: number): void {
if (i != null) {
if (!this.buffer.tabs[i]) {
i = this.prevStop(i);
}
} else {
this.buffer.tabs = {};
i = 0;
}
for (; i < this.cols; i += this.getOption('tabStopWidth')) {
this.buffer.tabs[i] = true;
}
}
/**
* Move the cursor to the previous tab stop from the given position (default is current).
* @param {number} x The position to move the cursor to the previous tab stop.
*/
public prevStop(x?: number): number {
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;
}
/**
* Move the cursor one tab stop forward from the given position (default is current).
* @param {number} x The position to move the cursor one tab stop forward.
*/
public nextStop(x?: number): number {
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;
}
/**
* Erase in the identified line everything from "x" to the end of the line (right).
* @param {number} x The column from which to start erasing to the end of the line.
@@ -2024,7 +1862,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (!line) {
return;
}
const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
for (; x < this.cols; x++) {
line[x] = ch;
}
@@ -2041,7 +1879,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (!line) {
return;
}
const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
x++;
while (x--) {
line[x] = ch;
@@ -2087,7 +1925,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
const attr = cur ? this.eraseAttr() : this.defAttr;
const ch: CharData = [attr, ' ', 1]; // width defaults to 1 halfwidth character
const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character
const line: LineData = [];
// TODO: It is not ideal that this is a property on an array, a buffer line
@@ -2109,7 +1947,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param cur
*/
public ch(cur?: boolean): CharData {
return cur ? [this.eraseAttr(), ' ', 1] : [this.defAttr, ' ', 1];
if (cur) {
return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */];
}
return [this.defAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */];
}
/**
@@ -2185,7 +2026,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// 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.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1);
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
@@ -2201,12 +2043,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.options.rows = this.rows;
this.options.cols = this.cols;
const customKeyEventHandler = this.customKeyEventHandler;
const cursorBlinkInterval = this.cursorBlinkInterval;
const inputHandler = this.inputHandler;
const buffers = this.buffers;
this.setup();
this.customKeyEventHandler = customKeyEventHandler;
this.cursorBlinkInterval = cursorBlinkInterval;
this.inputHandler = inputHandler;
this.buffers = buffers;
this.refresh(0, this.rows - 1);
@@ -2231,44 +2071,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return false;
}
// Expose to InputHandler
// TODO: Revise when truecolor is introduced.
// TODO: Remove when true color is implemented
public matchColor(r1: number, g1: number, b1: number): number {
const hash = (r1 << 16) | (g1 << 8) | b1;
if (matchColorCache[hash] != null) {
return matchColorCache[hash];
}
let ldiff = Infinity;
let li = -1;
let i = 0;
let c: number[];
let r2: number;
let g2: number;
let b2: number;
let diff: number;
for (; i < vcolors.length; i++) {
c = vcolors[i];
r2 = c[0];
g2 = c[1];
b2 = c[2];
diff = matchColorDistance(r1, g1, b1, r2, g2, b2);
if (diff === 0) {
li = i;
break;
}
if (diff < ldiff) {
ldiff = diff;
li = i;
}
}
return matchColorCache[hash] = li;
return matchColor_(r1, g1, b1);
}
private visualBell(): boolean {
@@ -2327,17 +2132,95 @@ function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
const matchColorCache: {[colorRGBHash: number]: number} = {};
// http://stackoverflow.com/questions/1633828
const matchColorDistance = function(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2);
};
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
ev.keyCode === 18; // Alt
}
/**
* TODO:
* The below color-related code can be removed when true color is implemented.
* It's only purpose is to match true color requests with the closest matching
* ANSI color code.
*/
// Colors 0-15 + 16-255
// Much thanks to TooTallNate for writing this.
const vcolors: number[][] = (function(): number[][] {
const result = DEFAULT_ANSI_COLORS.map(c => {
c = c.substring(1);
return [
parseInt(c.substring(0, 2), 16),
parseInt(c.substring(2, 4), 16),
parseInt(c.substring(4, 6), 16)
];
});
const r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
// 16-231
for (let i = 0; i < 216; i++) {
result.push([
r[(i / 36) % 6 | 0],
r[(i / 6) % 6 | 0],
r[i % 6]
]);
}
// 232-255 (grey)
let c: number;
for (let i = 0; i < 24; i++) {
c = 8 + i * 10;
result.push([c, c, c]);
}
return result;
})();
const matchColorCache: {[colorRGBHash: number]: number} = {};
// http://stackoverflow.com/questions/1633828
function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2);
};
function matchColor_(r1: number, g1: number, b1: number): number {
const hash = (r1 << 16) | (g1 << 8) | b1;
if (matchColorCache[hash] != null) {
return matchColorCache[hash];
}
let ldiff = Infinity;
let li = -1;
let i = 0;
let c: number[];
let r2: number;
let g2: number;
let b2: number;
let diff: number;
for (; i < vcolors.length; i++) {
c = vcolors[i];
r2 = c[0];
g2 = c[1];
b2 = c[2];
diff = matchColorDistance(r1, g1, b1, r2, g2, b2);
if (diff === 0) {
li = i;
break;
}
if (diff < ldiff) {
ldiff = diff;
li = i;
}
}
return matchColorCache[hash] = li;
}
+17 -2
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -6,15 +7,29 @@ export type LinkMatcher = {
id: number,
regex: RegExp,
handler: LinkMatcherHandler,
hoverTooltipCallback?: LinkMatcherHandler,
hoverLeaveCallback?: () => void,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type Charset = {[key: string]: string};
export type CharData = [number, string, number];
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkHoverEvent = {
x: number,
y: number,
length: number
};
export enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
};
-100
View File
@@ -1,100 +0,0 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { Viewport } from './Viewport';
import { BufferSet } from './BufferSet';
describe('Viewport', () => {
let terminal;
let viewportElement;
let charMeasure;
let viewport;
let scrollAreaElement;
const CHARACTER_HEIGHT = 10;
beforeEach(() => {
terminal = {
rows: 0,
ydisp: 0,
on: () => {},
rowContainer: {
style: {
lineHeight: 0
}
},
selectionContainer: {
style: {
height: 0
}
},
options: {
scrollback: 10
}
};
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
viewportElement = {
addEventListener: () => {},
style: {
height: 0,
lineHeight: 0
}
};
scrollAreaElement = {
style: {
height: 0
}
};
charMeasure = {
height: CHARACTER_HEIGHT
};
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure);
});
describe('refresh', () => {
it('should set the line-height of the terminal', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
charMeasure.height = 1;
viewport.refresh();
assert.equal(viewportElement.style.lineHeight, '1px');
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
done();
}, 0);
});
it('should set the height of the viewport when the line-height changed', () => {
terminal.buffer.lines.push('');
terminal.buffer.lines.push('');
terminal.rows = 1;
viewport.refresh();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
charMeasure.height = 2 * CHARACTER_HEIGHT;
viewport.refresh();
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
});
});
describe('syncScrollArea', () => {
it('should sync the scroll area', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
terminal.buffer.lines.push('');
terminal.rows = 1;
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
terminal.buffer.lines.push('');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
done();
}, 0);
});
});
});
+22 -22
View File
@@ -1,18 +1,21 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, IViewport } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { IColorSet } from './renderer/Interfaces';
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
private currentRowHeight: number;
private lastRecordedBufferLength: number;
private lastRecordedViewportHeight: number;
private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0;
private lastRecordedBufferHeight: number = 0;
private lastTouchY: number;
/**
@@ -28,37 +31,34 @@ export class Viewport implements IViewport {
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
) {
this.currentRowHeight = 0;
this.lastRecordedBufferLength = 0;
this.lastRecordedViewportHeight = 0;
this.terminal.on('scroll', this.syncScrollArea.bind(this));
this.terminal.on('resize', this.syncScrollArea.bind(this));
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChanged(colors: IColorSet): void {
this.viewportElement.style.backgroundColor = colors.background;
}
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight;
if (rowHeightChanged) {
this.currentRowHeight = this.charMeasure.height;
this.viewportElement.style.lineHeight = this.charMeasure.height + 'px';
this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px';
this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio;
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
}
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
if (rowHeightChanged || viewportHeightChanged) {
this.lastRecordedViewportHeight = this.terminal.rows;
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
}
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
}
}
@@ -70,12 +70,12 @@ export class Viewport implements IViewport {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
this.refresh();
} else {
// If size has changed, refresh viewport
if (this.charMeasure.height !== this.currentRowHeight) {
if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) {
this.refresh();
}
}
+3 -4
View File
@@ -1,7 +1,8 @@
/**
* Implements the attach method, that attaches the terminal to a WebSocket stream.
* @module xterm/addons/attach/attach
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
(function (attach) {
@@ -44,8 +45,6 @@
term._flushBuffer = function () {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
clearTimeout(term._attachSocketBufferTimer);
term._attachSocketBufferTimer = null;
};
term._pushToBuffer = function (data) {
+32 -37
View File
@@ -1,14 +1,16 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*
* Fit terminal columns and rows to the dimensions of its DOM element.
*
* ## Approach
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
*
* - Columns: Truncate the division of the terminal parent element width by the terminal character
* width (apply display: inline at the terminal row and truncate its width with the current
* number of columns).
* @module xterm/addons/fit/fit
* @license MIT
* Rows: Truncate the division of the terminal parent element height by the
* terminal row height.
* Columns: Truncate the division of the terminal parent element width by the
* terminal character width (apply display: inline at the terminal
* row and truncate its width with the current number of columns).
*/
(function (fit) {
@@ -35,43 +37,36 @@
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
elementStyle = window.getComputedStyle(term.element),
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
availableHeight = parentElementHeight - elementPaddingVer,
availableWidth = parentElementWidth - elementPaddingHor,
container = term.rowContainer,
subjectRow = term.rowContainer.firstElementChild,
contentBuffer = subjectRow.innerHTML,
characterHeight,
rows,
characterWidth,
cols,
geometry;
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
var elementStyle = window.getComputedStyle(term.element);
var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor;
var geometry = {
cols: Math.floor(availableWidth / term.charMeasure.width),
rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight')))
};
subjectRow.style.display = 'inline';
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = subjectRow.getBoundingClientRect().height;
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
cols = parseInt(availableWidth / characterWidth);
geometry = {cols: cols, rows: rows};
return geometry;
};
exports.fit = function (term) {
var geometry = exports.proposeGeometry(term);
// Wrap fit in a setTimeout as charMeasure needs time to get initialized
// after calling Terminal.open
setTimeout(() => {
var geometry = exports.proposeGeometry(term);
if (geometry) {
term.resize(geometry.cols, geometry.rows);
}
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
}, 0);
};
Terminal.prototype.proposeGeometry = function () {
+2 -2
View File
@@ -1,8 +1,8 @@
/**
* Fullscreen addon for xterm.js
* @module xterm/addons/fullscreen/fullscreen
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* @license MIT
*/
(function (fullscreen) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+4 -5
View File
@@ -1,8 +1,9 @@
/**
* This module provides methods for attaching a terminal to a terminado WebSocket stream.
*
* @module xterm/addons/terminado/terminado
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*
* This module provides methods for attaching a terminal to a terminado
* WebSocket stream.
*/
(function (attach) {
@@ -45,8 +46,6 @@
term._flushBuffer = function () {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
clearTimeout(term._attachSocketBufferTimer);
term._attachSocketBufferTimer = null;
};
term._pushToBuffer = function (data) {
+5
View File
@@ -1,3 +1,8 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import * as Terminal from '../Terminal';
import * as Clipboard from './Clipboard';
+1 -3
View File
@@ -1,7 +1,5 @@
/**
* Clipboard handler module: exports methods for handling all clipboard-related events in the
* terminal.
* @module xterm/handlers/Clipboard
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface IMouseZoneManager {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
export interface IMouseZone {
x1: number;
x2: number;
y: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback?: (e: MouseEvent) => any;
tooltipCallback?: (e: MouseEvent) => any;
leaveCallback?: () => any;
}
+174
View File
@@ -0,0 +1,174 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IMouseZoneManager, IMouseZone } from './Interfaces';
import { ITerminal } from '../Interfaces';
const HOVER_DURATION = 500;
/**
* The MouseZoneManager allows components to register zones within the terminal
* that trigger hover and click callbacks.
*
* This class was intentionally made not so robust initially as the only case it
* needed to support was single-line links which never overlap. Improvements can
* be made in the future.
*/
export class MouseZoneManager implements IMouseZoneManager {
private _zones: IMouseZone[] = [];
private _areZonesActive: boolean = false;
private _mouseMoveListener: (e: MouseEvent) => any;
private _mouseDownListener: (e: MouseEvent) => any;
private _clickListener: (e: MouseEvent) => any;
private _tooltipTimeout: number = null;
private _currentZone: IMouseZone = null;
private _lastHoverCoords: [number, number] = [null, null];
constructor(
private _terminal: ITerminal
) {
// These events are expensive, only listen to it when mouse zones are active
this._mouseMoveListener = e => this._onMouseMove(e);
this._clickListener = e => this._onClick(e);
}
public add(zone: IMouseZone): void {
this._zones.push(zone);
if (this._zones.length === 1) {
this._activate();
}
}
public clearAll(start?: number, end?: number): void {
// Exit if there's nothing to clear
if (this._zones.length === 0) {
return;
}
// Clear all if start/end weren't set
if (!end) {
start = 0;
end = this._terminal.rows - 1;
}
// Iterate through zones and clear them out if they're within the range
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if (zone.y > start && zone.y <= end + 1) {
if (this._currentZone && this._currentZone === zone) {
this._currentZone.leaveCallback();
this._currentZone = null;
}
this._zones.splice(i--, 1);
}
}
// Deactivate the mouse zone manager if all the zones have been removed
if (this._zones.length === 0) {
this._deactivate();
}
}
private _activate(): void {
if (!this._areZonesActive) {
this._areZonesActive = true;
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.addEventListener('click', this._clickListener);
}
}
private _deactivate(): void {
if (this._areZonesActive) {
this._areZonesActive = false;
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.removeEventListener('click', this._clickListener);
}
}
private _onMouseMove(e: MouseEvent): void {
// TODO: Ideally this would only clear the hover state when the mouse moves
// outside of the mouse zone
if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
this._onHover(e);
// Record the current coordinates
this._lastHoverCoords = [e.pageX, e.pageY];
}
}
private _onHover(e: MouseEvent): void {
const zone = this._findZoneEventAt(e);
// Do nothing if the zone is the same
if (zone === this._currentZone) {
return;
}
// Fire the hover end callback and cancel any existing timer if a new zone
// is being hovered
if (this._currentZone) {
this._currentZone.leaveCallback();
this._currentZone = null;
if (this._tooltipTimeout) {
clearTimeout(this._tooltipTimeout);
}
}
// Exit if there is not zone
if (!zone) {
return;
}
this._currentZone = zone;
// Trigger the hover callback
if (zone.hoverCallback) {
zone.hoverCallback(e);
}
// Restart the tooltip timeout
this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
}
private _onTooltip(e: MouseEvent): void {
this._tooltipTimeout = null;
const zone = this._findZoneEventAt(e);
if (zone && zone.tooltipCallback) {
zone.tooltipCallback(e);
}
}
private _onClick(e: MouseEvent): void {
const zone = this._findZoneEventAt(e);
if (zone) {
zone.clickCallback(e);
e.preventDefault();
}
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
return zone;
}
};
return null;
}
}
export class MouseZone implements IMouseZone {
constructor(
public x1: number,
public x2: number,
public y: number,
public clickCallback: (e: MouseEvent) => any,
public hoverCallback?: (e: MouseEvent) => any,
public tooltipCallback?: (e: MouseEvent) => any,
public leaveCallback?: () => void
) {
}
}
+301
View File
@@ -0,0 +1,301 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
import { ITerminal, ITerminalOptions } from '../Interfaces';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CharData } from '../Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1;
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx: CanvasRenderingContext2D;
private _scaledCharWidth: number;
private _scaledCharHeight: number;
private _scaledLineHeight: number;
private _scaledLineDrawY: number;
private _charAtlas: HTMLCanvasElement | ImageBitmap;
constructor(
container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d', {_alpha});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Draw the background if this is an opaque layer
if (!_alpha) {
this.clearAll();
}
container.appendChild(this._canvas);
}
public onOptionsChanged(terminal: ITerminal): void {}
public onBlur(terminal: ITerminal): void {}
public onFocus(terminal: ITerminal): void {}
public onCursorMove(terminal: ITerminal): void {}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {}
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
this._refreshCharAtlas(terminal, colorSet);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
result.then(bitmap => this._charAtlas = bitmap);
}
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledLineHeight = dim.scaledLineHeight;
this._scaledLineDrawY = dim.scaledLineDrawY;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this.clearAll();
}
if (charSizeChanged) {
this._refreshCharAtlas(terminal, this._colors);
}
}
public abstract reset(terminal: ITerminal): void;
/**
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
* @param x The column to start at.
* @param y The row to start at
* @param width The number of columns to fill.
* @param height The number of rows to fill.
*/
protected fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._scaledCharWidth,
y * this._scaledLineHeight,
width * this._scaledCharWidth,
height * this._scaledLineHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
this._ctx.fillRect(
x * this._scaledCharWidth,
(y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
width * this._scaledCharWidth,
window.devicePixelRatio);
}
/**
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected fillLeftLineAtCell(x: number, y: number): void {
this._ctx.fillRect(
x * this._scaledCharWidth,
y * this._scaledLineHeight,
window.devicePixelRatio,
this._scaledLineHeight);
}
/**
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
* strokeStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected strokeRectAtCell(x: number, y: number, width: number, height: number): void {
this._ctx.lineWidth = window.devicePixelRatio;
this._ctx.strokeRect(
x * this._scaledCharWidth + window.devicePixelRatio / 2,
y * this._scaledLineHeight + (window.devicePixelRatio / 2),
width * this._scaledCharWidth - window.devicePixelRatio,
(height * this._scaledLineHeight) - window.devicePixelRatio);
}
/**
* Clears the entire canvas.
*/
protected clearAll(): void {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
/**
* Clears 1+ cells completely.
* @param x The column to start at.
* @param y The row to start at.
* @param width The number of columns to clear.
* @param height The number of rows to clear.
*/
protected clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._scaledCharWidth,
y * this._scaledLineHeight,
width * this._scaledCharWidth,
height * this._scaledLineHeight);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillRect(
x * this._scaledCharWidth,
y * this._scaledLineHeight,
width * this._scaledCharWidth,
height * this._scaledLineHeight);
}
}
/**
* Draws a truecolor character at the cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character. This uses the existing fillStyle on the context.
* @param terminal The terminal.
* @param charData The char data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
* @param color The color of the character.
*/
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
this._ctx.textBaseline = 'top';
// Since uncached characters are not coming off the char atlas with source
// coordinates, it means that text drawn to the canvas (particularly '_')
// can bleed into other cells. This code will clip the following fillText,
// ensuring that its contents don't go beyond the cell bounds.
this._ctx.beginPath();
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight);
this._ctx.clip();
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight);
}
/**
* Draws a character at a cell. If possible this will draw using the character
* atlas to reduce draw time.
* @param terminal The terminal.
* @param char The character.
* @param code The character code.
* @param width The width of the character.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void {
// Clear the cell next to this character if it's wide
if (width === 2) {
this.clearCells(x + 1, y, 1, 1);
}
let colorIndex = 0;
if (fg < 256) {
colorIndex = fg + 2;
} else {
// If default color and bold
if (bold) {
colorIndex = 1;
}
}
const isAscii = code < 256;
const isBasicColor = (colorIndex > 1 && fg < 16);
const isDefaultColor = fg >= 256;
const isDefaultBackground = bg >= 256;
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight,
x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight);
} else {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
// this._ctx.drawImage(this._charAtlas, 0, 0);
}
/**
* Draws a character at a cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character.
* @param terminal The terminal.
* @param char The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void {
this._ctx.save();
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (bold) {
this._ctx.font = `bold ${this._ctx.font}`;
}
this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
} else {
this._ctx.fillStyle = this._colors.foreground;
}
// Since uncached characters are not coming off the char atlas with source
// coordinates, it means that text drawn to the canvas (particularly '_')
// can bleed into other cells. This code will clip the following fillText,
// ensuring that its contents don't go beyond the cell bounds.
this._ctx.beginPath();
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, width * this._scaledCharWidth, this._scaledCharHeight);
this._ctx.clip();
// Draw the character
this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY);
this._ctx.restore();
}
}
+191
View File
@@ -0,0 +1,191 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { IColorSet } from '../renderer/Interfaces';
import { isFirefox } from '../utils/Browser';
export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
scaledCharWidth: number;
scaledCharHeight: number;
colors: IColorSet;
}
interface ICharAtlasCacheEntry {
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
config: ICharAtlasConfig;
ownedBy: ITerminal[];
}
let charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.bitmap;
} else {
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.bitmap;
}
}
const newEntry: ICharAtlasCacheEntry = {
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.bitmap;
}
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
colors: clonedColors
};
}
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
let generator: CharAtlasGenerator;
/**
* Initializes the char atlas generator.
* @param document The document.
*/
export function initialize(document: Document): void {
if (!generator) {
generator = new CharAtlasGenerator(document);
}
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor(private _document: Document) {
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d', {alpha: false});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._canvas.width = 255 * cellWidth;
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
this._ctx.fillStyle = background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
this._ctx.save();
this._ctx.fillStyle = foreground;
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
}
// Default color bold
this._ctx.save();
this._ctx.font = `bold ${this._ctx.font}`;
for (let i = 0; i < 256; i++) {
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
}
this._ctx.restore();
// Colors 0-15
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = `bold ${this._ctx.font}`;
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.fillStyle = ansiColors[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
}
}
this._ctx.restore();
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox) {
// Regenerate canvas and context as they are now owned by the char atlas
const result = this._canvas;
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
return result;
}
const promise = window.createImageBitmap(charAtlasImageData);
// Clear the rect while the promise is in progress
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
return promise;
}
}
+289
View File
@@ -0,0 +1,289 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { ColorManager } from './ColorManager';
describe('ColorManager', () => {
let cm: ColorManager;
beforeEach(() => {
cm = new ColorManager();
});
describe('constructor', () => {
it('should fill all colors with values', () => {
for (let key in cm.colors) {
if (typeof key === 'string') {
// A #rrggbb or rgba(...)
assert.ok(cm.colors[key].length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
});
it('should fill 240 colors with expected values', () => {
assert.equal(cm.colors.ansi[16], '#000000');
assert.equal(cm.colors.ansi[17], '#00005f');
assert.equal(cm.colors.ansi[18], '#000087');
assert.equal(cm.colors.ansi[19], '#0000af');
assert.equal(cm.colors.ansi[20], '#0000d7');
assert.equal(cm.colors.ansi[21], '#0000ff');
assert.equal(cm.colors.ansi[22], '#005f00');
assert.equal(cm.colors.ansi[23], '#005f5f');
assert.equal(cm.colors.ansi[24], '#005f87');
assert.equal(cm.colors.ansi[25], '#005faf');
assert.equal(cm.colors.ansi[26], '#005fd7');
assert.equal(cm.colors.ansi[27], '#005fff');
assert.equal(cm.colors.ansi[28], '#008700');
assert.equal(cm.colors.ansi[29], '#00875f');
assert.equal(cm.colors.ansi[30], '#008787');
assert.equal(cm.colors.ansi[31], '#0087af');
assert.equal(cm.colors.ansi[32], '#0087d7');
assert.equal(cm.colors.ansi[33], '#0087ff');
assert.equal(cm.colors.ansi[34], '#00af00');
assert.equal(cm.colors.ansi[35], '#00af5f');
assert.equal(cm.colors.ansi[36], '#00af87');
assert.equal(cm.colors.ansi[37], '#00afaf');
assert.equal(cm.colors.ansi[38], '#00afd7');
assert.equal(cm.colors.ansi[39], '#00afff');
assert.equal(cm.colors.ansi[40], '#00d700');
assert.equal(cm.colors.ansi[41], '#00d75f');
assert.equal(cm.colors.ansi[42], '#00d787');
assert.equal(cm.colors.ansi[43], '#00d7af');
assert.equal(cm.colors.ansi[44], '#00d7d7');
assert.equal(cm.colors.ansi[45], '#00d7ff');
assert.equal(cm.colors.ansi[46], '#00ff00');
assert.equal(cm.colors.ansi[47], '#00ff5f');
assert.equal(cm.colors.ansi[48], '#00ff87');
assert.equal(cm.colors.ansi[49], '#00ffaf');
assert.equal(cm.colors.ansi[50], '#00ffd7');
assert.equal(cm.colors.ansi[51], '#00ffff');
assert.equal(cm.colors.ansi[52], '#5f0000');
assert.equal(cm.colors.ansi[53], '#5f005f');
assert.equal(cm.colors.ansi[54], '#5f0087');
assert.equal(cm.colors.ansi[55], '#5f00af');
assert.equal(cm.colors.ansi[56], '#5f00d7');
assert.equal(cm.colors.ansi[57], '#5f00ff');
assert.equal(cm.colors.ansi[58], '#5f5f00');
assert.equal(cm.colors.ansi[59], '#5f5f5f');
assert.equal(cm.colors.ansi[60], '#5f5f87');
assert.equal(cm.colors.ansi[61], '#5f5faf');
assert.equal(cm.colors.ansi[62], '#5f5fd7');
assert.equal(cm.colors.ansi[63], '#5f5fff');
assert.equal(cm.colors.ansi[64], '#5f8700');
assert.equal(cm.colors.ansi[65], '#5f875f');
assert.equal(cm.colors.ansi[66], '#5f8787');
assert.equal(cm.colors.ansi[67], '#5f87af');
assert.equal(cm.colors.ansi[68], '#5f87d7');
assert.equal(cm.colors.ansi[69], '#5f87ff');
assert.equal(cm.colors.ansi[70], '#5faf00');
assert.equal(cm.colors.ansi[71], '#5faf5f');
assert.equal(cm.colors.ansi[72], '#5faf87');
assert.equal(cm.colors.ansi[73], '#5fafaf');
assert.equal(cm.colors.ansi[74], '#5fafd7');
assert.equal(cm.colors.ansi[75], '#5fafff');
assert.equal(cm.colors.ansi[76], '#5fd700');
assert.equal(cm.colors.ansi[77], '#5fd75f');
assert.equal(cm.colors.ansi[78], '#5fd787');
assert.equal(cm.colors.ansi[79], '#5fd7af');
assert.equal(cm.colors.ansi[80], '#5fd7d7');
assert.equal(cm.colors.ansi[81], '#5fd7ff');
assert.equal(cm.colors.ansi[82], '#5fff00');
assert.equal(cm.colors.ansi[83], '#5fff5f');
assert.equal(cm.colors.ansi[84], '#5fff87');
assert.equal(cm.colors.ansi[85], '#5fffaf');
assert.equal(cm.colors.ansi[86], '#5fffd7');
assert.equal(cm.colors.ansi[87], '#5fffff');
assert.equal(cm.colors.ansi[88], '#870000');
assert.equal(cm.colors.ansi[89], '#87005f');
assert.equal(cm.colors.ansi[90], '#870087');
assert.equal(cm.colors.ansi[91], '#8700af');
assert.equal(cm.colors.ansi[92], '#8700d7');
assert.equal(cm.colors.ansi[93], '#8700ff');
assert.equal(cm.colors.ansi[94], '#875f00');
assert.equal(cm.colors.ansi[95], '#875f5f');
assert.equal(cm.colors.ansi[96], '#875f87');
assert.equal(cm.colors.ansi[97], '#875faf');
assert.equal(cm.colors.ansi[98], '#875fd7');
assert.equal(cm.colors.ansi[99], '#875fff');
assert.equal(cm.colors.ansi[100], '#878700');
assert.equal(cm.colors.ansi[101], '#87875f');
assert.equal(cm.colors.ansi[102], '#878787');
assert.equal(cm.colors.ansi[103], '#8787af');
assert.equal(cm.colors.ansi[104], '#8787d7');
assert.equal(cm.colors.ansi[105], '#8787ff');
assert.equal(cm.colors.ansi[106], '#87af00');
assert.equal(cm.colors.ansi[107], '#87af5f');
assert.equal(cm.colors.ansi[108], '#87af87');
assert.equal(cm.colors.ansi[109], '#87afaf');
assert.equal(cm.colors.ansi[110], '#87afd7');
assert.equal(cm.colors.ansi[111], '#87afff');
assert.equal(cm.colors.ansi[112], '#87d700');
assert.equal(cm.colors.ansi[113], '#87d75f');
assert.equal(cm.colors.ansi[114], '#87d787');
assert.equal(cm.colors.ansi[115], '#87d7af');
assert.equal(cm.colors.ansi[116], '#87d7d7');
assert.equal(cm.colors.ansi[117], '#87d7ff');
assert.equal(cm.colors.ansi[118], '#87ff00');
assert.equal(cm.colors.ansi[119], '#87ff5f');
assert.equal(cm.colors.ansi[120], '#87ff87');
assert.equal(cm.colors.ansi[121], '#87ffaf');
assert.equal(cm.colors.ansi[122], '#87ffd7');
assert.equal(cm.colors.ansi[123], '#87ffff');
assert.equal(cm.colors.ansi[124], '#af0000');
assert.equal(cm.colors.ansi[125], '#af005f');
assert.equal(cm.colors.ansi[126], '#af0087');
assert.equal(cm.colors.ansi[127], '#af00af');
assert.equal(cm.colors.ansi[128], '#af00d7');
assert.equal(cm.colors.ansi[129], '#af00ff');
assert.equal(cm.colors.ansi[130], '#af5f00');
assert.equal(cm.colors.ansi[131], '#af5f5f');
assert.equal(cm.colors.ansi[132], '#af5f87');
assert.equal(cm.colors.ansi[133], '#af5faf');
assert.equal(cm.colors.ansi[134], '#af5fd7');
assert.equal(cm.colors.ansi[135], '#af5fff');
assert.equal(cm.colors.ansi[136], '#af8700');
assert.equal(cm.colors.ansi[137], '#af875f');
assert.equal(cm.colors.ansi[138], '#af8787');
assert.equal(cm.colors.ansi[139], '#af87af');
assert.equal(cm.colors.ansi[140], '#af87d7');
assert.equal(cm.colors.ansi[141], '#af87ff');
assert.equal(cm.colors.ansi[142], '#afaf00');
assert.equal(cm.colors.ansi[143], '#afaf5f');
assert.equal(cm.colors.ansi[144], '#afaf87');
assert.equal(cm.colors.ansi[145], '#afafaf');
assert.equal(cm.colors.ansi[146], '#afafd7');
assert.equal(cm.colors.ansi[147], '#afafff');
assert.equal(cm.colors.ansi[148], '#afd700');
assert.equal(cm.colors.ansi[149], '#afd75f');
assert.equal(cm.colors.ansi[150], '#afd787');
assert.equal(cm.colors.ansi[151], '#afd7af');
assert.equal(cm.colors.ansi[152], '#afd7d7');
assert.equal(cm.colors.ansi[153], '#afd7ff');
assert.equal(cm.colors.ansi[154], '#afff00');
assert.equal(cm.colors.ansi[155], '#afff5f');
assert.equal(cm.colors.ansi[156], '#afff87');
assert.equal(cm.colors.ansi[157], '#afffaf');
assert.equal(cm.colors.ansi[158], '#afffd7');
assert.equal(cm.colors.ansi[159], '#afffff');
assert.equal(cm.colors.ansi[160], '#d70000');
assert.equal(cm.colors.ansi[161], '#d7005f');
assert.equal(cm.colors.ansi[162], '#d70087');
assert.equal(cm.colors.ansi[163], '#d700af');
assert.equal(cm.colors.ansi[164], '#d700d7');
assert.equal(cm.colors.ansi[165], '#d700ff');
assert.equal(cm.colors.ansi[166], '#d75f00');
assert.equal(cm.colors.ansi[167], '#d75f5f');
assert.equal(cm.colors.ansi[168], '#d75f87');
assert.equal(cm.colors.ansi[169], '#d75faf');
assert.equal(cm.colors.ansi[170], '#d75fd7');
assert.equal(cm.colors.ansi[171], '#d75fff');
assert.equal(cm.colors.ansi[172], '#d78700');
assert.equal(cm.colors.ansi[173], '#d7875f');
assert.equal(cm.colors.ansi[174], '#d78787');
assert.equal(cm.colors.ansi[175], '#d787af');
assert.equal(cm.colors.ansi[176], '#d787d7');
assert.equal(cm.colors.ansi[177], '#d787ff');
assert.equal(cm.colors.ansi[178], '#d7af00');
assert.equal(cm.colors.ansi[179], '#d7af5f');
assert.equal(cm.colors.ansi[180], '#d7af87');
assert.equal(cm.colors.ansi[181], '#d7afaf');
assert.equal(cm.colors.ansi[182], '#d7afd7');
assert.equal(cm.colors.ansi[183], '#d7afff');
assert.equal(cm.colors.ansi[184], '#d7d700');
assert.equal(cm.colors.ansi[185], '#d7d75f');
assert.equal(cm.colors.ansi[186], '#d7d787');
assert.equal(cm.colors.ansi[187], '#d7d7af');
assert.equal(cm.colors.ansi[188], '#d7d7d7');
assert.equal(cm.colors.ansi[189], '#d7d7ff');
assert.equal(cm.colors.ansi[190], '#d7ff00');
assert.equal(cm.colors.ansi[191], '#d7ff5f');
assert.equal(cm.colors.ansi[192], '#d7ff87');
assert.equal(cm.colors.ansi[193], '#d7ffaf');
assert.equal(cm.colors.ansi[194], '#d7ffd7');
assert.equal(cm.colors.ansi[195], '#d7ffff');
assert.equal(cm.colors.ansi[196], '#ff0000');
assert.equal(cm.colors.ansi[197], '#ff005f');
assert.equal(cm.colors.ansi[198], '#ff0087');
assert.equal(cm.colors.ansi[199], '#ff00af');
assert.equal(cm.colors.ansi[200], '#ff00d7');
assert.equal(cm.colors.ansi[201], '#ff00ff');
assert.equal(cm.colors.ansi[202], '#ff5f00');
assert.equal(cm.colors.ansi[203], '#ff5f5f');
assert.equal(cm.colors.ansi[204], '#ff5f87');
assert.equal(cm.colors.ansi[205], '#ff5faf');
assert.equal(cm.colors.ansi[206], '#ff5fd7');
assert.equal(cm.colors.ansi[207], '#ff5fff');
assert.equal(cm.colors.ansi[208], '#ff8700');
assert.equal(cm.colors.ansi[209], '#ff875f');
assert.equal(cm.colors.ansi[210], '#ff8787');
assert.equal(cm.colors.ansi[211], '#ff87af');
assert.equal(cm.colors.ansi[212], '#ff87d7');
assert.equal(cm.colors.ansi[213], '#ff87ff');
assert.equal(cm.colors.ansi[214], '#ffaf00');
assert.equal(cm.colors.ansi[215], '#ffaf5f');
assert.equal(cm.colors.ansi[216], '#ffaf87');
assert.equal(cm.colors.ansi[217], '#ffafaf');
assert.equal(cm.colors.ansi[218], '#ffafd7');
assert.equal(cm.colors.ansi[219], '#ffafff');
assert.equal(cm.colors.ansi[220], '#ffd700');
assert.equal(cm.colors.ansi[221], '#ffd75f');
assert.equal(cm.colors.ansi[222], '#ffd787');
assert.equal(cm.colors.ansi[223], '#ffd7af');
assert.equal(cm.colors.ansi[224], '#ffd7d7');
assert.equal(cm.colors.ansi[225], '#ffd7ff');
assert.equal(cm.colors.ansi[226], '#ffff00');
assert.equal(cm.colors.ansi[227], '#ffff5f');
assert.equal(cm.colors.ansi[228], '#ffff87');
assert.equal(cm.colors.ansi[229], '#ffffaf');
assert.equal(cm.colors.ansi[230], '#ffffd7');
assert.equal(cm.colors.ansi[231], '#ffffff');
assert.equal(cm.colors.ansi[232], '#080808');
assert.equal(cm.colors.ansi[233], '#121212');
assert.equal(cm.colors.ansi[234], '#1c1c1c');
assert.equal(cm.colors.ansi[235], '#262626');
assert.equal(cm.colors.ansi[236], '#303030');
assert.equal(cm.colors.ansi[237], '#3a3a3a');
assert.equal(cm.colors.ansi[238], '#444444');
assert.equal(cm.colors.ansi[239], '#4e4e4e');
assert.equal(cm.colors.ansi[240], '#585858');
assert.equal(cm.colors.ansi[241], '#626262');
assert.equal(cm.colors.ansi[242], '#6c6c6c');
assert.equal(cm.colors.ansi[243], '#767676');
assert.equal(cm.colors.ansi[244], '#808080');
assert.equal(cm.colors.ansi[245], '#8a8a8a');
assert.equal(cm.colors.ansi[246], '#949494');
assert.equal(cm.colors.ansi[247], '#9e9e9e');
assert.equal(cm.colors.ansi[248], '#a8a8a8');
assert.equal(cm.colors.ansi[249], '#b2b2b2');
assert.equal(cm.colors.ansi[250], '#bcbcbc');
assert.equal(cm.colors.ansi[251], '#c6c6c6');
assert.equal(cm.colors.ansi[252], '#d0d0d0');
assert.equal(cm.colors.ansi[253], '#dadada');
assert.equal(cm.colors.ansi[254], '#e4e4e4');
assert.equal(cm.colors.ansi[255], '#eeeeee');
});
});
describe('setTheme', () => {
it('should set a partial set of colors, using the default if not present', () => {
assert.equal(cm.colors.background, '#000000');
assert.equal(cm.colors.foreground, '#ffffff');
cm.setTheme({
background: '#FF0000',
foreground: '#00FF00'
});
assert.equal(cm.colors.background, '#FF0000');
assert.equal(cm.colors.foreground, '#00FF00');
cm.setTheme({
background: '#0000FF'
});
assert.equal(cm.colors.background, '#0000FF');
// FG reverts back to default
assert.equal(cm.colors.foreground, '#ffffff');
});
});
});
+110
View File
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorSet, IColorManager } from './Interfaces';
import { ITheme } from '../Interfaces';
const DEFAULT_FOREGROUND = '#ffffff';
const DEFAULT_BACKGROUND = '#000000';
const DEFAULT_CURSOR = '#ffffff';
const DEFAULT_CURSOR_ACCENT = '#000000';
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
export const DEFAULT_ANSI_COLORS = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
/**
* Fills an existing 16 length string with the remaining 240 ANSI colors.
* @param first16Colors The first 16 ANSI colors.
*/
function generate256Colors(first16Colors: string[]): string[] {
let colors = first16Colors.slice();
// Generate colors (16-231)
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = toPaddedHex(v[(i / 36) % 6 | 0]);
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
const b = toPaddedHex(v[i % 6]);
colors.push(`#${r}${g}${b}`);
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = toPaddedHex(8 + i * 10);
colors.push(`#${c}${c}${c}`);
}
return colors;
}
function toPaddedHex(c: number): string {
let s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
/**
* Manages the source of truth for a terminal's colors.
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
constructor() {
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
};
}
/**
* Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = theme.background || DEFAULT_BACKGROUND;
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2];
this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3];
this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4];
this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5];
this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6];
this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7];
this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8];
this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9];
this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10];
this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11];
this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12];
this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13];
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
}
}
+351
View File
@@ -0,0 +1,351 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData } from '../Types';
interface CursorState {
x: number;
y: number;
isFocused: boolean;
style: string;
width: number;
}
/**
* The time between cursor blinks.
*/
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: CursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'cursor', zIndex, true, colors);
this._state = {
x: null,
y: null,
isFocused: null,
style: null,
width: null,
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: null,
y: null,
isFocused: null,
style: null,
width: null,
};
}
public reset(terminal: ITerminal): void {
this._clearCursor();
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = null;
this.onOptionsChanged(terminal);
}
}
public onBlur(terminal: ITerminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
terminal.refresh(terminal.buffer.y, terminal.buffer.y);
}
public onFocus(terminal: ITerminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume(terminal);
} else {
terminal.refresh(terminal.buffer.y, terminal.buffer.y);
}
}
public onOptionsChanged(terminal: ITerminal): void {
if (terminal.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
this._render(terminal, true);
});
}
} else {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = null;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
terminal.refresh(terminal.buffer.y, terminal.buffer.y);
}
}
public onCursorMove(terminal: ITerminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Only render if the animation frame is not active
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
}
}
private _render(terminal: ITerminal, triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
if (!terminal.cursorState || terminal.cursorHidden) {
this._clearCursor();
return;
}
const cursorY = terminal.buffer.ybase + terminal.buffer.y;
const viewportRelativeCursorY = cursorY - terminal.buffer.ydisp;
// Don't draw the cursor if it's off-screen
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) {
this._clearCursor();
return;
}
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
if (!terminal.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
this._ctx.restore();
this._state.x = terminal.buffer.x;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = terminal.options.cursorStyle;
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
return;
}
// Don't draw the cursor if it's blinking
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
this._clearCursor();
return;
}
if (this._state) {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === terminal.buffer.x &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === terminal.isFocused &&
this._state.style === terminal.options.cursorStyle &&
this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) {
return;
}
this._clearCursor();
}
this._ctx.save();
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
this._ctx.restore();
this._state.x = terminal.buffer.x;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = terminal.options.cursorStyle;
this._state.width = charData[CHAR_DATA_WIDTH_INDEX];
}
private _clearCursor(): void {
if (this._state) {
this.clearCells(this._state.x, this._state.y, this._state.width, 1);
this._state = {
x: null,
y: null,
isFocused: null,
style: null,
width: null,
};
}
}
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this.fillLeftLineAtCell(x, y);
this._ctx.restore();
}
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.fillStyle = this._colors.cursorAccent;
this.fillCharTrueColor(terminal, charData, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor;
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.restore();
}
}
class CursorBlinkStateManager {
public isCursorVisible: boolean;
private _animationFrame: number;
private _blinkStartTimeout: number;
private _blinkInterval: number;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
private _animationTimeRestarted: number;
constructor(
terminal: ITerminal,
private renderCallback: () => void
) {
this.isCursorVisible = true;
if (terminal.isFocused) {
this._restartInterval();
}
}
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = null;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = null;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null;
}
}
public restartBlinkAnimation(terminal: ITerminal): void {
if (this.isPaused) {
return;
}
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._animationFrame = null;
});
}
}
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
}
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = <number><any>setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = null;
if (time > 0) {
this._restartInterval(time);
return;
}
}
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._animationFrame = null;
});
// Setup the blink interval
this._blinkInterval = <number><any>setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = null;
this._restartInterval(time);
return;
}
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._animationFrame = null;
});
}, BLINK_INTERVAL);
}, timeToStart);
}
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = null;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = null;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null;
}
}
public resume(terminal: ITerminal): void {
this._animationTimeRestarted = null;
this._restartInterval();
this.restartBlinkAnimation(terminal);
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { GridCache } from './GridCache';
describe('GridCache', () => {
let grid: GridCache<number>;
beforeEach(() => {
grid = new GridCache<number>();
});
describe('constructor', () => {
it('should create an empty cache', () => {
assert.equal(grid.cache.length, 0);
});
});
describe('resize', () => {
it('should fill all new elements with null', () => {
grid.resize(2, 2);
assert.equal(grid.cache.length, 2);
assert.equal(grid.cache[0].length, 2);
assert.equal(grid.cache[0][0], null);
assert.equal(grid.cache[0][1], null);
assert.equal(grid.cache[1].length, 2);
assert.equal(grid.cache[1][0], null);
assert.equal(grid.cache[1][1], null);
grid.resize(3, 2);
assert.equal(grid.cache.length, 3);
assert.equal(grid.cache[2][0], null);
assert.equal(grid.cache[2][1], null);
});
it('should remove rows/cols from the cache when reduced', () => {
grid.resize(2, 2);
grid.resize(1, 1);
assert.equal(grid.cache.length, 1);
assert.equal(grid.cache[0].length, 1);
});
it('should not touch existing cache entries if they fit in the new cache', () => {
grid.resize(1, 1);
assert.equal(grid.cache[0][0], null);
grid.cache[0][0] = 1;
grid.resize(2, 1);
assert.equal(grid.cache[0][0], 1);
});
});
describe('clear', () => {
it('should make all entries null', () => {
grid.resize(1, 1);
grid.cache[0][0] = 1;
grid.clear();
assert.equal(grid.cache[0][0], null);
});
});
});
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export class GridCache<T> {
public cache: T[][];
public constructor() {
this.cache = [];
}
public resize(width: number, height: number): void {
for (let x = 0; x < width; x++) {
if (this.cache.length <= x) {
this.cache.push([]);
}
for (let y = this.cache[x].length; y < height; y++) {
this.cache[x].push(null);
}
this.cache[x].length = height;
}
this.cache.length = width;
}
public clear(): void {
for (let x = 0; x < this.cache.length; x++) {
for (let y = 0; y < this.cache[x].length; y++) {
this.cache[x][y] = null;
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledLineHeight: number;
scaledLineDrawY: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: LinkHoverEvent = null;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
super(container, 'link', zIndex, true, colors);
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = null;
}
public reset(terminal: ITerminal): void {
this._clearCurrentLink();
}
private _clearCurrentLink(): void {
if (this._state) {
this.clearCells(this._state.x, this._state.y, this._state.length, 1);
this._state = null;
}
}
private _onLinkHover(e: LinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length);
this._state = e;
}
private _onLinkLeave(e: LinkHoverEvent): void {
this._clearCurrentLink();
}
}
+212
View File
@@ -0,0 +1,212 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
export class Renderer extends EventEmitter implements IRenderer {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public colorManager: ColorManager;
public dimensions: IRenderDimensions;
constructor(private _terminal: ITerminal, theme: ITheme) {
super();
this.colorManager = new ColorManager();
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
];
this.dimensions = {
scaledCharWidth: null,
scaledCharHeight: null,
scaledLineHeight: null,
scaledLineDrawY: null,
scaledCanvasWidth: null,
scaledCanvasHeight: null,
canvasWidth: null,
canvasHeight: null,
actualCellWidth: null,
actualCellHeight: null
};
this._devicePixelRatio = window.devicePixelRatio;
}
public onWindowResize(devicePixelRatio: number): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== devicePixelRatio) {
this._devicePixelRatio = devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows, true);
}
}
public setTheme(theme: ITheme): IColorSet {
this.colorManager.setTheme(theme);
// Clear layers and force a full render
this._renderLayers.forEach(l => {
l.onThemeChanged(this._terminal, this.colorManager.colors);
l.reset(this._terminal);
});
this._terminal.refresh(0, this._terminal.rows - 1);
return this.colorManager.colors;
}
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
// Calculate the scaled line height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
// Force a refresh
this._terminal.refresh(0, this._terminal.rows - 1);
this.emit('resize', {
width: this.dimensions.canvasWidth,
height: this.dimensions.canvasHeight
});
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows, true);
}
public onBlur(): void {
this._renderLayers.forEach(l => l.onBlur(this._terminal));
}
public onFocus(): void {
this._renderLayers.forEach(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end));
}
public onCursorMove(): void {
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
}
public onOptionsChanged(): void {
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
}
public clear(): void {
this._renderLayers.forEach(l => l.reset(this._terminal));
}
/**
* 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.
*/
public queueRefresh(start: number, end: number): void {
this._refreshRowsQueue.push({ start: start, end: end });
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
}
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
private _refreshLoop(): void {
let start;
let end;
if (this._refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this._terminal.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this._refreshRowsQueue[0].start;
end = this._refreshRowsQueue[0].end;
for (let 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._refreshAnimationFrame = null;
// Render
start = Math.max(start, 0);
end = Math.min(end, this._terminal.rows - 1);
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
}
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer {
private _state: {start: [number, number], end: [number, number]};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'selection', zIndex, true, colors);
this._state = {
start: null,
end: null
};
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
end: null
};
}
public reset(terminal: ITerminal): void {
if (this._state.start && this._state.end) {
this._state = {
start: null,
end: null
};
this.clearAll();
}
}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
// Selection has not changed
if (this._state.start === start || this._state.end === end) {
return;
}
// Remove all selections
this.clearAll();
// Selection does not exist
if (!start || !end) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.ydisp;
const viewportEndRow = end[1] - terminal.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
return;
}
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this._ctx.fillStyle = this._colors.selection;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 1);
}
// Save state for next render
this._state.start = [start[0], start[1]];
this._state.end = [end[0], end[1]];
}
}
+233
View File
@@ -0,0 +1,233 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS } from './Types';
import { GridCache } from './GridCache';
import { CharData } from '../Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
/**
* This CharData looks like a null character, which will forc a clear and render
* when the character changes (a regular space ' ' character may not as it's
* drawn state is a cleared cell).
*/
const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
export class TextRenderLayer extends BaseRenderLayer {
private _state: GridCache<CharData>;
private _characterWidth: number;
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'text', zIndex, false, colors);
this._state = new GridCache<CharData>();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Clear the character width cache if the font or width has changed
const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
// Resizing the canvas discards the contents of the canvas so clear state
this._state.clear();
this._state.resize(terminal.cols, terminal.rows);
}
public reset(terminal: ITerminal): void {
this._state.clear();
this.clearAll();
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
for (let y = startRow; y <= endRow; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row);
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (width === 0) {
this._state.cache[x][y] = null;
continue;
}
// If the character is a space and the character to the left is an
// overlapping character, skip the character and allow the overlapping
// char to take full control over this character's cell.
if (code === 32 /*' '*/) {
if (x > 0) {
const previousChar: CharData = line[x - 1];
if (this._isOverlapping(previousChar)) {
continue;
}
}
}
// Skip rendering if the character is identical
const state = this._state.cache[x][y];
if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) {
// Skip render, contents are identical
this._state.cache[x][y] = charData;
continue;
}
// Clear the old character was not a space with the default background
const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE);
if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) {
this._clearChar(x, y);
}
this._state.cache[x][y] = charData;
const flags = attr >> 18;
let bg = attr & 0x1ff;
// Skip rendering if the character is invisible
const isDefaultBackground = bg >= 256;
const isInvisible = flags & FLAGS.INVISIBLE;
const isInverted = flags & FLAGS.INVERSE;
if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) {
continue;
}
// If the character is an overlapping char and the character to the right is a
// space, take ownership of the cell to the right.
if (width !== 0 && this._isOverlapping(charData)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
// space is added. Without this, the first half of `b` would never
// get removed, and `a` would not re-render because it thinks it's
// already in the correct state.
this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
width = 2;
this._clearChar(x + 1, y);
// The overlapping char's char data will force a clear and render when the
// overlapping char is no longer to the left of the character and also when
// the space changes to another character.
this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
}
}
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (isInverted) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === 257) {
bg = INVERTED_DEFAULT_COLOR;
}
}
// Draw background
if (bg < 256) {
this._ctx.save();
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]);
this.fillCells(x, y, width, 1);
this._ctx.restore();
}
this._ctx.save();
if (flags & FLAGS.BOLD) {
this._ctx.font = `bold ${this._ctx.font}`;
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
} else {
this._ctx.fillStyle = this._colors.foreground;
}
this.fillBottomLineAtCells(x, y);
}
this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD));
this._ctx.restore();
}
}
}
/**
* Whether a character is overlapping to the
* next cell.
*/
private _isOverlapping(charData: CharData): boolean {
// We assume that any ascii character will not overlap
const code = charData[CHAR_DATA_CODE_INDEX];
if (code < 256) {
return false;
}
// Deliver from cache if available
const char = charData[CHAR_DATA_CHAR_INDEX];
if (this._characterOverlapCache.hasOwnProperty(char)) {
return this._characterOverlapCache[char];
}
// Setup the font
this._ctx.save();
this._ctx.font = this._characterFont;
// Measure the width of the character, but Math.floor it
// because that is what the renderer does when it calculates
// the character dimensions we are comparing against
const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth;
// Restore the original context
this._ctx.restore();
// Cache and return
this._characterOverlapCache[char] = overlaps;
return overlaps;
}
/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
* @param y The row of the char.
*/
private _clearChar(x: number, y: number): void {
let colsToClear = 1;
// Clear the adjacent character if it was wide
const state = this._state.cache[x][y];
if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
colsToClear = 2;
}
this.clearCells(x, y, colsToClear, 1);
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Flags used to render terminal text properly.
*/
export enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16
};
+1 -2
View File
@@ -1,6 +1,5 @@
/**
* Attributes and methods to help with identifying the current browser and platform.
* @module xterm/utils/Browser
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
+7 -6
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -25,13 +26,13 @@ describe('CharMeasure', () => {
describe('measure', () => {
it('should set _measureElement on first call', () => {
charMeasure.measure();
charMeasure.measure({});
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
});
it('should be performed async on first call', done => {
assert.equal(charMeasure.width, null);
charMeasure.measure();
charMeasure.measure({});
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
@@ -44,7 +45,7 @@ describe('CharMeasure', () => {
});
it('should be performed sync on successive calls', done => {
charMeasure.measure();
charMeasure.measure({});
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
@@ -55,19 +56,19 @@ describe('CharMeasure', () => {
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 2, height: 2 };
};
charMeasure.measure();
charMeasure.measure({});
assert.equal(charMeasure.width, firstWidth * 2);
done();
}, 0);
});
it('should NOT do a measure when the parent is hidden', done => {
charMeasure.measure();
charMeasure.measure({});
setTimeout(() => {
const firstWidth = charMeasure.width;
container.style.display = 'none';
container.style.fontSize = '2em';
charMeasure.measure();
charMeasure.measure({});
assert.equal(charMeasure.width, firstWidth);
done();
}, 0);
+14 -8
View File
@@ -1,14 +1,17 @@
/**
* @module xterm/utils/CharMeasure
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { EventEmitter } from '../EventEmitter.js';
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
/**
* Utility class that measures the size of a character.
* Utility class that measures the size of a character. Measurements are done in
* the DOM rather than with a canvas context because support for extracting the
* height of characters is patchy across browsers.
*/
export class CharMeasure extends EventEmitter {
export class CharMeasure extends EventEmitter implements ICharMeasure {
private _document: Document;
private _parentElement: HTMLElement;
private _measureElement: HTMLElement;
@@ -29,24 +32,27 @@ export class CharMeasure extends EventEmitter {
return this._height;
}
public measure(): void {
public measure(options: ITerminalOptions): void {
if (!this._measureElement) {
this._measureElement = this._document.createElement('span');
this._measureElement.style.position = 'absolute';
this._measureElement.style.top = '0';
this._measureElement.style.left = '-9999em';
this._measureElement.style.lineHeight = 'normal';
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
// Perform _doMeasure async if the element was just attached as sometimes
// getBoundingClientRect does not return accurate values without this.
setTimeout(() => this._doMeasure(), 0);
setTimeout(() => this._doMeasure(options), 0);
} else {
this._doMeasure();
this._doMeasure(options);
}
}
private _doMeasure(): void {
private _doMeasure(options: ITerminalOptions): void {
this._measureElement.style.fontFamily = options.fontFamily;
this._measureElement.style.fontSize = `${options.fontSize}px`;
const geometry = this._measureElement.getBoundingClientRect();
// The element is likely currently display:none, we should retain the
// previous value.
@@ -55,7 +61,7 @@ export class CharMeasure extends EventEmitter {
}
if (this._width !== geometry.width || this._height !== geometry.height) {
this._width = geometry.width;
this._height = geometry.height;
this._height = Math.ceil(geometry.height);
this.emit('charsizechanged');
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
+5 -3
View File
@@ -1,13 +1,15 @@
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
* @module xterm/utils/CircularList
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Interfaces';
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
protected _array: T[];
private _startIndex: number;
-51
View File
@@ -1,51 +0,0 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { DomElementObjectPool } from './DomElementObjectPool';
class MockDocument {
private _attr: {[key: string]: string} = {};
constructor() {}
public getAttribute(key: string): string { return this._attr[key]; };
public setAttribute(key: string, value: string): void { this._attr[key] = value; }
}
describe('DomElementObjectPool', () => {
let pool: DomElementObjectPool;
beforeEach(() => {
pool = new DomElementObjectPool('span');
(<any>global).document = {
createElement: () => new MockDocument()
};
});
it('should acquire distinct elements', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
assert.notEqual(element1, element2);
});
it('should acquire released elements', () => {
const element = pool.acquire();
pool.release(element);
assert.equal(pool.acquire(), element);
});
it('should handle a series of acquisitions and releases', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
pool.release(element1);
assert.equal(pool.acquire(), element1);
pool.release(element1);
pool.release(element2);
assert.equal(pool.acquire(), element2);
assert.equal(pool.acquire(), element1);
});
it('should throw when releasing an element that was not acquired', () => {
assert.throws(() => pool.release(document.createElement('span')));
});
});
-75
View File
@@ -1,75 +0,0 @@
/**
* @module xterm/utils/DomElementObjectPool
* @license MIT
*/
/**
* An object pool that manages acquisition and releasing of DOM elements for
* when reuse is desirable.
*/
export class DomElementObjectPool {
private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id';
private static _objectCount = 0;
private _type: string;
private _pool: HTMLElement[];
private _inUse: {[key: string]: HTMLElement};
/**
* @param type The DOM element type (div, span, etc.).
*/
constructor(private type: string) {
this._type = type;
this._pool = [];
this._inUse = {};
}
/**
* Acquire an element from the pool, creating it if the pool is empty.
*/
public acquire(): HTMLElement {
let element: HTMLElement;
if (this._pool.length === 0) {
element = this._createNew();
} else {
element = this._pool.pop();
}
this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element;
return element;
}
/**
* Release an element back into the pool. It's up to the caller of this
* function to ensure that all external references to the element have been
* removed.
* @param element The element being released.
*/
public release(element: HTMLElement): void {
if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) {
throw new Error('Could not release an element not yet acquired');
}
delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)];
this._cleanElement(element);
this._pool.push(element);
}
/**
* Creates a new element for the pool.
*/
private _createNew(): HTMLElement {
const element = document.createElement(this._type);
const id = DomElementObjectPool._objectCount++;
element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10));
return element;
}
/**
* Resets an element back to a "clean state".
* @param element The element to be cleaned.
*/
private _cleanElement(element: HTMLElement): void {
element.className = '';
element.innerHTML = '';
}
}
+1 -2
View File
@@ -1,6 +1,5 @@
/**
* Generic utilities module with methods that can be helpful at different parts of the code base.
* @module xterm/utils/Generic
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
-88
View File
@@ -1,88 +0,0 @@
/**
* @license MIT
*/
import { CharMeasure } from './CharMeasure';
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
return null;
}
const originalElement = element;
let x = event.pageX;
let y = event.pageY;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (element && element !== self.document.documentElement) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
}
element = originalElement;
while (element && element !== self.document.body) {
x += element.scrollLeft;
y += element.scrollTop;
element = <HTMLElement>element.parentElement;
}
return [x, y];
}
/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param event The mouse event.
* @param rowContainer The terminal's row container.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if charMeasure has not been initialized
if (!charMeasure.width || !charMeasure.height) {
return null;
}
const coords = getCoordsRelativeToElement(event, rowContainer);
if (!coords) {
return null;
}
// Convert to cols/rows.
coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
coords[1] = Math.ceil(coords[1] / charMeasure.height);
// Ensure coordinates are within the terminal viewport.
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
return coords;
}
/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
* @param event The mouse event.
* @param rowContainer The terminal's row container.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows in the terminal.
*/
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
let x = coords[0];
let y = coords[1];
// xterm sends raw bytes and starts at 32 (SP) for each.
x += 32;
y += 32;
return { x, y };
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { MouseHelper } from './MouseHelper';
import { MockCharMeasure, MockRenderer } from './TestUtils.test';
const CHAR_WIDTH = 10;
const CHAR_HEIGHT = 20;
describe('MouseHelper.getCoords', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let mouseHelper: MouseHelper;
let charMeasure: MockCharMeasure;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
charMeasure = new MockCharMeasure();
charMeasure.width = CHAR_WIDTH;
charMeasure.height = CHAR_HEIGHT;
const renderer = new MockRenderer();
renderer.dimensions = <any>{
actualCellWidth: CHAR_WIDTH,
actualCellHeight: CHAR_HEIGHT
};
mouseHelper = new MouseHelper(renderer);
});
describe('when charMeasure is not initialized', () => {
it('should return null', () => {
charMeasure = new MockCharMeasure();
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null);
});
});
describe('when pageX/pageY are not supported', () => {
it('should return null', () => {
assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null);
});
});
it('should return the cell that was clicked', () => {
let coords: [number, number];
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [1, 1]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [1, 2]);
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [2, 1]);
});
it('should ensure the coordinates are returned within the terminal bounds', () => {
let coords: [number, number];
coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [1, 1]);
// Event are double the cols/rows
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharMeasure } from '../Interfaces';
import { IRenderer } from '../renderer/Interfaces';
export class MouseHelper {
constructor(private _renderer: IRenderer) {}
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
return null;
}
const originalElement = element;
let x = event.pageX;
let y = event.pageY;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (element) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
}
element = originalElement;
while (element && element !== self.document.body) {
x += element.scrollLeft;
y += element.scrollTop;
element = <HTMLElement>element.parentElement;
}
return [x, y];
}
/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param event The mouse event.
* @param element The terminal's container element.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if charMeasure has not been initialized
if (!charMeasure.width || !charMeasure.height) {
return null;
}
const coords = MouseHelper.getCoordsRelativeToElement(event, element);
if (!coords) {
return null;
}
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth);
coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight);
// Ensure coordinates are within the terminal viewport.
coords[0] = Math.min(Math.max(coords[0], 1), colCount);
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
return coords;
}
/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
* @param event The mouse event.
* @param element The terminal's container element.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows in the terminal.
*/
public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } {
const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount);
let x = coords[0];
let y = coords[1];
// xterm sends raw bytes and starts at 32 (SP) for each.
x += 32;
y += 32;
return { x, y };
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+57 -4
View File
@@ -1,12 +1,19 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper } from '../Interfaces';
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces';
import { LineData } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from './Browser';
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces';
export class MockTerminal implements ITerminal {
mouseHelper: IMouseHelper;
renderer: IRenderer;
linkifier: ILinkifier;
isFocused: boolean;
options: ITerminalOptions = {};
element: HTMLElement;
rowContainer: HTMLElement;
@@ -52,16 +59,27 @@ export class MockTerminal implements ITerminal {
showCursor(): void {
throw new Error('Method not implemented.');
}
refresh(start: number, end: number): void {
throw new Error('Method not implemented.');
}
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
const line: LineData = [];
cols = cols || this.cols;
for (let i = 0; i < cols; i++) {
line.push([0, ' ', 1]);
line.push([0, ' ', 1, 32]);
}
return line;
}
}
export class MockCharMeasure implements ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void {
throw new Error('Method not implemented.');
}
}
export class MockInputHandlingTerminal implements IInputHandlingTerminal {
element: HTMLElement;
options: ITerminalOptions = {};
@@ -125,7 +143,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
eraseLeft(x: number, y: number): void {
throw new Error('Method not implemented.');
}
blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] {
blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] {
throw new Error('Method not implemented.');
}
prevStop(x?: number): number {
@@ -176,7 +194,8 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
}
export class MockBuffer implements IBuffer {
lines: ICircularList<[number, string, number][]>;
isCursorInViewport: boolean;
lines: ICircularList<[number, string, number, number][]>;
ydisp: number;
ybase: number;
y: number;
@@ -187,11 +206,45 @@ export class MockBuffer implements IBuffer {
savedY: number;
savedX: number;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
}
prevStop(x?: number): number {
throw new Error('Method not implemented.');
}
}
export class MockRenderer implements IRenderer {
colorManager: IColorManager;
on(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
throw new Error('Method not implemented.');
}
dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
onCharSizeChanged(): void {}
onBlur(): void {}
onFocus(): void {}
onSelectionChanged(start: [number, number], end: [number, number]): void {}
onCursorMove(): void {}
onOptionsChanged(): void {}
onWindowResize(devicePixelRatio: number): void {}
clear(): void {}
queueRefresh(start: number, end: number): void {}
}
export class MockViewport implements IViewport {
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
onWheel(ev: WheelEvent): void {
throw new Error('Method not implemented.');
}
+13 -2160
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
+91 -6
View File
@@ -41,6 +41,21 @@ interface ITerminalOptions {
*/
disableStdin?: boolean;
/**
* The font size used to render text.
*/
fontSize?: number;
/**
* The font family used to render text.
*/
fontFamily?: string;
/**
* The line height used to render text.
*/
lineHeight?: number;
/**
* The number of rows in the terminal.
*/
@@ -56,6 +71,59 @@ interface ITerminalOptions {
* The size of tab stops in the terminal.
*/
tabStopWidth?: number;
/**
* The color theme of the terminal.
*/
theme?: ITheme;
}
/**
* Contains colors to theme the terminal with.
*/
interface ITheme {
/** The default foreground color */
foreground?: string,
/** The default background color */
background?: string,
/** The cursor color */
cursor?: string,
/** The accent color of the cursor (used as the foreground color for a block cursor) */
cursorAccent?: string,
/** The selection color (can be transparent) */
selection?: string,
/** ANSI black (eg. `\x1b[30m`) */
black?: string,
/** ANSI red (eg. `\x1b[31m`) */
red?: string,
/** ANSI green (eg. `\x1b[32m`) */
green?: string,
/** ANSI yellow (eg. `\x1b[33m`) */
yellow?: string,
/** ANSI blue (eg. `\x1b[34m`) */
blue?: string,
/** ANSI magenta (eg. `\x1b[35m`) */
magenta?: string,
/** ANSI cyan (eg. `\x1b[36m`) */
cyan?: string,
/** ANSI white (eg. `\x1b[37m`) */
white?: string,
/** ANSI bright black (eg. `\x1b[1;30m`) */
brightBlack?: string,
/** ANSI bright red (eg. `\x1b[1;31m`) */
brightRed?: string,
/** ANSI bright green (eg. `\x1b[1;32m`) */
brightGreen?: string,
/** ANSI bright yellow (eg. `\x1b[1;33m`) */
brightYellow?: string,
/** ANSI bright blue (eg. `\x1b[1;34m`) */
brightBlue?: string,
/** ANSI bright magenta (eg. `\x1b[1;35m`) */
brightMagenta?: string,
/** ANSI bright cyan (eg. `\x1b[1;36m`) */
brightCyan?: string,
/** ANSI bright white (eg. `\x1b[1;37m`) */
brightWhite?: string
}
/**
@@ -72,7 +140,18 @@ interface ILinkMatcherOptions {
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void;
/**
* A callback that fires when the mouse hovers over a link for a moment.
*/
tooltipCallback?: (event: MouseEvent, uri: string) => boolean | void;
/**
* A callback that fires when the mouse leaves a link. Note that this can
* happen even when tooltipCallback hasn't fired for the link yet.
*/
leaveCallback?: (event: MouseEvent, uri: string) => boolean | void;
/**
* The priority of the link matcher, this defines the order in which the link
@@ -226,7 +305,7 @@ declare module 'xterm' {
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void , options?: ILinkMatcherOptions): number;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number;
/**
* (EXPERIMENTAL) Deregisters a link matcher if it has been registered.
@@ -313,7 +392,7 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'termName'): string;
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -328,7 +407,7 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'cols' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
getOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -350,7 +429,7 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'termName' | 'bellSound', value: string): void;
setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void;
/**
* Sets an option on the terminal.
* @param key The option key.
@@ -380,7 +459,7 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'cols' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void;
setOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void;
/**
* Sets an option on the terminal.
* @param key The option key.
@@ -393,6 +472,12 @@ declare module 'xterm' {
* @param value The option value.
*/
setOption(key: 'handler', value: (data: string) => void): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'theme', value: ITheme): void;
/**
* Sets an option on the terminal.
* @param key The option key.