Merge branch 'master' into dynamic-char-atlas

This commit is contained in:
Benjamin Woodruff
2018-04-23 22:43:43 -07:00
14 changed files with 288 additions and 212 deletions
+7
View File
@@ -76,6 +76,12 @@ var xterm = new Terminal(); // Instantiate the terminal
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support
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:
@@ -128,6 +134,7 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies
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.
+1 -1
View File
@@ -77,7 +77,7 @@
"scripts": {
"start": "node demo/app",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint src/*.ts src/**/*.ts",
"lint": "tslint src/*.ts src/**/*.ts src/addons/**/*.ts",
"test": "npm-run-all mocha lint",
"mocha": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
+20 -25
View File
@@ -22,8 +22,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
* - scroll position
*/
export class Buffer implements IBuffer {
private _lines: CircularList<LineData>;
public lines: CircularList<LineData>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -48,10 +47,6 @@ export class Buffer implements IBuffer {
this.clear();
}
public get lines(): CircularList<LineData> {
return this._lines;
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
@@ -81,7 +76,7 @@ export class Buffer implements IBuffer {
* Fills the buffer's viewport with blank lines.
*/
public fillViewportRows(): void {
if (this._lines.length === 0) {
if (this.lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(this._terminal.blankLine());
@@ -97,7 +92,7 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
@@ -112,19 +107,19 @@ export class Buffer implements IBuffer {
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
if (newMaxLength > this._lines.maxLength) {
this._lines.maxLength = newMaxLength;
if (newMaxLength > this.lines.maxLength) {
this.lines.maxLength = newMaxLength;
}
// The following adjustments should only happen if the buffer has been
// initialized/filled.
if (this._lines.length > 0) {
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++) {
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
for (let i = 0; i < this.lines.length; i++) {
while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
}
}
}
@@ -133,8 +128,8 @@ export class Buffer implements IBuffer {
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) {
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--;
@@ -146,16 +141,16 @@ export class Buffer implements IBuffer {
} 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));
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) {
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();
this.lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
@@ -167,15 +162,15 @@ export class Buffer implements IBuffer {
// 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) {
if (newMaxLength < this.lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
const amountToTrim = this.lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
}
this._lines.maxLength = newMaxLength;
this.lines.maxLength = newMaxLength;
}
// Make sure that the cursor stays on screen
@@ -310,7 +305,7 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this._lines.addDisposableListener('trim', amount => {
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
+2 -2
View File
@@ -6,7 +6,7 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
export class EventEmitter implements IEventEmitter {
export class EventEmitter implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
@@ -75,7 +75,7 @@ export class EventEmitter implements IEventEmitter {
return this._events[type] || [];
}
protected destroy(): void {
public dispose(): void {
this._events = {};
}
}
+110 -70
View File
@@ -23,6 +23,10 @@ export class InputHandler implements IInputHandler {
public addChar(char: string, code: number): void {
if (char >= ' ') {
// make buffer local for faster access
const buffer = this._terminal.buffer;
// calculate print space
// expensive call, therefore we save width in line buffer
const chWidth = wcwidth(code);
@@ -35,42 +39,42 @@ export class InputHandler implements IInputHandler {
this._terminal.emit('a11y.char', char);
}
let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let row = buffer.y + buffer.ybase;
// insert combining char in last cell
// FIXME: needs handling after cursor jumps
if (!chWidth && this._terminal.buffer.x) {
if (!chWidth && buffer.x) {
// 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]) {
if (buffer.lines.get(row)[buffer.x - 1]) {
if (!buffer.lines.get(row)[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]) {
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);
if (buffer.lines.get(row)[buffer.x - 2]) {
buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[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);
buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0);
}
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(buffer.y);
}
return;
}
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) {
if (buffer.x + chWidth - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
this._terminal.buffer.x = 0;
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
buffer.x = 0;
buffer.y++;
if (buffer.y > buffer.scrollBottom) {
buffer.y--;
this._terminal.scroll(true);
} else {
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
}
} else {
if (chWidth === 2) { // FIXME: check for xterm behavior
@@ -78,7 +82,7 @@ export class InputHandler implements IInputHandler {
}
}
}
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
row = buffer.y + buffer.ybase;
// insert mode: move characters to right
if (this._terminal.insertMode) {
@@ -86,26 +90,26 @@ export class InputHandler implements IInputHandler {
for (let moves = 0; moves < chWidth; ++moves) {
// remove last cell, if it's width is 0
// we have to adjust the second last cell as well
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
const removed = buffer.lines.get(buffer.y + buffer.ybase).pop();
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, ' '.charCodeAt(0)];
&& buffer.lines.get(row)[this._terminal.cols - 2]
&& buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
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, ' '.charCodeAt(0)]);
buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
buffer.x++;
this._terminal.updateRange(buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (chWidth === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined];
buffer.x++;
}
}
}
@@ -123,17 +127,20 @@ export class InputHandler implements IInputHandler {
* Line Feed or New Line (NL). (LF is Ctrl-J).
*/
public lineFeed(): void {
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (this._terminal.convertEol) {
this._terminal.buffer.x = 0;
buffer.x = 0;
}
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
buffer.y++;
if (buffer.y > buffer.scrollBottom) {
buffer.y--;
this._terminal.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
if (buffer.x >= this._terminal.cols) {
buffer.x--;
}
/**
* This event is emitted whenever the terminal outputs a LF or NL.
@@ -199,13 +206,16 @@ export class InputHandler implements IInputHandler {
let param = params[0];
if (param < 1) param = 1;
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
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);
this._terminal.buffer.lines.get(row).pop();
buffer.lines.get(row).splice(j++, 0, ch);
buffer.lines.get(row).pop();
}
}
@@ -447,20 +457,24 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
let row: number = this._terminal.buffer.y + this._terminal.buffer.ybase;
let scrollBottomRowsOffset = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
let scrollBottomAbsolute = this._terminal.rows - 1 + this._terminal.buffer.ybase - scrollBottomRowsOffset + 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
let row: number = buffer.y + buffer.ybase;
let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom;
let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1;
while (param--) {
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(scrollBottomAbsolute - 1, 1);
this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true));
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
buffer.lines.splice(row, 0, this._terminal.blankLine(true));
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -472,21 +486,25 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
const row: number = this._terminal.buffer.y + this._terminal.buffer.ybase;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row: number = buffer.y + buffer.ybase;
let j: number;
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j;
j = this._terminal.rows - 1 - buffer.scrollBottom;
j = this._terminal.rows - 1 + buffer.ybase - j;
while (param--) {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(row, 1);
this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true));
buffer.lines.splice(row, 1);
buffer.lines.splice(j, 0, this._terminal.blankLine(true));
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -499,14 +517,17 @@ export class InputHandler implements IInputHandler {
param = 1;
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param--) {
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
this._terminal.buffer.lines.get(row).push(ch);
buffer.lines.get(row).splice(buffer.x, 1);
buffer.lines.get(row).push(ch);
}
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(buffer.y);
}
/**
@@ -514,13 +535,17 @@ export class InputHandler implements IInputHandler {
*/
public scrollUp(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -528,13 +553,17 @@ export class InputHandler implements IInputHandler {
*/
public scrollDown(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -547,12 +576,15 @@ export class InputHandler implements IInputHandler {
param = 1;
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row)[j++] = ch;
buffer.lines.get(row)[j++] = ch;
}
}
@@ -561,8 +593,12 @@ export class InputHandler implements IInputHandler {
*/
public cursorBackwardTab(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.x = this._terminal.buffer.prevStop();
buffer.x = buffer.prevStop();
}
}
@@ -602,11 +638,15 @@ 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, 32];
// make buffer local for faster access
const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y);
const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
while (param--) {
line[this._terminal.buffer.x++] = ch;
line[buffer.x++] = ch;
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ describe('Linkifier', () => {
terminal = new MockTerminal();
terminal.cols = 100;
terminal.buffer = new MockBuffer();
terminal.buffer.lines = new CircularList<LineData>(20);
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
+28 -18
View File
@@ -38,6 +38,7 @@ import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './shared/utils/Browser';
import * as Dom from './utils/Dom';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
@@ -46,7 +47,8 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './input/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
import { ITheme, ILocalizableStrings, IMarker } from 'xterm';
import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm';
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS = {
@@ -121,15 +123,15 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
tabStopWidth: 8,
theme: null,
rightClickSelectsWord: Browser.isMac
// programFeatures: false,
// focusKeys: false,
};
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement;
public element: HTMLElement;
public screenElement: HTMLElement;
private _disposables: IDisposable[];
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
@@ -251,7 +253,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this._setup();
}
public dispose(): void {
super.dispose();
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
removeTerminalFromCache(this);
this.handler = () => {};
this.write = () => {};
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
}
/**
* @deprecated Use dispose instead.
*/
public destroy(): void {
this.dispose();
}
private _setup(): void {
this._disposables = [];
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key];
@@ -694,7 +717,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
@@ -1087,19 +1110,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
});
}
/**
* Destroys the terminal.
*/
public destroy(): void {
super.destroy();
this.handler = () => {};
this.write = () => {};
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
// this.emit('close');
}
/**
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
* opportunity.
+1 -1
View File
@@ -248,7 +248,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
}
export interface IBuffer {
lines: ICircularList<LineData>;
readonly lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
+5 -5
View File
@@ -43,7 +43,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
if (typeof ev.data == 'object') {
if (typeof ev.data === 'object') {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
@@ -53,14 +53,14 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
} else {
let fileReader = new FileReader();
fileReader.addEventListener('load', function() {
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(this.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
}
} else if (typeof ev.data == 'string') {
displayData(ev.data)
} else if (typeof ev.data === 'string') {
displayData(ev.data);
} else {
throw Error(`Cannot handle "${typeof ev.data}" websocket message.`);
}
@@ -73,7 +73,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
* @param str String decoded by FileReader.
* @param data The data of the EventMessage.
*/
function displayData(str?: string, data?: string) {
function displayData(str?: string, data?: string): void {
if (buffered) {
addonTerminal.__pushToBuffer(str || data);
} else {
+1 -2
View File
@@ -43,8 +43,7 @@ export interface IZmodemOptions {
}
function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
var term = this;
const term = this;
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
let zsentry;
+75 -86
View File
@@ -48,23 +48,24 @@ export class TextRenderLayer extends BaseRenderLayer {
this.clearAll();
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
this._charAtlas.beginFrame();
for (let y = startRow; y <= endRow; y++) {
private _forEachCell(
terminal: ITerminal,
firstRow: number,
lastRow: number,
callback: (
code: number,
char: string,
width: number,
x: number,
y: number,
fg: number,
bg: number,
flags: number
) => void
): void {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row);
this.clearCells(0, y, terminal.cols, 1);
// for (let x = 0; x < terminal.cols; x++) {
// this._state.cache[x][y] = null;
// }
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
@@ -75,51 +76,12 @@ export class TextRenderLayer extends BaseRenderLayer {
// 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 (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
@@ -137,10 +99,12 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (isInverted) {
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
@@ -152,47 +116,72 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
// Clear the cell next to this character if it's wide
if (width === 2) {
// this.clearCells(x + 1, y, 1, 1);
}
// Draw background
if (bg < 256) {
this._ctx.save();
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css);
this.fillCells(x, y, width, 1);
this._ctx.restore();
}
this._ctx.save();
if (flags & FLAGS.BOLD) {
this._ctx.font = this._getFont(terminal, true);
// 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.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y);
}
this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM));
this._ctx.restore();
callback(code, char, width, x, y, fg, bg, flags);
}
}
}
private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
const isDefaultBackground = bg >= 256;
if (!isDefaultBackground) {
this._ctx.save();
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css);
this.fillCells(x, y, width, 1);
this._ctx.restore();
}
});
}
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
if (flags & FLAGS.UNDERLINE) {
this._ctx.save();
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
this.drawChar(
terminal, char, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM)
);
});
}
public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
this._charAtlas.beginFrame();
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
this._drawBackground(terminal, firstRow, lastRow);
this._drawForeground(terminal, firstRow, lastRow);
}
public onOptionsChanged(terminal: ITerminal): void {
this.setTransparency(terminal, terminal.options.allowTransparency);
}
+22
View File
@@ -42,6 +42,8 @@ export function acquireCharAtlas(
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
@@ -82,3 +84,23 @@ export function acquireCharAtlas(
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
* @param terminal The terminal to remove.
*/
export function removeTerminalFromCache(terminal: ITerminal): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only terminal
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
+6
View File
@@ -63,6 +63,9 @@ export class MockTerminal implements ITerminal {
selectAll(): void {
throw new Error('Method not implemented.');
}
dispose(): void {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
@@ -306,6 +309,9 @@ export class MockBuffer implements IBuffer {
prevStop(x?: number): number {
throw new Error('Method not implemented.');
}
setLines(lines: ICircularList<[number, string, number, number][]>): void {
this.lines = lines;
}
}
export class MockRenderer implements IRenderer {
+9 -1
View File
@@ -269,7 +269,7 @@ declare module 'xterm' {
/**
* The class that represents an xterm.js terminal.
*/
export class Terminal implements IEventEmitter {
export class Terminal implements IEventEmitter, IDisposable {
/**
* The element containing the terminal.
*/
@@ -469,8 +469,16 @@ declare module 'xterm' {
*/
selectLines(start: number, end: number): void;
/*
* Disposes of the terminal, detaching it from the DOM and removing any
* active listeners.
*/
dispose(): void;
/**
* Destroys the terminal and detaches it from the DOM.
*
* @deprecated Use dispose() instead.
*/
destroy(): void;