diff --git a/demo/client.ts b/demo/client.ts index acc83cb86f..a3a912f630 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -205,8 +205,7 @@ function initOptions(term: TerminalType): void { fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - rendererType: ['dom', 'canvas'], - experimentalBufferLineImpl: ['JsArray', 'TypedArray'] + rendererType: ['dom', 'canvas'] }; const options = Object.keys((term)._core.options); const booleanOptions = []; diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 13da69616b..87d911c2a4 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -4,7 +4,7 @@ /// -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; namespace constructor { { @@ -119,6 +119,12 @@ namespace methods_core { const t: Terminal = new Terminal(); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); + const d1: IDisposable = t.addCsiHandler("x", + (params: number[], collect: string): boolean => params[0]===1); + d1.dispose(); + const d2: IDisposable = t.addOscHandler(199, + (data: string): boolean => true); + d2.dispose(); } namespace options { { diff --git a/package.json b/package.json index 0d797f789e..5ed3a4b800 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.9.0", + "version": "3.10.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", diff --git a/src/Buffer.ts b/src/Buffer.ts index 861471b5bd..74cec10769 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine, BufferLineJSArray } from './BufferLine'; +import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -47,7 +47,6 @@ export class Buffer implements IBuffer { public savedX: number; public savedCurAttr: number; public markers: Marker[] = []; - private _bufferLineConstructor: IBufferLineConstructor; private _cols: number; private _rows: number; @@ -66,35 +65,9 @@ export class Buffer implements IBuffer { this.clear(); } - public setBufferLineFactory(type: string): void { - if (type === 'JsArray') { - if (this._bufferLineConstructor !== BufferLineJSArray) { - this._bufferLineConstructor = BufferLineJSArray; - this._recreateLines(); - } - } else { - if (this._bufferLineConstructor !== BufferLine) { - this._bufferLineConstructor = BufferLine; - this._recreateLines(); - } - } - } - - private _recreateLines(): void { - if (!this.lines) return; - for (let i = 0; i < this.lines.length; ++i) { - const oldLine = this.lines.get(i); - const newLine = new this._bufferLineConstructor(oldLine.length); - for (let j = 0; j < oldLine.length; ++j) { - newLine.set(j, oldLine.get(j)); - } - this.lines.set(i, newLine); - } - } - public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new this._bufferLineConstructor(this._cols, fillCharData, isWrapped); + return new BufferLine(this._cols, fillCharData, isWrapped); } public get hasScrollback(): boolean { @@ -141,7 +114,6 @@ export class Buffer implements IBuffer { * Clears the buffer to it's initial state, discarding all previous data. */ public clear(): void { - this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl); this.ydisp = 0; this.ybase = 0; this.y = 0; @@ -192,7 +164,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); } } } diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 1c39147184..2fa4ef139d 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -3,133 +3,8 @@ * @license MIT */ import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer'; -/** - * Class representing a terminal line. - * - * @deprecated to be removed with one of the next releases - */ -export class BufferLineJSArray implements IBufferLine { - protected _data: CharData[]; - public isWrapped = false; - public length: number; - - constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) { - this._data = []; - if (!fillCharData) { - fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - for (let i = 0; i < cols; i++) { - this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior) - } - if (isWrapped) { - this.isWrapped = true; - } - this.length = this._data.length; - } - - private _pop(): CharData | undefined { - const data = this._data.pop(); - this.length = this._data.length; - return data; - } - - private _push(data: CharData): void { - this._data.push(data); - this.length = this._data.length; - } - - private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { - const removed = this._data.splice(start, deleteCount, ...items); - this.length = this._data.length; - return removed; - } - - public get(index: number): CharData { - return this._data[index]; - } - - public set(index: number, data: CharData): void { - this._data[index] = data; - } - - /** insert n cells ch at pos, right cells are lost (stable length) */ - public insertCells(pos: number, n: number, ch: CharData): void { - while (n--) { - this._splice(pos, 0, ch); - this._pop(); - } - } - - /** delete n cells at pos, right side is filled with fill (stable length) */ - public deleteCells(pos: number, n: number, fillCharData: CharData): void { - while (n--) { - this._splice(pos, 1); - this._push(fillCharData); - } - } - - /** replace cells from pos to pos + n - 1 with fill */ - public replaceCells(start: number, end: number, fillCharData: CharData): void { - while (start < end && start < this.length) { - this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior) - } - } - - /** resize line to cols filling new cells with fill */ - public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { - while (this._data.length < cols) { - this._data.push(fillCharData); - } - if (shrink) { - while (this._data.length > cols) { - this._data.pop(); - } - } - this.length = this._data.length; - } - - public fill(fillCharData: CharData): void { - for (let i = 0; i < this.length; ++i) { - this.set(i, fillCharData); - } - } - - public copyFrom(line: BufferLineJSArray): void { - this._data = line._data.slice(0); - this.length = line.length; - this.isWrapped = line.isWrapped; - } - - public clone(): IBufferLine { - const newLine = new BufferLineJSArray(0); - newLine.copyFrom(this); - return newLine; - } - - public getTrimmedLength(): number { - for (let i = this.length - 1; i >= 0; --i) { - const ch = this.get(i); - if (ch[CHAR_DATA_CHAR_INDEX] !== '') { - return i + ch[CHAR_DATA_WIDTH_INDEX]; - } - } - return 0; - } - - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { - if (trimRight) { - endCol = Math.min(endCol, this.getTrimmedLength()); - } - let result = ''; - while (startCol < endCol) { - result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; - startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1; - } - return result; - } -} /** typed array slots taken by one cell */ const CELL_SIZE = 3; diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index e92f412574..0f1d63cc0d 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1169,6 +1169,73 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(csi).eql([]); }); + describe('CSI custom handlers', () => { + it('Prevent fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Allow fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers fallback once', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers no fallback', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([], 'Should not fallback once'); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setCsiHandler('m', () => order.push(1)); + parser2.addCsiHandler('m', () => { order.push(2); return false; }); + parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.parse('\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { exe.push('\n'); @@ -1196,6 +1263,73 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(osc).eql([]); }); + describe('OSC custom handlers', () => { + it('Prevent fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Allow fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers fallback once', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers no fallback', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([], 'Should not fallback once'); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setOscHandler(1, () => order.push(1)); + parser2.addOscHandler(1, () => { order.push(2); return false; }); + parser2.addOscHandler(1, () => { order.push(3); return false; }); + parser2.parse('\x1b]1;foo=bar\x1b\\'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { hook: function (collect: string, params: number[], flag: number): void { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index f489884177..6d3de0e0ed 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,8 +4,16 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +interface IHandlerCollection { + [key: string]: T[]; +} + +type CsiHandler = (params: number[], collect: string) => boolean | void; +type OscHandler = (data: string) => boolean | void; + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -222,9 +230,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: (data: string, start: number, end: number) => void; protected _executeHandlers: any; - protected _csiHandlers: any; + protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; - protected _oscHandlers: any; + protected _oscHandlers: IHandlerCollection; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler | null; protected _errorHandler: (state: IParsingState) => IParsingState; @@ -278,8 +286,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandlerFb = null; this._printHandler = null; this._executeHandlers = null; - this._csiHandlers = null; this._escHandlers = null; + this._csiHandlers = null; this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -303,8 +311,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + addCsiHandler(flag: string, callback: CsiHandler): IDisposable { + const index = flag.charCodeAt(0); + if (this._csiHandlers[index] === undefined) { + this._csiHandlers[index] = []; + } + const handlerList = this._csiHandlers[index]; + handlerList.push(callback); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + this._csiHandlers[flag.charCodeAt(0)] = [callback]; } clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; @@ -323,8 +347,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + if (this._oscHandlers[ident] === undefined) { + this._oscHandlers[ident] = []; + } + const handlerList = this._oscHandlers[ident]; + handlerList.push(callback); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + this._oscHandlers[ident] = [callback]; } clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; @@ -461,9 +500,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + // Trigger CSI Handler + const handlers = this._csiHandlers[code]; + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { + if (handlers[j](params, collect)) { + break; + } + } + if (j < 0) { + this._csiHandlerFb(collect, params, code); + } break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -538,9 +585,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + // Trigger OSC Handler + const handlers = this._oscHandlers[identifier]; + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { + if (handlers[j](content)) { + break; + } + } + if (j < 0) { + this._oscHandlerFb(identifier, content); + } } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f69..eb1ca1050b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; /** @@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._parser.addCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index bc8fb103a2..bc97de29ff 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -105,8 +105,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas', - experimentalBufferLineImpl: 'TypedArray' + rendererType: 'canvas' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -497,11 +496,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'experimentalBufferLineImpl': - this.buffers.normal.setBufferLineFactory(value); - this.buffers.alt.setBufferLineFactory(value); - this._blankLine = null; - break; } // Inform renderer of changes if (this.renderer) { @@ -1180,17 +1174,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; - const useRecycling = this.options.experimentalBufferLineImpl !== 'JsArray'; - if (useRecycling) { - newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { - newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); - this._blankLine = newLine; - } - newLine.isWrapped = isWrapped; - } else { + newLine = this._blankLine; + if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + this._blankLine = newLine; } + newLine.isWrapped = isWrapped; const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1201,17 +1190,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - if (useRecycling) { - if (willBufferBeTrimmed) { - this.buffer.lines.recycle().copyFrom(newLine); - } else { - this.buffer.lines.push(newLine.clone()); - } + if (willBufferBeTrimmed) { + this.buffer.lines.recycle().copyFrom(newLine); } else { - this.buffer.lines.push(newLine); + this.buffer.lines.push(newLine.clone()); } } else { - this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine); + this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -1233,7 +1218,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine); + this.buffer.lines.set(bottomRow, newLine.clone()); } // Move the viewport to the bottom of the buffer unless the user is @@ -1413,6 +1398,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(flag, callback); + } + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. diff --git a/src/Types.ts b/src/Types.ts index d48362b8ef..aac029f215 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -492,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; @@ -527,7 +529,3 @@ export interface IBufferLine { getTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; } - -export interface IBufferLineConstructor { - new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine; -} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b3b..87fcfaef99 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -59,6 +59,12 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._core.addCsiHandler(flag, callback); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._core.addOscHandler(ident, callback); + } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { return this._core.registerLinkMatcher(regex, handler, options); } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2afdebb54e..3e0b864307 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -236,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } /** @@ -295,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { this._ctx.save(); this._ctx.font = this._getFont(terminal, bold, italic); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index e40215cfe8..cadcce2ec1 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -29,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.save(); ctx.fillStyle = config.colors.foreground.css; ctx.font = getFont(config.fontWeight, config); - ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle'; // Default color for (let i = 0; i < 256; i++) { @@ -37,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2); ctx.restore(); } // Default color bold @@ -48,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5); ctx.restore(); } ctx.restore(); @@ -64,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } @@ -80,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 72010768a3..e311c369a3 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -250,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'top'; + this._tmpCtx.textBaseline = 'middle'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -259,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, 0); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index a0cefd677c..c5ef212da6 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -364,9 +364,11 @@ export class DomRenderer extends EventEmitter implements IRenderer { return; } const span = row.children[x]; - span.style.textDecoration = enabled ? 'underline' : 'none'; - x = (x + 1) % cols; - if (x === 0) { + if (span) { + span.style.textDecoration = enabled ? 'underline' : 'none'; + } + if (++x >= cols) { + x = 0; y++; } } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a3355..e6e4aaa32f 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { throw new Error('Method not implemented.'); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb5540..c5d2b0204e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -101,17 +101,6 @@ declare module 'xterm' { */ experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; - /** - * (EXPERIMENTAL) Defines which implementation to use for buffer lines. - * - * - 'JsArray': The default/stable implementation. - * - 'TypedArray': The new experimental implementation based on TypedArrays that is expected to - * significantly boost performance and memory consumption. Use at your own risk. - * - * @deprecated This option will be removed in the future. - */ - experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; - /** * The font size used to render text. */ @@ -481,6 +470,29 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + /** + * (EXPERIMENTAL) Adds a handler for CSI escape sequences. + * @param flag The flag should be one-character string, which specifies the + * final character (e.g "m" for SGR) of the CSI sequence. + * @param callback The function to handle the escape sequence. The callback + * is called with the numerical params, as well as the special characters + * (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if + * we should try a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + + /** + * (EXPERIMENTAL) Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the escape sequence. The callback + * is called with OSC data string. Return true if the sequence was handled; + * false if we should try a previous handler (set by addOscHandler or + * setOscHandler). The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled.