Merge remote-tracking branch 'origin/master' into pr/princjef/1460

This commit is contained in:
Daniel Imms
2018-07-03 11:50:07 -07:00
91 changed files with 2746 additions and 1376 deletions
+19 -21
View File
@@ -6,9 +6,9 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
BOTTOM
}
export class AccessibilityManager implements IDisposable {
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
@@ -179,7 +177,7 @@ export class AccessibilityManager implements IDisposable {
this._refreshRowsDimensions();
}
public _createAccessibilityTreeNode(): HTMLElement {
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
+135 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
@@ -48,6 +48,63 @@ describe('Buffer', () => {
});
});
describe('getWrappedRangeForLine', () => {
describe('non-wrapped', () => {
it('should return a single row for the first row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 });
});
it('should return a single row for a middle row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 });
});
it('should return a single row for the last row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 });
});
});
describe('wrapped', () => {
it('should return a range for the first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
});
it('should return a range for a middle row wrapping upwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(12)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
});
it('should return a range for a middle row wrapping downwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(13)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
});
it('should return a range for a middle row wrapping both ways', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(11)).isWrapped = true;
(<any> buffer.lines.get(12)).isWrapped = true;
(<any> buffer.lines.get(13)).isWrapped = true;
(<any> buffer.lines.get(14)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
});
it('should return a range for the last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(23)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
});
it('should return a range for a row that wraps upward to first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
});
it('should return a range for a row that wraps downward to last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(buffer.lines.length - 1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
});
});
});
describe('resize', () => {
describe('column size is reduced', () => {
it('should not trim the data in the buffer', () => {
@@ -212,4 +269,81 @@ describe('Buffer', () => {
assert.equal(buffer.markers.length, 0);
});
});
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
buffer.lines.set(0, [
[ null, 'a', 1, 'a'.charCodeAt(0)],
[ null, 'b', 1, 'b'.charCodeAt(0)],
[ null, 'c', 1, 'c'.charCodeAt(0)],
[ null, 'd', 1, 'd'.charCodeAt(0)]
]);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '語');
});
it('should handle a zero width character in the middle of the string by not including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str1, '語');
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str2, '語a');
});
it('should handle single width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁a');
});
it('should handle double width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str3, '😁a');
});
});
});
+25 -11
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
import { IMarker } from 'xterm';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
@@ -116,7 +117,7 @@ export class Buffer implements IBuffer {
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?
const ch: CharData = [DEFAULT_ATTR, ' ', 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);
@@ -226,7 +227,7 @@ export class Buffer implements IBuffer {
if (startCol >= i) {
startIndex--;
}
if (endCol >= i) {
if (endCol > i) {
endIndex--;
}
} else {
@@ -259,6 +260,20 @@ export class Buffer implements IBuffer {
return lineString.substring(startIndex, endIndex);
}
public getWrappedRangeForLine(y: number): { first: number, last: number } {
let first = y;
let last = y;
// Scan upwards for wrapped lines
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
first--;
}
// Scan downwards for wrapped lines
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
last++;
}
return { first, last };
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
@@ -305,14 +320,14 @@ 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.register(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) {
marker.dispose();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
@@ -323,11 +338,10 @@ export class Buffer implements IBuffer {
}
export class Marker extends EventEmitter implements IMarker {
private static NEXT_ID = 1;
private static _nextId = 1;
private _id: number = Marker.NEXT_ID++;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
@@ -342,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+2 -2
View File
@@ -121,7 +121,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function init_table(): number[] | Uint32Array {
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
@@ -161,7 +161,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
if (num < 127) {
return 1;
}
const t = table || init_table();
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
+33 -32
View File
@@ -5,13 +5,14 @@
import { assert } from 'chai';
import { CompositionHelper } from './CompositionHelper';
import { ITerminal } from './Types';
describe('CompositionHelper', () => {
let terminal;
let compositionHelper;
let compositionView;
let textarea;
let handledText;
let terminal: ITerminal;
let compositionHelper: CompositionHelper;
let compositionView: HTMLElement;
let textarea: HTMLTextAreaElement;
let handledText: string;
beforeEach(() => {
compositionView = {
@@ -27,14 +28,14 @@ describe('CompositionHelper', () => {
top: 0
},
textContent: ''
};
} as any;
textarea = {
value: '',
style: {
left: 0,
top: 0
}
};
} as any;
terminal = {
element: {
querySelector: () => {
@@ -54,7 +55,7 @@ describe('CompositionHelper', () => {
options: {
lineHeight: 1
}
};
} as any;
handledText = '';
compositionHelper = new CompositionHelper(textarea, compositionView, terminal);
});
@@ -63,7 +64,7 @@ describe('CompositionHelper', () => {
it('Should insert simple characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -71,7 +72,7 @@ describe('CompositionHelper', () => {
assert.equal(handledText, 'ㅇ');
// Second character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -87,13 +88,13 @@ describe('CompositionHelper', () => {
it('Should insert complex characters', (done) => {
// First character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -101,13 +102,13 @@ describe('CompositionHelper', () => {
assert.equal(handledText, '앙');
// Second character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = '앙ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '앙아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -127,19 +128,19 @@ describe('CompositionHelper', () => {
it('Should insert complex characters that change with following character', (done) => {
// First character '아'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
// Start second character '아' in first character
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -156,14 +157,14 @@ describe('CompositionHelper', () => {
it('Should insert multi-characters compositions', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'あ'
compositionHelper.compositionupdate({ data: 'だあ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だあ' });
textarea.value = 'だあ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -179,18 +180,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'ー'
compositionHelper.compositionupdate({ data: 'だー' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だー' });
textarea.value = 'だー';
setTimeout(() => { // wait for any textarea updates
// Convert to katakana 'ダー'
compositionHelper.compositionupdate({ data: 'ダー' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ダー' });
textarea.value = 'ダー';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -207,18 +208,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => {
// First character 'い'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'い' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'い' });
textarea.value = 'い';
setTimeout(() => { // wait for any textarea updates
// Second character 'ま'
compositionHelper.compositionupdate({ data: 'いm' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いm' });
textarea.value = 'いm';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'いま' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いま' });
textarea.value = 'いま';
setTimeout(() => { // wait for any textarea updates
// Convert to kanji '今'
compositionHelper.compositionupdate({ data: '今' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '今' });
textarea.value = '今';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -235,7 +236,7 @@ describe('CompositionHelper', () => {
it('Should insert non-composition characters input immediately after composition characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
+16 -16
View File
@@ -169,12 +169,12 @@ describe('EscapeSequenceParser', function (): void {
});
it('constructor', function (): void {
let p: EscapeSequenceParser = new EscapeSequenceParser();
chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE);
chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE);
p = new EscapeSequenceParser(VT500_TRANSITION_TABLE);
chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE);
chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE);
const tansitions: TransitionTable = new TransitionTable(10);
p = new EscapeSequenceParser(tansitions);
chai.expect(p.transitions).equal(tansitions);
chai.expect(p.TRANSITIONS).equal(tansitions);
});
it('inital states', function (): void {
chai.expect(parser.initialState).equal(ParserState.GROUND);
@@ -237,7 +237,7 @@ describe('EscapeSequenceParser', function (): void {
'\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f',
'\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a'
];
const exceptions = {
const exceptions: { [key: number]: { [key: string]: any[] } } = {
8: { '\x18': [], '\x1a': [] } // simply abort osc state
};
parser.reset();
@@ -247,7 +247,7 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = state;
parser.parse(exes[i]);
chai.expect(parser.currentState).equal(ParserState.GROUND);
testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]);
testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]);
parser.reset();
testTerminal.clear();
}
@@ -1099,20 +1099,20 @@ describe('EscapeSequenceParser', function (): void {
describe('set/clear handler', function (): void {
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\';
let parser2 = null;
let parser2: TestEscapeSequenceParser = null;
let print = '';
let esc = [];
let csi = [];
let exe = [];
let osc = [];
let dcs = [];
const esc: string[] = [];
const csi: [string, number[], string][] = [];
const exe: string[] = [];
const osc: [number, string][] = [];
const dcs: ([string] | [string, string] | [string, string, number[], number])[] = [];
function clearAccu(): void {
print = '';
esc = [];
csi = [];
exe = [];
osc = [];
dcs = [];
esc.length = 0;
csi.length = 0;
exe.length = 0;
osc.length = 0;
dcs.length = 0;
}
beforeEach(function (): void {
parser2 = new TestEscapeSequenceParser();
+24 -3
View File
@@ -4,6 +4,7 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { Disposable } from './common/Lifecycle';
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
@@ -207,7 +208,7 @@ class DcsDummy implements IDcsHandler {
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser implements IEscapeSequenceParser {
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
@@ -235,7 +236,9 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
protected _dcsHandlerFb: IDcsHandler;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly transitions: TransitionTable = VT500_TRANSITION_TABLE) {
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
@@ -260,6 +263,24 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
this._errorHandler = this._errorHandlerFb;
}
public dispose(): void {
this._printHandlerFb = null;
this._executeHandlerFb = null;
this._csiHandlerFb = null;
this._escHandlerFb = null;
this._oscHandlerFb = null;
this._dcsHandlerFb = null;
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
this._errorHandler = null;
}
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback;
}
@@ -342,7 +363,7 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
let osc = this._osc;
let collect = this._collect;
let params = this._params;
const table: Uint8Array | number[] = this.transitions.table;
const table: Uint8Array | number[] = this.TRANSITIONS.table;
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
let callback: Function | null = null;
+5 -1
View File
@@ -5,11 +5,13 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter, IDisposable {
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
@@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
return {
dispose: () => {
@@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
}
public dispose(): void {
super.dispose();
this._events = {};
}
}
+4
View File
@@ -12,18 +12,22 @@ describe('InputHandler', () => {
const terminal = new MockInputHandlingTerminal();
terminal.buffer.x = 1;
terminal.buffer.y = 2;
terminal.curAttr = 3;
const inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
terminal.curAttr = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
+36 -23
View File
@@ -4,18 +4,20 @@
* @license MIT
*/
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, ICharset } from './Types';
import { C0, C1 } from './EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
/**
@@ -77,7 +79,7 @@ class DECRQSS implements IDcsHandler {
// TODO: report real settings instead of 0m
return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`);
case ' q': // DECSCUSR
const STYLES = {'block': 2, 'underline': 4, 'bar': 6};
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
let style = STYLES[this._terminal.getOption('cursorStyle')];
style -= this._terminal.getOption('cursorBlink');
return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
@@ -110,13 +112,17 @@ class DECRQSS implements IDcsHandler {
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler implements IInputHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
this.register(this._parser);
this._surrogateHigh = '';
/**
@@ -163,12 +169,12 @@ export class InputHandler implements IInputHandler {
this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params));
this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params));
this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params));
this._parser.setCsiHandler('a', (params, collect) => this.HPositionRelative(params));
this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params));
this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params));
this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect));
this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params));
this._parser.setCsiHandler('e', (params, collect) => this.VPositionRelative(params));
this._parser.setCsiHandler('f', (params, collect) => this.HVPosition(params));
this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params));
this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params));
this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params));
this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect));
this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect));
@@ -284,6 +290,11 @@ export class InputHandler implements IInputHandler {
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
super.dispose();
this._terminal = null;
}
public parse(data: string): void {
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
@@ -949,7 +960,7 @@ export class InputHandler implements IInputHandler {
* [columns] (default = [row,col+1]) (HPR)
* reuse CSI Ps C ?
*/
public HPositionRelative(params: number[]): void {
public hPositionRelative(params: number[]): void {
let param = params[0];
if (param < 1) {
param = 1;
@@ -970,7 +981,7 @@ export class InputHandler implements IInputHandler {
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];
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
while (param--) {
line[buffer.x++] = ch;
@@ -1063,7 +1074,7 @@ export class InputHandler implements IInputHandler {
* [rows] (default = [row+1,column])
* reuse CSI Ps B ?
*/
public VPositionRelative(params: number[]): void {
public vPositionRelative(params: number[]): void {
let param = params[0];
if (param < 1) {
param = 1;
@@ -1083,7 +1094,7 @@ export class InputHandler implements IInputHandler {
* Horizontal and Vertical Position [row;column] (default =
* [1,1]) (HVP).
*/
public HVPosition(params: number[]): void {
public hVPosition(params: number[]): void {
if (params[0] < 1) params[0] = 1;
if (params[1] < 1) params[1] = 1;
@@ -1553,7 +1564,7 @@ export class InputHandler implements IInputHandler {
public charAttributes(params: number[]): void {
// Optimize a single SGR0.
if (params.length === 1 && params[0] === 0) {
this._terminal.curAttr = this._terminal.defAttr;
this._terminal.curAttr = DEFAULT_ATTR;
return;
}
@@ -1581,9 +1592,9 @@ export class InputHandler implements IInputHandler {
bg = p - 100;
} else if (p === 0) {
// default
flags = this._terminal.defAttr >> 18;
fg = (this._terminal.defAttr >> 9) & 0x1ff;
bg = this._terminal.defAttr & 0x1ff;
flags = DEFAULT_ATTR >> 18;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
// flags = 0;
// fg = 0x1ff;
// bg = 0x1ff;
@@ -1627,10 +1638,10 @@ export class InputHandler implements IInputHandler {
flags &= ~FLAGS.INVISIBLE;
} else if (p === 39) {
// reset fg
fg = (this._terminal.defAttr >> 9) & 0x1ff;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
} else if (p === 49) {
// reset bg
bg = this._terminal.defAttr & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
} else if (p === 38) {
// fg color 256
if (params[i + 1] === 2) {
@@ -1663,8 +1674,8 @@ export class InputHandler implements IInputHandler {
}
} else if (p === 100) {
// reset fg/bg
fg = (this._terminal.defAttr >> 9) & 0x1ff;
bg = this._terminal.defAttr & 0x1ff;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
} else {
this._terminal.error('Unknown SGR attribute: %d.', p);
}
@@ -1759,7 +1770,7 @@ export class InputHandler implements IInputHandler {
this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
this._terminal.curAttr = this._terminal.defAttr;
this._terminal.curAttr = DEFAULT_ATTR;
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
this._terminal.charset = null;
this._terminal.glevel = 0; // ??
@@ -1822,6 +1833,7 @@ export class InputHandler implements IInputHandler {
public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.savedCurAttr = this._terminal.curAttr;
}
@@ -1833,6 +1845,7 @@ export class InputHandler implements IInputHandler {
public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR;
}
+7 -5
View File
@@ -4,16 +4,16 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(_terminal: ITerminal) {
super(_terminal);
Linkifier.TIME_BEFORE_LINKIFY = 0;
constructor(terminal: ITerminal) {
super(terminal);
(<any>Linkifier).TIME_BEFORE_LINKIFY = 0;
}
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
@@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier {
}
class TestMouseZoneManager implements IMouseZoneManager {
dispose(): void {
}
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
+3 -3
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
/**
@@ -17,7 +17,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* the costly operation of searching every row multiple times, potentially a
* huge amount of times.
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected static readonly TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: ILinkMatcher[] = [];
+5 -5
View File
@@ -4,9 +4,9 @@
* @license MIT
*/
import { C0 } from './EscapeSequences';
import { C0 } from './common/data/EscapeSequences';
import { IInputHandler, IInputHandlingTerminal } from './Types';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
normalStateHandler[C0.BEL] = (parser, handler) => handler.bell();
@@ -124,12 +124,12 @@ csiStateHandler['T'] = (handler, params, prefix) => {
csiStateHandler['X'] = (handler, params, prefix) => handler.eraseChars(params);
csiStateHandler['Z'] = (handler, params, prefix) => handler.cursorBackwardTab(params);
csiStateHandler['`'] = (handler, params, prefix) => handler.charPosAbsolute(params);
csiStateHandler['a'] = (handler, params, prefix) => handler.HPositionRelative(params);
csiStateHandler['a'] = (handler, params, prefix) => handler.hPositionRelative(params);
csiStateHandler['b'] = (handler, params, prefix) => handler.repeatPrecedingCharacter(params);
csiStateHandler['c'] = (handler, params, prefix) => handler.sendDeviceAttributes(params);
csiStateHandler['d'] = (handler, params, prefix) => handler.linePosAbsolute(params);
csiStateHandler['e'] = (handler, params, prefix) => handler.VPositionRelative(params);
csiStateHandler['f'] = (handler, params, prefix) => handler.HVPosition(params);
csiStateHandler['e'] = (handler, params, prefix) => handler.vPositionRelative(params);
csiStateHandler['f'] = (handler, params, prefix) => handler.hVPosition(params);
csiStateHandler['g'] = (handler, params, prefix) => handler.tabClear(params);
csiStateHandler['h'] = (handler, params, prefix) => handler.setMode(params);
csiStateHandler['l'] = (handler, params, prefix) => handler.resetMode(params);
+71 -2
View File
@@ -4,8 +4,8 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './utils/CharMeasure';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
@@ -25,6 +25,8 @@ class TestSelectionManager extends SelectionManager {
public get model(): SelectionModel { return this._model; }
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
@@ -298,6 +300,16 @@ describe('SelectionManager', () => {
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
});
it('should select the entire wrapped line', () => {
buffer.lines.set(0, stringToRow('foo'));
const line2 = stringToRow('bar');
(<any>line2).isWrapped = true;
buffer.lines.set(1, line2);
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column');
});
});
describe('selectAll', () => {
@@ -368,4 +380,61 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.hasSelection, true);
});
});
describe('column selection', () => {
it('should select a column of text', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('abcdefghij'));
buffer.lines.set(1, stringToRow('klmnopqrst'));
buffer.lines.set(2, stringToRow('uvwxyz'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [2, 0];
selectionManager.model.selectionEnd = [4, 2];
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
});
it('should select a column of text without chopping up double width characters', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('語'));
buffer.lines.set(2, stringToRow('b'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n語\nb');
});
it('should select a column of text with single character emojis', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('☃'));
buffer.lines.set(2, stringToRow('c'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
});
it('should select a column of text with double character emojis', () => {
// TODO the case this is testing works for me in the demo webapp,
// but doing it programmatically fails.
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a '));
buffer.lines.set(1, stringArrayToRow(['😁', ' ']));
buffer.lines.set(2, stringToRow('c '));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
});
});
});
+65 -28
View File
@@ -6,7 +6,7 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -54,10 +54,11 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
const enum SelectionMode {
export const enum SelectionMode {
NORMAL,
WORD,
LINE
LINE,
COLUMN
}
/**
@@ -80,7 +81,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
/**
* The current selection mode.
*/
private _activeSelectionMode: SelectionMode;
protected _activeSelectionMode: SelectionMode;
/**
* A setInterval timer that is active while the mouse is down whose callback
@@ -116,6 +117,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
@@ -177,30 +183,43 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return '';
}
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
const result: string[] = [];
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
if (this._activeSelectionMode === SelectionMode.COLUMN) {
// Ignore zero width selections
if (start[0] === end[0]) {
return '';
}
for (let i = start[1]; i <= end[1]; i++) {
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
result.push(lineText);
}
}
} else {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
}
@@ -249,7 +268,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
this.emit('refresh', {
start: this._model.finalSelectionStart,
end: this._model.finalSelectionEnd,
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
});
}
/**
@@ -358,7 +381,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
public shouldForceSelection(event: MouseEvent): boolean {
return Browser.isMac ? event.altKey : event.shiftKey;
if (Browser.isMac) {
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
}
return event.shiftKey;
}
/**
@@ -449,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _onSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = SelectionMode.NORMAL;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
// Initialize the new selection
this._model.selectionStart = this._getMouseBufferCoords(event);
@@ -502,6 +529,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
}
/**
* Returns whether the selection manager should operate in column select mode
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
}
/**
* Handles the mousemove event when the mouse button is down, recording the
* end of the selection and refreshing the selection.
@@ -803,7 +838,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param line The line index.
*/
protected _selectLineAt(line: number): void {
this._model.selectionStart = [0, line];
this._model.selectionStartLength = this._terminal.cols;
const wrappedRange = this._buffer.getWrappedRangeForLine(line);
this._model.selectionStart = [0, wrappedRange.first];
this._model.selectionEnd = [this._terminal.cols, wrappedRange.last];
this._model.selectionStartLength = 0;
}
}
+15 -9
View File
@@ -14,6 +14,11 @@ import * as pty from 'node-pty';
import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
import { IViewport } from './Types';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
}
let primitivePty: any;
@@ -74,25 +79,25 @@ function terminalToString(term: Terminal): string {
// Skip tests on Windows since pty.open isn't supported
if (os.platform() !== 'win32') {
const CONSOLE_LOG = console.log;
const consoleLog = console.log;
// expect files need terminal at 80x25!
const COLS = 80;
const ROWS = 25;
const cols = 80;
const rows = 25;
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitivePty = pty.native.open(COLS, ROWS);
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', () => {
let xterm;
let xterm: TestTerminal;
beforeEach(() => {
xterm = new Terminal({ cols: COLS, rows: ROWS });
xterm = new TestTerminal({ cols: cols, rows: rows });
xterm.refresh = () => {};
xterm.viewport = {
xterm.viewport = <IViewport>{
syncScrollArea: () => {}
};
});
@@ -125,11 +130,12 @@ if (os.platform() !== 'win32') {
// Perform a synchronous .write(data)
xterm.writeBuffer.push(fromPty);
xterm._innerWrite();
xterm.innerWrite();
const fromEmulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
console.log = consoleLog;
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
+22 -266
View File
@@ -5,7 +5,6 @@
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import * as attach from './addons/attach/attach';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
@@ -13,7 +12,6 @@ const INIT_COLS = 80;
const INIT_ROWS = 24;
class TestTerminal extends Terminal {
public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollLines: number} { return this._evaluateKeyEscapeSequence(<KeyboardEvent>ev); }
public keyDown(ev: any): boolean { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
}
@@ -53,13 +51,6 @@ describe('term.js addons', () => {
});
});
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
// prototype.
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
});
describe('getOption', () => {
it('should retrieve the option correctly', () => {
// In the `options` namespace.
@@ -129,6 +120,17 @@ describe('term.js addons', () => {
});
});
describe('reset', () => {
it('should not affect cursorState', () => {
term.cursorState = 1;
term.reset();
assert.equal(term.cursorState, 1);
term.cursorState = 0;
term.reset();
assert.equal(term.cursorState, 0);
});
});
describe('clear', () => {
it('should clear a buffer equal to rows', () => {
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -176,7 +178,7 @@ describe('term.js addons', () => {
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 2; i++) {
term.writeln('test');
@@ -211,7 +213,7 @@ describe('term.js addons', () => {
});
describe('scrollPages', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -248,7 +250,7 @@ describe('term.js addons', () => {
});
describe('scrollToBottom', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -269,7 +271,7 @@ describe('term.js addons', () => {
});
describe('scrollToLine', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -296,22 +298,19 @@ describe('term.js addons', () => {
});
});
describe('keyDown', () => {
describe('keyPress', () => {
it('should scroll down, when a key is pressed and terminal is scrolled up', () => {
// Override _evaluateKeyEscapeSequence to return cancel code
(<any>term)._evaluateKeyEscapeSequence = () => {
return { key: 'a' };
};
const event = <KeyboardEvent>{
type: 'keydown',
keyCode: 0,
key: 'a',
keyCode: 65,
preventDefault: () => {},
stopPropagation: () => {}
};
term.buffer.ydisp = 0;
term.buffer.ybase = 40;
term.keyDown(event);
term.keyPress(event);
// Ensure that now the terminal is scrolled to bottom
assert.equal(term.buffer.ydisp, term.buffer.ybase);
@@ -330,7 +329,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollLines(-1);
assert.equal(term.buffer.ydisp, startYDisp - 1);
term.keyDown(<KeyboardEvent>{ keyCode: 0 });
term.keyPress(<KeyboardEvent>{ keyCode: 0 });
assert.equal(term.buffer.ydisp, startYDisp - 1);
});
});
@@ -468,252 +467,9 @@ describe('term.js addons', () => {
});
});
describe('evaluateKeyEscapeSequence', () => {
it('should return the correct escape sequence for unmodified keys', () => {
// Backspace
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^?
// Tab
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t');
// Return/enter
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR
// Escape
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b');
// Page up, page down
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B
// Insert
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
// Delete
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
// F1-F12
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[3;5~ for ctrl+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
});
it('should return \\x1b[3;2~ for shift+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
});
it('should return \\x1b[3;3~ for alt+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
});
it('should return \\x1b[5D for ctrl+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5A for ctrl+up', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for ctrl+down', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
describe('On non-macOS platforms', () => {
beforeEach(() => {
term.browser.isMac = false;
});
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
beforeEach(() => {
term.browser.isMac = true;
});
it('should return \\x1bb for alt+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D
});
it('should return \\x1bf for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
});
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
});
describe('Third level shift', () => {
let evKeyDown;
let evKeyPress;
let evKeyDown: any;
let evKeyPress: any;
beforeEach(() => {
term.handler = () => {};
+183 -522
View File
File diff suppressed because it is too large Load Diff
+10 -13
View File
@@ -3,9 +3,10 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -45,7 +46,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
savedCols: number;
x10Mouse: boolean;
@@ -88,7 +88,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
@@ -141,12 +141,12 @@ export interface IInputHandler {
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI a */ hPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI e */ vPositionRelative(params?: number[]): void;
/** CSI f */ hVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[], collect?: string): void;
/** CSI l */ resetMode(params?: number[], collect?: string): void;
@@ -194,10 +194,6 @@ export interface ILinkMatcher {
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x1: number;
y1: number;
@@ -215,7 +211,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
buffers: IBufferSet;
@@ -263,6 +258,7 @@ export interface ICharMeasure {
// TODO: The options that are not in the public API should be reviewed
export interface ITerminalOptions extends IPublicTerminalOptions {
[key: string]: any;
cancelEvents?: boolean;
convertEol?: boolean;
debug?: boolean;
@@ -286,6 +282,7 @@ export interface IBuffer {
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
}
@@ -473,7 +470,7 @@ export interface IDcsHandler {
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser {
export interface IEscapeSequenceParser extends IDisposable {
/**
* Reset the parser to its initial state (handlers are kept).
*/
+7 -3
View File
@@ -5,7 +5,9 @@
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -13,7 +15,7 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
* 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 {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
@@ -39,11 +41,13 @@ export class Viewport implements IViewport {
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this._viewportElement.addEventListener('scroll', this._onScroll.bind(this));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
+5 -1
View File
@@ -5,9 +5,13 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;
+22 -7
View File
@@ -5,7 +5,7 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
@@ -36,10 +36,11 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
}
};
let myTextDecoder;
// TODO: This should be typed but there seem to be issues importing the type
let myTextDecoder: any;
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
let str: string;
if (typeof ev.data === 'object') {
if (!myTextDecoder) {
@@ -86,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
socket.send(data);
};
socket.addEventListener('message', addonTerminal.__getMessage);
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
}
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));
}
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {
socket.addEventListener(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
socket.removeEventListener(type, handler);
handler = null;
}
};
}
/**
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+3
View File
@@ -0,0 +1,3 @@
## fit addon
The fit addon adjusts the dimensions of the terminal to match best fit its parent element container. `fit` will only work when the element is visible.
+4 -4
View File
@@ -37,10 +37,10 @@ export function proposeGeometry(term: Terminal): IGeometry {
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
};
return geometry;
}
@@ -50,7 +50,7 @@ export function fit(term: Terminal): void {
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
(<any>term).renderer.clear();
(<any>term)._core.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+7 -4
View File
@@ -5,14 +5,17 @@
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
// TODO: Don't rely on this private API
export interface ITerminalCore {
buffer: any;
selectionManager: any;
}
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
_core: ITerminalCore;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
+12 -12
View File
@@ -35,14 +35,14 @@ export class SearchHelper implements ISearchHelper {
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionEnd) {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionEnd[1];
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal.buffer.ybase + this._terminal.rows; y++) {
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
@@ -76,10 +76,10 @@ export class SearchHelper implements ISearchHelper {
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionStart) {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionStart[1];
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
// Search from ydisp + 1 to end
@@ -92,7 +92,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
@@ -111,11 +111,11 @@ export class SearchHelper implements ISearchHelper {
* @return The search result if it was found.
*/
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
const line = this._terminal.buffer.lines.get(y);
const line = this._terminal._core.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
@@ -147,8 +147,8 @@ export class SearchHelper implements ISearchHelper {
if (!result) {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
return true;
}
}
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+5 -1
View File
@@ -5,6 +5,10 @@
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
export interface ITerminalCore {
buffer: any;
}
export interface IWinptyCompatAddonTerminal extends Terminal {
_core: ITerminalCore;
}
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+2 -2
View File
@@ -26,11 +26,11 @@ export function winptyCompatInit(terminal: Terminal): void {
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
+2 -1
View File
@@ -6,7 +6,8 @@
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true,
"declaration": true
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
+3 -3
View File
@@ -46,13 +46,13 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
let zsentry;
function _shouldWrite(): boolean {
function shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new zmodem.Sentry({
to_terminal: (octets: ArrayLike<number>) => {
if (_shouldWrite()) {
if (shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
@@ -70,7 +70,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
// may be specific to xterm.js’s demo, ultimately we
// should reject anything that isn’t binary.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
if (shouldWrite()) {
term.write(evt.data);
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
* object and its components.
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
constructor() {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
/**
* Registers a disposable object.
* @param d The disposable to register.
*/
public register<T extends IDisposable>(d: T): void {
this._disposables.push(d);
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
* this event.
*/
export interface IKeyboardEvent {
altKey: boolean;
ctrlKey: boolean;
shiftKey: boolean;
metaKey: boolean;
keyCode: number;
key: string;
type: string;
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export const enum KeyboardResultType {
SEND_KEY,
SELECT_ALL,
PAGE_UP,
PAGE_DOWN
}
export interface IKeyboardResult {
type: KeyboardResultType;
cancel: boolean;
key: string | undefined;
}
export interface ICharset {
[key: string]: string;
}
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharset } from './Types';
import { ICharset } from '../Types';
/**
* The character sets supported by the terminal. These enable several languages
+284
View File
@@ -0,0 +1,284 @@
import { assert } from 'chai';
import { evaluateKeyboardEvent } from './Keyboard';
import { IKeyboardResult } from '../Types';
/**
* A helper function for testing which allows passing in a partial event and defaults will be filled
* in on it.
*/
function testEvaluateKeyboardEvent(partialEvent: {
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
keyCode?: number;
key?: string;
type?: string;
}, partialOptions: {
applicationCursorMode?: boolean;
isMac?: boolean;
macOptionIsMeta?: boolean;
} = {}): IKeyboardResult {
const event = {
altKey: partialEvent.altKey || false,
ctrlKey: partialEvent.ctrlKey || false,
shiftKey: partialEvent.shiftKey || false,
metaKey: partialEvent.metaKey || false,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : undefined,
key: partialEvent.key || '',
type: partialEvent.type || ''
};
const options = {
applicationCursorMode: partialOptions.applicationCursorMode || false,
isMac: partialOptions.isMac || false,
macOptionIsMeta: partialOptions.macOptionIsMeta || false
};
return evaluateKeyboardEvent(event, options.applicationCursorMode, options.isMac, options.macOptionIsMeta);
}
describe('Keyboard', () => {
describe('evaluateKeyEscapeSequence', () => {
it('should return the correct escape sequence for unmodified keys', () => {
// Backspace
assert.equal(testEvaluateKeyboardEvent({ keyCode: 8 }).key, '\x7f'); // ^?
// Tab
assert.equal(testEvaluateKeyboardEvent({ keyCode: 9 }).key, '\t');
// Return/enter
assert.equal(testEvaluateKeyboardEvent({ keyCode: 13 }).key, '\r'); // CR
// Escape
assert.equal(testEvaluateKeyboardEvent({ keyCode: 27 }).key, '\x1b');
// Page up, page down
assert.equal(testEvaluateKeyboardEvent({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(testEvaluateKeyboardEvent({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(testEvaluateKeyboardEvent({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(testEvaluateKeyboardEvent({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(testEvaluateKeyboardEvent({ keyCode: 38 }).key, '\x1b[A'); // CSI A
assert.equal(testEvaluateKeyboardEvent({ keyCode: 39 }).key, '\x1b[C'); // CSI C
assert.equal(testEvaluateKeyboardEvent({ keyCode: 40 }).key, '\x1b[B'); // CSI B
// Insert
assert.equal(testEvaluateKeyboardEvent({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
// Delete
assert.equal(testEvaluateKeyboardEvent({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
// F1-F12
assert.equal(testEvaluateKeyboardEvent({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
assert.equal(testEvaluateKeyboardEvent({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
assert.equal(testEvaluateKeyboardEvent({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
assert.equal(testEvaluateKeyboardEvent({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
assert.equal(testEvaluateKeyboardEvent({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[3;5~ for ctrl+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
});
it('should return \\x1b[3;2~ for shift+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
});
it('should return \\x1b[3;3~ for alt+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
});
it('should return \\x1b[5D for ctrl+left', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5A for ctrl+up', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for ctrl+down', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
describe('On non-macOS platforms', () => {
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
it('should return \\x1bb for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
});
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
});
});
describe('with macOptionIsMeta', () => {
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true, macOptionIsMeta: true }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
it('should handle mobile arrow events', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }).key, '\x1b[A');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }, { applicationCursorMode: true }).key, '\x1bOA');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }).key, '\x1b[D');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }, { applicationCursorMode: true }).key, '\x1bOD');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }).key, '\x1b[C');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }, { applicationCursorMode: true }).key, '\x1bOC');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
});
});
});
+357
View File
@@ -0,0 +1,357 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
import { IKeyboardEvent } from '../../common/Types';
import { IKeyboardResult, KeyboardResultType } from '../Types';
import { C0 } from '../../common/data/EscapeSequences';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = {
// digits 0-9
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
// special chars
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ['\'', '"']
};
export function evaluateKeyboardEvent(
ev: IKeyboardEvent,
applicationCursorMode: boolean,
isMac: boolean,
macOptionIsMeta: boolean
): IKeyboardResult {
const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY,
// Whether to cancel event propogation (NOTE: this may not be needed since the event is
// canceled at the end of keyDown
cancel: false,
// The new key even to emit
key: undefined
};
const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0);
switch (ev.keyCode) {
case 0:
if (ev.key === 'UIKeyInputUpArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
}
else if (ev.key === 'UIKeyInputLeftArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
}
else if (ev.key === 'UIKeyInputRightArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
}
else if (ev.key === 'UIKeyInputDownArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
}
break;
case 8:
// backspace
if (ev.shiftKey) {
result.key = C0.BS; // ^H
break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^?
break;
}
result.key = C0.DEL; // ^?
break;
case 9:
// tab
if (ev.shiftKey) {
result.key = C0.ESC + '[Z';
break;
}
result.key = C0.HT;
result.cancel = true;
break;
case 13:
// return/enter
result.key = C0.CR;
result.cancel = true;
break;
case 27:
// escape
result.key = C0.ESC;
result.cancel = true;
break;
case 37:
// left-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
break;
case 39:
// right-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
break;
case 38:
// up-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
break;
case 40:
// down-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
break;
case 45:
// insert
if (!ev.shiftKey && !ev.ctrlKey) {
// <Ctrl> or <Shift> + <Insert> are used to
// copy-paste on some systems.
result.key = C0.ESC + '[2~';
}
break;
case 46:
// delete
if (modifiers) {
result.key = C0.ESC + '[3;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[3~';
}
break;
case 36:
// home
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OH';
} else {
result.key = C0.ESC + '[H';
}
break;
case 35:
// end
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OF';
} else {
result.key = C0.ESC + '[F';
}
break;
case 33:
// page up
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_UP;
} else {
result.key = C0.ESC + '[5~';
}
break;
case 34:
// page down
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_DOWN;
} else {
result.key = C0.ESC + '[6~';
}
break;
case 112:
// F1-F12
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P';
} else {
result.key = C0.ESC + 'OP';
}
break;
case 113:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q';
} else {
result.key = C0.ESC + 'OQ';
}
break;
case 114:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R';
} else {
result.key = C0.ESC + 'OR';
}
break;
case 115:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S';
} else {
result.key = C0.ESC + 'OS';
}
break;
case 116:
if (modifiers) {
result.key = C0.ESC + '[15;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[15~';
}
break;
case 117:
if (modifiers) {
result.key = C0.ESC + '[17;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[17~';
}
break;
case 118:
if (modifiers) {
result.key = C0.ESC + '[18;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[18~';
}
break;
case 119:
if (modifiers) {
result.key = C0.ESC + '[19;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[19~';
}
break;
case 120:
if (modifiers) {
result.key = C0.ESC + '[20;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[20~';
}
break;
case 121:
if (modifiers) {
result.key = C0.ESC + '[21;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[21~';
}
break;
case 122:
if (modifiers) {
result.key = C0.ESC + '[23;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[23~';
}
break;
case 123:
if (modifiers) {
result.key = C0.ESC + '[24;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[24~';
}
break;
default:
// a-z and space
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode === 32) {
// NUL
result.key = String.fromCharCode(0);
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
// escape, file sep, group sep, record sep, unit sep
result.key = String.fromCharCode(ev.keyCode - 51 + 27);
} else if (ev.keyCode === 56) {
// delete
result.key = String.fromCharCode(127);
} else if (ev.keyCode === 219) {
// ^[ - Control Sequence Introducer (CSI)
result.key = String.fromCharCode(27);
} else if (ev.keyCode === 220) {
// ^\ - String Terminator (ST)
result.key = String.fromCharCode(28);
} else if (ev.keyCode === 221) {
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
if (key) {
result.key = C0.ESC + key;
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
result.key = C0.ESC + String.fromCharCode(keyCode);
}
} else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL;
}
}
break;
}
return result;
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
UP = 'A',
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Terminal } from './Terminal';
import * as attach from '../addons/attach/attach';
describe('Terminal', () => {
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
// prototype.
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
});
});
+154
View File
@@ -0,0 +1,154 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm';
import { ITerminal } from '../Types';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../Strings';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
}
public get element(): HTMLElement { return this._core.element; }
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get markers(): IMarker[] { return this._core.markers; }
public blur(): void {
this._core.blur();
}
public focus(): void {
this._core.focus();
}
public on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void;
public on(type: 'data', listener: (...args: any[]) => void): void;
public on(type: 'key', listener: (key?: string, event?: KeyboardEvent) => void): void;
public on(type: 'keypress' | 'keydown', listener: (event?: KeyboardEvent) => void): void;
public on(type: 'refresh', listener: (data?: { start: number; end: number; }) => void): void;
public on(type: 'resize', listener: (data?: { cols: number; rows: number; }) => void): void;
public on(type: 'scroll', listener: (ydisp?: number) => void): void;
public on(type: 'title', listener: (title?: string) => void): void;
public on(type: string, listener: (...args: any[]) => void): void;
public on(type: any, listener: any): void {
this._core.on(type, listener);
}
public off(type: string, listener: (...args: any[]) => void): void {
this._core.off(type, listener);
}
public emit(type: string, data?: any): void {
this._core.emit(type, data);
}
public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable {
return this._core.addDisposableListener(type, handler);
}
public resize(columns: number, rows: number): void {
this._core.resize(columns, rows);
}
public writeln(data: string): void {
this._core.writeln(data);
}
public open(parent: HTMLElement): void {
this._core.open(parent);
}
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
public deregisterLinkMatcher(matcherId: number): void {
this._core.deregisterLinkMatcher(matcherId);
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
return this._core.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): void {
this._core.deregisterCharacterJoiner(joinerId);
}
public addMarker(cursorYOffset: number): IMarker {
return this._core.addMarker(cursorYOffset);
}
public hasSelection(): boolean {
return this._core.hasSelection();
}
public getSelection(): string {
return this._core.getSelection();
}
public clearSelection(): void {
this._core.clearSelection();
}
public selectAll(): void {
this._core.selectAll();
}
public selectLines(start: number, end: number): void {
this._core.selectLines(start, end);
}
public dispose(): void {
this._core.dispose();
}
public destroy(): void {
this._core.destroy();
}
public scrollLines(amount: number): void {
this._core.scrollLines(amount);
}
public scrollPages(pageCount: number): void {
this._core.scrollPages(pageCount);
}
public scrollToTop(): void {
this._core.scrollToTop();
}
public scrollToBottom(): void {
this._core.scrollToBottom();
}
public scrollToLine(line: number): void {
this._core.scrollToLine(line);
}
public clear(): void {
this._core.clear();
}
public write(data: string): void {
this._core.write(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
public getOption(key: 'colors'): string[];
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
public getOption(key: 'handler'): (data: string) => void;
public getOption(key: string): any;
public getOption(key: any): any {
return this._core.getOption(key);
}
public setOption(key: 'bellSound' | 'fontFamily' | 'termName', value: string): void;
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
public setOption(key: 'colors', value: string[]): void;
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
public setOption(key: 'handler', value: (data: string) => void): void;
public setOption(key: 'theme', value: ITheme): void;
public setOption(key: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void;
public setOption(key: any, value: any): void {
this._core.setOption(key, value);
}
public refresh(start: number, end: number): void {
this._core.refresh(start, end);
}
public reset(): void {
this._core.reset();
}
public static applyAddon(addon: any): void {
addon.apply(Terminal);
}
public static get strings(): ILocalizableStrings {
return Strings;
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
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 onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
this._refreshCharAtlas(terminal, colorSet);
+1 -1
View File
@@ -36,7 +36,7 @@ describe('ColorManager', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi') {
// A #rrggbb or rgba(...)
assert.ok(cm.colors[key].css.length >= 7);
assert.ok((<any>cm.colors)[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
+6 -4
View File
@@ -11,8 +11,8 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoiner
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { RenderDebouncer } from '../ui/RenderDebouncer';
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer {
@@ -64,12 +64,14 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
observer.observe(this._terminal.element);
this.register({ dispose: () => observer.disconnect() });
}
}
@@ -143,8 +145,8 @@ export class Renderer extends EventEmitter implements IRenderer {
this._runOperation(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
}
public onCursorMove(): void {
+44 -18
View File
@@ -8,13 +8,14 @@ import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer {
private _state: {start: [number, number], end: [number, number]};
private _state: { start: [number, number], end: [number, number], columnSelectMode: boolean };
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'selection', zIndex, true, colors);
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
}
@@ -23,7 +24,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
}
@@ -31,15 +33,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
if (this._state.start && this._state.end) {
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
this.clearAll();
}
}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Selection has not changed
if (this._state.start === start || this._state.end === end) {
if (!this._didStateChange(start, end, columnSelectMode)) {
return;
}
@@ -62,25 +65,48 @@ export class SelectionRenderLayer extends BaseRenderLayer {
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.css;
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);
if (columnSelectMode) {
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this.fillCells(startCol, viewportCappedStartRow, width, height);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 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 viewportStartRow
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]];
this._state.columnSelectMode = columnSelectMode;
}
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean): boolean {
return !this._areCoordinatesEqual(start, this._state.start) ||
!this._areCoordinatesEqual(end, this._state.end) ||
columnSelectMode !== this._state.columnSelectMode;
}
private _areCoordinatesEqual(coord1: [number, number], coord2: [number, number]): boolean {
if (!coord1 || !coord2) {
return false;
}
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
}
}
+3 -1
View File
@@ -260,7 +260,9 @@ export class TextRenderLayer extends BaseRenderLayer {
return;
}
this._charAtlas.beginFrame();
if (this._charAtlas) {
this._charAtlas.beginFrame();
}
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
this._drawBackground(terminal, firstRow, lastRow);
+9 -4
View File
@@ -4,7 +4,7 @@
*/
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
@@ -20,17 +20,22 @@ export const enum FLAGS {
ITALIC = 64
}
export interface IRenderer extends IEventEmitter {
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IEventEmitter, IDisposable {
dimensions: IRenderDimensions;
colorManager: IColorManager;
dispose(): void;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
@@ -96,7 +101,7 @@ export interface IRenderLayer {
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
+1 -1
View File
@@ -9,7 +9,7 @@ import { ICharAtlasConfig } from '../../shared/atlas/Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors = {
const clonedColors = <IColorSet>{
foreground: colors.foreground,
background: colors.background,
cursor: null,
+1 -1
View File
@@ -11,7 +11,7 @@ interface ILinkedListNode<T> {
}
export default class LRUMap<T> {
private _map = {};
private _map: { [key: string]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> = null;
private _tail: ILinkedListNode<T> = null;
private _nodePool: ILinkedListNode<T>[] = [];
+1 -1
View File
@@ -26,7 +26,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
return canvas;
}
public _doWarmUp(): void {
protected _doWarmUp(): void {
const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config);
if (result instanceof HTMLCanvasElement) {
this._texture = result;
+325
View File
@@ -0,0 +1,325 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
import { ITerminal, CharacterJoinerHandler } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
const FG_CLASS_PREFIX = 'xterm-fg-';
const BG_CLASS_PREFIX = 'xterm-bg-';
const FOCUS_CLASS = 'xterm-focus';
const SELECTION_CLASS = 'xterm-selection';
let nextTerminalId = 1;
// TODO: Pull into an addon when TS composite projects allow easier sharing of code (not just
// interfaces) between core and addons
/**
* A fallback renderer for when canvas is slow. This is not meant to be
* particularly fast or feature complete, more just stable and usable for when
* canvas is not an option.
*/
export class DomRenderer extends EventEmitter implements IRenderer {
private _renderDebouncer: RenderDebouncer;
private _rowFactory: DomRendererRowFactory;
private _terminalClass: number = nextTerminalId++;
private _themeStyleElement: HTMLStyleElement;
private _dimensionsStyleElement: HTMLStyleElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement;
public dimensions: IRenderDimensions;
public colorManager: ColorManager;
constructor(private _terminal: ITerminal, theme: ITheme | undefined) {
super();
const allowTransparency = this._terminal.options.allowTransparency;
this.colorManager = new ColorManager(document, allowTransparency);
this.setTheme(theme);
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true');
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
this._selectionContainer = document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');
this.dimensions = {
scaledCharWidth: null,
scaledCharHeight: null,
scaledCellWidth: null,
scaledCellHeight: null,
scaledCharLeft: null,
scaledCharTop: null,
scaledCanvasWidth: null,
scaledCanvasHeight: null,
canvasWidth: null,
canvasHeight: null,
actualCellWidth: null,
actualCellHeight: null
};
this._updateDimensions();
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._rowFactory = new DomRendererRowFactory(document);
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.appendChild(this._rowContainer);
this._terminal.screenElement.appendChild(this._selectionContainer);
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth;
this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight;
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows;
this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols;
this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows;
this.dimensions.actualCellWidth = this._terminal.charMeasure.width;
this.dimensions.actualCellHeight = this._terminal.charMeasure.height;
this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`;
element.style.height = `${this._terminal.charMeasure.height}px`;
});
if (!this._dimensionsStyleElement) {
this._dimensionsStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._dimensionsStyleElement);
}
const styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
` display: inline-block;` +
` height: 100%;` +
` vertical-align: top;` +
` width: ${this._terminal.charMeasure.width}px` +
`}`;
this._dimensionsStyleElement.innerHTML = styles;
this._selectionContainer.style.height = (<any>this._terminal)._viewportElement.style.height;
this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`;
this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`;
}
public setTheme(theme: ITheme | undefined): IColorSet {
if (theme) {
this.colorManager.setTheme(theme);
}
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._themeStyleElement);
}
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this.colorManager.colors.foreground.css};` +
` background-color: ${this.colorManager.colors.background.css};` +
` font-family: ${this._terminal.getOption('fontFamily')};` +
` font-size: ${this._terminal.getOption('fontSize')}px;` +
`}`;
// Text styles
styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
` font-weight: ${this._terminal.options.fontWeight};` +
`}` +
`${this._terminalSelector} span.${BOLD_CLASS} {` +
` font-weight: ${this._terminal.options.fontWeightBold};` +
`}` +
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
` font-style: italic;` +
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` +
` background-color: ${this.colorManager.colors.cursor.css};` +
` color: ${this.colorManager.colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid #fff;` +
` outline-offset: -1px;` +
`}`;
// Selection
styles +=
`${this._terminalSelector} .${SELECTION_CLASS} {` +
` position: absolute;` +
` top: 0;` +
` left: 0;` +
` z-index: 1;` +
` pointer-events: none;` +
`}` +
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
` position: absolute;` +
` background-color: ${this.colorManager.colors.selection.css};` +
`}`;
// Colors
this.colorManager.colors.ansi.forEach((c, i) => {
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
});
this._themeStyleElement.innerHTML = styles;
return this.colorManager.colors;
}
public onWindowResize(devicePixelRatio: number): void {
this._updateDimensions();
}
private _refreshRowElements(cols: number, rows: number): void {
// Add missing elements
for (let i = this._rowElements.length; i <= rows; i++) {
const row = document.createElement('div');
this._rowContainer.appendChild(row);
this._rowElements.push(row);
}
// Remove excess elements
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop());
}
}
public onResize(cols: number, rows: number): void {
this._refreshRowElements(cols, rows);
this._updateDimensions();
}
public onCharSizeChanged(): void {
this._updateDimensions();
}
public onBlur(): void {
this._rowContainer.classList.remove(FOCUS_CLASS);
}
public onFocus(): void {
this._rowContainer.classList.add(FOCUS_CLASS);
}
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Remove all selections
while (this._selectionContainer.children.length) {
this._selectionContainer.removeChild(this._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();
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
if (columnSelectMode) {
documentFragment.appendChild(
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
);
} else {
// Draw first row
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._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;
}
public onCursorMove(): void {
// No-op, the cursor is drawn when rows are drawn
}
public onOptionsChanged(): void {
// Force a refresh
this._updateDimensions();
this.setTheme(undefined);
this._terminal.refresh(0, this._terminal.rows - 1);
}
public clear(): void {
this._rowElements.forEach(e => e.innerHTML = '');
}
public refreshRows(start: number, end: number): void {
this._renderDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const terminal = this._terminal;
const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
const cursorX = this._terminal.buffer.x;
for (let y = start; y <= end; y++) {
const rowElement = this._rowElements[y];
rowElement.innerHTML = '';
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
}
private get _terminalSelector(): string {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
}
@@ -0,0 +1,156 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory';
import { LineData } from '../../Types';
import { DEFAULT_ATTR } from '../../Buffer';
import { FLAGS } from '../Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
let rowFactory: DomRendererRowFactory;
let lineData: LineData;
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document);
lineData = createEmptyLineData(2);
});
describe('createRow', () => {
it('should create an element for every character in the row', () => {
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
);
});
it('should set correct attributes for double width characters', () => {
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
// There should be no element for the following "empty" cell
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
});
it('should add class for cursor', () => {
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-cursor"> </span>' +
'<span> </span>'
);
});
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
});
describe('attributes', () => {
it('should add class for bold', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
);
});
it('should add class for italic', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
);
});
it('should add classes for 256 foreground colors', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
);
}
});
it('should add classes for 256 background colors', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
);
}
});
it('should correctly invert colors', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
);
});
it('should correctly invert default fg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
'<span> </span>'
);
});
it('should correctly invert default bg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
'<span> </span>'
);
});
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
'<span> </span>'
);
}
});
});
});
function getFragmentHtml(fragment: DocumentFragment): string {
const element = dom.window.document.createElement('div');
element.appendChild(fragment);
return element.innerHTML;
}
function createEmptyLineData(cols: number): LineData {
const lineData: LineData = [];
for (let i = 0; i < cols; i++) {
lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
}
return lineData;
}
});
+90
View File
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { LineData } from '../../Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
import { FLAGS } from '../Types';
export const BOLD_CLASS = 'xterm-bold';
export const ITALIC_CLASS = 'xterm-italic';
export const CURSOR_CLASS = 'xterm-cursor';
export class DomRendererRowFactory {
constructor(
private _document: Document
) {
}
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
for (let x = 0; x < lineData.length; x++) {
// Don't allow any buffer to the right to be displayed
if (colCount >= cols) {
continue;
}
const charData = lineData[x];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
const 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) {
continue;
}
const charElement = this._document.createElement('span');
if (width > 1) {
charElement.style.width = `${cellWidth * width}px`;
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
}
// If inverse flag is on, the foreground should become the background.
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
fg = 0;
}
if (bg === 257) {
bg = 15;
}
}
if (flags & FLAGS.BOLD) {
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
charElement.classList.add(BOLD_CLASS);
}
if (flags & FLAGS.ITALIC) {
charElement.classList.add(ITALIC_CLASS);
}
charElement.textContent = char;
if (fg !== 257) {
charElement.classList.add(`xterm-fg-${fg}`);
}
if (bg !== 256) {
charElement.classList.add(`xterm-bg-${bg}`);
}
fragment.appendChild(charElement);
colCount += width;
}
return fragment;
}
}
+1 -1
View File
@@ -106,7 +106,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
return canvas;
}
// Transfer to an ImageBitmap is this is an OffscreenCanvas
return new Promise(r => r(canvas.transferToImageBitmap()));
return new Promise((r: (bitmap: ImageBitmap) => void) => r(canvas.transferToImageBitmap()));
}
const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+1 -1
View File
@@ -10,7 +10,7 @@ import { IDisposable } from 'xterm';
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableListener(
export function addDisposableDomListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
@@ -5,6 +5,8 @@
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from '../common/Lifecycle';
import { addDisposableDomListener } from './Lifecycle';
const HOVER_DURATION = 500;
@@ -16,7 +18,7 @@ const HOVER_DURATION = 500;
* needed to support was single-line links which never overlap. Improvements can
* be made in the future.
*/
export class MouseZoneManager implements IMouseZoneManager {
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _zones: IMouseZone[] = [];
private _areZonesActive: boolean = false;
@@ -30,13 +32,20 @@ export class MouseZoneManager implements IMouseZoneManager {
constructor(
private _terminal: ITerminal
) {
this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e));
super();
this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e)));
// 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 dispose(): void {
super.dispose();
this._deactivate();
}
public add(zone: IMouseZone): void {
this._zones.push(zone);
if (this._zones.length === 1) {
@@ -3,6 +3,8 @@
* @license MIT
*/
import { Disposable } from '../common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
/**
@@ -15,7 +17,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat
* The listener should fire on both window zoom changes and switching to a
* monitor with a different DPI.
*/
export class ScreenDprMonitor {
export class ScreenDprMonitor extends Disposable {
private _currentDevicePixelRatio: number;
private _outerListener: MediaQueryListListener;
private _listener: ScreenDprListener;
@@ -33,6 +35,11 @@ export class ScreenDprMonitor {
this._updateDpr();
}
public dispose(): void {
super.dispose();
this.clearListener();
}
private _updateDpr(): void {
// Clear listeners for old DPR
if (this._resolutionMediaMatchList) {
+3 -1
View File
@@ -3,7 +3,9 @@
* @license MIT
*/
export interface IMouseZoneManager {
import { IDisposable } from 'xterm';
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
+1 -1
View File
@@ -39,7 +39,7 @@ describe('clone', () => {
});
it('should clone null values', () => {
const test = {
const test: any = {
a: null
};
+11 -3
View File
@@ -107,7 +107,6 @@ export class MockTerminal implements ITerminal {
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
@@ -184,8 +183,8 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -269,7 +268,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
throw new Error('Method not implemented.');
}
setOption(key: string, value: any): void {
this.options[key] = value;
(<any>this.options)[key] = value;
}
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
@@ -304,6 +303,9 @@ export class MockBuffer implements IBuffer {
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
getWrappedRangeForLine(y: number): { first: number; last: number; } {
throw new Error('Method not implemented.');
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
}
@@ -316,6 +318,9 @@ export class MockBuffer implements IBuffer {
}
export class MockRenderer implements IRenderer {
dispose(): void {
throw new Error('Method not implemented.');
}
colorManager: IColorManager;
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
@@ -346,6 +351,9 @@ export class MockRenderer implements IRenderer {
}
export class MockViewport implements IViewport {
dispose(): void {
throw new Error('Method not implemented.');
}
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
+5
View File
@@ -139,6 +139,11 @@
cursor: pointer;
}
.xterm.xterm-cursor-crosshair {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
+1 -1
View File
@@ -5,6 +5,6 @@
* This file is the entry point for browserify.
*/
import { Terminal } from './Terminal';
import { Terminal } from './public/Terminal';
module.exports = Terminal;