From fd34caee2322af515fc1e94785d3a41dbc7e85da Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Fri, 7 Dec 2018 22:50:59 -0500 Subject: [PATCH 01/77] Use a time-based limit to Terminal._innerWrite The idea is that it should run for a bit and then let the renderer draw a frame so that the terminal look responsive. The existing approach limits the work done using a fixed number elements from the write buffer so the duration of a frame can vary widely. This approach looks at the clock to determine when to stop, we basically allocate an amount of time each frame to write, while the rest can be used for rendering. From my tests this change makes the terminal feel a lot smoother. --- src/Terminal.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 2cfc1ca8..a641a86b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -64,10 +64,12 @@ const document = (typeof window !== 'undefined') ? window.document : null; const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. */ -const WRITE_BATCH_SIZE = 300; +const WRITE_TIMEOUT_MS = 12; /** * The set of options that only have an effect when set in the Terminal constructor. @@ -1358,13 +1360,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.writeBuffer = []; } - const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - const data = writeBatch.shift(); + const time = Date.now(); + while (this.writeBuffer.length > 0) { + const data = this.writeBuffer.shift(); // If XOFF was sent in order to catch up with the pty process, resume it if // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + if (this._xoffSentToCatchUp && this.writeBuffer.length === 0 && this.writeBuffer.length === 0) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1382,6 +1384,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - time >= WRITE_TIMEOUT_MS) { + break; + } } if (this.writeBuffer.length > 0) { // Allow renderer to catch up before processing the next batch From 3d2ae2b01edd34e9475c7d58884a501c2426237f Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 9 Dec 2018 17:58:44 -0500 Subject: [PATCH 02/77] Removing redundant condition. Clearer variable name --- src/Terminal.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index a641a86b..2c7f648b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1360,13 +1360,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.writeBuffer = []; } - const time = Date.now(); + const startTime = Date.now(); while (this.writeBuffer.length > 0) { const data = this.writeBuffer.shift(); // If XOFF was sent in order to catch up with the pty process, resume it if // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBuffer.length === 0 && this.writeBuffer.length === 0) { + if (this._xoffSentToCatchUp && this.writeBuffer.length === 0) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1385,7 +1385,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); - if (Date.now() - time >= WRITE_TIMEOUT_MS) { + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; } } From c444eebd89b448afd64be59f7206cc6febeaeb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 13 Jan 2019 23:19:56 +0100 Subject: [PATCH 03/77] RGB support in buffer and attributes --- src/Buffer.test.ts | 8 +- src/Buffer.ts | 41 ++- src/BufferLine.test.ts | 16 +- src/BufferLine.ts | 240 +++++++++++++++++- src/BufferSet.ts | 4 +- src/InputHandler.test.ts | 134 ++++++++-- src/InputHandler.ts | 153 +++++------ src/Terminal.test.ts | 8 +- src/Terminal.ts | 95 ++----- src/Types.ts | 60 ++++- src/renderer/TextRenderLayer.ts | 7 +- .../dom/DomRendererRowFactory.test.ts | 48 +++- src/renderer/dom/DomRendererRowFactory.ts | 7 +- src/ui/TestUtils.test.ts | 15 +- 14 files changed, 589 insertions(+), 247 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 27e0e836..f6907864 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,7 +5,7 @@ import { assert, expect } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR } from './Buffer'; +import { Buffer, DEFAULT_ATTR_DATA } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { BufferLine, CellData } from './BufferLine'; @@ -37,7 +37,7 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).asCharData; + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).asCharData; buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { @@ -184,7 +184,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -204,7 +204,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; diff --git a/src/Buffer.ts b/src/Buffer.ts index 6295d175..a5a285b0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,13 +4,16 @@ */ import { CircularList } from './common/CircularList'; -import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; +import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine, CellData } from './BufferLine'; +import { BufferLine, CellData, AttributeData } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); + +export const DEFAULT_ATTR_DATA = new AttributeData(); + export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -43,7 +46,7 @@ export class Buffer implements IBuffer { public tabs: any; public savedY: number; public savedX: number; - public savedCurAttr: number; + public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); @@ -61,19 +64,29 @@ export class Buffer implements IBuffer { this.clear(); } - public getNullCell(fg: number = 0, bg: number = 0): ICellData { - this._nullCell.fg = fg; - this._nullCell.bg = bg; + public getNullCell(attr?: IAttributeData): ICellData { + if (attr) { + this._nullCell.fg = attr.fg; + this._nullCell.bg = attr.bg; + } else { + this._nullCell.fg = 0; + this._nullCell.bg = 0; + } return this._nullCell; } - public getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { - this._whitespaceCell.fg = fg; - this._whitespaceCell.bg = bg; + public getWhitespaceCell(attr?: IAttributeData): ICellData { + if (attr) { + this._whitespaceCell.fg = attr.fg; + this._whitespaceCell.bg = attr.bg; + } else { + this._whitespaceCell.fg = 0; + this._whitespaceCell.bg = 0; + } return this._whitespaceCell; } - public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { + public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); } @@ -105,10 +118,10 @@ export class Buffer implements IBuffer { /** * Fills the buffer's viewport with blank lines. */ - public fillViewportRows(fillAttr?: number): void { + public fillViewportRows(fillAttr?: IAttributeData): void { if (this.lines.length === 0) { if (fillAttr === undefined) { - fillAttr = DEFAULT_ATTR; + fillAttr = DEFAULT_ATTR_DATA; } let i = this._terminal.rows; while (i--) { @@ -149,7 +162,7 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const cell = this.getNullCell(DEFAULT_ATTR); // does xterm use the default attr? + const cell = this.getNullCell(DEFAULT_ATTR_DATA); // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { this.lines.get(i).resize(newCols, cell); } @@ -172,7 +185,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 BufferLine(newCols, this.getNullCell(DEFAULT_ATTR))); + this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR_DATA))); } } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 97508ace..ec03c064 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -24,23 +24,23 @@ describe('CellData', () => { // ASCII cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.asCharData, [123, '𝄞', 1, 0x1D11E]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.asCharData, [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); }); @@ -366,7 +366,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.asCharData, [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); it('should add char to combining string in cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -379,7 +379,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -392,7 +392,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 048bcab6..e306346f 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,11 +2,67 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine, ICellData } from './Types'; +import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { stringFromCodePoint } from './core/input/TextDecoder'; +import { FLAGS } from './renderer/Types'; +import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; +/** + * TODO: + * The below color-related code can be removed when true color is implemented. + * It's only purpose is to match true color requests with the closest matching + * ANSI color code. + */ +const matchColorCache: {[colorRGBHash: number]: number} = {}; + +// http://stackoverflow.com/questions/1633828 +function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { + return Math.pow(30 * (r1 - r2), 2) + + Math.pow(59 * (g1 - g2), 2) + + Math.pow(11 * (b1 - b2), 2); +} + +function matchColor(r1: number, g1: number, b1: number): number { + const hash = (r1 << 16) | (g1 << 8) | b1; + + if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) { + return matchColorCache[hash]; + } + + let ldiff = Infinity; + let li = -1; + let i = 0; + let c: number; + let r2: number; + let g2: number; + let b2: number; + let diff: number; + + for (; i < DEFAULT_ANSI_COLORS.length; i++) { + c = DEFAULT_ANSI_COLORS[i].rgba; + r2 = c >>> 24; + g2 = c >>> 16 & 0xFF; + b2 = c >>> 8 & 0xFF; + // assume that alpha is 0xFF + + diff = matchColorDistance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; +} + /** * buffer memory layout: * @@ -76,12 +132,188 @@ export const enum Content { WIDTH_SHIFT = 22 } +export enum Attributes { + /** + * bit 1..8 blue in RGB, color in P256 and P16 + */ + BLUE_MASK = 0xFF, + BLUE_SHIFT = 0, + PCOLOR_MASK = 0xFF, + PCOLOR_SHIFT = 0, + + /** + * bit 9..16 green in RGB + */ + GREEN_MASK = 0xFF00, + GREEN_SHIFT = 8, + + /** + * bit 17..24 red in RGB + */ + RED_MASK = 0xFF0000, + RED_SHIFT = 16, + + /** + * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) + */ + CM_MASK = 0x3000000, + CM_DEFAULT = 0, + CM_P16 = 0x1000000, + CM_P256 = 0x2000000, + CM_RGB = 0x3000000, + + /** + * bit 1..24 RGB room + */ + RGB_MASK = 0xFFFFFF +} + +export enum FgFlags { + /** + * bit 27..31 (32th bit unused) + */ + INVERSE = 0x4000000, + BOLD = 0x8000000, + UNDERLINE = 0x10000000, + BLINK = 0x20000000, + INVISIBLE = 0x40000000 +} + +export enum BgFlags { + /** + * bit 27..32 (upper 4 unused) + */ + ITALIC = 0x4000000, + DIM = 0x8000000 +} + +export class AttributeData implements IAttributeData { + static toRGB(value: number): IColorRGB { + return [ + value >>> Attributes.RED_SHIFT & 255, + value >>> Attributes.GREEN_SHIFT & 255, + value & 255 + ]; + } + static fromRGB(value: IColorRGB): number { + return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; + } + + public clone(): IAttributeData { + const newObj = new AttributeData(); + newObj.fg = this.fg; + newObj.bg = this.bg; + return newObj; + } + + // data + public fg: number = 0; + public bg: number = 0; + + // flags + public isInverse(): number { return this.fg & FgFlags.INVERSE; } + public isBold(): number { return this.fg & FgFlags.BOLD; } + public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isBlink(): number { return this.fg & FgFlags.BLINK; } + public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } + public isItalic(): number { return this.bg & BgFlags.ITALIC; } + public isDim(): number { return this.bg & BgFlags.DIM; } + + // color modes + public getColormodeFg(): number { return this.fg & Attributes.CM_MASK; } + public getColormodeBg(): number { return this.bg & Attributes.CM_MASK; } + public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } + public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } + + // colors + public getFgColor(channels: boolean = false): number | IColorRGB { + switch (this.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return (channels) ? AttributeData.toRGB(this.fg & Attributes.RGB_MASK) : this.fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + public getBgColor(channels: boolean = false): number | IColorRGB { + switch (this.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return (channels) ? AttributeData.toRGB(this.bg & Attributes.RGB_MASK) : this.bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + + public getOldFlags(): number { + let flags = 0; + if (this.isBold()) { + flags |= FLAGS.BOLD; + } + if (this.isUnderline()) { + flags |= FLAGS.UNDERLINE; + } + if (this.isBlink()) { + flags |= FLAGS.BLINK; + } + if (this.isDim()) { + flags |= FLAGS.DIM; + } + if (this.isInvisible()) { + flags |= FLAGS.INVISIBLE; + } + if (this.isInverse()) { + flags |= FLAGS.INVERSE; + } + if (this.isItalic()) { + flags |= FLAGS.ITALIC; + } + return flags; + } + public getOldFgColor(): number { + let color = this.getFgColor() as number; + if (color === -1) { + return 256; + } + if (this.isFgRGB()) { + color = matchColor( + (this.fg & Attributes.RED_MASK) >> Attributes.RED_SHIFT, + (this.fg & Attributes.GREEN_MASK) >> Attributes.GREEN_SHIFT, + (this.fg & Attributes.BLUE_MASK) >> Attributes.BLUE_SHIFT + ); + if (color === -1) { + color = 256; + } + } + return color; + } + public getOldBgColor(): number { + let color = this.getBgColor() as number; + if (color === -1) { + return 256; + } + if (this.isBgRGB()) { + color = matchColor( + (this.bg & Attributes.RED_MASK) >> Attributes.RED_SHIFT, + (this.bg & Attributes.GREEN_MASK) >> Attributes.GREEN_SHIFT, + (this.bg & Attributes.BLUE_MASK) >> Attributes.BLUE_SHIFT + ); + if (color === -1) { + color = 256; + } + } + return color; + } +} + /** * CellData - represents a single Cell in the terminal buffer. * * TODO: attr getter */ -export class CellData implements ICellData { +export class CellData extends AttributeData implements ICellData { /** Helper to create CellData from CharData. */ public static fromCharData(value: CharData): CellData { @@ -97,7 +329,7 @@ export class CellData implements ICellData { public combinedData: string = ''; /** Whether cell contains a combined string. */ - public get combined(): number { + public get isCombined(): number { return this.content & Content.IS_COMBINED; } @@ -119,7 +351,7 @@ export class CellData implements ICellData { /** Codepoint of cell (or last charCode of combined string) */ public get code(): number { - return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); + return ((this.isCombined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); } /** Set data from CharData */ diff --git a/src/BufferSet.ts b/src/BufferSet.ts index f84757d1..268c2d88 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, IBufferSet } from './Types'; +import { ITerminal, IBufferSet, IAttributeData } from './Types'; import { Buffer } from './Buffer'; import { EventEmitter } from './common/EventEmitter'; @@ -77,7 +77,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Sets the alt Buffer of the BufferSet as its currently active Buffer */ - public activateAltBuffer(fillAttr?: number): void { + public activateAltBuffer(fillAttr?: IAttributeData): void { if (this._activeBuffer === this._alt) { return; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 24eb0884..df34cf04 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,33 +5,33 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; -import { MockInputHandlingTerminal } from './ui/TestUtils.test'; -import { DEFAULT_ATTR } from './Buffer'; +import { MockInputHandlingTerminal, TestTerminal } from './ui/TestUtils.test'; +import { DEFAULT_ATTR_DATA } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; -import { CellData } from './BufferLine'; +import { CellData, Attributes } from './BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); terminal.buffer.x = 1; terminal.buffer.y = 2; - terminal.curAttr = 3; + terminal.curAttrData.fg = 3; const inputHandler = new InputHandler(terminal); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); - assert.equal(terminal.curAttr, 3); + assert.equal(terminal.curAttrData.fg, 3); // Change cursor position terminal.buffer.x = 10; terminal.buffer.y = 20; - terminal.curAttr = 30; + terminal.curAttrData.fg = 30; // Restore cursor position inputHandler.restoreCursor([]); assert.equal(terminal.buffer.x, 1); assert.equal(terminal.buffer.y, 2); - assert.equal(terminal.curAttr, 3); + assert.equal(terminal.curAttrData.fg, 3); }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { @@ -357,45 +357,149 @@ describe('InputHandler', () => { expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getOldFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getOldFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); // Text color of 'JUNK' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getOldFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); }); it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); handler.parse('\x1b[?1049h\x1b[uTEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getOldFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { handler.parse('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background - expect(term.buffer.lines.get(20).loadCell(10, new CellData()).fg & 0x1ff).to.equal(2); + expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getOldBgColor()).to.equal(2); + }); + }); + + describe('text attributes', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal(); + }); + it('bold', () => { + term.writeSync('\x1b[1m'); + assert.equal(!!term.curAttrData.isBold(), true); + term.writeSync('\x1b[22m'); + assert.equal(!!term.curAttrData.isBold(), false); + }); + it('dim', () => { + term.writeSync('\x1b[2m'); + assert.equal(!!term.curAttrData.isDim(), true); + term.writeSync('\x1b[22m'); + assert.equal(!!term.curAttrData.isDim(), false); + }); + it('italic', () => { + term.writeSync('\x1b[3m'); + assert.equal(!!term.curAttrData.isItalic(), true); + term.writeSync('\x1b[23m'); + assert.equal(!!term.curAttrData.isItalic(), false); + }); + it('underline', () => { + term.writeSync('\x1b[4m'); + assert.equal(!!term.curAttrData.isUnderline(), true); + term.writeSync('\x1b[24m'); + assert.equal(!!term.curAttrData.isUnderline(), false); + }); + it('blink', () => { + term.writeSync('\x1b[5m'); + assert.equal(!!term.curAttrData.isBlink(), true); + term.writeSync('\x1b[25m'); + assert.equal(!!term.curAttrData.isBlink(), false); + }); + it('inverse', () => { + term.writeSync('\x1b[7m'); + assert.equal(!!term.curAttrData.isInverse(), true); + term.writeSync('\x1b[27m'); + assert.equal(!!term.curAttrData.isInverse(), false); + }); + it('invisible', () => { + term.writeSync('\x1b[8m'); + assert.equal(!!term.curAttrData.isInvisible(), true); + term.writeSync('\x1b[28m'); + assert.equal(!!term.curAttrData.isInvisible(), false); + }); + it('colormode palette 16', () => { + assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT + assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 8; ++i) { + term.writeSync(`\x1b[${i + 30};${i + 40}m`); + assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_P16); + assert.equal(term.curAttrData.getFgColor(), i); + assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_P16); + assert.equal(term.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getColormodeFg(), 0); + assert.equal(term.curAttrData.getColormodeBg(), 0); + }); + it('colormode palette 256', () => { + assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT + assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 256; ++i) { + term.writeSync(`\x1b[38;5;${i};48;5;${i}m`); + assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_P256); + assert.equal(term.curAttrData.getFgColor(), i); + assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_P256); + assert.equal(term.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getColormodeFg(), 0); + assert.equal(term.curAttrData.getFgColor(), -1); + assert.equal(term.curAttrData.getColormodeBg(), 0); + assert.equal(term.curAttrData.getBgColor(), -1); + }); + it('colormode RGB', () => { + assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT + assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); + assert.deepEqual(term.curAttrData.getFgColor(true), [1, 2, 3]); + assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_RGB); + assert.deepEqual(term.curAttrData.getBgColor(true), [4, 5, 6]); + // reset to DEFAULT + term.writeSync(`\x1b[39;49m`); + assert.equal(term.curAttrData.getColormodeFg(), 0); + assert.equal(term.curAttrData.getFgColor(), -1); + assert.equal(term.curAttrData.getColormodeBg(), 0); + assert.equal(term.curAttrData.getBgColor(), -1); + }); + it('should zero missing RGB values', () => { + term.writeSync(`\x1b[38;2;1;2;3m`); + term.writeSync(`\x1b[38;2;5m`); + assert.deepEqual(term.curAttrData.getFgColor(true), [5, 0, 0]); }); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index c41b4165..178507fa 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,19 +4,17 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { DEFAULT_ATTR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; -import { FLAGS } from './renderer/Types'; +import { NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './Buffer'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; -import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat, utf32ToString } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; -import { CellData } from './BufferLine'; +import { CellData, Attributes, FgFlags, BgFlags } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -314,13 +312,13 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; - const buffer: IBuffer = this._terminal.buffer; - const charset: ICharset = this._terminal.charset; - const screenReaderMode: boolean = this._terminal.options.screenReaderMode; - const cols: number = this._terminal.cols; - const wraparoundMode: boolean = this._terminal.wraparoundMode; - const insertMode: boolean = this._terminal.insertMode; - const curAttr: number = this._terminal.curAttr; + const buffer = this._terminal.buffer; + const charset = this._terminal.charset; + const screenReaderMode = this._terminal.options.screenReaderMode; + const cols = this._terminal.cols; + const wraparoundMode = this._terminal.wraparoundMode; + const insertMode = this._terminal.insertMode; + const curAttr = this._terminal.curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._terminal.updateRange(buffer.y); @@ -401,12 +399,12 @@ export class InputHandler extends Disposable implements IInputHandler { // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { - bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); + bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg); } } // write current char to buffer and advance cursor - bufferRow.setDataFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); + bufferRow.setDataFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero @@ -414,7 +412,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr, 0); + bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg); } } } @@ -520,7 +518,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -694,7 +692,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); if (clearWrap) { line.isWrapped = false; @@ -817,7 +815,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr())); + buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } // this.maxRange(); @@ -847,7 +845,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr())); + buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } // this.maxRange(); @@ -863,7 +861,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -879,7 +877,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -898,7 +896,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -914,7 +912,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), - this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) + this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()) ); } @@ -973,7 +971,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.loadCell(buffer.x - 1, this._cell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR) + (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR_DATA) ); // FIXME: no updateRange here? } @@ -1307,7 +1305,7 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer - this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr()); + this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttrData()); this._terminal.refresh(0, this._terminal.rows - 1); if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); @@ -1567,127 +1565,128 @@ export class InputHandler extends Disposable implements IInputHandler { public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { - this._terminal.curAttr = DEFAULT_ATTR; + this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg; + this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg; return; } const l = params.length; - let flags = this._terminal.curAttr >> 18; - let fg = (this._terminal.curAttr >> 9) & 0x1ff; - let bg = this._terminal.curAttr & 0x1ff; let p; + const attr = this._terminal.curAttrData; for (let i = 0; i < l; i++) { p = params[i]; if (p >= 30 && p <= 37) { // fg color 8 - fg = p - 30; + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 30); } else if (p >= 40 && p <= 47) { // bg color 8 - bg = p - 40; + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 40); } else if (p >= 90 && p <= 97) { // fg color 16 - p += 8; - fg = p - 90; + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 90) | 8; } else if (p >= 100 && p <= 107) { // bg color 16 - p += 8; - bg = p - 100; + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 100) | 8; } else if (p === 0) { // default - flags = DEFAULT_ATTR >> 18; - fg = (DEFAULT_ATTR >> 9) & 0x1ff; - bg = DEFAULT_ATTR & 0x1ff; - // flags = 0; - // fg = 0x1ff; - // bg = 0x1ff; + attr.fg = DEFAULT_ATTR_DATA.fg; + attr.bg = DEFAULT_ATTR_DATA.bg; } else if (p === 1) { // bold text - flags |= FLAGS.BOLD; + attr.fg |= FgFlags.BOLD; } else if (p === 3) { // italic text - flags |= FLAGS.ITALIC; + attr.bg |= BgFlags.ITALIC; } else if (p === 4) { // underlined text - flags |= FLAGS.UNDERLINE; + attr.fg |= FgFlags.UNDERLINE; } else if (p === 5) { // blink - flags |= FLAGS.BLINK; + attr.fg |= FgFlags.BLINK; } else if (p === 7) { // inverse and positive // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' - flags |= FLAGS.INVERSE; + attr.fg |= FgFlags.INVERSE; } else if (p === 8) { // invisible - flags |= FLAGS.INVISIBLE; + attr.fg |= FgFlags.INVISIBLE; } else if (p === 2) { // dimmed text - flags |= FLAGS.DIM; + attr.bg |= BgFlags.DIM; } else if (p === 22) { // not bold nor faint - flags &= ~FLAGS.BOLD; - flags &= ~FLAGS.DIM; + attr.fg &= ~FgFlags.BOLD; + attr.bg &= ~BgFlags.DIM; } else if (p === 23) { // not italic - flags &= ~FLAGS.ITALIC; + attr.bg &= ~BgFlags.ITALIC; } else if (p === 24) { // not underlined - flags &= ~FLAGS.UNDERLINE; + attr.fg &= ~FgFlags.UNDERLINE; } else if (p === 25) { // not blink - flags &= ~FLAGS.BLINK; + attr.fg &= ~FgFlags.BLINK; } else if (p === 27) { // not inverse - flags &= ~FLAGS.INVERSE; + attr.fg &= ~FgFlags.INVERSE; } else if (p === 28) { // not invisible - flags &= ~FLAGS.INVISIBLE; + attr.fg &= ~FgFlags.INVISIBLE; } else if (p === 39) { // reset fg - fg = (DEFAULT_ATTR >> 9) & 0x1ff; + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else if (p === 49) { // reset bg - bg = DEFAULT_ATTR & 0x1ff; + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else if (p === 38) { // fg color 256 if (params[i + 1] === 2) { i += 2; - fg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (fg === -1) fg = 0x1ff; + attr.fg |= Attributes.CM_RGB; + attr.fg &= ~Attributes.RGB_MASK; + attr.fg |= (params[i] & 0xFF) << Attributes.RED_SHIFT; + attr.fg |= (params[i + 1] & 0xFF) << Attributes.GREEN_SHIFT; + attr.fg |= (params[i + 2] & 0xFF) << Attributes.BLUE_SHIFT; i += 2; } else if (params[i + 1] === 5) { i += 2; p = params[i] & 0xff; - fg = p; + attr.fg &= ~Attributes.PCOLOR_MASK; + attr.fg |= Attributes.CM_P256 | p; } } else if (p === 48) { // bg color 256 if (params[i + 1] === 2) { i += 2; - bg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (bg === -1) bg = 0x1ff; + attr.bg |= Attributes.CM_RGB; + attr.bg &= ~Attributes.RGB_MASK; + attr.bg |= (params[i] & 0xFF) << Attributes.RED_SHIFT; + attr.bg |= (params[i + 1] & 0xFF) << Attributes.GREEN_SHIFT; + attr.bg |= (params[i + 2] & 0xFF) << Attributes.BLUE_SHIFT; i += 2; } else if (params[i + 1] === 5) { i += 2; p = params[i] & 0xff; - bg = p; + attr.bg &= ~Attributes.PCOLOR_MASK; + attr.bg |= Attributes.CM_P256 | p; } } else if (p === 100) { // reset fg/bg - fg = (DEFAULT_ATTR >> 9) & 0x1ff; - bg = DEFAULT_ATTR & 0x1ff; + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); } else { this._terminal.error('Unknown SGR attribute: %d.', p); } } - - this._terminal.curAttr = (flags << 18) | (fg << 9) | bg; } /** @@ -1774,7 +1773,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = DEFAULT_ATTR; + this._terminal.curAttrData = DEFAULT_ATTR_DATA; this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? this._terminal.charset = null; this._terminal.glevel = 0; // ?? @@ -1837,7 +1836,8 @@ export class InputHandler extends Disposable implements IInputHandler { public saveCursor(params: number[]): void { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.y; - this._terminal.buffer.savedCurAttr = this._terminal.curAttr; + this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; + this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; } @@ -1849,7 +1849,8 @@ export class InputHandler extends Disposable implements IInputHandler { public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; - this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; + this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg; + this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index e06ceb78..e6ed7b98 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; -import { DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR_DATA } from './Buffer'; import { CellData } from './BufferLine'; const INIT_COLS = 80; @@ -260,7 +260,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should clear a buffer larger than rows', () => { @@ -277,7 +277,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should not break the prompt when cleared twice', () => { @@ -290,7 +290,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 33d8e60f..0a488ea2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,11 +21,11 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IBufferLine, IAttributeData } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR_DATA } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; @@ -41,7 +41,6 @@ import { addDisposableDomListener } from './ui/Lifecycle'; import * as Strings from './Strings'; import { MouseHelper } from './ui/MouseHelper'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; -import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './ui/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; @@ -52,6 +51,7 @@ import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; import { clone } from './common/Clone'; +import { Attributes } from './BufferLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -168,7 +168,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _refreshEnd: number; public savedCols: number; - public curAttr: number; + public curAttrData: IAttributeData; + private _eraseAttrData: IAttributeData; public params: (string | number)[]; public currentParam: string | number; @@ -288,7 +289,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Can this be just []? this.charsets = [null]; - this.curAttr = DEFAULT_ATTR; + this.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); this.params = []; this.currentParam = 0; @@ -328,9 +330,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * back_color_erase feature for xterm. */ - public eraseAttr(): number { - // if (this.is('screen')) return DEFAULT_ATTR; - return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff); + public eraseAttrData(): IAttributeData { + this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000; + return this._eraseAttrData; } /** @@ -1175,8 +1178,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== this.eraseAttr()) { - newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + const eraseAttr = this.eraseAttrData(); + if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== eraseAttr.fg || newLine.getBG(0) !== eraseAttr.bg) { + newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); this._blankLine = newLine; } newLine.isWrapped = isWrapped; @@ -1745,23 +1749,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.ybase = 0; this.buffer.y = 0; for (let i = 1; i < this.rows; i++) { - this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR)); + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); } - /** - * If cur return the back color xterm feature attribute. Else return default attribute. - * @param cur - */ - public ch(cur?: boolean): CharData { - if (cur) { - return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - /** * Evaluate if the current terminal is the given argument. * @param term The terminal name to evaluate @@ -1837,7 +1830,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // blankLine(true) is xterm/linux behavior const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop; this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1); - this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr())); + this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData())); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); } else { @@ -1882,46 +1875,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return false; } - // TODO: Remove when true color is implemented - public matchColor(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < DEFAULT_ANSI_COLORS.length; i++) { - c = DEFAULT_ANSI_COLORS[i].rgba; - r2 = c >>> 24; - g2 = c >>> 16 & 0xFF; - b2 = c >>> 8 & 0xFF; - // assume that alpha is 0xFF - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; - } - private _visualBell(): boolean { return false; // return this.options.bellStyle === 'visual' || @@ -1944,19 +1897,3 @@ function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean { ev.keyCode === 17 || // Ctrl ev.keyCode === 18; // Alt } - -/** - * TODO: - * The below color-related code can be removed when true color is implemented. - * It's only purpose is to match true color requests with the closest matching - * ANSI color code. - */ - -const matchColorCache: {[colorRGBHash: number]: number} = {}; - -// http://stackoverflow.com/questions/1633828 -function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { - return Math.pow(30 * (r1 - r2), 2) - + Math.pow(59 * (g1 - g2), 2) - + Math.pow(11 * (b1 - b2), 2); -} diff --git a/src/Types.ts b/src/Types.ts index d176799f..b46f858f 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -48,7 +48,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttr: number; + curAttrData: IAttributeData; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -70,7 +70,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { updateRange(y: number): void; scroll(isWrapped?: boolean): void; setgLevel(g: number): void; - eraseAttr(): number; + eraseAttrData(): IAttributeData; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -78,7 +78,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { reset(): void; showCursor(): void; refresh(start: number, end: number): void; - matchColor(r1: number, g1: number, b1: number): number; error(text: string, data?: any): void; setOption(key: string, value: any): void; tabSet(): void; @@ -289,17 +288,17 @@ export interface IBuffer { hasScrollback: boolean; savedY: number; savedX: number; - savedCurAttr: number; + savedCurAttrData: IAttributeData; isCursorInViewport: boolean; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; - getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; - getNullCell(fg?: number, bg?: number): ICellData; - getWhitespaceCell(fg?: number, bg?: number): ICellData; + getNullCell(attr?: IAttributeData): ICellData; + getWhitespaceCell(attr?: IAttributeData): ICellData; } export interface IBufferSet extends IEventEmitter { @@ -308,7 +307,7 @@ export interface IBufferSet extends IEventEmitter { active: IBuffer; activateNormalBuffer(): void; - activateAltBuffer(fillAttr?: number): void; + activateAltBuffer(fillAttr?: IAttributeData): void; } export interface ISelectionManager { @@ -513,13 +512,50 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } -/** Cell data */ -export interface ICellData { - content: number; +/** RGB color type */ +export type IColorRGB = [number, number, number]; + +/** Attribute data */ +export interface IAttributeData { fg: number; bg: number; + + clone(): IAttributeData; + + // flags + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; + + // color modes + getColormodeFg(): number; + getColormodeBg(): number; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; + + // colors + getFgColor(channels?: boolean): number | IColorRGB; + getBgColor(channels?: boolean): number | IColorRGB; + + // shim for old API + getOldFlags(): number; + getOldFgColor(): number; + getOldBgColor(): number; +} + +/** Cell data */ +export interface ICellData extends IAttributeData { + content: number; combinedData: string; - combined: number; + isCombined: number; width: number; chars: string; code: number; diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index bf7c5616..2b2ba1bb 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -80,7 +80,6 @@ export class TextRenderLayer extends BaseRenderLayer { // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. let chars = this._cell.chars || WHITESPACE_CELL_CHAR; - const attr = this._cell.fg; let width = this._cell.width; // If true, indicates that the current character(s) to draw were joined. @@ -137,9 +136,9 @@ export class TextRenderLayer extends BaseRenderLayer { } } - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; + const flags = this._cell.getOldFlags(); + let bg = this._cell.getOldBgColor(); + let fg = this._cell.getOldFgColor(); // If inverse flag is on, the foreground should become the background. if (flags & FLAGS.INVERSE) { diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index e06990d7..c80cea27 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,11 +6,9 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; -import { FLAGS } from '../Types'; -import { BufferLine, CellData } from '../../BufferLine'; +import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR_DATA } from '../../Buffer'; +import { BufferLine, CellData, FgFlags, BgFlags, Attributes } from '../../BufferLine'; import { IBufferLine } from '../../Types'; -import { DEFAULT_COLOR } from '../atlas/Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -61,7 +59,9 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { - lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)])); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' @@ -69,7 +69,9 @@ describe('DomRendererRowFactory', () => { }); it('should add class for italic', () => { - lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)])); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' @@ -77,9 +79,12 @@ describe('DomRendererRowFactory', () => { }); it('should add classes for 256 foreground colors', () => { - const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P256; for (let i = 0; i < 256; i++) { - lineData.setCell(0, CellData.fromCharData([defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)])); + cell.fg &= ~Attributes.PCOLOR_MASK; + cell.fg |= i; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` @@ -88,9 +93,12 @@ describe('DomRendererRowFactory', () => { }); it('should add classes for 256 background colors', () => { - const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg |= Attributes.CM_P256; for (let i = 0; i < 256; i++) { - lineData.setCell(0, CellData.fromCharData([defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)])); + cell.bg &= ~Attributes.PCOLOR_MASK; + cell.bg |= i; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` @@ -99,7 +107,10 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert colors', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE; + cell.bg |= Attributes.CM_P16 | 1; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' @@ -107,7 +118,10 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default fg color', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= FgFlags.INVERSE; + cell.bg |= Attributes.CM_P16 | 1; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' @@ -115,7 +129,9 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default bg color', () => { - lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' @@ -123,8 +139,12 @@ describe('DomRendererRowFactory', () => { }); it('should turn bold fg text bright', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= FgFlags.BOLD | Attributes.CM_P16; for (let i = 0; i < 8; i++) { - lineData.setCell(0, CellData.fromCharData([(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); + cell.fg &= ~Attributes.PCOLOR_MASK; + cell.fg |= i; + lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 83a4651e..41091a63 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -41,7 +41,6 @@ export class DomRendererRowFactory { for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._cell); - const attr = this._cell.fg; const width = this._cell.width; // The character to the left is a wide character, drawing is owned by the char at x-1 @@ -54,9 +53,9 @@ export class DomRendererRowFactory { charElement.style.width = `${cellWidth * width}px`; } - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; + const flags = this._cell.getOldFlags(); + let bg = this._cell.getOldBgColor(); + let fg = this._cell.getOldFgColor(); if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 9d525fbf..4772a9d8 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -4,12 +4,13 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData } from '../Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData, IAttributeData } from '../Types'; import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../core/Platform'; import { ITheme, IDisposable, IMarker } from 'xterm'; import { Terminal } from '../Terminal'; +import { AttributeData } from '../BufferLine'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -187,7 +188,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - curAttr: number; + curAttrData = new AttributeData(); savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -222,7 +223,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { setgLevel(g: number): void { throw new Error('Method not implemented.'); } - eraseAttr(): number { + eraseAttrData(): IAttributeData { throw new Error('Method not implemented.'); } eraseRight(x: number, y: number): void { @@ -309,7 +310,7 @@ export class MockBuffer implements IBuffer { scrollTop: number; savedY: number; savedX: number; - savedCurAttr: number; + savedCurAttrData = new AttributeData(); translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { return Buffer.prototype.translateBufferLineToString.apply(this, arguments); } @@ -325,7 +326,7 @@ export class MockBuffer implements IBuffer { setLines(lines: ICircularList): void { this.lines = lines; } - getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { return Buffer.prototype.getBlankLine.apply(this, arguments); } stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { @@ -334,10 +335,10 @@ export class MockBuffer implements IBuffer { iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { return Buffer.prototype.iterator.apply(this, arguments); } - getNullCell(fg: number = 0, bg: number = 0): ICellData { + getNullCell(attr?: IAttributeData): ICellData { throw new Error('Method not implemented.'); } - getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + getWhitespaceCell(attr?: IAttributeData): ICellData { throw new Error('Method not implemented.'); } } From 70ded42beea95605e57e94fe3e2590f000fc77fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 13 Jan 2019 23:50:58 +0100 Subject: [PATCH 04/77] follow naming scheme --- src/BufferLine.ts | 4 ++-- src/InputHandler.test.ts | 36 ++++++++++++++++++------------------ src/Types.ts | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index e306346f..341e3009 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -220,8 +220,8 @@ export class AttributeData implements IAttributeData { public isDim(): number { return this.bg & BgFlags.DIM; } // color modes - public getColormodeFg(): number { return this.fg & Attributes.CM_MASK; } - public getColormodeBg(): number { return this.bg & Attributes.CM_MASK; } + public getFgColormode(): number { return this.fg & Attributes.CM_MASK; } + public getBgColormode(): number { return this.bg & Attributes.CM_MASK; } public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index df34cf04..7ca2d6fc 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -447,53 +447,53 @@ describe('InputHandler', () => { assert.equal(!!term.curAttrData.isInvisible(), false); }); it('colormode palette 16', () => { - assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT - assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 8; ++i) { term.writeSync(`\x1b[${i + 30};${i + 40}m`); - assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_P16); + assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_P16); assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_P16); + assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_P16); assert.equal(term.curAttrData.getBgColor(), i); } // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getColormodeFg(), 0); - assert.equal(term.curAttrData.getColormodeBg(), 0); + assert.equal(term.curAttrData.getFgColormode(), 0); + assert.equal(term.curAttrData.getBgColormode(), 0); }); it('colormode palette 256', () => { - assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT - assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 256; ++i) { term.writeSync(`\x1b[38;5;${i};48;5;${i}m`); - assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_P256); + assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_P256); assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_P256); + assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_P256); assert.equal(term.curAttrData.getBgColor(), i); } // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getColormodeFg(), 0); + assert.equal(term.curAttrData.getFgColormode(), 0); assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getColormodeBg(), 0); + assert.equal(term.curAttrData.getBgColormode(), 0); assert.equal(term.curAttrData.getBgColor(), -1); }); it('colormode RGB', () => { - assert.equal(term.curAttrData.getColormodeFg(), 0); // DEFAULT - assert.equal(term.curAttrData.getColormodeBg(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); - assert.equal(term.curAttrData.getColormodeFg(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_RGB); assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); assert.deepEqual(term.curAttrData.getFgColor(true), [1, 2, 3]); - assert.equal(term.curAttrData.getColormodeBg(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_RGB); assert.deepEqual(term.curAttrData.getBgColor(true), [4, 5, 6]); // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getColormodeFg(), 0); + assert.equal(term.curAttrData.getFgColormode(), 0); assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getColormodeBg(), 0); + assert.equal(term.curAttrData.getBgColormode(), 0); assert.equal(term.curAttrData.getBgColor(), -1); }); it('should zero missing RGB values', () => { diff --git a/src/Types.ts b/src/Types.ts index b46f858f..0de25e2d 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -532,8 +532,8 @@ export interface IAttributeData { isDim(): number; // color modes - getColormodeFg(): number; - getColormodeBg(): number; + getFgColormode(): number; + getBgColormode(): number; isFgRGB(): boolean; isBgRGB(): boolean; isFgPalette(): boolean; From 3515c0b220e90539b21f9b158b09f88e4400807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 14 Jan 2019 00:46:04 +0100 Subject: [PATCH 05/77] DOM renderer with RGB support --- .../dom/DomRendererRowFactory.test.ts | 6 +- src/renderer/dom/DomRendererRowFactory.ts | 60 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index c80cea27..6a69a4ba 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -113,7 +113,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -124,7 +124,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -134,7 +134,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 41091a63..7ef26e1f 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -4,9 +4,8 @@ */ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; -import { FLAGS } from '../Types'; import { IBufferLine } from '../../Types'; -import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; import { CellData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; @@ -53,10 +52,6 @@ export class DomRendererRowFactory { charElement.style.width = `${cellWidth * width}px`; } - const flags = this._cell.getOldFlags(); - let bg = this._cell.getOldBgColor(); - let fg = this._cell.getOldFgColor(); - if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); @@ -73,39 +68,44 @@ export class DomRendererRowFactory { } } - // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - if (flags & FLAGS.BOLD) { - // Convert the FG color to the bold variant. This should not happen when - // the fg is the inverse default color as there is no bold variant. - if (fg < 8) { - fg += 8; - } + if (this._cell.isBold()) { charElement.classList.add(BOLD_CLASS); } - if (flags & FLAGS.ITALIC) { + if (this._cell.isItalic()) { charElement.classList.add(ITALIC_CLASS); } charElement.textContent = this._cell.chars || WHITESPACE_CELL_CHAR; - if (fg !== DEFAULT_COLOR) { - charElement.classList.add(`xterm-fg-${fg}`); + + const swapColor = !!this._cell.isInverse(); + + // fg + if (this._cell.isFgRGB()) { + let style = charElement.getAttribute('style') || ''; + style += `${swapColor ? 'background-' : ''}color: rgb(${(this._cell.getFgColor(true) as number[]).join(',')});`; + charElement.setAttribute('style', style); + } else if (this._cell.isFgPalette()) { + let fg = this._cell.getFgColor() as number; + if (this._cell.isBold() && fg < 8 && !swapColor) { + fg += 8; + } + charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`); + } else if (swapColor) { + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); } - if (bg !== DEFAULT_COLOR) { - charElement.classList.add(`xterm-bg-${bg}`); + + // bg + if (this._cell.isBgRGB()) { + let style = charElement.getAttribute('style') || ''; + style += `${swapColor ? '' : 'background-'}color: rgb(${(this._cell.getBgColor(true) as number[]).join(',')});`; + charElement.setAttribute('style', style); + } else if (this._cell.isBgPalette()) { + charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._cell.getBgColor()}`); + } else if (swapColor) { + charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } + fragment.appendChild(charElement); } return fragment; From 0802582c9bbbb62034742affdae97c0fddcec5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 14 Jan 2019 01:00:07 +0100 Subject: [PATCH 06/77] test cases for DOM renderer style changes --- .../dom/DomRendererRowFactory.test.ts | 22 +++++++++++++++++++ src/renderer/dom/DomRendererRowFactory.ts | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 6a69a4ba..d3b2100c 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -151,6 +151,28 @@ describe('DomRendererRowFactory', () => { ); } }); + + it('should set style attribute for RBG', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3; + cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + + it('should correctly invert RGB colors', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE; + cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); }); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 7ef26e1f..39b13d48 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -83,7 +83,7 @@ export class DomRendererRowFactory { // fg if (this._cell.isFgRGB()) { let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? 'background-' : ''}color: rgb(${(this._cell.getFgColor(true) as number[]).join(',')});`; + style += `${swapColor ? 'background-' : ''}color:rgb(${(this._cell.getFgColor(true) as number[]).join(',')});`; charElement.setAttribute('style', style); } else if (this._cell.isFgPalette()) { let fg = this._cell.getFgColor() as number; @@ -98,7 +98,7 @@ export class DomRendererRowFactory { // bg if (this._cell.isBgRGB()) { let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? '' : 'background-'}color: rgb(${(this._cell.getBgColor(true) as number[]).join(',')});`; + style += `${swapColor ? '' : 'background-'}color:rgb(${(this._cell.getBgColor(true) as number[]).join(',')});`; charElement.setAttribute('style', style); } else if (this._cell.isBgPalette()) { charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._cell.getBgColor()}`); From 9cf479bcfa462af3077e8c34578ca83948e2abc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 14 Jan 2019 08:26:14 +0100 Subject: [PATCH 07/77] cleanup rgb channel conversion --- src/BufferLine.ts | 16 ++++++++-------- src/InputHandler.test.ts | 8 ++++---- src/InputHandler.ts | 10 +++------- src/Types.ts | 4 ++-- src/renderer/dom/DomRendererRowFactory.ts | 10 +++++----- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 341e3009..fcb57a4d 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -188,14 +188,14 @@ export enum BgFlags { } export class AttributeData implements IAttributeData { - static toRGB(value: number): IColorRGB { + static toColorRGB(value: number): IColorRGB { return [ value >>> Attributes.RED_SHIFT & 255, value >>> Attributes.GREEN_SHIFT & 255, value & 255 ]; } - static fromRGB(value: IColorRGB): number { + static fromColorRGB(value: IColorRGB): number { return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; } @@ -230,19 +230,19 @@ export class AttributeData implements IAttributeData { public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } // colors - public getFgColor(channels: boolean = false): number | IColorRGB { + public getFgColor(): number { switch (this.fg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return (channels) ? AttributeData.toRGB(this.fg & Attributes.RGB_MASK) : this.fg & Attributes.RGB_MASK; + case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK; default: return -1; // CM_DEFAULT defaults to -1 } } - public getBgColor(channels: boolean = false): number | IColorRGB { + public getBgColor(): number { switch (this.bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return (channels) ? AttributeData.toRGB(this.bg & Attributes.RGB_MASK) : this.bg & Attributes.RGB_MASK; + case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK; default: return -1; // CM_DEFAULT defaults to -1 } } @@ -273,7 +273,7 @@ export class AttributeData implements IAttributeData { return flags; } public getOldFgColor(): number { - let color = this.getFgColor() as number; + let color = this.getFgColor(); if (color === -1) { return 256; } @@ -290,7 +290,7 @@ export class AttributeData implements IAttributeData { return color; } public getOldBgColor(): number { - let color = this.getBgColor() as number; + let color = this.getBgColor(); if (color === -1) { return 256; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 7ca2d6fc..95e41175 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -9,7 +9,7 @@ import { MockInputHandlingTerminal, TestTerminal } from './ui/TestUtils.test'; import { DEFAULT_ATTR_DATA } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; -import { CellData, Attributes } from './BufferLine'; +import { CellData, Attributes, AttributeData } from './BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -486,9 +486,9 @@ describe('InputHandler', () => { term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_RGB); assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); - assert.deepEqual(term.curAttrData.getFgColor(true), [1, 2, 3]); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]); assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_RGB); - assert.deepEqual(term.curAttrData.getBgColor(true), [4, 5, 6]); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]); // reset to DEFAULT term.writeSync(`\x1b[39;49m`); assert.equal(term.curAttrData.getFgColormode(), 0); @@ -499,7 +499,7 @@ describe('InputHandler', () => { it('should zero missing RGB values', () => { term.writeSync(`\x1b[38;2;1;2;3m`); term.writeSync(`\x1b[38;2;5m`); - assert.deepEqual(term.curAttrData.getFgColor(true), [5, 0, 0]); + assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]); }); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 178507fa..4ea340bb 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,7 +14,7 @@ import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat, utf32ToString } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; -import { CellData, Attributes, FgFlags, BgFlags } from './BufferLine'; +import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -1651,9 +1651,7 @@ export class InputHandler extends Disposable implements IInputHandler { i += 2; attr.fg |= Attributes.CM_RGB; attr.fg &= ~Attributes.RGB_MASK; - attr.fg |= (params[i] & 0xFF) << Attributes.RED_SHIFT; - attr.fg |= (params[i + 1] & 0xFF) << Attributes.GREEN_SHIFT; - attr.fg |= (params[i + 2] & 0xFF) << Attributes.BLUE_SHIFT; + attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); i += 2; } else if (params[i + 1] === 5) { i += 2; @@ -1667,9 +1665,7 @@ export class InputHandler extends Disposable implements IInputHandler { i += 2; attr.bg |= Attributes.CM_RGB; attr.bg &= ~Attributes.RGB_MASK; - attr.bg |= (params[i] & 0xFF) << Attributes.RED_SHIFT; - attr.bg |= (params[i + 1] & 0xFF) << Attributes.GREEN_SHIFT; - attr.bg |= (params[i + 2] & 0xFF) << Attributes.BLUE_SHIFT; + attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]); i += 2; } else if (params[i + 1] === 5) { i += 2; diff --git a/src/Types.ts b/src/Types.ts index 0de25e2d..50ea560a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -542,8 +542,8 @@ export interface IAttributeData { isBgDefault(): boolean; // colors - getFgColor(channels?: boolean): number | IColorRGB; - getBgColor(channels?: boolean): number | IColorRGB; + getFgColor(): number; + getBgColor(): number; // shim for old API getOldFlags(): number; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 39b13d48..b37eb02b 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -6,7 +6,7 @@ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { IBufferLine } from '../../Types'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { CellData } from '../../BufferLine'; +import { CellData, AttributeData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -78,15 +78,15 @@ export class DomRendererRowFactory { charElement.textContent = this._cell.chars || WHITESPACE_CELL_CHAR; - const swapColor = !!this._cell.isInverse(); + const swapColor = this._cell.isInverse(); // fg if (this._cell.isFgRGB()) { let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? 'background-' : ''}color:rgb(${(this._cell.getFgColor(true) as number[]).join(',')});`; + style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._cell.getFgColor())).join(',')});`; charElement.setAttribute('style', style); } else if (this._cell.isFgPalette()) { - let fg = this._cell.getFgColor() as number; + let fg = this._cell.getFgColor(); if (this._cell.isBold() && fg < 8 && !swapColor) { fg += 8; } @@ -98,7 +98,7 @@ export class DomRendererRowFactory { // bg if (this._cell.isBgRGB()) { let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? '' : 'background-'}color:rgb(${(this._cell.getBgColor(true) as number[]).join(',')});`; + style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._cell.getBgColor())).join(',')});`; charElement.setAttribute('style', style); } else if (this._cell.isBgPalette()) { charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._cell.getBgColor()}`); From 94c19fb92a90ecf843954115cf45a1813035ebda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 15 Jan 2019 03:13:44 +0100 Subject: [PATCH 08/77] preliminarly RGB support in canvas renderer --- src/renderer/BaseRenderLayer.ts | 14 +++++++++++--- src/renderer/TextRenderLayer.ts | 14 ++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index f84fbe6b..4ef6bada 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -9,7 +9,7 @@ import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/T import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { is256Color } from './atlas/CharAtlasUtils'; -import { CellData } from '../BufferLine'; +import { CellData, AttributeData } from '../BufferLine'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -258,9 +258,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { * This is used to validate whether a cached image can be used. * @param bold Whether the text is bold. */ - protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void { + protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean, cell: CellData): void { const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + if (cell.isFgRGB()) { + this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic, cell); + return; + } + fg += drawInBrightColor ? 8 : 0; this._currentGlyphIdentifier.chars = chars; this._currentGlyphIdentifier.code = code; @@ -292,7 +297,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { + private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean, cell?: CellData): void { this._ctx.save(); this._ctx.font = this._getFont(terminal, bold, italic); this._ctx.textBaseline = 'middle'; @@ -302,6 +307,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { } else if (is256Color(fg)) { // 256 color support this._ctx.fillStyle = this._colors.ansi[fg].css; + if (cell && cell.isFgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } } else { this._ctx.fillStyle = this._colors.foreground.css; } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 2b2ba1bb..1e374dd9 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -10,7 +10,7 @@ import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; import { is256Color } from './atlas/CharAtlasUtils'; -import { CellData } from '../BufferLine'; +import { CellData, AttributeData } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -126,7 +126,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).code === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the @@ -189,7 +189,12 @@ export class TextRenderLayer extends BaseRenderLayer { if (bg === INVERTED_DEFAULT_COLOR) { nextFillStyle = this._colors.foreground.css; } else if (is256Color(bg)) { - nextFillStyle = this._colors.ansi[bg].css; + if (this._cell.isBgRGB()) { + console.log(`rgb(${AttributeData.toColorRGB(this._cell.getBgColor()).join(',')})`); + nextFillStyle = `rgb(${AttributeData.toColorRGB(this._cell.getBgColor()).join(',')})`; + } else { + nextFillStyle = this._colors.ansi[bg].css; + } } if (prevFillStyle === null) { @@ -245,7 +250,8 @@ export class TextRenderLayer extends BaseRenderLayer { terminal, chars, code, width, x, y, fg, bg, - !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC) + !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC), + this._cell ); }); } From 1536e795424ff5499d93199a8772f5cd6654fecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 15 Jan 2019 03:24:47 +0100 Subject: [PATCH 09/77] remove printf remnant --- src/renderer/TextRenderLayer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 1e374dd9..e0ef0f2d 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -190,7 +190,6 @@ export class TextRenderLayer extends BaseRenderLayer { nextFillStyle = this._colors.foreground.css; } else if (is256Color(bg)) { if (this._cell.isBgRGB()) { - console.log(`rgb(${AttributeData.toColorRGB(this._cell.getBgColor()).join(',')})`); nextFillStyle = `rgb(${AttributeData.toColorRGB(this._cell.getBgColor()).join(',')})`; } else { nextFillStyle = this._colors.ansi[bg].css; From ae50d84c5af5805d6b3b289d161a5a64aadf89b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 17 Jan 2019 00:07:25 +0100 Subject: [PATCH 10/77] fix characterjoiner to support AttrData --- src/renderer/CharacterJoinerRegistry.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 4cad7c72..459975c7 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,6 +1,7 @@ import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; import { CellData } from '../BufferLine'; +import { WHITESPACE_CELL_CHAR } from '../Buffer'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { @@ -43,7 +44,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { } const ranges: [number, number][] = []; - const lineStr = this._terminal.buffer.translateBufferLineToString(row, true); + const lineStr = line.translateToString(true); // Because some cells can be represented by multiple javascript characters, // we track the cell and the string indexes separately. This allows us to @@ -52,21 +53,19 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.getFG(0) >> 9; + let rangeAttrFG = line.getFG(0); + let rangeAttrBG = line.getBG(0); - for (let x = 0; x < this._terminal.cols; x++) { + for (let x = 0; x < line.getTrimmedLength(); x++) { line.loadCell(x, this._cell); - const chars = this._cell.chars; - const width = this._cell.width; - const attr = this._cell.fg >> 9; - if (width === 0) { + if (this._cell.width === 0) { // If this character is of width 0, skip it. continue; } // End of range - if (attr !== rangeAttr) { + if (this._cell.fg !== rangeAttrFG || this._cell.bg !== rangeAttrBG) { // If we ended up with a sequence of more than one character, // look for ranges to join. if (x - rangeStartColumn > 1) { @@ -85,10 +84,11 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { // Reset our markers for a new range. rangeStartColumn = x; rangeStartStringIndex = currentStringIndex; - rangeAttr = attr; + rangeAttrFG = this._cell.fg; + rangeAttrBG = this._cell.bg; } - currentStringIndex += chars.length; + currentStringIndex += this._cell.chars.length || WHITESPACE_CELL_CHAR.length; } // Process any trailing ranges. @@ -154,7 +154,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { for (let x = startCol; x < this._terminal.cols; x++) { const width = line.getWidth(x); - const length = line.getString(x).length; + const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length; // We skip zero-width characters when creating the string to join the text // so we do the same here From 8d850d6c8b2c0c4d310712b69c51ca51febf3896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 17 Jan 2019 01:25:16 +0100 Subject: [PATCH 11/77] apply CellData to canvas renderer --- src/renderer/BaseRenderLayer.ts | 66 ++++++++------ src/renderer/TextRenderLayer.ts | 149 +++++++++++++------------------- 2 files changed, 102 insertions(+), 113 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 4ef6bada..e266e52a 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,12 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { ITerminal } from '../Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; +import { ITerminal, ICellData } from '../Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier, DEFAULT_COLOR } from './atlas/Types'; import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { is256Color } from './atlas/CharAtlasUtils'; import { CellData, AttributeData } from '../BufferLine'; +import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -258,22 +258,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { * This is used to validate whether a cached image can be used. * @param bold Whether the text is bold. */ - protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean, cell: CellData): void { - const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { - if (cell.isFgRGB()) { - this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic, cell); + // skip cache right away if we draw in RGB + if (cell.isFgRGB() || (cell.isInverse() && cell.isBgRGB())) { + this._drawUncachedChars(terminal, cell, x, y); return; } + let fg; + let bg; + if (cell.isInverse()) { + fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); + bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); + } else { + bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); + fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); + } + + const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + fg += drawInBrightColor ? 8 : 0; - this._currentGlyphIdentifier.chars = chars; - this._currentGlyphIdentifier.code = code; + this._currentGlyphIdentifier.chars = cell.chars || WHITESPACE_CELL_CHAR; + this._currentGlyphIdentifier.code = cell.code || WHITESPACE_CELL_CODE; this._currentGlyphIdentifier.bg = bg; this._currentGlyphIdentifier.fg = fg; - this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold; - this._currentGlyphIdentifier.dim = dim; - this._currentGlyphIdentifier.italic = italic; + this._currentGlyphIdentifier.bold = cell.isBold() && terminal.options.enableBold; + this._currentGlyphIdentifier.dim = !!cell.isDim(); + this._currentGlyphIdentifier.italic = !!cell.isItalic(); const atlasDidDraw = this._charAtlas && this._charAtlas.draw( this._ctx, this._currentGlyphIdentifier, @@ -282,7 +294,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { ); if (!atlasDidDraw) { - this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic); + this._drawUncachedChars(terminal, cell, x, y); } } @@ -297,32 +309,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean, cell?: CellData): void { + private _drawUncachedChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { this._ctx.save(); - this._ctx.font = this._getFont(terminal, bold, italic); + this._ctx.font = this._getFont(terminal, cell.isBold() && terminal.options.enableBold, !!cell.isItalic()); this._ctx.textBaseline = 'middle'; - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (is256Color(fg)) { - // 256 color support - this._ctx.fillStyle = this._colors.ansi[fg].css; - if (cell && cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + if (cell.isInverse()) { + if (cell.isBgDefault()) { + this._ctx.fillStyle = this._colors.background.css; + } else if (cell.isBgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else { + this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; } - } else { - this._ctx.fillStyle = this._colors.foreground.css; + } else if (cell.isFgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } else if (cell.isFgPalette()) { + this._ctx.fillStyle = this._colors.ansi[cell.getFgColor()].css; } this._clipRow(terminal, y); // Apply alpha to dim the character - if (dim) { + if (cell.isDim()) { this._ctx.globalAlpha = DIM_OPACITY; } // Draw the character this._ctx.fillText( - chars, + cell.chars, x * this._scaledCellWidth + this._scaledCharLeft, (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index e0ef0f2d..7ff2d002 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,14 +3,12 @@ * @license MIT */ -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; -import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; -import { CharData, ITerminal } from '../Types'; -import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; +import { NULL_CELL_CODE } from '../Buffer'; +import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; +import { CharData, ITerminal, ICellData } from '../Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { is256Color } from './atlas/CharAtlasUtils'; -import { CellData, AttributeData } from '../BufferLine'; +import { CellData, AttributeData, Content } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -59,14 +57,9 @@ export class TextRenderLayer extends BaseRenderLayer { lastRow: number, joinerRegistry: ICharacterJoinerRegistry | null, callback: ( - code: number, - chars: string, - width: number, + cell: ICellData, x: number, - y: number, - fg: number, - bg: number, - flags: number + y: number ) => void ): void { for (let y = firstRow; y <= lastRow; y++) { @@ -74,13 +67,8 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - (line as any).loadCell(x, this._cell); - let code: number = this._cell.code || WHITESPACE_CELL_CODE; - - // Can either represent character(s) for a single cell or multiple cells - // if indicated by a character joiner. - let chars = this._cell.chars || WHITESPACE_CELL_CHAR; - let width = this._cell.width; + line.loadCell(x, this._cell); + let cell = this._cell; // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -88,7 +76,7 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 - if (width === 0) { + if (cell.width === 0) { continue; } @@ -101,14 +89,15 @@ export class TextRenderLayer extends BaseRenderLayer { // We already know the exact start and end column of the joined range, // so we get the string and width representing it directly - chars = terminal.buffer.translateBufferLineToString( - row, - true, - range[0], - range[1] - ); - width = range[1] - range[0]; - code = Infinity; + cell = CellData.fromCharData([ + 0, + line.translateToString(true, range[0], range[1]), + range[1] - range[0], + 0xFFFFFF + ]); + // hacky: patch attrs + cell.fg = this._cell.fg; + cell.bg = this._cell.bg; // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; @@ -118,7 +107,7 @@ export class TextRenderLayer extends BaseRenderLayer { // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't // yield the same result as rendering the last character individually. - if (!isJoined && this._isOverlapping(chars, width, code)) { + if (!isJoined && this._isOverlapping(cell)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -127,7 +116,9 @@ export class TextRenderLayer extends BaseRenderLayer { // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) { - width = 2; + // patch width to 2 + cell.content &= ~Content.WIDTH_MASK; + cell.content |= 2 << Content.WIDTH_SHIFT; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the // overlapping char is no longer to the left of the character and also when @@ -136,32 +127,10 @@ export class TextRenderLayer extends BaseRenderLayer { } } - const flags = this._cell.getOldFlags(); - let bg = this._cell.getOldBgColor(); - let fg = this._cell.getOldFgColor(); - - // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - callback( - code, - chars, - width, + cell, x, - y, - fg, - bg, - flags + y ); x = lastCharX; @@ -182,18 +151,23 @@ export class TextRenderLayer extends BaseRenderLayer { ctx.save(); - this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => { + this._forEachCell(terminal, firstRow, lastRow, null, (cell, x, y) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. let nextFillStyle = null; // null represents default background color - if (bg === INVERTED_DEFAULT_COLOR) { - nextFillStyle = this._colors.foreground.css; - } else if (is256Color(bg)) { - if (this._cell.isBgRGB()) { - nextFillStyle = `rgb(${AttributeData.toColorRGB(this._cell.getBgColor()).join(',')})`; + + if (cell.isInverse()) { + if (cell.isFgDefault()) { + nextFillStyle = this._colors.foreground.css; + } else if (cell.isFgRGB()) { + nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; } else { - nextFillStyle = this._colors.ansi[bg].css; + nextFillStyle = this._colors.ansi[cell.getFgColor()].css; } + } else if (cell.isBgRGB()) { + nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else if (cell.isBgPalette()) { + nextFillStyle = this._colors.ansi[cell.getBgColor()].css; } if (prevFillStyle === null) { @@ -228,30 +202,31 @@ export class TextRenderLayer extends BaseRenderLayer { } private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void { - this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => { - if (flags & FLAGS.INVISIBLE) { + this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => { + if (cell.isInvisible()) { return; } - if (flags & FLAGS.UNDERLINE) { + if (cell.isUnderline()) { this._ctx.save(); - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (is256Color(fg)) { - // 256 color support - this._ctx.fillStyle = this._colors.ansi[fg].css; - } else { - this._ctx.fillStyle = this._colors.foreground.css; + + if (cell.isInverse()) { + if (cell.isBgDefault()) { + this._ctx.fillStyle = this._colors.background.css; + } else if (cell.isBgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; + } else { + this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + } + } else if (cell.isFgRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; + } else if (cell.isFgPalette()) { + this._ctx.fillStyle = this._colors.ansi[cell.getFgColor()].css; } - this.fillBottomLineAtCells(x, y, width); + + this.fillBottomLineAtCells(x, y, cell.width); this._ctx.restore(); } - this.drawChars( - terminal, chars, code, - width, x, y, - fg, bg, - !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC), - this._cell - ); + this.drawChars(terminal, cell, x, y); }); } @@ -277,21 +252,21 @@ export class TextRenderLayer extends BaseRenderLayer { /** * Whether a character is overlapping to the next cell. */ - private _isOverlapping(char: string, width: number, code: number): boolean { + private _isOverlapping(cell: ICellData): boolean { // Only single cell characters can be overlapping, rendering issues can // occur without this check - if (width !== 1) { + if (cell.width !== 1) { return false; } // We assume that any ascii character will not overlap - if (code < 256) { + if (cell.code < 256) { return false; } // Deliver from cache if available - if (this._characterOverlapCache.hasOwnProperty(char)) { - return this._characterOverlapCache[char]; + if (this._characterOverlapCache.hasOwnProperty(cell.chars)) { + return this._characterOverlapCache[cell.chars]; } // Setup the font @@ -301,13 +276,13 @@ export class TextRenderLayer extends BaseRenderLayer { // Measure the width of the character, but Math.floor it // because that is what the renderer does when it calculates // the character dimensions we are comparing against - const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth; + const overlaps = Math.floor(this._ctx.measureText(cell.chars).width) > this._characterWidth; // Restore the original context this._ctx.restore(); // Cache and return - this._characterOverlapCache[char] = overlaps; + this._characterOverlapCache[cell.chars] = overlaps; return overlaps; } From df739ab8146fe8e5f3a776641ef7b889f044f414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 17 Jan 2019 01:31:59 +0100 Subject: [PATCH 12/77] remove old color and flag shims --- src/BufferLine.ts | 116 --------------------------------------- src/InputHandler.test.ts | 10 ++-- src/Types.ts | 5 -- 3 files changed, 5 insertions(+), 126 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index fcb57a4d..3d94d5f6 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -5,64 +5,8 @@ import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { stringFromCodePoint } from './core/input/TextDecoder'; -import { FLAGS } from './renderer/Types'; -import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; -/** - * TODO: - * The below color-related code can be removed when true color is implemented. - * It's only purpose is to match true color requests with the closest matching - * ANSI color code. - */ -const matchColorCache: {[colorRGBHash: number]: number} = {}; - -// http://stackoverflow.com/questions/1633828 -function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { - return Math.pow(30 * (r1 - r2), 2) - + Math.pow(59 * (g1 - g2), 2) - + Math.pow(11 * (b1 - b2), 2); -} - -function matchColor(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < DEFAULT_ANSI_COLORS.length; i++) { - c = DEFAULT_ANSI_COLORS[i].rgba; - r2 = c >>> 24; - g2 = c >>> 16 & 0xFF; - b2 = c >>> 8 & 0xFF; - // assume that alpha is 0xFF - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; -} - /** * buffer memory layout: * @@ -246,66 +190,6 @@ export class AttributeData implements IAttributeData { default: return -1; // CM_DEFAULT defaults to -1 } } - - public getOldFlags(): number { - let flags = 0; - if (this.isBold()) { - flags |= FLAGS.BOLD; - } - if (this.isUnderline()) { - flags |= FLAGS.UNDERLINE; - } - if (this.isBlink()) { - flags |= FLAGS.BLINK; - } - if (this.isDim()) { - flags |= FLAGS.DIM; - } - if (this.isInvisible()) { - flags |= FLAGS.INVISIBLE; - } - if (this.isInverse()) { - flags |= FLAGS.INVERSE; - } - if (this.isItalic()) { - flags |= FLAGS.ITALIC; - } - return flags; - } - public getOldFgColor(): number { - let color = this.getFgColor(); - if (color === -1) { - return 256; - } - if (this.isFgRGB()) { - color = matchColor( - (this.fg & Attributes.RED_MASK) >> Attributes.RED_SHIFT, - (this.fg & Attributes.GREEN_MASK) >> Attributes.GREEN_SHIFT, - (this.fg & Attributes.BLUE_MASK) >> Attributes.BLUE_SHIFT - ); - if (color === -1) { - color = 256; - } - } - return color; - } - public getOldBgColor(): number { - let color = this.getBgColor(); - if (color === -1) { - return 256; - } - if (this.isBgRGB()) { - color = matchColor( - (this.bg & Attributes.RED_MASK) >> Attributes.RED_SHIFT, - (this.bg & Attributes.GREEN_MASK) >> Attributes.GREEN_SHIFT, - (this.bg & Attributes.BLUE_MASK) >> Attributes.BLUE_SHIFT - ); - if (color === -1) { - color = 256; - } - } - return color; - } } /** diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 95e41175..0b7f7ad3 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -357,14 +357,14 @@ describe('InputHandler', () => { expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getOldFgColor())).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getOldFgColor())).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); @@ -373,7 +373,7 @@ describe('InputHandler', () => { // Text color of 'TEST' should be default expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); // Text color of 'JUNK' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getOldFgColor())).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); @@ -390,12 +390,12 @@ describe('InputHandler', () => { handler.parse('\x1b[?1049h\x1b[uTEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getOldFgColor())).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); }); it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { handler.parse('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background - expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getOldBgColor()).to.equal(2); + expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2); }); }); diff --git a/src/Types.ts b/src/Types.ts index 50ea560a..6636b04d 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -544,11 +544,6 @@ export interface IAttributeData { // colors getFgColor(): number; getBgColor(): number; - - // shim for old API - getOldFlags(): number; - getOldFgColor(): number; - getOldBgColor(): number; } /** Cell data */ From 2370038523499ab8cf8f6b9566482b6e44530fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 17 Jan 2019 01:41:48 +0100 Subject: [PATCH 13/77] always draw RGB uncached --- src/renderer/BaseRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index e266e52a..e0d3e731 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -261,7 +261,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { // skip cache right away if we draw in RGB - if (cell.isFgRGB() || (cell.isInverse() && cell.isBgRGB())) { + if (cell.isFgRGB() || cell.isBgRGB()) { this._drawUncachedChars(terminal, cell, x, y); return; } From 2a1f25e7dce1254dde2b24e74bfbd2c97fc8e29c Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Sun, 27 Jan 2019 13:15:37 +0100 Subject: [PATCH 14/77] Make textarea positioning work with css transformations on parent elements --- src/Terminal.ts | 6 +++--- src/ui/Clipboard.ts | 44 +++++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4ddd0431..c1fc8ec8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -566,12 +566,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Firefox doesn't appear to fire the contextmenu event on right click this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); })); } @@ -583,7 +583,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // that the regular click event doesn't fire for the middle mouse button. this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea); + moveTextAreaUnderMouseCursor(event, this); } })); } diff --git a/src/ui/Clipboard.ts b/src/ui/Clipboard.ts index b1acba9d..2570a8b1 100644 --- a/src/ui/Clipboard.ts +++ b/src/ui/Clipboard.ts @@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void { - // Bring textarea at the cursor position - textarea.style.position = 'fixed'; - textarea.style.width = '20px'; - textarea.style.height = '20px'; - textarea.style.left = (ev.clientX - 10) + 'px'; - textarea.style.top = (ev.clientY - 10) + 'px'; - textarea.style.zIndex = '1000'; +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { - textarea.focus(); + // Calculate textarea position relative to the screen element + const pos = term.screenElement.getBoundingClientRect(); + const left = ev.clientX - pos.left - 10; + const top = ev.clientY - pos.top - 10; + + // Bring textarea at the cursor position + term.textarea.style.position = 'absolute'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = `${left}px`; + term.textarea.style.top = `${top}px`; + term.textarea.style.zIndex = '1000'; + + term.textarea.focus(); // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - textarea.style.position = null; - textarea.style.width = null; - textarea.style.height = null; - textarea.style.left = null; - textarea.style.top = null; - textarea.style.zIndex = null; + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; }, 200); } @@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param selectionManager The terminal's selection manager. * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { - moveTextAreaUnderMouseCursor(ev, textarea); +export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, term); if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { selectionManager.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; - textarea.select(); + term.textarea.value = selectionManager.selectionText; + term.textarea.select(); } From fc1692b3eb10cacb52284dd11e7e343c40ace716 Mon Sep 17 00:00:00 2001 From: Nikita Chuklinov Date: Wed, 30 Jan 2019 20:34:16 +0300 Subject: [PATCH 15/77] clear state if selection doesn't exists --- src/renderer/SelectionRenderLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 81782ee8..fc16a2fb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -55,6 +55,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Selection does not exist if (!start || !end) { + this._clearState(); return; } From 65dbf88460853785ce259131ea788980b25b1b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 1 Feb 2019 21:44:51 +0100 Subject: [PATCH 16/77] apply bright shift for uncached draws --- src/renderer/BaseRenderLayer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 11de5860..2df61521 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -325,7 +325,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { } else if (cell.isFgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; } else if (cell.isFgPalette()) { - this._ctx.fillStyle = this._colors.ansi[cell.getFgColor()].css; + let fg = cell.getFgColor(); + if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } + this._ctx.fillStyle = this._colors.ansi[fg].css; } this._clipRow(terminal, y); From 54a1319f57c4a8f38f91209705d4d85f402940b0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Feb 2019 18:23:10 -0800 Subject: [PATCH 17/77] v3.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ed3a4b8..fa9cc070 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.10.0", + "version": "3.11.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", From 1829cb7609029405230e69803c0262f2768a40ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Feb 2019 19:28:07 -0800 Subject: [PATCH 18/77] Add roadmap wiki link to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35ccc9b6..f22c6c00 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht Xterm.js follows a monthly release cycle roughly. -All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). ## Contributing From f3aac10bbd32efb4c5f648d7fe7b7cdd10a25a55 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Feb 2019 10:46:20 -0800 Subject: [PATCH 19/77] Update license year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 28adbdad..4472336c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) +Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) From 69d3a4667f6d3ade4a89e71be066de0e69d82d16 Mon Sep 17 00:00:00 2001 From: coderaiser Date: Mon, 4 Feb 2019 13:28:14 +0200 Subject: [PATCH 20/77] fix: Renderer: IntersectionObserver can produce more then 1 entry --- src/renderer/Renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 02328877..b8ef87aa 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -70,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 }); + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(this._terminal.element); this.register({ dispose: () => observer.disconnect() }); } From b88230db8ba0d54fb194f5b903215a53b017a743 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 5 Feb 2019 08:20:16 -0800 Subject: [PATCH 21/77] Make sure the viewport is filled when reflowing a row change Fixes #1926 --- src/Buffer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7f4b6071..e40fa8b4 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -233,22 +233,22 @@ export class Buffer implements IBuffer { // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - this._reflowLarger(newCols); + this._reflowLarger(newCols, newRows); } else { this._reflowSmaller(newCols, newRows); } } - private _reflowLarger(newCols: number): void { + private _reflowLarger(newCols: number, newRows: number): void { const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); - this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); + this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); } } - private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { + private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { // Adjust viewport based on number of items removed let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { @@ -256,7 +256,7 @@ export class Buffer implements IBuffer { if (this.y > 0) { this.y--; } - if (this.lines.length < this._rows) { + if (this.lines.length < newRows) { // Add an extra row at the bottom of the viewport this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); } From d17dfbc73fc61d467ad079e4e0b90ae778687a88 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Feb 2019 12:13:00 -0800 Subject: [PATCH 22/77] Cover a case when resizing smaller making y go out of bounds --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index e40fa8b4..d017aa7e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -360,7 +360,7 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._rows - 1) { + if (this.y < newRows - 1) { this.y++; this.lines.pop(); } else { From 92be01dd8188b9b2eb730048526f51443b4fac6c Mon Sep 17 00:00:00 2001 From: Ahtsham Raziq Date: Sat, 9 Feb 2019 23:24:32 +0500 Subject: [PATCH 23/77] Compose file: fix variable substitution --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8e6a2f46..6eefed89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./:/usr/src/app ports: - - ${XTERMJS_PORT:3000}:3000 + - ${XTERMJS_PORT:-3000}:3000 command: ["npm", "start"] watch: From 84d7bfeacce308c0dc762e038a20ea36b03b44cd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 05:14:55 -0800 Subject: [PATCH 24/77] Remove font-family from .css file Fixes #1935 --- src/xterm.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xterm.css b/src/xterm.css index 24cd475f..2e47b1a1 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -36,7 +36,6 @@ */ .xterm { - font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; user-select: none; From 817401bbcd08c45ffa8169341213d49b7de81823 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 05:32:47 -0800 Subject: [PATCH 25/77] Align y draw coord with how cache draws it Fixes #1937 --- src/renderer/BaseRenderLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3e0b8643..a609d79c 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -241,7 +241,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } /** @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); this._ctx.restore(); } From 78426d8a12c56f40cf1c2d74fb53c23f2982ebb5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 06:05:14 -0800 Subject: [PATCH 26/77] Make the composition view use the same font as the terminal --- src/CompositionHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 31ad866b..5f838c7a 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -203,6 +203,7 @@ export class CompositionHelper { this._compositionView.style.top = cursorTop + 'px'; this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._terminal.options.fontFamily; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); From c934200c86ccaca419cae6aae210880a52791c46 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 06:07:31 -0800 Subject: [PATCH 27/77] Also set font size --- src/CompositionHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 5f838c7a..840bef55 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -204,6 +204,7 @@ export class CompositionHelper { this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; this._compositionView.style.fontFamily = this._terminal.options.fontFamily; + this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); From 9f29eeed2338dbc8861c65a1a35631fc34d980e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E8=99=AB?= Date: Tue, 19 Feb 2019 17:19:19 +0800 Subject: [PATCH 28/77] Update README.md (Jumpserver)[https://github.com/jumpserver/] use xterm.js --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f22c6c00..59e406e9 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. From e1c1c7a4f217f75eea4ed71fd0afcf5a1f14a93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E8=99=AB?= Date: Wed, 20 Feb 2019 14:57:16 +0800 Subject: [PATCH 29/77] Update README.md move it to the bottom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59e406e9..17bc9fb4 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. @@ -155,6 +154,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. - [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 3285374618a2ea112e5124c3b551ae0ac0761035 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Feb 2019 07:03:40 -0800 Subject: [PATCH 30/77] Disable reflow when winptyCompat is on Fixes #1943 --- src/Buffer.ts | 6 +++++- src/addons/winptyCompat/winptyCompat.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7f4b6071..bf0bfe17 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -211,7 +211,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback) { + if (this._isReflowEnabled) { this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk @@ -226,6 +226,10 @@ export class Buffer implements IBuffer { this._rows = newRows; } + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled; + } + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index aec580ed..d162f4e9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -19,6 +19,8 @@ export function winptyCompatInit(terminal: Terminal): void { return; } + (addonTerminal._core as any).isWinptyCompatEnabled = true; + // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening From 0854f846533689b253e3a6b6924216b2b52592f3 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Tue, 26 Feb 2019 11:28:51 +0100 Subject: [PATCH 31/77] actually fix mouse handler before term attached --- src/InputHandler.ts | 8 ++++++-- src/Terminal.ts | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2f53cfcb..7405ff9f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1284,7 +1284,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); } - this._terminal.selectionManager.disable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.disable(); + } this._terminal.log('Binding to mouse events.'); break; case 1004: // send focusin/focusout events @@ -1474,7 +1476,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events'); } - this._terminal.selectionManager.enable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.enable(); + } break; case 1004: // send focusin/focusout events this._terminal.sendFocus = false; diff --git a/src/Terminal.ts b/src/Terminal.ts index c1fc8ec8..cb9bc675 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -738,6 +738,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.mouseHelper = new MouseHelper(this.renderer); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); + if (this.mouseEvents) { + this.selectionManager.disable() + } else { + this.selectionManager.enable() + } if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to From e178139907a8a9a098a249849931faf89bdec5dc Mon Sep 17 00:00:00 2001 From: Nick Shaffner Date: Wed, 27 Feb 2019 22:37:22 -0800 Subject: [PATCH 32/77] Fix for issue #812: Xterm.js's encoding of mouse coordinate See: https://github.com/xtermjs/xterm.js/issues/812 Changed the utf-8 mouse encoding to match iTerm --- src/Terminal.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c1fc8ec8..0539a904 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -854,16 +854,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 127) ch = 127; data.push(ch); } else { - if (ch === 2047) { - data.push(0); + if (ch > 2047) { + data.push(2047); return; - } - if (ch < 127) { - data.push(ch); } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); + data.push(ch); } } } From 4e479b455a659a53c7a26d05549bd38ba301c9be Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 14:40:21 +0200 Subject: [PATCH 33/77] docs: Consistent style on lists. Added some missing dots, capitalized a couple of lines. --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7924027..e8102a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,21 +33,21 @@ opening an issue, read these pointers. You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: - Fork [xterm.js](https://github.com/sourcelair/xterm.js/) - ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) -- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running -- Make your changes + ([how to fork a repo](https://help.github.com/articles/fork-a-repo)). +- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running. +- Make your changes. - If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory. - Follow the general code style of the rest of the project (see below). - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)). Don't put more than one feature/fix in a single pull request. -By contributing code to xterm.js you +By contributing code to xterm.js you: - - agree to license the contributed code under xterm.js' [MIT + - Agree to license the contributed code under xterm.js' [MIT license](LICENSE). - - confirm that you have the right to contribute and license the code + - Confirm that you have the right to contribute and license the code in question. (Either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct From a8a0344d1ac5ea411ea244a73754a06ffda2d308 Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 14:51:19 +0200 Subject: [PATCH 34/77] docs: style improvements - Added a trailing dot at the end of each list line. - Consistent usage of `xterm.js` in "Real-world uses". --- README.md | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 17bc9fb4..d70e8b7e 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## Features -- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support -- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer -- **Rich unicode support**: Supports CJK, emojis and IMEs -- **Self-contained**: Requires zero dependencies to work -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option +- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support. +- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. +- **Rich unicode support**: Supports CJK, emojis and IMEs. +- **Self-contained**: Requires zero dependencies to work. +- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. - **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not -- Xterm.js is not a terminal application that you can download and use on your computer -- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) +- Xterm.js is not a terminal application that you can download and use on your computer. +- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output). ## Getting Started @@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t @@ -106,9 +106,9 @@ Note that some APIs are marked *experimental*, these are added to enable experim ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. -- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js -- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js +- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on `xterm.js`. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on `xterm.js`. +- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on `xterm.js`. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. @@ -125,11 +125,10 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. -- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible -computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. +- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. -- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. +- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses `xterm.js` for container terminals and the host shell. - [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. - [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. - [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. @@ -138,23 +137,23 @@ computational environment for Jupyter, supporting interactive data science and s - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. - [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. -- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS +- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. -- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users +- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users. - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. -- [**Hyper**](https://hyper.is): A terminal built on web technologies +- [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. -- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. +- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on `xterm.js`. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. -- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js +- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to `xterm.js`. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. -- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard. -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. +- [**info-beamer hosted**](https://info-beamer.com): Uses `xterm.js` to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use `xterm.js` for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 09754fddf8e3ba0dbbd3840b65a69822482fb64b Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 21:15:20 +0200 Subject: [PATCH 35/77] xterm.js references to the library should not be backticked --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d70e8b7e..5378f443 100644 --- a/README.md +++ b/README.md @@ -106,17 +106,17 @@ Note that some APIs are marked *experimental*, these are added to enable experim ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. -- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on `xterm.js`. -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on `xterm.js`. -- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on `xterm.js`. +- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js. +- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. -- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`. -- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. +- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js. +- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. -- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`. +- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. @@ -124,11 +124,11 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job. - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. -- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. +- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets. - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. -- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses `xterm.js` for container terminals and the host shell. +- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. - [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. - [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. - [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. @@ -144,16 +144,16 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. - [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. -- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on `xterm.js`. +- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. -- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to `xterm.js`. +- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. -- [**info-beamer hosted**](https://info-beamer.com): Uses `xterm.js` to manage digital signage devices from the web dashboard. -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use `xterm.js` for web terminal emulation. +- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From c63f15a9b26c770dcbcceca6dfaf33acb3121d67 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Mar 2019 10:00:04 -0800 Subject: [PATCH 36/77] Fix lint --- src/Terminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index cb9bc675..5de369fe 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -739,9 +739,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { - this.selectionManager.disable() + this.selectionManager.disable(); } else { - this.selectionManager.enable() + this.selectionManager.enable(); } if (this.options.screenReaderMode) { From 32e157bfaa43164171bac02798fc293df30d151c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Mar 2019 10:04:52 -0800 Subject: [PATCH 37/77] Remove unnecessary else --- src/Terminal.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 0539a904..25c92fdf 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -857,9 +857,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 2047) { data.push(2047); return; - } else { - data.push(ch); } + data.push(ch); } } From 07430a4892365e65946c05b34d51ba668fbc2b9f Mon Sep 17 00:00:00 2001 From: Jesse Stolwijk Date: Tue, 5 Mar 2019 00:00:54 +0100 Subject: [PATCH 38/77] Replace array shift with offset (#1955) --- src/Terminal.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4d480eb6..19b5f125 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1350,19 +1350,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - protected _innerWrite(): void { + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { this.writeBuffer = []; } const startTime = Date.now(); - while (this.writeBuffer.length > 0) { - const data = this.writeBuffer.shift(); + while (this.writeBuffer.length > bufferOffset) { + const data = this.writeBuffer[bufferOffset]; + bufferOffset++; // If XOFF was sent in order to catch up with the pty process, resume it if - // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBuffer.length === 0) { + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1385,11 +1386,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; } } - if (this.writeBuffer.length > 0) { + if (this.writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; + this.writeBuffer = []; } } From 51e2cdfafb697a8076d9122fcf0e7bbc8f1840e8 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Wed, 6 Mar 2019 21:45:53 +0000 Subject: [PATCH 39/77] WIP: First draft --- src/addons/webLinks/webLinks.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index f0d69cc5..b9f2925a 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -36,6 +36,13 @@ function handleLink(event: MouseEvent, uri: string): void { */ export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; + + handler = (event, uri) => { + if (!term.hasSelection()) { + window.open(uri, '_blank'); + } + }; + term.registerLinkMatcher(strictUrlRegex, handler, options); } From bc41cc7d279e7edd2b7f50b8032252efc4d88f31 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 00:15:03 +0000 Subject: [PATCH 40/77] Fix #1908 --- src/ui/MouseZoneManager.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index a232f5b9..79022723 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -23,6 +23,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; @@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); this._clickListener = e => this._onClick(e); } @@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (!this._areZonesActive) { this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.addEventListener('click', this._clickListener); } } @@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (this._areZonesActive) { this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.removeEventListener('click', this._clickListener); } } @@ -169,6 +173,18 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } } + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + private _onClick(e: MouseEvent): void { // Find the active zone and click it if found const zone = this._findZoneEventAt(e); From 5878aa099dcf093a9480bf110cde28f8ee011b69 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 21:00:07 +0000 Subject: [PATCH 41/77] Cleaner aproach --- src/addons/webLinks/webLinks.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index b9f2925a..19200c90 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -24,9 +24,7 @@ const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); -function handleLink(event: MouseEvent, uri: string): void { - window.open(uri, '_blank'); -} +let handleLink: (event: MouseEvent, uri: string) => void; /** * Initialize the web links addon, registering the link matcher. @@ -37,17 +35,17 @@ function handleLink(event: MouseEvent, uri: string): void { export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; - handler = (event, uri) => { - if (!term.hasSelection()) { - window.open(uri, '_blank'); - } - }; - term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { + handleLink = (event, uri) => { + if (!this.hasSelection()) { + window.open(uri, '_blank'); + } + }; + webLinksInit(this, handler, options); }; } From 525af9c36a3b57702d6048737d37aeb394464892 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 21:30:32 +0000 Subject: [PATCH 42/77] Fix #1773 --- src/renderer/dom/DomRenderer.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 11 +++++++++-- src/renderer/dom/DomRendererRowFactory.ts | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..5bff3d1c 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(document); + this._rowFactory = new DomRendererRowFactory(_terminal, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 67342da0..ab308077 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,17 +9,24 @@ import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; import { BufferLine } from '../../BufferLine'; -import { IBufferLine } from '../../Types'; +import { IBufferLine, ITerminal } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; +import { MockTerminal } from '../../ui/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; + let term: ITerminal; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document); + + term = new MockTerminal(); + term.options.enableBold = true; + term.options.drawBoldTextInBrightColors = true; + + rowFactory = new DomRendererRowFactory(term, dom.window.document); lineData = createEmptyLineData(2); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..f1865175 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { IBufferLine } from '../../Types'; +import { IBufferLine, ITerminal } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -17,6 +17,7 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { constructor( + private _terminal: ITerminal, private _document: Document ) { } @@ -88,10 +89,10 @@ export class DomRendererRowFactory { } } - if (flags & FLAGS.BOLD) { + if (flags & FLAGS.BOLD && this._terminal.options.enableBold) { // Convert the FG color to the bold variant. This should not happen when // the fg is the inverse default color as there is no bold variant. - if (fg < 8) { + if (fg < 8 && this._terminal.options.drawBoldTextInBrightColors) { fg += 8; } charElement.classList.add(BOLD_CLASS); From 4046d682c9770276240746cd5d46647ddfb2e10d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Mar 2019 09:21:01 -0800 Subject: [PATCH 43/77] v3.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa9cc070..c5fad515 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.11.0", + "version": "3.12.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", From b4bef0986cb99f2b43e4cb8ab372ec839bdb0422 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Sun, 10 Mar 2019 22:03:38 +0000 Subject: [PATCH 44/77] Remove circular dependency --- src/renderer/dom/DomRenderer.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 12 +++++------- src/renderer/dom/DomRendererRowFactory.ts | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 5bff3d1c..3a5f29e6 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(_terminal, document); + this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index ab308077..76c07781 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,24 +9,22 @@ import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; import { BufferLine } from '../../BufferLine'; -import { IBufferLine, ITerminal } from '../../Types'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; -import { MockTerminal } from '../../ui/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; - let term: ITerminal; + const options: ITerminalOptions = {}; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - term = new MockTerminal(); - term.options.enableBold = true; - term.options.drawBoldTextInBrightColors = true; + options.enableBold = true; + options.drawBoldTextInBrightColors = true; - rowFactory = new DomRendererRowFactory(term, dom.window.document); + rowFactory = new DomRendererRowFactory(options, dom.window.document); lineData = createEmptyLineData(2); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index f1865175..206c0c81 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { IBufferLine, ITerminal } from '../../Types'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -17,7 +17,7 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { constructor( - private _terminal: ITerminal, + private _terminalOptions: ITerminalOptions, private _document: Document ) { } @@ -89,10 +89,10 @@ export class DomRendererRowFactory { } } - if (flags & FLAGS.BOLD && this._terminal.options.enableBold) { + if (flags & FLAGS.BOLD && this._terminalOptions.enableBold) { // Convert the FG color to the bold variant. This should not happen when // the fg is the inverse default color as there is no bold variant. - if (fg < 8 && this._terminal.options.drawBoldTextInBrightColors) { + if (fg < 8 && this._terminalOptions.drawBoldTextInBrightColors) { fg += 8; } charElement.classList.add(BOLD_CLASS); From 19d36f92286267e43fe94ee010e3743c421810a3 Mon Sep 17 00:00:00 2001 From: turtle0x1 <12494629+turtle0x1@users.noreply.github.com> Date: Mon, 11 Mar 2019 13:47:25 +0000 Subject: [PATCH 45/77] Update readme with link for lxdmosaic --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5378f443..481590a7 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. - [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. +- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 62cfa642cbe5b2bce1d8cc0d71c2a8cdc1198bdb Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 11 Mar 2019 20:54:17 +0000 Subject: [PATCH 46/77] Better aproach, check i a selection is being performed --- src/addons/webLinks/webLinks.ts | 11 +++-------- src/ui/MouseZoneManager.ts | 12 ++++++++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 19200c90..f0d69cc5 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -24,7 +24,9 @@ const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); -let handleLink: (event: MouseEvent, uri: string) => void; +function handleLink(event: MouseEvent, uri: string): void { + window.open(uri, '_blank'); +} /** * Initialize the web links addon, registering the link matcher. @@ -34,18 +36,11 @@ let handleLink: (event: MouseEvent, uri: string) => void; */ export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; - term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { - handleLink = (event, uri) => { - if (!this.hasSelection()) { - window.open(uri, '_blank'); - } - }; - webLinksInit(this, handler, options); }; } diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 79022723..3b848795 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -29,6 +29,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; + private _initialSelectionLenght: number; constructor( private _terminal: ITerminal @@ -157,6 +158,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onMouseDown(e: MouseEvent): void { + // Store current terminal selection length, to check if we're performing + // a selection operation + this._initialSelectionLenght = this._terminal.getSelection().length; + // Ignore the event if there are no zones active if (!this._areZonesActive) { return; @@ -186,9 +191,12 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onClick(e: MouseEvent): void { - // Find the active zone and click it if found + // Find the active zone and click it if found and no selection was + // being performed const zone = this._findZoneEventAt(e); - if (zone) { + const currentSelectionLength = this._terminal.getSelection().length; + + if (zone && currentSelectionLength === this._initialSelectionLenght) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); From 27ebe8c353cb8988f71e665ce4765e9c5f02c597 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 11 Mar 2019 20:58:42 +0000 Subject: [PATCH 47/77] Use new vscode serverReadyAction --- .vscode/launch.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c7bf7381..2ec26fca 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,7 +38,11 @@ "run", "start-debug" ], - "port": 9229 + "port": 9229, + "serverReadyAction": { + "action": "openExternally", + "pattern": "App listening to (http://.*?:[0-9]+)" + } } ] } From 0d63cecc6bed2561cae1ca99e6c64d3f35a3cea6 Mon Sep 17 00:00:00 2001 From: Jianhui Zhao Date: Tue, 19 Mar 2019 14:04:16 +0800 Subject: [PATCH 48/77] Modify uses Signed-off-by: Jianhui Zhao --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 481590a7..40de04cf 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters. - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. -- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. +- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web. - [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. From 131e4f78bdc1e036feed1a0aee03cddee25f6e63 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 23 Mar 2019 09:08:55 -0700 Subject: [PATCH 49/77] Fix renderer pausing to not full refresh every time Fixes #1975 --- src/renderer/Renderer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b8ef87aa..2c1b516a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -85,6 +85,7 @@ export class Renderer extends EventEmitter implements IRenderer { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { this._terminal.refresh(0, this._terminal.rows - 1); + this._needsFullRefresh = false; } } From 4dbe8ec1db2d6005186b547cd2fb87f710aeb48c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 23 Mar 2019 10:58:26 -0700 Subject: [PATCH 50/77] Let consumers decide whether winptyCompat should be active --- demo/client.ts | 5 ++++- src/addons/winptyCompat/winptyCompat.ts | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index a3a912f6..70996c4a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -30,7 +30,10 @@ Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); -Terminal.applyAddon(winptyCompat); +const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; +if (isWindows) { + Terminal.applyAddon(winptyCompat); +} let term; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index d162f4e9..58f59fd9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -13,12 +13,6 @@ const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; - // Don't do anything when the platform is not Windows - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - if (!isWindows) { - return; - } - (addonTerminal._core as any).isWinptyCompatEnabled = true; // Winpty does not support wraparound mode which means that lines will never From f9df863ee05244f34536ae9c64606d38abd04a61 Mon Sep 17 00:00:00 2001 From: Jesse Stolwijk Date: Mon, 25 Mar 2019 22:17:16 +0100 Subject: [PATCH 51/77] Add blinking cursor to DomRenderer --- src/renderer/dom/DomRenderer.ts | 15 +++++++-- .../dom/DomRendererRowFactory.test.ts | 31 ++++++++++++------- src/renderer/dom/DomRendererRowFactory.ts | 7 ++++- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..1da20d95 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { ITheme } from 'xterm'; import { EventEmitter } from '../../common/EventEmitter'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; -import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; +import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -165,12 +165,22 @@ export class DomRenderer extends EventEmitter implements IRenderer { `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + `}`; + // Blink animation + styles += + `@keyframes blink {` + + ` 0 % { opacity: 1.0; }` + + ` 50% { opacity: 0.0; }` + + ` 100 % { opacity: 1.0; }` + + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + ` outline: 1px solid ${this.colorManager.colors.cursor.css};` + ` outline-offset: -1px;` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` + + ` animation: blink 1s step-end infinite;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this.colorManager.colors.cursor.css};` + ` color: ${this.colorManager.colors.cursorAccent.css};` + @@ -328,6 +338,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; const cursorX = this._terminal.buffer.x; + const cursorBlink = this._terminal.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; @@ -336,7 +347,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const row = y + terminal.buffer.ydisp; const lineData = terminal.buffer.lines.get(row); const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); } this._terminal.emit('refresh', {start, end}); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 67342da0..07e7686d 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -25,7 +25,7 @@ describe('DomRendererRowFactory', () => { describe('createRow', () => { it('should not create anything for an empty row', () => { - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -35,7 +35,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]); // There should be no element for the following "empty" cell lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -43,17 +43,24 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); } }); + it('should add class for cursor blink', () => { + const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20); + assert.equal(getFragmentHtml(fragment), + ` ` + ); + }); + it('should not render cells that go beyond the terminal\'s columns', () => { lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]); lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -62,7 +69,7 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -70,7 +77,7 @@ describe('DomRendererRowFactory', () => { it('should add class for italic', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -80,7 +87,7 @@ describe('DomRendererRowFactory', () => { const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0); for (let i = 0; i < 256; i++) { lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -91,7 +98,7 @@ describe('DomRendererRowFactory', () => { const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0); for (let i = 0; i < 256; i++) { lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -100,7 +107,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert colors', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -108,7 +115,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert default fg color', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -116,7 +123,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert default bg color', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -125,7 +132,7 @@ describe('DomRendererRowFactory', () => { it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..fd263b21 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -11,6 +11,7 @@ import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; export const CURSOR_CLASS = 'xterm-cursor'; +export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; @@ -21,7 +22,7 @@ export class DomRendererRowFactory { ) { } - public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); // Find the line length first, this prevents the need to output a bunch of @@ -62,6 +63,10 @@ export class DomRendererRowFactory { if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); + if (cursorBlink) { + charElement.classList.add(CURSOR_BLINK_CLASS); + } + switch (cursorStyle) { case 'bar': charElement.classList.add(CURSOR_STYLE_BAR_CLASS); From e346f8bba37ee094f4710cd1c8dc43438fa01a79 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 29 Mar 2019 19:46:24 -0700 Subject: [PATCH 52/77] Prevent scroll on focus Fixes #1981 --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4a85758f..c2497df4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -343,7 +343,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public focus(): void { if (this.textarea) { - this.textarea.focus(); + this.textarea.focus({ preventScroll: true }); } } From 0b78011fa34e2f79dd3f2cd68c111a171f45c68c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:05:24 -0700 Subject: [PATCH 53/77] Adopt project references Recent versions of TypeScript has improved the performance of project references so they are now viable to switch over to. --- demo/index.html | 4 ++-- demo/server.js | 1 + gulpfile.js | 10 +++++----- package.json | 10 +++------- src/common/tsconfig.json | 14 ++------------ src/core/tsconfig.json | 20 ++++++-------------- src/tsconfig-base.json | 15 +++++++++++++++ src/tsconfig-library-base.json | 11 +++++++++++ src/tsconfig.all.json | 16 ++++++++++++++++ tsconfig.json => src/tsconfig.json | 21 ++++++++++++--------- yarn.lock | 8 ++++---- 11 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 src/tsconfig-base.json create mode 100644 src/tsconfig-library-base.json create mode 100644 src/tsconfig.all.json rename tsconfig.json => src/tsconfig.json (53%) diff --git a/demo/index.html b/demo/index.html index 12f7cb98..370a51ed 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,8 +2,8 @@ xterm.js demo - - + + diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..37df9916 100644 --- a/demo/server.js +++ b/demo/server.js @@ -11,6 +11,7 @@ function startServer() { logs = {}; app.use('/build', express.static(__dirname + '/../build')); + app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); diff --git a/gulpfile.js b/gulpfile.js index 9af8d6e4..bbb4d6e1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,9 +16,9 @@ const ts = require('gulp-typescript'); const util = require('gulp-util'); const buildDir = process.env.BUILD_DIR || 'build'; -const tsProject = ts.createProject('tsconfig.json'); -let srcDir = tsProject.config.compilerOptions.rootDir; -let outDir = tsProject.config.compilerOptions.outDir; +const tsProject = ts.createProject('src/tsconfig.json'); +let srcDir = './src'; +let outDir = './lib'; const addons = fs.readdirSync(`${__dirname}/src/addons`); @@ -61,7 +61,7 @@ gulp.task('browserify', function() { }; let bundleStream = browserify(browserifyOptions) .bundle() - .pipe(source('xterm.js')) + .pipe(source(`xterm.js`)) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) .pipe(sourcemaps.write('./')) @@ -136,6 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () { }) }); -gulp.task('build', ['sorcery', 'sorcery-addons']); +gulp.task('build', ['css', 'sorcery', 'sorcery-addons']); gulp.task('test', ['mocha']); gulp.task('default', ['build']); diff --git a/package.json b/package.json index c5fad515..946db609 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.1", + "typescript": "3.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", @@ -50,20 +50,16 @@ "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", - "pretest": "npm run layering", "test": "npm run mocha", "posttest": "npm run lint", "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "mocha": "gulp test", - "tsc": "tsc", - "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", + "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", - "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", - "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" + "watch": "tsc -b -w ./src/tsconfig.all.json" } } diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 19dd0273..b40bb2f5 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,17 +1,7 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ "./**/*" diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 4f024a28..41e41f0c 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,20 +1,12 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ - "./**/*", - "../common/**/*" + "./**/*" + ], + "references": [ + { "path": "../common" } ] } diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json new file mode 100644 index 00000000..5c6afcc5 --- /dev/null +++ b/src/tsconfig-base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ "es5" ], + "rootDir": ".", + + "sourceMap": true, + "removeComments": true, + "pretty": true, + + "incremental": true, + + "skipLibCheck": true + } +} diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json new file mode 100644 index 00000000..c82e0873 --- /dev/null +++ b/src/tsconfig-library-base.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "types": [ + "../../node_modules/@types/mocha", + "../../" + ], + "composite": true, + "strict": true + } +} diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json new file mode 100644 index 00000000..bee5df32 --- /dev/null +++ b/src/tsconfig.all.json @@ -0,0 +1,16 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "." }, + { "path": "./addons/attach" }, + { "path": "./addons/fit" }, + { "path": "./addons/fullscreen" }, + { "path": "./addons/search" }, + { "path": "./addons/terminado" }, + { "path": "./addons/webLinks" }, + { "path": "./addons/winptyCompat" }, + { "path": "./addons/zmodem" } + ] +} + \ No newline at end of file diff --git a/tsconfig.json b/src/tsconfig.json similarity index 53% rename from tsconfig.json rename to src/tsconfig.json index 2d1d6e35..0aa3abb8 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -1,7 +1,7 @@ { + "extends": "./tsconfig-base", "compilerOptions": { "module": "commonjs", - "target": "es5", "lib": [ "dom", "es5", @@ -9,19 +9,22 @@ "scripthost", "es2015.promise" ], - "rootDir": "src", - "outDir": "lib", - "sourceMap": true, - "removeComments": true, - "preserveWatchOutput": true, + "rootDir": ".", + "outDir": "../lib", + "noUnusedLocals": true, "noImplicitAny": true }, "include": [ - "src/**/*", - "typings/xterm.d.ts" + "./**/*", + "../typings/xterm.d.ts" ], "exclude": [ - "src/addons/**/*" + "./addons/**/*" + ], + "references": [ + { "path": "./common" }, + { "path": "./core" } ] } + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5555321d..896db4bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6633,10 +6633,10 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@3.4: + version "3.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" + integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== uglify-es@^3.3.4: version "3.3.9" From b400f9ca3b74fbcb0c7506ec6b4456d835f1fc5a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:08:24 -0700 Subject: [PATCH 54/77] Don't clear terminal on yarn watch --- package.json | 2 +- src/addons/attach/tsconfig.json | 3 +-- src/addons/fit/tsconfig.json | 1 - src/addons/fullscreen/tsconfig.json | 1 - src/addons/search/tsconfig.json | 1 - src/addons/terminado/tsconfig.json | 1 - src/addons/webLinks/tsconfig.json | 1 - src/addons/winptyCompat/tsconfig.json | 1 - src/addons/zmodem/tsconfig.json | 1 - 9 files changed, 2 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 946db609..d917039b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,6 @@ "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "tsc -b -w ./src/tsconfig.all.json" + "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" } } diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index 359fbd24..2f39102c 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -10,8 +10,7 @@ "outDir": "../../../lib/addons/attach/", "sourceMap": true, "removeComments": true, - "declaration": true, - "preserveWatchOutput": true + "declaration": true }, "include": [ "**/*.ts", diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 489ccdfe..3458d23a 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 05e6df68..0c74c25c 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 87899cda..6a1611a5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 91c18314..e2e19445 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 18105aa2..9c4f1176 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index 9fc4d25e..fa48c963 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 2b49f537..7d821b7c 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] From 18f4dc6b3eda8432231cc580fa4221c13e1113ec Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:09:51 -0700 Subject: [PATCH 55/77] Remove concurrently --- package.json | 1 - yarn.lock | 87 +++------------------------------------------------- 2 files changed, 4 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index d917039b..539b96da 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", - "concurrently": "^3.5.1", "coveralls": "^3.0.1", "express": "4.13.4", "express-ws": "2.0.0-rc.1", diff --git a/yarn.lock b/yarn.lock index 896db4bf..440aa4f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1275,11 +1275,6 @@ combined-stream@1.0.6, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" - integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= - commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -1338,21 +1333,6 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" -concurrently@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.6.0.tgz#c25e34b156a9d5bd4f256a0d85f6192438ae481f" - integrity sha512-6XiIYtYzmGEccNZFkih5JOH92jLA4ulZArAYy5j1uDSdrPLB3KzdE8GW7t2fHPcg9ry2+5LP9IEYzXzxw9lFdA== - dependencies: - chalk "^2.4.1" - commander "2.6.0" - date-fns "^1.23.0" - lodash "^4.5.1" - read-pkg "^3.0.0" - rx "2.3.24" - spawn-command "^0.0.2-1" - supports-color "^3.2.3" - tree-kill "^1.1.0" - configstore@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" @@ -1595,11 +1575,6 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" -date-fns@^1.23.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" - integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1933,7 +1908,7 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3625,7 +3600,7 @@ jsesc@^1.3.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -3815,16 +3790,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4037,7 +4002,7 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.5.1: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== @@ -4984,14 +4949,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -5092,13 +5049,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5384,15 +5334,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -5688,11 +5629,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx@2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" - integrity sha1-FPlQpCF9fjXapxu8vljv9o6ksrc= - rxjs@^6.1.0: version "6.3.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.1.tgz#878a1a8c64b8a5da11dcf74b5033fe944cdafb84" @@ -6025,11 +5961,6 @@ sparkles@^1.0.0: resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= - spawn-wrap@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" @@ -6271,11 +6202,6 @@ strip-bom@^1.0.0: first-chunk-stream "^1.0.0" is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -6305,7 +6231,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -6521,11 +6447,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tree-kill@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" - integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" From f80924fb8d5155142a3bd258f64a71176680f7de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:12:15 -0700 Subject: [PATCH 56/77] Remove zmodem demo --- demo/server.js | 1 - demo/zmodem/app.js | 87 --------- demo/zmodem/index.html | 128 -------------- demo/zmodem/main.js | 388 ----------------------------------------- 4 files changed, 604 deletions(-) delete mode 100644 demo/zmodem/app.js delete mode 100644 demo/zmodem/index.html delete mode 100644 demo/zmodem/main.js diff --git a/demo/server.js b/demo/server.js index 37df9916..c41110ff 100644 --- a/demo/server.js +++ b/demo/server.js @@ -10,7 +10,6 @@ function startServer() { var terminals = {}, logs = {}; - app.use('/build', express.static(__dirname + '/../build')); app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ diff --git a/demo/zmodem/app.js b/demo/zmodem/app.js deleted file mode 100644 index 7124c222..00000000 --- a/demo/zmodem/app.js +++ /dev/null @@ -1,87 +0,0 @@ -var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); -var os = require('os'); -var pty = require('node-pty'); - -var terminals = {}, - logs = {}; - -app.use('/build', express.static(__dirname + '/../../build')); -app.use('/demo', express.static(__dirname + '/../../demo')); -app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist')); - -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); - -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '../style.css'); -}); - -app.get('/main.js', function(req, res){ - res.sendFile(__dirname + '/main.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - encoding: null, - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); - res.send(term.pid.toString()); - res.end(); -}); - -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; - - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); - -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); - - term.on('data', function(data) { - try { - ws.send(data); - } catch (ex) { - // The WebSocket is not open, ignore - } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); - -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); diff --git a/demo/zmodem/index.html b/demo/zmodem/index.html deleted file mode 100644 index aee7742a..00000000 --- a/demo/zmodem/index.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - xterm.js demo - - - - - - - - - - - - - - - - -

xterm.js: xterm, in the browser

- -
- -
- - - - - - - - - -
- -
-

Actions

-

- - -

-
-
-

Options

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-
-

Size

-
-
- - -
-
- - -
-
-
-
-

Attention: The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.

-

* ZMODEM file transfers are supported via an addon. To try it out, install lrzsz onto the remote peer, then run rz to send from your browser or sz <file> to send from the remote peer.

- - - diff --git a/demo/zmodem/main.js b/demo/zmodem/main.js deleted file mode 100644 index 619ef2b8..00000000 --- a/demo/zmodem/main.js +++ /dev/null @@ -1,388 +0,0 @@ -"use strict"; - -var term, - protocol, - socketURL, - socket, - pid; - -Terminal.applyAddon(fit); -Terminal.applyAddon(attach); -Terminal.applyAddon(zmodem); -Terminal.applyAddon(search); - -var terminalContainer = document.getElementById('terminal-container'), - actionElements = { - findNext: document.querySelector('#find-next'), - findPrevious: document.querySelector('#find-previous') - }, - optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink'), - cursorStyle: document.querySelector('#option-cursor-style'), - scrollback: document.querySelector('#option-scrollback'), - tabstopwidth: document.querySelector('#option-tabstopwidth'), - bellStyle: document.querySelector('#option-bell-style') - }, - colsElement = document.getElementById('cols'), - rowsElement = document.getElementById('rows'); - -function setTerminalSize() { - var cols = parseInt(colsElement.value, 10); - var rows = parseInt(rowsElement.value, 10); - var viewportElement = document.querySelector('.xterm-viewport'); - var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; - var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px'; - var height = (rows * term.charMeasure.height).toString() + 'px'; - - terminalContainer.style.width = width; - terminalContainer.style.height = height; - term.resize(cols, rows); -} - -colsElement.addEventListener('change', setTerminalSize); -rowsElement.addEventListener('change', setTerminalSize); - -actionElements.findNext.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findNext(actionElements.findNext.value); - } -}); -actionElements.findPrevious.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findPrevious(actionElements.findPrevious.value); - } -}); - -optionElements.cursorBlink.addEventListener('change', function () { - term.setOption('cursorBlink', optionElements.cursorBlink.checked); -}); -optionElements.cursorStyle.addEventListener('change', function () { - term.setOption('cursorStyle', optionElements.cursorStyle.value); -}); -optionElements.bellStyle.addEventListener('change', function () { - term.setOption('bellStyle', optionElements.bellStyle.value); -}); -optionElements.scrollback.addEventListener('change', function () { - term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); -}); -optionElements.tabstopwidth.addEventListener('change', function () { - term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); -}); - -createTerminal(); - -function createTerminal() { - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - term = new Terminal({ - cursorBlink: optionElements.cursorBlink.checked, - scrollback: parseInt(optionElements.scrollback.value, 10), - tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) - }); - term.on('resize', function (size) { - if (!pid) { - return; - } - var cols = size.cols, - rows = size.rows, - url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - - fetch(url, {method: 'POST'}); - }); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - - term.open(terminalContainer); - term.fit(); - - // fit is called within a setTimeout, cols and rows need this. - setTimeout(function () { - colsElement.value = term.cols; - rowsElement.value = term.rows; - - // Set terminal size again to set the specific dimensions on the demo - setTerminalSize(); - - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { - - res.text().then(function (pid) { - window.pid = pid; - socketURL += pid; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - - term.zmodemAttach(socket, { - noTerminalWriteOutsideSession: true, - } ); - - term.on("zmodemRetract", () => { - start_form.style.display = "none"; - start_form.onsubmit = null; - }); - - term.on("zmodemDetect", (detection) => { - function do_zmodem() { - term.detach(); - let zsession = detection.confirm(); - - var promise; - - if (zsession.type === "receive") { - promise = _handle_receive_session(zsession); - } - else { - promise = _handle_send_session(zsession); - } - - promise.catch( console.error.bind(console) ).then( () => { - term.attach(socket); - } ); - } - - if (_auto_zmodem()) { - do_zmodem(); - } - else { - start_form.style.display = ""; - start_form.onsubmit = function(e) { - start_form.style.display = "none"; - - if (document.getElementById("zmstart_yes").checked) { - do_zmodem(); - } - else { - detection.deny(); - } - }; - } - }); - }); - }); - }, 0); -} - -//---------------------------------------------------------------------- -// UI STUFF - -function _show_file_info(xfer) { - var file_info = xfer.get_details(); - - document.getElementById("name").textContent = file_info.name; - document.getElementById("size").textContent = file_info.size; - document.getElementById("mtime").textContent = file_info.mtime; - document.getElementById("files_remaining").textContent = file_info.files_remaining; - document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining; - - document.getElementById("mode").textContent = "0" + file_info.mode.toString(8); - - var xfer_opts = xfer.get_options(); - ["conversion", "management", "transport", "sparse"].forEach( (lbl) => { - document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl]; - } ); - - document.getElementById("zm_file").style.display = ""; -} -function _hide_file_info() { - document.getElementById("zm_file").style.display = "none"; -} - -function _save_to_disk(xfer, buffer) { - return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name); -} - -var skipper_button = document.getElementById("zm_progress_skipper"); -var skipper_button_orig_text = skipper_button.textContent; - -function _show_progress() { - skipper_button.disabled = false; - skipper_button.textContent = skipper_button_orig_text; - - document.getElementById("bytes_received").textContent = 0; - document.getElementById("percent_received").textContent = 0; - - document.getElementById("zm_progress").style.display = ""; -} - -function _update_progress(xfer) { - var total_in = xfer.get_offset(); - - document.getElementById("bytes_received").textContent = total_in; - - var percent_received = 100 * total_in / xfer.get_details().size; - document.getElementById("percent_received").textContent = percent_received.toFixed(2); -} - -function _hide_progress() { - document.getElementById("zm_progress").style.display = "none"; -} - -var start_form = document.getElementById("zm_start"); - -function _auto_zmodem() { - return document.getElementById("zmodem-auto").checked; -} - -// END UI STUFF -//---------------------------------------------------------------------- - -function _handle_receive_session(zsession) { - zsession.on("offer", function(xfer) { - current_receive_xfer = xfer; - - _show_file_info(xfer); - - var offer_form = document.getElementById("zm_offer"); - - function on_form_submit() { - offer_form.style.display = "none"; - - //START - //if (offer_form.zmaccept.value) { - if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) { - _show_progress(); - - var FILE_BUFFER = []; - xfer.on("input", (payload) => { - _update_progress(xfer); - FILE_BUFFER.push( new Uint8Array(payload) ); - }); - xfer.accept().then( - () => { - _save_to_disk(xfer, FILE_BUFFER); - }, - console.error.bind(console) - ); - } - else { - xfer.skip(); - } - //END - } - - if (_auto_zmodem()) { - on_form_submit(); - } - else { - offer_form.onsubmit = on_form_submit; - offer_form.style.display = ""; - } - } ); - - var promise = new Promise( (res) => { - zsession.on("session_end", () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - } ); - - zsession.start(); - - return promise; -} - -function _handle_send_session(zsession) { - var choose_form = document.getElementById("zm_choose"); - choose_form.style.display = ""; - - var file_el = document.getElementById("zm_files"); - - var promise = new Promise( (res) => { - file_el.onchange = function(e) { - choose_form.style.display = "none"; - - var files_obj = file_el.files; - - Zmodem.Browser.send_files( - zsession, - files_obj, - { - on_offer_response(obj, xfer) { - if (xfer) _show_progress(); - //console.log("offer", xfer ? "accepted" : "skipped"); - }, - on_progress(obj, xfer) { - _update_progress(xfer); - }, - on_file_complete(obj) { - //console.log("COMPLETE", obj); - _hide_progress(); - }, - } - ).then(_hide_progress).then( - zsession.close.bind(zsession), - console.error.bind(console) - ).then( () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - }; - } ); - - return promise; -} - -//This is here to allow canceling of an in-progress ZMODEM transfer. -var current_receive_xfer; - -//Called from HTML directly. -function skip_current_file() { - current_receive_xfer.skip(); - - skipper_button.disabled = true; - skipper_button.textContent = "Waiting for server to acknowledge skip …"; -} - -function runRealTerminal() { - term.attach(socket); - - term._initialized = true; -} - -function runFakeTerminal() { - if (term._initialized) { - return; - } - - term._initialized = true; - - var shellprompt = '$ '; - - term.prompt = function () { - term.write('\r\n' + shellprompt); - }; - - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); - - term.on('key', function (key, ev) { - var printable = ( - !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey - ); - - if (ev.keyCode == 13) { - term.prompt(); - } else if (ev.keyCode == 8) { - // Do not delete the prompt - if (term.x > 2) { - term.write('\b \b'); - } - } else if (printable) { - term.write(key); - } - }); - - term.on('paste', function (data, ev) { - term.write(data); - }); -} From e182e3d4e43af884a61d0fadf0d5f34792a76cd5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:52:48 -0700 Subject: [PATCH 57/77] Fix demo on non-Windows Broke in #1978 --- demo/client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index 70996c4a..aaaf2829 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -102,7 +102,9 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.winptyCompatInit(); + if (isWindows) { + term.winptyCompatInit(); + } term.webLinksInit(); term.fit(); term.focus(); From bf9d879efa9897827fbeb9f58022cbc0960ad439 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:59:31 -0700 Subject: [PATCH 58/77] Fix typo --- src/ui/MouseZoneManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 3b848795..372dccc5 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -29,7 +29,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; - private _initialSelectionLenght: number; + private _initialSelectionLength: number; constructor( private _terminal: ITerminal @@ -160,7 +160,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _onMouseDown(e: MouseEvent): void { // Store current terminal selection length, to check if we're performing // a selection operation - this._initialSelectionLenght = this._terminal.getSelection().length; + this._initialSelectionLength = this._terminal.getSelection().length; // Ignore the event if there are no zones active if (!this._areZonesActive) { @@ -196,7 +196,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { const zone = this._findZoneEventAt(e); const currentSelectionLength = this._terminal.getSelection().length; - if (zone && currentSelectionLength === this._initialSelectionLenght) { + if (zone && currentSelectionLength === this._initialSelectionLength) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); From 4d008c66f11ed06835e6b2270305397033c81ebe Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 15:06:25 -0700 Subject: [PATCH 59/77] Recommend 127.0.0.1:3000 on mac and linux too Fixes #1986 --- .vscode/launch.json | 5 +---- demo/server.js | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ec26fca..e5bad7b3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,10 +23,7 @@ "type": "chrome", "request": "launch", "name": "Demo Client", - "url": "http://0.0.0.0:3000", - "windows": { - "url": "http://127.0.0.1:3000" - }, + "url": "http://127.0.0.1:3000", "webRoot": "${workspaceFolder}/" }, { diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..0587977c 100644 --- a/demo/server.js +++ b/demo/server.js @@ -99,7 +99,7 @@ function startServer() { var port = process.env.PORT || 3000, host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - console.log('App listening to http://' + host + ':' + port); + console.log('App listening to http://127.0.0.1:' + port); app.listen(port, host); } From 4a3609f8051f19bf3f53c65cfdfcead9116e2f2e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 31 Mar 2019 23:21:11 -0700 Subject: [PATCH 60/77] Move platform to common and remove dom dependence in common Because common/ imports 'xterm', it also imported dom accidentally. Fixes #1990 --- src/AccessibilityManager.ts | 2 +- src/SelectionManager.ts | 2 +- src/Terminal.ts | 2 +- src/common/EventEmitter.ts | 3 +-- src/common/Lifecycle.ts | 2 +- src/{core => common}/Platform.ts | 7 +++++++ src/common/Types.ts | 11 ++++++++++- src/common/tsconfig.json | 5 ++++- src/core/tsconfig.json | 2 +- src/renderer/atlas/CharAtlasGenerator.ts | 2 +- src/renderer/atlas/DynamicCharAtlas.ts | 2 +- src/tsconfig-base.json | 4 +--- src/tsconfig-library-base.json | 4 ---- src/tsconfig.json | 3 +-- src/ui/TestUtils.test.ts | 2 +- 15 files changed, 32 insertions(+), 21 deletions(-) rename src/{core => common}/Platform.ts (90%) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index fa0121ad..877676c9 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -5,7 +5,7 @@ import * as Strings from './Strings'; import { ITerminal, IBuffer } from './Types'; -import { isMac } from './core/Platform'; +import { isMac } from './common/Platform'; import { RenderDebouncer } from './ui/RenderDebouncer'; import { addDisposableDomListener } from './ui/Lifecycle'; import { Disposable } from './common/Lifecycle'; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f93328fe..e4bf87ea 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -6,7 +6,7 @@ import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types'; import { XtermListener } from './common/Types'; import { MouseHelper } from './ui/MouseHelper'; -import * as Browser from './core/Platform'; +import * as Browser from './common/Platform'; import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; diff --git a/src/Terminal.ts b/src/Terminal.ts index c2497df4..ebdb8a89 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -36,7 +36,7 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './ui/CharMeasure'; -import * as Browser from './core/Platform'; +import * as Browser from './common/Platform'; import { addDisposableDomListener } from './ui/Lifecycle'; import * as Strings from './Strings'; import { MouseHelper } from './ui/MouseHelper'; diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 68eb60f7..74a794cd 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { XtermListener } from './Types'; -import { IEventEmitter, IDisposable } from 'xterm'; +import { IDisposable, IEventEmitter, XtermListener } from './Types'; import { Disposable } from './Lifecycle'; export class EventEmitter extends Disposable implements IEventEmitter, IDisposable { diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index 209a3e2a..5fac6e82 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable } from 'xterm'; +import { IDisposable } from './Types'; /** * A base class that can be extended to provide convenience methods for managing the lifecycle of an diff --git a/src/core/Platform.ts b/src/common/Platform.ts similarity index 90% rename from src/core/Platform.ts rename to src/common/Platform.ts index 42c20d9d..bb0ad54b 100644 --- a/src/core/Platform.ts +++ b/src/common/Platform.ts @@ -3,6 +3,13 @@ * @license MIT */ +interface INavigator { + userAgent: string; + language: string; + platform: string; +} +declare const navigator: INavigator; + const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; diff --git a/src/common/Types.ts b/src/common/Types.ts index 8a416bf1..8ad98d99 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -3,7 +3,16 @@ * @license MIT */ -import { IEventEmitter } from 'xterm'; +export interface IDisposable { + dispose(): void; +} + +export interface IEventEmitter { + on(type: string, listener: (...args: any[]) => void): void; + off(type: string, listener: (...args: any[]) => void): void; + emit(type: string, data?: any): void; + addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; +} export type XtermListener = (...args: any[]) => void; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index b40bb2f5..6d8d1a56 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../tsconfig-library-base", "compilerOptions": { - "outDir": "../../lib" + "outDir": "../../lib", + "types": [ + "../../node_modules/@types/mocha" + ] }, "include": [ "./**/*" diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 41e41f0c..1fcf9e47 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "../../lib" }, "include": [ - "./**/*" + "./**/*", "../common/Platform.ts" ], "references": [ { "path": "../common" } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index cadcce2e..38950766 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -4,7 +4,7 @@ */ import { FontWeight } from 'xterm'; -import { isFirefox, isSafari } from '../../core/Platform'; +import { isFirefox, isSafari } from '../../common/Platform'; import { IColor } from '../Types'; import { ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types'; diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index e311c369..cb03a48f 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -8,7 +8,7 @@ import BaseCharAtlas from './BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from '../ColorManager'; import { clearColor } from './CharAtlasGenerator'; import LRUMap from './LRUMap'; -import { isFirefox, isSafari } from '../../core/Platform'; +import { isFirefox, isSafari } from '../../common/Platform'; import { IColor } from '../Types'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json index 5c6afcc5..84d0c924 100644 --- a/src/tsconfig-base.json +++ b/src/tsconfig-base.json @@ -8,8 +8,6 @@ "removeComments": true, "pretty": true, - "incremental": true, - - "skipLibCheck": true + "incremental": true } } diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json index c82e0873..66b61f09 100644 --- a/src/tsconfig-library-base.json +++ b/src/tsconfig-library-base.json @@ -1,10 +1,6 @@ { "extends": "./tsconfig-base.json", "compilerOptions": { - "types": [ - "../../node_modules/@types/mocha", - "../../" - ], "composite": true, "strict": true } diff --git a/src/tsconfig.json b/src/tsconfig.json index 0aa3abb8..f0b1c749 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -11,7 +11,7 @@ ], "rootDir": ".", "outDir": "../lib", - + "noUnusedLocals": true, "noImplicitAny": true }, @@ -27,4 +27,3 @@ { "path": "./core" } ] } - \ No newline at end of file diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index e6e4aaa3..b418ee6c 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -7,7 +7,7 @@ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../rende import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types'; import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; -import * as Browser from '../core/Platform'; +import * as Browser from '../common/Platform'; import { ITheme, IDisposable, IMarker } from 'xterm'; import { Terminal } from '../Terminal'; From 5c8c680dac3717e927069699168272ebd23ce54e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 1 Apr 2019 00:01:41 -0700 Subject: [PATCH 61/77] Fix feedback --- src/Buffer.ts | 2 +- src/BufferLine.test.ts | 10 +-- src/BufferLine.ts | 101 +++++++++++----------- src/InputHandler.ts | 10 +-- src/Linkifier.ts | 2 +- src/SelectionManager.ts | 22 ++--- src/Terminal.ts | 2 +- src/Types.ts | 4 +- src/renderer/CharacterJoinerRegistry.ts | 12 +-- src/renderer/TextRenderLayer.ts | 14 +-- src/renderer/dom/DomRendererRowFactory.ts | 13 +-- 11 files changed, 98 insertions(+), 94 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 69a36e43..9cc1adba 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -29,7 +29,7 @@ export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; /** - * Whilespace cell. + * Whitespace cell. * This is meant as a replacement for empty cells when needed * during rendering lines to preserve correct aligment. */ diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 7dfcbd2c..29a783ae 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData, Content } from './BufferLine'; +import { BufferLine, CellData, ContentMasks } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -32,7 +32,7 @@ describe('CellData', () => { // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); @@ -40,7 +40,7 @@ describe('CellData', () => { // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); @@ -350,7 +350,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -363,7 +363,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 14518454..c4aa55be 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -34,9 +34,9 @@ const enum Cell { } /** - * Bitmasks and helper for accessing data in `content`. + * Bitmasks for accessing data in `content`. */ -export const enum Content { +export const enum ContentMasks { /** * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) * read: `codepoint = content & Content.codepointMask;` @@ -44,7 +44,7 @@ export const enum Content { * shortcut if precondition `codepoint <= 0x10FFFF` is met: * `content |= codepoint;` */ - CODEPOINT_MASK = 0x1FFFFF, + CODEPOINT = 0x1FFFFF, /** * bit 22 flag indication whether a cell contains combined content @@ -72,10 +72,11 @@ export const enum Content { * shortcut if precondition `0 <= width <= 3` is met: * `content |= width << Content.widthShift;` */ - WIDTH_MASK = 0xC00000, // 3 << 22 - WIDTH_SHIFT = 22 + WIDTH = 0xC00000 // 3 << 22 } +const WIDTH_MASK_SHIFT = 22; + /** * CellData - represents a single Cell in the terminal buffer. */ @@ -96,21 +97,21 @@ export class CellData implements ICellData { /** Whether cell contains a combined string. */ public isCombined(): number { - return this.content & Content.IS_COMBINED; + return this.content & ContentMasks.IS_COMBINED; } /** Width of the cell. */ public getWidth(): number { - return this.content >> Content.WIDTH_SHIFT; + return this.content >> WIDTH_MASK_SHIFT; } /** JS string of the content. */ public getChars(): string { - if (this.content & Content.IS_COMBINED) { + if (this.content & ContentMasks.IS_COMBINED) { return this.combinedData; } - if (this.content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + if (this.content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(this.content & ContentMasks.CODEPOINT); } return ''; } @@ -124,7 +125,7 @@ export class CellData implements ICellData { public getCode(): number { return (this.isCombined()) ? this.combinedData.charCodeAt(this.combinedData.length - 1) - : this.content & Content.CODEPOINT_MASK; + : this.content & ContentMasks.CODEPOINT; } /** Set data from CharData */ @@ -143,7 +144,7 @@ export class CellData implements ICellData { if (0xD800 <= code && code <= 0xDBFF) { const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { - this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } else { combined = true; } @@ -151,11 +152,11 @@ export class CellData implements ICellData { combined = true; } } else { - this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } if (combined) { this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } } @@ -203,14 +204,14 @@ export class BufferLine implements IBufferLine { */ public get(index: number): CharData { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; + const cp = content & ContentMasks.CODEPOINT; return [ this._data[index * CELL_SIZE + Cell.FG], - (content & Content.IS_COMBINED) + (content & ContentMasks.IS_COMBINED) ? this._combined[index] : (cp) ? stringFromCodePoint(cp) : '', - content >> Content.WIDTH_SHIFT, - (content & Content.IS_COMBINED) + content >> WIDTH_MASK_SHIFT, + (content & ContentMasks.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : cp ]; @@ -224,9 +225,9 @@ export class BufferLine implements IBufferLine { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = index | ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } else { - this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } } @@ -235,21 +236,21 @@ export class BufferLine implements IBufferLine { * use these when only one value is needed, otherwise use `loadCell` */ public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + return this._data[index * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT; } /** Test whether content has width. */ public hasWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.WIDTH; } /** Get FG cell component. */ - public getFG(index: number): number { + public getFg(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } /** Get BG cell component. */ - public getBG(index: number): number { + public getBg(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } @@ -259,7 +260,7 @@ export class BufferLine implements IBufferLine { * from real empty cells. * */ public hasContent(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT; } /** @@ -269,38 +270,40 @@ export class BufferLine implements IBufferLine { */ public getCodePoint(index: number): number { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { return this._combined[index].charCodeAt(this._combined[index].length - 1); } - return content & Content.CODEPOINT_MASK; + return content & ContentMasks.CODEPOINT; } /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.IS_COMBINED; } /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { return this._combined[index]; } - if (content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(content & Content.CODEPOINT_MASK); + if (content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(content & ContentMasks.CODEPOINT); } // return empty string for empty cells return ''; } /** - * Load data at `index` into `cell`. + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. */ public loadCell(index: number, cell: ICellData): ICellData { - cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; - cell.fg = this._data[index * CELL_SIZE + Cell.FG]; - cell.bg = this._data[index * CELL_SIZE + Cell.BG]; - if (cell.content & Content.IS_COMBINED) { + const startIndex = index * CELL_SIZE; + cell.content = this._data[startIndex + Cell.CONTENT]; + cell.fg = this._data[startIndex + Cell.FG]; + cell.bg = this._data[startIndex + Cell.BG]; + if (cell.content & ContentMasks.IS_COMBINED) { cell.combinedData = this._combined[index]; } return cell; @@ -310,7 +313,7 @@ export class BufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - if (cell.content & Content.IS_COMBINED) { + if (cell.content & ContentMasks.IS_COMBINED) { this._combined[index] = cell.combinedData; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; @@ -324,7 +327,7 @@ export class BufferLine implements IBufferLine { * it gets an optimized access method. */ public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { - this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << WIDTH_MASK_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } @@ -337,21 +340,21 @@ export class BufferLine implements IBufferLine { */ public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { // we already have a combined string, simply add this._combined[index] += stringFromCodePoint(codePoint); } else { - if (content & Content.CODEPOINT_MASK) { + if (content & ContentMasks.CODEPOINT) { // normal case for combining chars: // - move current leading char + new one into combined string // - set combined flag - this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 - content |= Content.IS_COMBINED; + this._combined[index] = stringFromCodePoint(content & ContentMasks.CODEPOINT) + stringFromCodePoint(codePoint); + content &= ~ContentMasks.CODEPOINT; // set codepoint in buffer to 0 + content |= ContentMasks.IS_COMBINED; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 - content = codePoint | (1 << Content.WIDTH_SHIFT); + content = codePoint | (1 << WIDTH_MASK_SHIFT); } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } @@ -473,8 +476,8 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT); } } return 0; @@ -513,9 +516,9 @@ export class BufferLine implements IBufferLine { let result = ''; while (startCol < endCol) { const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - result += (content & Content.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; - startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 + const cp = content & ContentMasks.CODEPOINT; + result += (content & ContentMasks.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> WIDTH_MASK_SHIFT) || 1; // always advance by 1 } return result; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 65f4aaaf..37c9bbe5 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -106,7 +106,7 @@ class DECRQSS implements IDcsHandler { export class InputHandler extends Disposable implements IInputHandler { private _parseBuffer: Uint32Array = new Uint32Array(4096); private _stringDecoder: StringToUtf32 = new StringToUtf32(); - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor( protected _terminal: IInputHandlingTerminal, @@ -351,7 +351,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - if (!bufferRow.loadCell(buffer.x - 1, this._cell).getWidth()) { + if (!bufferRow.getWidth(buffer.x - 1)) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars @@ -400,7 +400,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell - if (bufferRow.loadCell(cols - 1, this._cell).getWidth() === 2) { + if (bufferRow.getWidth(cols - 1) === 2) { bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } @@ -970,10 +970,10 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - line.loadCell(buffer.x - 1, this._cell); + line.loadCell(buffer.x - 1, this._workCell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR) + (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR) ); // FIXME: no updateRange here? } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a2b9045b..80399904 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -231,7 +231,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - const attr = line.getFG(bufferIndex[1]); + const attr = line.getFg(bufferIndex[1]); let fg: number | undefined; if (attr) { fg = (attr >> 9) & 0x1ff; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 9d49a860..361b1123 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -103,7 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; private _trimListener: XtermListener; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -669,8 +669,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const length = bufferLine.loadCell(i, this._cell).getChars().length; - if (this._cell.getWidth() === 0) { + const length = bufferLine.loadCell(i, this._workCell).getChars().length; + if (this._workCell.getWidth() === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; @@ -755,10 +755,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._cell))) { - bufferLine.loadCell(startCol - 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 0) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + bufferLine.loadCell(startCol - 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -771,10 +771,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._cell))) { - bufferLine.loadCell(endCol + 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 2) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + bufferLine.loadCell(endCol + 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; diff --git a/src/Terminal.ts b/src/Terminal.ts index f3ec6e31..db696bd6 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1181,7 +1181,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== this.eraseAttr()) { + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } diff --git a/src/Types.ts b/src/Types.ts index a08bd485..10665f25 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -560,8 +560,8 @@ export interface IBufferLine { /* direct access to cell attrs */ getWidth(index: number): number; hasWidth(index: number): number; - getFG(index: number): number; - getBG(index: number): number; + getFg(index: number): number; + getBg(index: number): number; hasContent(index: number): number; getCodePoint(index: number): number; isCombined(index: number): number; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index eb44bb58..4a899d72 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -6,7 +6,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor(private _terminal: ITerminal) { } @@ -52,13 +52,13 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.getFG(0) >> 9; + let rangeAttr = line.getFg(0) >> 9; for (let x = 0; x < this._terminal.cols; x++) { - line.loadCell(x, this._cell); - const chars = this._cell.getChars(); - const width = this._cell.getWidth(); - const attr = this._cell.fg >> 9; + line.loadCell(x, this._workCell); + const chars = this._workCell.getChars(); + const width = this._workCell.getWidth(); + const attr = this._workCell.fg >> 9; if (width === 0) { // If this character is of width 0, skip it. diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index be022239..f56ccf3a 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -25,7 +25,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterJoinerRegistry: ICharacterJoinerRegistry; - private _cell = new CellData(); + private _workCell = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) { super(container, 'text', zIndex, alpha, colors); @@ -74,14 +74,14 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - (line as any).loadCell(x, this._cell); - let code: number = this._cell.getCode() || WHITESPACE_CELL_CODE; + line.loadCell(x, this._workCell); + let code: number = this._workCell.getCode() || WHITESPACE_CELL_CODE; // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. - let chars = this._cell.getChars() || WHITESPACE_CELL_CHAR; - const attr = this._cell.fg; - let width = this._cell.getWidth(); + let chars = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + const attr = this._workCell.fg; + let width = this._workCell.getWidth(); // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -127,7 +127,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).getCode() === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._workCell).getCode() === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index dfd3a154..47232981 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -18,7 +18,8 @@ export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); + constructor( private _terminalOptions: ITerminalOptions, private _document: Document @@ -35,16 +36,16 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - if (lineData.loadCell(x, this._cell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; } } for (let x = 0; x < lineLength; x++) { - lineData.loadCell(x, this._cell); - const attr = this._cell.fg; - const width = this._cell.getWidth(); + lineData.loadCell(x, this._workCell); + const attr = this._workCell.fg; + const width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -106,7 +107,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = this._cell.getChars() || WHITESPACE_CELL_CHAR; + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); } From 2e10c8c25e9823dfa96a98bc12475f1b36bdf594 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 08:04:39 -0700 Subject: [PATCH 62/77] Clean up --- src/common/Platform.ts | 3 +++ src/common/tsconfig.json | 4 +--- src/core/tsconfig.json | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/common/Platform.ts b/src/common/Platform.ts index bb0ad54b..ee82cff4 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -8,6 +8,9 @@ interface INavigator { language: string; platform: string; } + +// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but +// we want this module to live in common. declare const navigator: INavigator; const isNode = (typeof navigator === 'undefined') ? true : false; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 6d8d1a56..ccf742e5 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -6,7 +6,5 @@ "../../node_modules/@types/mocha" ] }, - "include": [ - "./**/*" - ] + "include": [ "./**/*" ] } diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 1fcf9e47..99bf48ca 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../tsconfig-library-base", "compilerOptions": { - "outDir": "../../lib" + "outDir": "../../lib", + "types": [ + "../../node_modules/@types/mocha" + ] }, - "include": [ - "./**/*", "../common/Platform.ts" - ], + "include": [ "./**/*" ], "references": [ { "path": "../common" } ] From fa505111a210333b3aa6887ab1bc4f9831aeefb3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 08:51:23 -0700 Subject: [PATCH 63/77] Replace winptyCompat addon with windowsMode option --- demo/client.ts | 16 +++----- src/Buffer.ts | 2 +- src/Terminal.ts | 25 +++++++++++- src/WindowsMode.ts | 30 ++++++++++++++ src/addons/winptyCompat/Interfaces.ts | 14 ------- src/addons/winptyCompat/package.json | 5 --- src/addons/winptyCompat/tsconfig.json | 21 ---------- src/addons/winptyCompat/winptyCompat.test.ts | 19 --------- src/addons/winptyCompat/winptyCompat.ts | 43 -------------------- typings/xterm.d.ts | 12 ++++++ 10 files changed, 73 insertions(+), 114 deletions(-) create mode 100644 src/WindowsMode.ts delete mode 100644 src/addons/winptyCompat/Interfaces.ts delete mode 100644 src/addons/winptyCompat/package.json delete mode 100644 src/addons/winptyCompat/tsconfig.json delete mode 100644 src/addons/winptyCompat/winptyCompat.test.ts delete mode 100644 src/addons/winptyCompat/winptyCompat.ts diff --git a/demo/client.ts b/demo/client.ts index aaaf2829..fa98ee4e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,12 +13,11 @@ import * as fit from '../lib/addons/fit/fit'; import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; import * as search from '../lib/addons/search/search'; import * as webLinks from '../lib/addons/webLinks/webLinks'; -import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat'; import { ISearchOptions } from '../lib/addons/search/Interfaces'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module -import { Terminal as TerminalType } from 'xterm'; +import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; @@ -30,10 +29,6 @@ Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); -const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; -if (isWindows) { - Terminal.applyAddon(winptyCompat); -} let term; @@ -86,7 +81,10 @@ function createTerminal(): void { while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); } - term = new Terminal({}); + const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; + term = new Terminal({ + windowsMode: isWindows + } as ITerminalOptions); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', (size: { cols: number, rows: number }) => { if (!pid) { @@ -102,9 +100,7 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - if (isWindows) { - term.winptyCompatInit(); - } + term.webLinksInit(); term.fit(); term.focus(); diff --git a/src/Buffer.ts b/src/Buffer.ts index 9cc1adba..4d844ea1 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -252,7 +252,7 @@ export class Buffer implements IBuffer { } private get _isReflowEnabled(): boolean { - return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled; + return this._hasScrollback && !this._terminal.options.windowsMode; } private _reflow(newCols: number, newRows: number): void { diff --git a/src/Terminal.ts b/src/Terminal.ts index db696bd6..5ed99729 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,6 +52,7 @@ import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; import { clone } from './common/Clone'; +import { applyWindowsMode } from './WindowsMode'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -110,7 +111,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas' + rendererType: 'canvas', + windowsMode: false }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -210,6 +212,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _accessibilityManager: AccessibilityManager; private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; + private _windowsMode: IDisposable | undefined; // bufferline to clone/copy from for new blank lines private _blankLine: IBufferLine = null; @@ -239,6 +242,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public dispose(): void { super.dispose(); + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } this._customKeyEventHandler = null; removeTerminalFromCache(this); this.handler = () => {}; @@ -321,6 +328,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.selectionManager.clearSelection(); this.selectionManager.initBuffersListeners(); } + + if (this.options.windowsMode) { + this._windowsMode = applyWindowsMode(this); + } } /** @@ -501,6 +512,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'windowsMode': + if (value) { + if (!this._windowsMode) { + this._windowsMode = applyWindowsMode(this); + } + } else { + if (this._windowsMode) { + this._windowsMode.dispose(); + this._windowsMode = undefined; + } + } + break; } // Inform renderer of changes if (this.renderer) { diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts new file mode 100644 index 00000000..33a9bed5 --- /dev/null +++ b/src/WindowsMode.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'xterm'; +import { ITerminal } from './Types'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './Buffer'; + +export function applyWindowsMode(terminal: ITerminal): IDisposable { + // Winpty does not support wraparound mode which means that lines will never + // be marked as wrapped. This causes issues for things like copying a line + // retaining the wrapped new line characters or if consumers are listening + // in on the data stream. + // + // The workaround for this is to listen to every incoming line feed and mark + // the line as wrapped if the last character in the previous line is not a + // space. This is certainly not without its problems, but generally on + // Windows when text reaches the end of the terminal it's likely going to be + // wrapped. + return terminal.addDisposableListener('linefeed', () => { + const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); + const lastChar = line.get(terminal.cols - 1); + + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { + const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); + nextLine.isWrapped = true; + } + }); +} diff --git a/src/addons/winptyCompat/Interfaces.ts b/src/addons/winptyCompat/Interfaces.ts deleted file mode 100644 index 6217c860..00000000 --- a/src/addons/winptyCompat/Interfaces.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; - -export interface ITerminalCore { - buffer: any; -} - -export interface IWinptyCompatAddonTerminal extends Terminal { - _core: ITerminalCore; -} diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json deleted file mode 100644 index fc929497..00000000 --- a/src/addons/winptyCompat/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.winptycompat", - "main": "winptyCompat.js", - "private": true -} diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json deleted file mode 100644 index fa48c963..00000000 --- a/src/addons/winptyCompat/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "outDir": "../../../lib/addons/winptyCompat/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts deleted file mode 100644 index c3a7e479..00000000 --- a/src/addons/winptyCompat/winptyCompat.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as winptyCompat from './winptyCompat'; - -class MockTerminal {} - -describe('winptyCompat addon', () => { - describe('apply', () => { - it('should do register the `winptyCompatInit` method', () => { - winptyCompat.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.winptyCompatInit, 'function'); - }); - }); -}); diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts deleted file mode 100644 index 58f59fd9..00000000 --- a/src/addons/winptyCompat/winptyCompat.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; -import { IWinptyCompatAddonTerminal } from './Interfaces'; - -const CHAR_DATA_CODE_INDEX = 3; -const NULL_CELL_CODE = 0; -const WHITESPACE_CELL_CODE = 32; - -export function winptyCompatInit(terminal: Terminal): void { - const addonTerminal = terminal; - - (addonTerminal._core as any).isWinptyCompatEnabled = true; - - // Winpty does not support wraparound mode which means that lines will never - // be marked as wrapped. This causes issues for things like copying a line - // retaining the wrapped new line characters or if consumers are listening - // in on the data stream. - // - // The workaround for this is to listen to every incoming line feed and mark - // the line as wrapped if the last character in the previous line is not a - // space. This is certainly not without its problems, but generally on - // Windows when text reaches the end of the terminal it's likely going to be - // wrapped. - addonTerminal.on('linefeed', () => { - const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); - const lastChar = line.get(addonTerminal.cols - 1); - - if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { - const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); - nextLine.isWrapped = true; - } - }); -} - -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).winptyCompatInit = function (): void { - winptyCompatInit(this); - }; -} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d813bc5f..ec647a10 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -188,6 +188,18 @@ declare module 'xterm' { * The color theme of the terminal. */ theme?: ITheme; + + /** + * Whether "Windows mode" is enabled. Because Windows backends winpty and + * conpty operate by doing line wrapping on their side, xterm.js does not + * have access to wrapped lines. When Windows mode is enabled the following + * changes will be in effect: + * + * - Reflow is disabled. + * - Lines are assumed to be wrapped if the last character of the line is + * not whitespace. + */ + windowsMode?: boolean; } /** From b12d2c218eab24ecb981be861fd086c6379f7b84 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:03:24 -0700 Subject: [PATCH 64/77] Remove reference to winptyCompat tsconfig --- src/tsconfig.all.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json index bee5df32..2a53ab89 100644 --- a/src/tsconfig.all.json +++ b/src/tsconfig.all.json @@ -9,8 +9,6 @@ { "path": "./addons/search" }, { "path": "./addons/terminado" }, { "path": "./addons/webLinks" }, - { "path": "./addons/winptyCompat" }, { "path": "./addons/zmodem" } ] } - \ No newline at end of file From 68e3d7fa164bfff08ac143d8ef3f9f62e7b14ed0 Mon Sep 17 00:00:00 2001 From: Vadim Zakondyrin Date: Wed, 3 Apr 2019 12:10:32 +0600 Subject: [PATCH 65/77] Fix CSI scroll down handler The issue has been introduced in cd8477a942e1fac85de167eab7e8c09c85f55255 --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 37c9bbe5..237e0849 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -898,7 +898,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); From b18829963cd7f7ff4598f00fd8eedf2dc035793c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:01:43 -0400 Subject: [PATCH 66/77] Fix backspace on demo buffer.x will probably be public API soon (#1994) Fixes #1989 --- demo/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index fa98ee4e..7a601898 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -172,7 +172,7 @@ function runFakeTerminal(): void { term.prompt(); } else if (ev.keyCode === 8) { // Do not delete the prompt - if (term.x > 2) { + if (term._core.buffer.x > 2) { term.write('\b \b'); } } else if (printable) { From e8a99945830de257a62f0554183b71ee613776fc Mon Sep 17 00:00:00 2001 From: Asad Memon Date: Thu, 4 Apr 2019 11:58:19 -0700 Subject: [PATCH 67/77] add CodeInterview.io in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 40de04cf..5083a9e3 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD +- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From bb324ba9c18816539c1120e91d66fe9c8a2f4965 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Apr 2019 14:55:43 -0400 Subject: [PATCH 68/77] Move width shift back into enum --- src/BufferLine.test.ts | 10 ++-- src/BufferLine.ts | 88 ++++++++++++++++----------------- src/renderer/TextRenderLayer.ts | 6 +-- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 29a783ae..5b029cb8 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData, ContentMasks } from './BufferLine'; +import { BufferLine, CellData, Content } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -32,7 +32,7 @@ describe('CellData', () => { // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); @@ -40,7 +40,7 @@ describe('CellData', () => { // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); @@ -350,7 +350,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -363,7 +363,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index f0bfe858..6ce5e498 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -36,7 +36,7 @@ const enum Cell { /** * Bitmasks for accessing data in `content`. */ -export const enum ContentMasks { +export const enum Content { /** * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) * read: `codepoint = content & Content.codepointMask;` @@ -44,7 +44,7 @@ export const enum ContentMasks { * shortcut if precondition `codepoint <= 0x10FFFF` is met: * `content |= codepoint;` */ - CODEPOINT = 0x1FFFFF, + CODEPOINT_MASK = 0x1FFFFF, /** * bit 22 flag indication whether a cell contains combined content @@ -52,7 +52,7 @@ export const enum ContentMasks { * set: `content |= Content.isCombined;` * clear: `content &= ~Content.isCombined;` */ - IS_COMBINED = 0x200000, // 1 << 21 + IS_COMBINED_MASK = 0x200000, // 1 << 21 /** * bit 1..22 mask to check whether a cell contains any string data @@ -60,7 +60,7 @@ export const enum ContentMasks { * whether a cell contains anything * read: `isEmtpy = !(content & Content.hasContent)` */ - HAS_CONTENT = 0x3FFFFF, + HAS_CONTENT_MASK = 0x3FFFFF, /** * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) @@ -72,10 +72,10 @@ export const enum ContentMasks { * shortcut if precondition `0 <= width <= 3` is met: * `content |= width << Content.widthShift;` */ - WIDTH = 0xC00000 // 3 << 22 + WIDTH_MASK = 0xC00000, // 3 << 22 + WIDTH_SHIFT = 22 } -export const WIDTH_MASK_SHIFT = 22; export enum Attributes { /** @@ -213,21 +213,21 @@ export class CellData extends AttributeData implements ICellData { /** Whether cell contains a combined string. */ public isCombined(): number { - return this.content & ContentMasks.IS_COMBINED; + return this.content & Content.IS_COMBINED_MASK; } /** Width of the cell. */ public getWidth(): number { - return this.content >> WIDTH_MASK_SHIFT; + return this.content >> Content.WIDTH_SHIFT; } /** JS string of the content. */ public getChars(): string { - if (this.content & ContentMasks.IS_COMBINED) { + if (this.content & Content.IS_COMBINED_MASK) { return this.combinedData; } - if (this.content & ContentMasks.CODEPOINT) { - return stringFromCodePoint(this.content & ContentMasks.CODEPOINT); + if (this.content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); } return ''; } @@ -241,7 +241,7 @@ export class CellData extends AttributeData implements ICellData { public getCode(): number { return (this.isCombined()) ? this.combinedData.charCodeAt(this.combinedData.length - 1) - : this.content & ContentMasks.CODEPOINT; + : this.content & Content.CODEPOINT_MASK; } /** Set data from CharData */ @@ -260,7 +260,7 @@ export class CellData extends AttributeData implements ICellData { if (0xD800 <= code && code <= 0xDBFF) { const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { - this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { combined = true; } @@ -268,11 +268,11 @@ export class CellData extends AttributeData implements ICellData { combined = true; } } else { - this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } if (combined) { this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } @@ -320,14 +320,14 @@ export class BufferLine implements IBufferLine { */ public get(index: number): CharData { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - const cp = content & ContentMasks.CODEPOINT; + const cp = content & Content.CODEPOINT_MASK; return [ this._data[index * CELL_SIZE + Cell.FG], - (content & ContentMasks.IS_COMBINED) + (content & Content.IS_COMBINED_MASK) ? this._combined[index] : (cp) ? stringFromCodePoint(cp) : '', - content >> WIDTH_MASK_SHIFT, - (content & ContentMasks.IS_COMBINED) + content >> Content.WIDTH_SHIFT, + (content & Content.IS_COMBINED_MASK) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : cp ]; @@ -341,9 +341,9 @@ export class BufferLine implements IBufferLine { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.CONTENT] = index | ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { - this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } @@ -352,12 +352,12 @@ export class BufferLine implements IBufferLine { * use these when only one value is needed, otherwise use `loadCell` */ public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT; + return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; } /** Test whether content has width. */ public hasWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.WIDTH; + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; } /** Get FG cell component. */ @@ -376,7 +376,7 @@ export class BufferLine implements IBufferLine { * from real empty cells. * */ public hasContent(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT; + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; } /** @@ -386,25 +386,25 @@ export class BufferLine implements IBufferLine { */ public getCodePoint(index: number): number { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & ContentMasks.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { return this._combined[index].charCodeAt(this._combined[index].length - 1); } - return content & ContentMasks.CODEPOINT; + return content & Content.CODEPOINT_MASK; } /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.IS_COMBINED; + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK; } /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & ContentMasks.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { return this._combined[index]; } - if (content & ContentMasks.CODEPOINT) { - return stringFromCodePoint(content & ContentMasks.CODEPOINT); + if (content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(content & Content.CODEPOINT_MASK); } // return empty string for empty cells return ''; @@ -419,7 +419,7 @@ export class BufferLine implements IBufferLine { cell.content = this._data[startIndex + Cell.CONTENT]; cell.fg = this._data[startIndex + Cell.FG]; cell.bg = this._data[startIndex + Cell.BG]; - if (cell.content & ContentMasks.IS_COMBINED) { + if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; } return cell; @@ -429,7 +429,7 @@ export class BufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - if (cell.content & ContentMasks.IS_COMBINED) { + if (cell.content & Content.IS_COMBINED_MASK) { this._combined[index] = cell.combinedData; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; @@ -443,7 +443,7 @@ export class BufferLine implements IBufferLine { * it gets an optimized access method. */ public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { - this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << WIDTH_MASK_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } @@ -456,21 +456,21 @@ export class BufferLine implements IBufferLine { */ public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & ContentMasks.IS_COMBINED) { + if (content & Content.IS_COMBINED_MASK) { // we already have a combined string, simply add this._combined[index] += stringFromCodePoint(codePoint); } else { - if (content & ContentMasks.CODEPOINT) { + if (content & Content.CODEPOINT_MASK) { // normal case for combining chars: // - move current leading char + new one into combined string // - set combined flag - this._combined[index] = stringFromCodePoint(content & ContentMasks.CODEPOINT) + stringFromCodePoint(codePoint); - content &= ~ContentMasks.CODEPOINT; // set codepoint in buffer to 0 - content |= ContentMasks.IS_COMBINED; + this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED_MASK; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 - content = codePoint | (1 << WIDTH_MASK_SHIFT); + content = codePoint | (1 << Content.WIDTH_SHIFT); } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } @@ -592,8 +592,8 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT); + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } return 0; @@ -632,9 +632,9 @@ export class BufferLine implements IBufferLine { let result = ''; while (startCol < endCol) { const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; - const cp = content & ContentMasks.CODEPOINT; - result += (content & ContentMasks.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; - startCol += (content >> WIDTH_MASK_SHIFT) || 1; // always advance by 1 + const cp = content & Content.CODEPOINT_MASK; + result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 } return result; } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index f9821e4a..2547ecb2 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -8,7 +8,7 @@ import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types' import { CharData, ITerminal, ICellData } from '../Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CellData, AttributeData, ContentMasks, WIDTH_MASK_SHIFT } from '../BufferLine'; +import { CellData, AttributeData, Content } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -117,8 +117,8 @@ export class TextRenderLayer extends BaseRenderLayer { // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) { // patch width to 2 - cell.content &= ~ContentMasks.WIDTH; - cell.content |= 2 << WIDTH_MASK_SHIFT; + cell.content &= ~Content.WIDTH_MASK; + cell.content |= 2 << Content.WIDTH_SHIFT; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the // overlapping char is no longer to the left of the character and also when From 171b9337cb544417b052486634ce9fc36a8bcbaf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Apr 2019 14:56:15 -0400 Subject: [PATCH 69/77] Make BufferLine enums const --- src/BufferLine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 6ce5e498..bf0683d0 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -77,7 +77,7 @@ export const enum Content { } -export enum Attributes { +export const enum Attributes { /** * bit 1..8 blue in RGB, color in P256 and P16 */ @@ -113,7 +113,7 @@ export enum Attributes { RGB_MASK = 0xFFFFFF } -export enum FgFlags { +export const enum FgFlags { /** * bit 27..31 (32th bit unused) */ @@ -124,7 +124,7 @@ export enum FgFlags { INVISIBLE = 0x40000000 } -export enum BgFlags { +export const enum BgFlags { /** * bit 27..32 (upper 4 unused) */ From 09a5fd4e61e45479b64e29a1e639cb5e53d1c735 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Apr 2019 15:06:42 -0400 Subject: [PATCH 70/77] Fix capitalization in color mode --- src/BufferLine.ts | 4 ++-- src/InputHandler.test.ts | 36 ++++++++++++++++++------------------ src/Types.ts | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index bf0683d0..2bd5a113 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -165,8 +165,8 @@ export class AttributeData implements IAttributeData { public isDim(): number { return this.bg & BgFlags.DIM; } // color modes - public getFgColormode(): number { return this.fg & Attributes.CM_MASK; } - public getBgColormode(): number { return this.bg & Attributes.CM_MASK; } + public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } + public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 0b7f7ad3..01596f80 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -447,53 +447,53 @@ describe('InputHandler', () => { assert.equal(!!term.curAttrData.isInvisible(), false); }); it('colormode palette 16', () => { - assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 8; ++i) { term.writeSync(`\x1b[${i + 30};${i + 40}m`); - assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_P16); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_P16); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(term.curAttrData.getBgColor(), i); } // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColormode(), 0); - assert.equal(term.curAttrData.getBgColormode(), 0); + assert.equal(term.curAttrData.getFgColorMode(), 0); + assert.equal(term.curAttrData.getBgColorMode(), 0); }); it('colormode palette 256', () => { - assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 256; ++i) { term.writeSync(`\x1b[38;5;${i};48;5;${i}m`); - assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_P256); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_P256); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(term.curAttrData.getBgColor(), i); } // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColormode(), 0); + assert.equal(term.curAttrData.getFgColorMode(), 0); assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getBgColormode(), 0); + assert.equal(term.curAttrData.getBgColorMode(), 0); assert.equal(term.curAttrData.getBgColor(), -1); }); it('colormode RGB', () => { - assert.equal(term.curAttrData.getFgColormode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColormode(), 0); // DEFAULT + assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); - assert.equal(term.curAttrData.getFgColormode(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB); assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]); - assert.equal(term.curAttrData.getBgColormode(), Attributes.CM_RGB); + assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB); assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]); // reset to DEFAULT term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColormode(), 0); + assert.equal(term.curAttrData.getFgColorMode(), 0); assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getBgColormode(), 0); + assert.equal(term.curAttrData.getBgColorMode(), 0); assert.equal(term.curAttrData.getBgColor(), -1); }); it('should zero missing RGB values', () => { diff --git a/src/Types.ts b/src/Types.ts index c05a3479..6e0108ae 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -540,8 +540,8 @@ export interface IAttributeData { isDim(): number; // color modes - getFgColormode(): number; - getBgColormode(): number; + getFgColorMode(): number; + getBgColorMode(): number; isFgRGB(): boolean; isBgRGB(): boolean; isFgPalette(): boolean; From ddd02b49683a357f41f4c8577ef9b2555be7584b Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Sat, 6 Apr 2019 16:04:50 -0400 Subject: [PATCH 71/77] feat: add DIM support to DomRenderer part of #1896 --- src/renderer/dom/DomRendererRowFactory.ts | 5 +++++ src/xterm.css | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 47232981..707d3466 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -10,6 +10,7 @@ import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; import { CellData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; +export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; export const CURSOR_CLASS = 'xterm-cursor'; export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; @@ -107,6 +108,10 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } + if (flags & FLAGS.DIM) { + charElement.classList.add(exports.DIM_CLASS); + } + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); diff --git a/src/xterm.css b/src/xterm.css index 2e47b1a1..e0319f87 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -161,3 +161,7 @@ height: 1px; overflow: hidden; } + +.xterm-dim { + opacity: 0.6; +} From 92c435a16dfa6d7ae366db66985d2d6b6a2247fd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 09:45:54 -0400 Subject: [PATCH 72/77] Use same dim opacity as canvas renderer --- src/renderer/dom/DomRendererRowFactory.ts | 2 +- src/xterm.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 707d3466..4d2143ee 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -109,7 +109,7 @@ export class DomRendererRowFactory { } if (flags & FLAGS.DIM) { - charElement.classList.add(exports.DIM_CLASS); + charElement.classList.add(DIM_CLASS); } charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; diff --git a/src/xterm.css b/src/xterm.css index e0319f87..e80c2524 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -163,5 +163,5 @@ } .xterm-dim { - opacity: 0.6; + opacity: 0.5; } From ab44650d87a4d701171d891feeee60796dc56f48 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 13:16:19 -0400 Subject: [PATCH 73/77] Fix reflow smaller case related to ended \t chars Part of #1932 --- src/Buffer.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 8 ++++---- src/BufferLine.ts | 2 +- src/BufferReflow.ts | 26 +++++++++++++++++++----- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 59475adb..e2ef35fe 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -518,6 +518,29 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(5, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), 'ab c'); + assert.equal(buffer.lines.get(1).translateToString(false), 'd '); + buffer.resize(6, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), 'ab cd'); + assert.equal(buffer.lines.get(1).translateToString(false), ' '); + }); it('should wrap wide characters correctly when reflowing larger', () => { buffer.fillViewportRows(); buffer.resize(12, 10); @@ -553,6 +576,32 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); }); + it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(3, 10); + assert.equal(buffer.y, 2); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(false), 'ab '); + assert.equal(buffer.lines.get(1).translateToString(false), ' cd'); + buffer.resize(2, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(false), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(false), ' '); + assert.equal(buffer.lines.get(2).translateToString(false), 'cd'); + }); it('should wrap wide characters correctly when reflowing smaller', () => { buffer.fillViewportRows(); buffer.resize(12, 10); diff --git a/src/Buffer.ts b/src/Buffer.ts index 4d844ea1..770b027c 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -8,7 +8,7 @@ import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IB import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine, CellData } from './BufferLine'; -import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; @@ -269,7 +269,7 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number, newRows: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); @@ -375,8 +375,8 @@ export class Buffer implements IBuffer { srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - // TODO: srcCol shoudl take trimmed length into account - srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols; + const wrappedLinesIndex = Math.max(srcLineIndex, 0); + srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols); } } diff --git a/src/BufferLine.ts b/src/BufferLine.ts index c4aa55be..07207248 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -58,7 +58,7 @@ export const enum ContentMasks { * bit 1..22 mask to check whether a cell contains any string data * we need to check for codepoint and isCombined bits to see * whether a cell contains anything - * read: `isEmtpy = !(content & Content.hasContent)` + * read: `isEmpty = !(content & Content.hasContent)` */ HAS_CONTENT = 0x3FFFFF, diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index d27d7c48..d3adfc6d 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -19,7 +19,7 @@ export interface INewLayoutResult { * @param lines The buffer lines. * @param newCols The columns after resize. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number): number[] { const nullCell = CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once @@ -49,11 +49,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n // Copy buffer data to new locations let destLineIndex = 0; - let destCol = wrappedLines[destLineIndex].getTrimmedLength(); + let destCol = wrappedLines.length === 1 ? wrappedLines[destLineIndex].getTrimmedLength() : oldCols; let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcTrimmedTineLength = srcLineIndex === wrappedLines.length - 1 ? wrappedLines[srcLineIndex].getTrimmedLength() : oldCols; const srcRemainingCells = srcTrimmedTineLength - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); @@ -174,7 +174,7 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new */ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and // linesNeeded @@ -188,7 +188,7 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo break; } srcCol += newCols; - const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength(); + const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols); if (srcCol > oldTrimmedLength) { srcCol -= oldTrimmedLength; srcLine++; @@ -204,3 +204,19 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo return newLineLengths; } + +export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number { + // If this is the last row in the wrapped line, get the actual trimmed length + if (i === lines.length - 1) { + return lines[i].getTrimmedLength(); + } + // Detect whether the following line starts with a wide character and the end of the current line + // is null, if so then we can be pretty sure the null character should be excluded from the line + // length] + const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1; + const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2; + if (endsInNull && followingLineStartsWithWide) { + return cols - 1; + } + return cols; +} From 3ac313f8e12ef7ded12e60e60ceb41d1f6105f37 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 13:18:05 -0400 Subject: [PATCH 74/77] Fix reflow larger case Fixes #1932 --- src/BufferReflow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index d3adfc6d..c090b0d7 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -49,11 +49,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o // Copy buffer data to new locations let destLineIndex = 0; - let destCol = wrappedLines.length === 1 ? wrappedLines[destLineIndex].getTrimmedLength() : oldCols; + let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols); let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = srcLineIndex === wrappedLines.length - 1 ? wrappedLines[srcLineIndex].getTrimmedLength() : oldCols; + const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols); const srcRemainingCells = srcTrimmedTineLength - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); From 74b68ac1c786ed8121fa1457f134dfe913672b54 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 13:49:56 -0400 Subject: [PATCH 75/77] Fix compile error from conflicting PRs --- src/renderer/dom/DomRendererRowFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index a6728873..a6cbeb78 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -84,7 +84,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - if (flags & FLAGS.DIM) { + if (this._workCell.isDim()) { charElement.classList.add(DIM_CLASS); } From a0b4ee56f66fde4e20baa82a08c6b88cc8781ada Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 14:17:38 -0400 Subject: [PATCH 76/77] Add a test for dim flag in dom renderer --- src/renderer/dom/DomRendererRowFactory.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 026a947d..4402943c 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -90,6 +90,16 @@ describe('DomRendererRowFactory', () => { ); }); + it('should add class for dim', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add classes for 256 foreground colors', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P256; From e5f94391ae5985cbbb7a6f52d4005473236745b5 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Tue, 9 Apr 2019 09:10:29 -0400 Subject: [PATCH 77/77] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5083a9e3..1fa8ad41 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD - [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access. +- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)