Merge branch 'master' into reverse_wraparound
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { RenderDebouncer } from 'browser/RenderDebouncer';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
const enum BoundaryPosition {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
export class AccessibilityManager extends Disposable {
|
||||
private _accessibilityTreeRoot: HTMLElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[];
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* not announced (added to live region) because it has already been announced
|
||||
* by the textarea event (which cannot be canceled). There are some race
|
||||
* condition cases if there is typing while data is streaming, but this covers
|
||||
* the main case of typing into the prompt and inputting the answer to a
|
||||
* question (Y/N, etc.).
|
||||
*/
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
private _charsToAnnounce: string = '';
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: ITerminal,
|
||||
private readonly _renderService: IRenderService
|
||||
) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.classList.add('xterm-accessibility-tree');
|
||||
this._rowContainer.setAttribute('role', 'list');
|
||||
this._rowElements = [];
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
this._liveRegion.classList.add('live-region');
|
||||
this._liveRegion.setAttribute('aria-live', 'assertive');
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
|
||||
if (!this._terminal.element) {
|
||||
throw new Error('Cannot enable accessibility before Terminal.open');
|
||||
}
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.onA11yChar(char => this._onChar(char)));
|
||||
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
|
||||
this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._onKey(e.key)));
|
||||
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this.register(this._screenDprMonitor);
|
||||
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the ScreenDprMonitor isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._terminal.element?.removeChild(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = <HTMLElement>e.target;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
const posInSet = boundaryElement.getAttribute('aria-posinset');
|
||||
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
|
||||
if (posInSet === lastRowPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't scroll when the last focused item was not the second row (focus is going the other
|
||||
// direction)
|
||||
if (e.relatedTarget !== beforeBoundaryElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old boundary element from array
|
||||
let topBoundaryElement: HTMLElement;
|
||||
let bottomBoundaryElement: HTMLElement;
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
topBoundaryElement = boundaryElement;
|
||||
bottomBoundaryElement = this._rowElements.pop()!;
|
||||
this._rowContainer.removeChild(bottomBoundaryElement);
|
||||
} else {
|
||||
topBoundaryElement = this._rowElements.shift()!;
|
||||
bottomBoundaryElement = boundaryElement;
|
||||
this._rowContainer.removeChild(topBoundaryElement);
|
||||
}
|
||||
|
||||
// Remove listeners from old boundary elements
|
||||
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
|
||||
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Add new element to array/DOM
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.unshift(newElement);
|
||||
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
|
||||
} else {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.push(newElement);
|
||||
this._rowContainer.appendChild(newElement);
|
||||
}
|
||||
|
||||
// Add listeners to new boundary elements
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Scroll up
|
||||
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
|
||||
|
||||
// Focus new boundary before element
|
||||
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
|
||||
|
||||
// Prevent the standard behavior
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Grow rows as required
|
||||
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
// Shrink rows as required
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop()!);
|
||||
}
|
||||
|
||||
// Add bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
private _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
this._refreshRowDimensions(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _onTab(spaceCount: number): void {
|
||||
for (let i = 0; i < spaceCount; i++) {
|
||||
this._onChar(' ');
|
||||
}
|
||||
}
|
||||
|
||||
private _onChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
if (shiftedChar !== char) {
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
} else {
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
|
||||
if (char === '\n') {
|
||||
this._liveRegionLineCount++;
|
||||
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
|
||||
this._liveRegion.textContent += Strings.tooMuchOutput;
|
||||
}
|
||||
}
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
|
||||
setTimeout(() => {
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearLiveRegion(): void {
|
||||
this._liveRegion.textContent = '';
|
||||
this._liveRegionLineCount = 0;
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.parentNode) {
|
||||
this._accessibilityTreeRoot.removeChild(this._liveRegion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onKey(keyChar: string): void {
|
||||
this._clearLiveRegion();
|
||||
this._charsToConsume.push(keyChar);
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const setSize = buffer.lines.length.toString();
|
||||
for (let i = start; i <= end; i++) {
|
||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||
const element = this._rowElements[i];
|
||||
if (element) {
|
||||
if (lineData.length === 0) {
|
||||
element.innerHTML = ' ';
|
||||
} else {
|
||||
element.textContent = lineData;
|
||||
}
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
this._announceCharacters();
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._renderService.dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._onResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._renderService.dimensions.actualCellHeight}px`;
|
||||
}
|
||||
|
||||
private _announceCharacters(): void {
|
||||
if (this._charsToAnnounce.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._liveRegion.textContent += this._charsToAnnounce;
|
||||
this._charsToAnnounce = '';
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,17 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
|
||||
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
|
||||
ev.stopPropagation();
|
||||
if (ev.clipboardData) {
|
||||
const text = ev.clipboardData.getData('text/plain');
|
||||
paste(text, textarea, bracketedPasteMode, coreService);
|
||||
paste(text, textarea, coreService);
|
||||
}
|
||||
}
|
||||
|
||||
export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
|
||||
export function paste(text: string, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = bracketTextForPaste(text, bracketedPasteMode);
|
||||
text = bracketTextForPaste(text, coreService.decPrivateModes.bracketedPasteMode);
|
||||
coreService.triggerDataEvent(text, true);
|
||||
textarea.value = '';
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from './Types';
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
@@ -26,7 +26,7 @@ export class RenderDebouncer implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart: number, rowEnd: number, rowCount: number): void {
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as glob from 'glob';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as pty from 'node-pty';
|
||||
import { Terminal } from 'browser/Terminal';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
// all test files expect terminal in 80x25
|
||||
const COLS = 80;
|
||||
const ROWS = 25;
|
||||
|
||||
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')});
|
||||
const SKIP_FILES = [
|
||||
't0084-CBT.in',
|
||||
't0101-NLM.in',
|
||||
't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose
|
||||
't0504-vim.in'
|
||||
];
|
||||
if (os.platform() === 'darwin') {
|
||||
// These are failing on macOS only (termios related?)
|
||||
SKIP_FILES.push(
|
||||
't0003-line_wrap.in',
|
||||
't0005-CR.in',
|
||||
't0009-NEL.in',
|
||||
't0503-zsh_ls_color.in'
|
||||
);
|
||||
}
|
||||
// filter skipFilenames
|
||||
const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1);
|
||||
|
||||
describe('Escape Sequence Files', function(): void {
|
||||
this.timeout(1000);
|
||||
|
||||
let ptyTerm: any;
|
||||
let slaveEnd: any;
|
||||
let term: Terminal;
|
||||
let customHandler: IDisposable | undefined;
|
||||
|
||||
before(() => {
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
ptyTerm = (pty as any).open({cols: COLS, rows: ROWS});
|
||||
slaveEnd = ptyTerm._slave;
|
||||
term = new Terminal({cols: COLS, rows: ROWS});
|
||||
ptyTerm._master.on('data', (data: string) => term.write(data));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
ptyTerm._master.end();
|
||||
ptyTerm._master.destroy();
|
||||
});
|
||||
|
||||
FILES.forEach(filename => {
|
||||
(process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => {
|
||||
// reset terminal and handler
|
||||
if (customHandler) {
|
||||
customHandler.dispose();
|
||||
}
|
||||
slaveEnd.write('\r\n');
|
||||
term.reset();
|
||||
slaveEnd.write('\x1bc\x1b[H');
|
||||
|
||||
// register handler to trigger viewport scraping, wait for it to finish
|
||||
let content = '';
|
||||
const OSC_CODE = 12345;
|
||||
await new Promise(resolve => {
|
||||
customHandler = term.addOscHandler(OSC_CODE, () => {
|
||||
// grab terminal viewport content
|
||||
content = terminalToString(term);
|
||||
resolve();
|
||||
return true;
|
||||
});
|
||||
// write file to slave
|
||||
slaveEnd.write(fs.readFileSync(filename, 'utf8'));
|
||||
// trigger custom sequence
|
||||
slaveEnd.write(`\x1b]${OSC_CODE};\x07`);
|
||||
});
|
||||
|
||||
// compare with expected output (right trimmed)
|
||||
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
|
||||
if (content !== expectedRightTrimmed) {
|
||||
throw new Error(formatError(fs.readFileSync(filename, 'utf8'), content, expected));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
// generate colorful noisy output to compare xterm and emulator cell states
|
||||
function formatError(input: string, output: string, expected: string): string {
|
||||
function addLineNumber(start: number, color: string): (s: string) => string {
|
||||
let counter = start || 0;
|
||||
return function(s: string): string {
|
||||
counter += 1;
|
||||
return '\x1b[33m' + (' ' + counter).slice(-2) + color + s;
|
||||
};
|
||||
}
|
||||
const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
|
||||
let s = '';
|
||||
s += `\n\x1b[34m${JSON.stringify(input)}`;
|
||||
s += `\n\x1b[33m ${line80}\n`;
|
||||
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
|
||||
s += `\n\x1b[33m ${line80}\n`;
|
||||
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
|
||||
return s;
|
||||
}
|
||||
|
||||
// simple debug output of terminal cells
|
||||
function terminalToString(term: Terminal): string {
|
||||
let result = '';
|
||||
let lineText = '';
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = term.buffer.lines.get(line)!.translateToString(true);
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
result += lineText;
|
||||
result += '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -3,13 +3,331 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService, IMouseService, IRenderService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { Terminal } from 'browser/Terminal';
|
||||
import { IUnicodeService, IOptionsService } from 'common/services/Services';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
public onBlur!: IEvent<void>;
|
||||
public onFocus!: IEvent<void>;
|
||||
public onA11yChar!: IEvent<string>;
|
||||
public onA11yTab!: IEvent<number>;
|
||||
public onCursorMove!: IEvent<void>;
|
||||
public onLineFeed!: IEvent<void>;
|
||||
public onSelectionChange!: IEvent<void>;
|
||||
public onData!: IEvent<string>;
|
||||
public onBinary!: IEvent<string>;
|
||||
public onTitleChange!: IEvent<string>;
|
||||
public onScroll!: IEvent<number>;
|
||||
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
||||
public markers!: IMarker[];
|
||||
public optionsService!: IOptionsService;
|
||||
public unicodeService!: IUnicodeService;
|
||||
public addMarker(cursorYOffset: number): IMarker {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectLines(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToLine(line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public static string: any;
|
||||
public setOption(key: any, value: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public blur(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public focus(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(columns: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public writeln(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public paste(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public open(parent: HTMLElement): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public deregisterLinkMatcher(matcherId: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public hasSelection(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public getSelection(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public getSelectionPosition(): ISelectionPosition | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearSelection(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public select(column: number, row: number, length: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectAll(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToTop(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToBottom(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clear(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public write(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public writeUtf8(data: Uint8Array): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public bracketedPasteMode!: boolean;
|
||||
public renderer!: IRenderer;
|
||||
public linkifier!: ILinkifier;
|
||||
public linkifier2!: ILinkifier2;
|
||||
public isFocused!: boolean;
|
||||
public options: ITerminalOptions = {};
|
||||
public element!: HTMLElement;
|
||||
public screenElement!: HTMLElement;
|
||||
public rowContainer!: HTMLElement;
|
||||
public selectionContainer!: HTMLElement;
|
||||
public selectionService!: ISelectionService;
|
||||
public textarea!: HTMLTextAreaElement;
|
||||
public rows!: number;
|
||||
public cols!: number;
|
||||
public browser: IBrowser = <any>Browser;
|
||||
public writeBuffer!: string[];
|
||||
public children!: HTMLElement[];
|
||||
public cursorHidden!: boolean;
|
||||
public cursorState!: number;
|
||||
public scrollback!: number;
|
||||
public buffers!: IBufferSet;
|
||||
public buffer!: IBuffer;
|
||||
public viewport!: IViewport;
|
||||
public applicationCursor!: boolean;
|
||||
public handler(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public on(event: string, callback: (...args: any[]) => void): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public off(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollLines(disp: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToRow(absoluteRow: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public cancel(ev: Event, force?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public log(text: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public emit(event: string, data: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public reset(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public refresh(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
public deregisterCharacterJoiner(joinerId: number): void { }
|
||||
}
|
||||
|
||||
export class MockBuffer implements IBuffer {
|
||||
public markers!: IMarker[];
|
||||
public addMarker(y: number): IMarker {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public isCursorInViewport!: boolean;
|
||||
public lines!: ICircularList<IBufferLine>;
|
||||
public ydisp!: number;
|
||||
public ybase!: number;
|
||||
public hasScrollback!: boolean;
|
||||
public y!: number;
|
||||
public x!: number;
|
||||
public tabs: any;
|
||||
public scrollBottom!: number;
|
||||
public scrollTop!: number;
|
||||
public savedY!: number;
|
||||
public savedX!: number;
|
||||
public savedCharset: ICharset | undefined;
|
||||
public savedCurAttrData = new AttributeData();
|
||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
|
||||
return Buffer.prototype.translateBufferLineToString.apply(this, arguments as any);
|
||||
}
|
||||
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
||||
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments as any);
|
||||
}
|
||||
public nextStop(x?: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public prevStop(x?: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setLines(lines: ICircularList<IBufferLine>): void {
|
||||
this.lines = lines;
|
||||
}
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return Buffer.prototype.getBlankLine.apply(this, arguments as any);
|
||||
}
|
||||
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
|
||||
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments as any);
|
||||
}
|
||||
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
|
||||
return Buffer.prototype.iterator.apply(this, arguments as any);
|
||||
}
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
public onRequestRedraw!: IEvent<IRequestRedrawEvent>;
|
||||
public onCanvasResize!: IEvent<{ width: number, height: number }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public colorManager!: IColorManager;
|
||||
public on(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public off(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public emit(type: string, data?: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public dimensions!: IRenderDimensions;
|
||||
public setColors(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onResize(cols: number, rows: number): void { }
|
||||
public onCharSizeChanged(): void { }
|
||||
public onBlur(): void { }
|
||||
public onFocus(): void { }
|
||||
public onSelectionChanged(start: [number, number], end: [number, number]): void { }
|
||||
public onCursorMove(): void { }
|
||||
public onOptionsChanged(): void { }
|
||||
public onDevicePixelRatioChange(): void { }
|
||||
public clear(): void { }
|
||||
public renderRows(start: number, end: number): void { }
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
public deregisterCharacterJoiner(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollBarWidth: number = 0;
|
||||
public onThemeChange(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onWheel(ev: WheelEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onTouchStart(ev: TouchEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onTouchMove(ev: TouchEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public syncScrollArea(): void { }
|
||||
public getLinesScrolled(ev: WheelEvent): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCompositionHelper implements ICompositionHelper {
|
||||
public compositionstart(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public compositionend(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public updateCompositionElements(dontRecurse?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCharSizeService implements ICharSizeService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
public onCharSizeChange: IEvent<void> = new EventEmitter<void>().event;
|
||||
constructor(public width: number, public height: number) {}
|
||||
@@ -17,7 +335,7 @@ export class MockCharSizeService implements ICharSizeService {
|
||||
}
|
||||
|
||||
export class MockMouseService implements IMouseService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
@@ -28,7 +346,7 @@ export class MockMouseService implements IMouseService {
|
||||
}
|
||||
|
||||
export class MockRenderService implements IRenderService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
public onDimensionsChange: IEvent<IRenderDimensions> = new EventEmitter<IRenderDimensions>().event;
|
||||
public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
|
||||
Vendored
+99
-1
@@ -3,9 +3,107 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
|
||||
export interface ITerminal extends IPublicTerminal, ICoreTerminal {
|
||||
element: HTMLElement | undefined;
|
||||
screenElement: HTMLElement | undefined;
|
||||
browser: IBrowser;
|
||||
buffer: IBuffer;
|
||||
buffers: IBufferSet;
|
||||
viewport: IViewport | undefined;
|
||||
// TODO: We should remove options once components adopt optionsService
|
||||
options: ITerminalOptions;
|
||||
linkifier: ILinkifier;
|
||||
linkifier2: ILinkifier2;
|
||||
|
||||
onBlur: IEvent<void>;
|
||||
onFocus: IEvent<void>;
|
||||
onA11yChar: IEvent<string>;
|
||||
onA11yTab: IEvent<number>;
|
||||
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
}
|
||||
|
||||
// Portions of the public API that are required by the internal Terminal
|
||||
export interface IPublicTerminal extends IDisposable {
|
||||
textarea: HTMLTextAreaElement | undefined;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onBinary: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
|
||||
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
|
||||
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker | undefined;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string | Uint8Array, callback?: () => void): void;
|
||||
paste(data: string): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type LineData = CharData[];
|
||||
|
||||
export interface ICompositionHelper {
|
||||
compositionstart(): void;
|
||||
compositionupdate(ev: CompositionEvent): void;
|
||||
compositionend(): void;
|
||||
updateCompositionElements(dontRecurse?: boolean): void;
|
||||
keydown(ev: KeyboardEvent): boolean;
|
||||
}
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
userAgent: string;
|
||||
platform: string;
|
||||
isFirefox: boolean;
|
||||
isMac: boolean;
|
||||
isIpad: boolean;
|
||||
isIphone: boolean;
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
colors: IColorSet;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { AddonManager, ILoadedAddon } from './AddonManager';
|
||||
import { ITerminalAddon } from 'xterm';
|
||||
|
||||
class TestAddonManager extends AddonManager {
|
||||
public get addons(): ILoadedAddon[] {
|
||||
return this._addons;
|
||||
}
|
||||
}
|
||||
|
||||
describe('AddonManager', () => {
|
||||
let manager: TestAddonManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new TestAddonManager();
|
||||
});
|
||||
|
||||
describe('loadAddon', () => {
|
||||
it('should call addon constructor', () => {
|
||||
let called = false;
|
||||
class Addon implements ITerminalAddon {
|
||||
public activate(terminal: any): void {
|
||||
assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal');
|
||||
called = true;
|
||||
}
|
||||
public dispose(): void { }
|
||||
}
|
||||
manager.loadAddon('foo' as any, new Addon());
|
||||
assert.equal(called, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should dispose all loaded addons', () => {
|
||||
let called = 0;
|
||||
class Addon implements ITerminalAddon {
|
||||
public activate(): void {}
|
||||
public dispose(): void { called++; }
|
||||
}
|
||||
manager.loadAddon(null!, new Addon());
|
||||
manager.loadAddon(null!, new Addon());
|
||||
manager.loadAddon(null!, new Addon());
|
||||
assert.equal(manager.addons.length, 3);
|
||||
manager.dispose();
|
||||
assert.equal(called, 3);
|
||||
assert.equal(manager.addons.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalAddon, IDisposable, Terminal } from 'xterm';
|
||||
|
||||
export interface ILoadedAddon {
|
||||
instance: ITerminalAddon;
|
||||
dispose: () => void;
|
||||
isDisposed: boolean;
|
||||
}
|
||||
|
||||
export class AddonManager implements IDisposable {
|
||||
protected _addons: ILoadedAddon[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (let i = this._addons.length - 1; i >= 0; i--) {
|
||||
this._addons[i].instance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {
|
||||
const loadedAddon: ILoadedAddon = {
|
||||
instance,
|
||||
dispose: instance.dispose,
|
||||
isDisposed: false
|
||||
};
|
||||
this._addons.push(loadedAddon);
|
||||
instance.dispose = () => this._wrappedAddonDispose(loadedAddon);
|
||||
instance.activate(<any>terminal);
|
||||
}
|
||||
|
||||
private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {
|
||||
if (loadedAddon.isDisposed) {
|
||||
// Do nothing if already disposed
|
||||
return;
|
||||
}
|
||||
let index = -1;
|
||||
for (let i = 0; i < this._addons.length; i++) {
|
||||
if (this._addons[i] === loadedAddon) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) {
|
||||
throw new Error('Could not dispose an addon that has not been loaded');
|
||||
}
|
||||
loadedAddon.isDisposed = true;
|
||||
loadedAddon.dispose.apply(loadedAddon.instance);
|
||||
this._addons.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider } from 'xterm';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { IBufferLine, ICellData } from 'common/Types';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Terminal as TerminalCore } from '../Terminal';
|
||||
import * as Strings from '../LocalizableStrings';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { AddonManager } from './AddonManager';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
|
||||
export class Terminal implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
private _addonManager: AddonManager;
|
||||
private _parser: IParser | undefined;
|
||||
|
||||
constructor(options?: ITerminalOptions) {
|
||||
this._core = new TerminalCore(options);
|
||||
this._addonManager = new AddonManager();
|
||||
}
|
||||
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onBinary(): IEvent<string> { return this._core.onBinary; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
if (!this._parser) {
|
||||
this._parser = new ParserApi(this._core);
|
||||
}
|
||||
return this._parser;
|
||||
}
|
||||
public get unicode(): IUnicodeHandling {
|
||||
return new UnicodeApi(this._core);
|
||||
}
|
||||
public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get buffer(): IBufferNamespaceApi { return new BufferNamespaceApi(this._core.buffers); }
|
||||
public get markers(): ReadonlyArray<IMarker> { return this._core.markers; }
|
||||
public blur(): void {
|
||||
this._core.blur();
|
||||
}
|
||||
public focus(): void {
|
||||
this._core.focus();
|
||||
}
|
||||
public resize(columns: number, rows: number): void {
|
||||
this._verifyIntegers(columns, rows);
|
||||
this._core.resize(columns, rows);
|
||||
}
|
||||
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 registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
return this._core.registerLinkProvider(linkProvider);
|
||||
}
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
return this._core.registerCharacterJoiner(handler);
|
||||
}
|
||||
public deregisterCharacterJoiner(joinerId: number): void {
|
||||
this._core.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
public registerMarker(cursorYOffset: number): IMarker | undefined {
|
||||
this._verifyIntegers(cursorYOffset);
|
||||
return this._core.addMarker(cursorYOffset);
|
||||
}
|
||||
public addMarker(cursorYOffset: number): IMarker | undefined {
|
||||
return this.registerMarker(cursorYOffset);
|
||||
}
|
||||
public hasSelection(): boolean {
|
||||
return this._core.hasSelection();
|
||||
}
|
||||
public select(column: number, row: number, length: number): void {
|
||||
this._verifyIntegers(column, row, length);
|
||||
this._core.select(column, row, length);
|
||||
}
|
||||
public getSelection(): string {
|
||||
return this._core.getSelection();
|
||||
}
|
||||
public getSelectionPosition(): ISelectionPosition | undefined {
|
||||
return this._core.getSelectionPosition();
|
||||
}
|
||||
public clearSelection(): void {
|
||||
this._core.clearSelection();
|
||||
}
|
||||
public selectAll(): void {
|
||||
this._core.selectAll();
|
||||
}
|
||||
public selectLines(start: number, end: number): void {
|
||||
this._verifyIntegers(start, end);
|
||||
this._core.selectLines(start, end);
|
||||
}
|
||||
public dispose(): void {
|
||||
this._addonManager.dispose();
|
||||
this._core.dispose();
|
||||
}
|
||||
public scrollLines(amount: number): void {
|
||||
this._verifyIntegers(amount);
|
||||
this._core.scrollLines(amount);
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
this._verifyIntegers(pageCount);
|
||||
this._core.scrollPages(pageCount);
|
||||
}
|
||||
public scrollToTop(): void {
|
||||
this._core.scrollToTop();
|
||||
}
|
||||
public scrollToBottom(): void {
|
||||
this._core.scrollToBottom();
|
||||
}
|
||||
public scrollToLine(line: number): void {
|
||||
this._verifyIntegers(line);
|
||||
this._core.scrollToLine(line);
|
||||
}
|
||||
public clear(): void {
|
||||
this._core.clear();
|
||||
}
|
||||
public write(data: string | Uint8Array, callback?: () => void): void {
|
||||
this._core.write(data, callback);
|
||||
}
|
||||
public writeUtf8(data: Uint8Array, callback?: () => void): void {
|
||||
this._core.write(data, callback);
|
||||
}
|
||||
public writeln(data: string | Uint8Array, callback?: () => void): void {
|
||||
this._core.write(data);
|
||||
this._core.write('\r\n', callback);
|
||||
}
|
||||
public paste(data: string): void {
|
||||
this._core.paste(data);
|
||||
}
|
||||
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
|
||||
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean;
|
||||
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
|
||||
public getOption(key: string): any;
|
||||
public getOption(key: any): any {
|
||||
return this._core.optionsService.getOption(key);
|
||||
}
|
||||
public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void;
|
||||
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
|
||||
public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): 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' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void;
|
||||
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): 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.optionsService.setOption(key, value);
|
||||
}
|
||||
public refresh(start: number, end: number): void {
|
||||
this._verifyIntegers(start, end);
|
||||
this._core.refresh(start, end);
|
||||
}
|
||||
public reset(): void {
|
||||
this._core.reset();
|
||||
}
|
||||
public loadAddon(addon: ITerminalAddon): void {
|
||||
return this._addonManager.loadAddon(this, addon);
|
||||
}
|
||||
public static get strings(): ILocalizableStrings {
|
||||
return Strings;
|
||||
}
|
||||
|
||||
private _verifyIntegers(...values: number[]): void {
|
||||
values.forEach(value => {
|
||||
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
|
||||
throw new Error('This API only accepts integers');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BufferApiView implements IBufferApi {
|
||||
constructor(
|
||||
private _buffer: IBuffer,
|
||||
public readonly type: 'normal' | 'alternate'
|
||||
) { }
|
||||
|
||||
public init(buffer: IBuffer): BufferApiView {
|
||||
this._buffer = buffer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get cursorY(): number { return this._buffer.y; }
|
||||
public get cursorX(): number { return this._buffer.x; }
|
||||
public get viewportY(): number { return this._buffer.ydisp; }
|
||||
public get baseY(): number { return this._buffer.ybase; }
|
||||
public get length(): number { return this._buffer.lines.length; }
|
||||
public getLine(y: number): IBufferLineApi | undefined {
|
||||
const line = this._buffer.lines.get(y);
|
||||
if (!line) {
|
||||
return undefined;
|
||||
}
|
||||
return new BufferLineApiView(line);
|
||||
}
|
||||
public getNullCell(): IBufferCellApi { return new CellData(); }
|
||||
}
|
||||
|
||||
class BufferNamespaceApi implements IBufferNamespaceApi {
|
||||
private _normal: BufferApiView;
|
||||
private _alternate: BufferApiView;
|
||||
private _onBufferChange = new EventEmitter<IBufferApi>();
|
||||
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
|
||||
|
||||
constructor(private _buffers: IBufferSet) {
|
||||
this._normal = new BufferApiView(this._buffers.normal, 'normal');
|
||||
this._alternate = new BufferApiView(this._buffers.alt, 'alternate');
|
||||
this._buffers.onBufferActivate(() => this._onBufferChange.fire(this.active));
|
||||
}
|
||||
public get active(): IBufferApi {
|
||||
if (this._buffers.active === this._buffers.normal) { return this.normal; }
|
||||
if (this._buffers.active === this._buffers.alt) { return this.alternate; }
|
||||
throw new Error('Active buffer is neither normal nor alternate');
|
||||
}
|
||||
public get normal(): IBufferApi {
|
||||
return this._normal.init(this._buffers.normal);
|
||||
}
|
||||
public get alternate(): IBufferApi {
|
||||
return this._alternate.init(this._buffers.alt);
|
||||
}
|
||||
}
|
||||
|
||||
class BufferLineApiView implements IBufferLineApi {
|
||||
constructor(private _line: IBufferLine) { }
|
||||
|
||||
public get isWrapped(): boolean { return this._line.isWrapped; }
|
||||
public get length(): number { return this._line.length; }
|
||||
public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {
|
||||
if (x < 0 || x >= this._line.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
this._line.loadCell(x, <ICellData>cell);
|
||||
return cell;
|
||||
}
|
||||
return this._line.loadCell(x, new CellData());
|
||||
}
|
||||
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
|
||||
return this._line.translateToString(trimRight, startColumn, endColumn);
|
||||
}
|
||||
}
|
||||
|
||||
class ParserApi implements IParser {
|
||||
constructor(private _core: ITerminal) { }
|
||||
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
|
||||
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
|
||||
}
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
|
||||
return this.registerCsiHandler(id, callback);
|
||||
}
|
||||
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
|
||||
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
|
||||
}
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
|
||||
return this.registerDcsHandler(id, callback);
|
||||
}
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
return this._core.addEscHandler(id, handler);
|
||||
}
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
return this.registerEscHandler(id, handler);
|
||||
}
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._core.addOscHandler(ident, callback);
|
||||
}
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this.registerOscHandler(ident, callback);
|
||||
}
|
||||
}
|
||||
|
||||
class UnicodeApi implements IUnicodeHandling {
|
||||
constructor(private _core: ITerminal) { }
|
||||
|
||||
public register(provider: IUnicodeVersionProvider): void {
|
||||
this._core.unicodeService.register(provider);
|
||||
}
|
||||
|
||||
public get versions(): string[] {
|
||||
return this._core.unicodeService.versions;
|
||||
}
|
||||
|
||||
public get activeVersion(): string {
|
||||
return this._core.unicodeService.activeVersion;
|
||||
}
|
||||
|
||||
public set activeVersion(version: string) {
|
||||
this._core.unicodeService.activeVersion = version;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
|
||||
export class CharSizeService implements ICharSizeService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
public width: number = 0;
|
||||
public height: number = 0;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ICoreBrowserService } from './Services';
|
||||
|
||||
export class CoreBrowserService implements ICoreBrowserService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
private _textarea: HTMLTextAreaElement
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ICharSizeService, IRenderService, IMouseService } from './Services';
|
||||
import { getCoords, getRawByteCoords } from 'browser/input/Mouse';
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
@@ -20,7 +20,7 @@ interface ISelectionState {
|
||||
}
|
||||
|
||||
export class RenderService extends Disposable implements IRenderService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
@@ -51,7 +51,8 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
private _rowCount: number,
|
||||
screenElement: HTMLElement,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharSizeService charSizeService: ICharSizeService
|
||||
@ICharSizeService charSizeService: ICharSizeService,
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
super();
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end));
|
||||
@@ -61,6 +62,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
|
||||
this.register(this._screenDprMonitor);
|
||||
|
||||
this.register(this._bufferService.onResize(e => this._fullRefresh()));
|
||||
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
|
||||
this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export const enum SelectionMode {
|
||||
* when the selection is ready to be redrawn (on an animation frame).
|
||||
*/
|
||||
export class SelectionService implements ISelectionService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
protected _model: SelectionModel;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IDisposable } from 'common/Types';
|
||||
|
||||
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
|
||||
export interface ICharSizeService {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
@@ -25,14 +25,14 @@ export interface ICharSizeService {
|
||||
|
||||
export const ICoreBrowserService = createDecorator<ICoreBrowserService>('CoreBrowserService');
|
||||
export interface ICoreBrowserService {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly isFocused: boolean;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined;
|
||||
@@ -40,7 +40,7 @@ export interface IMouseService {
|
||||
|
||||
export const IRenderService = createDecorator<IRenderService>('RenderService');
|
||||
export interface IRenderService extends IDisposable {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
onDimensionsChange: IEvent<IRenderDimensions>;
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ export interface IRenderService extends IDisposable {
|
||||
|
||||
export const ISelectionService = createDecorator<ISelectionService>('SelectionService');
|
||||
export interface ISelectionService {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly selectionText: string;
|
||||
readonly hasSelection: boolean;
|
||||
@@ -100,7 +100,7 @@ export interface ISelectionService {
|
||||
|
||||
export const ISoundService = createDecorator<ISoundService>('SoundService');
|
||||
export interface ISoundService {
|
||||
serviceBrand: any;
|
||||
serviceBrand: undefined;
|
||||
|
||||
playBellSound(): void;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IOptionsService } from 'common/services/Services';
|
||||
import { ISoundService } from 'browser/services/Services';
|
||||
|
||||
export class SoundService implements ISoundService {
|
||||
public serviceBrand: any;
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private static _audioContext: AudioContext;
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"common/*": [ "./common/*" ]
|
||||
}
|
||||
},
|
||||
"include": [ "./**/*" ],
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user