diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 27e0e836d..f6907864d 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 6295d1757..a5a285b02 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 97508ace4..ec03c064c 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 048bcab6b..e306346fb 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 f84757d19..268c2d88b 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 24eb08849..df34cf049 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 c41b4165c..178507fa3 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 e06ceb78c..e6ed7b98e 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 33d8e60f2..0a488ea2f 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 d176799f4..b46f858f0 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 bf7c5616d..2b2ba1bb2 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 e06990d78..c80cea27b 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 83a4651e2..41091a637 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 9d525fbf9..4772a9d8c 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.'); } }