Merge remote-tracking branch 'origin/master' into pr/princjef/1460
This commit is contained in:
+19
-21
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+10
-13
@@ -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
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/attach/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/fit/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/fullscreen/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/search/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/terminado/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/webLinks/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/winptyCompat/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"outDir": "../../../lib/addons/zmodem/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>[] = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ describe('clone', () => {
|
||||
});
|
||||
|
||||
it('should clone null values', () => {
|
||||
const test = {
|
||||
const test: any = {
|
||||
a: null
|
||||
};
|
||||
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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
@@ -5,6 +5,6 @@
|
||||
* This file is the entry point for browserify.
|
||||
*/
|
||||
|
||||
import { Terminal } from './Terminal';
|
||||
import { Terminal } from './public/Terminal';
|
||||
|
||||
module.exports = Terminal;
|
||||
|
||||
Reference in New Issue
Block a user