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 01/15] 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 27e0e836d3..f6907864d5 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 6295d17571..a5a285b026 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 97508ace48..ec03c064ce 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 048bcab6b7..e306346fbb 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 f84757d195..268c2d88bd 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 24eb088490..df34cf049f 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 c41b4165c9..178507fa32 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 e06ceb78cd..e6ed7b98ed 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 33d8e60f28..0a488ea2f5 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 d176799f4a..b46f858f0b 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 bf7c5616d5..2b2ba1bb2b 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 e06990d78c..c80cea27b3 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 83a4651e25..41091a6372 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 9d525fbf92..4772a9d8c2 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 02/15] 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 e306346fbb..341e300909 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 df34cf049f..7ca2d6fc25 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 b46f858f0b..0de25e2da2 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 03/15] 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 c80cea27b3..6a69a4ba7f 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 41091a6372..7ef26e1fae 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 04/15] 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 6a69a4ba7f..d3b2100c8b 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 7ef26e1fae..39b13d4845 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 05/15] 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 341e300909..fcb57a4d77 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 7ca2d6fc25..95e4117511 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 178507fa32..4ea340bb19 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 0de25e2da2..50ea560af2 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 39b13d4845..b37eb02b06 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 06/15] 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 f84fbe6bcc..4ef6badac7 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 2b2ba1bb2b..1e374dd936 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 07/15] 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 1e374dd936..e0ef0f2dde 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 08/15] 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 4cad7c72c9..459975c7ec 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 09/15] 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 4ef6badac7..e266e52a26 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 e0ef0f2dde..7ff2d002da 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 10/15] 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 fcb57a4d77..3d94d5f60a 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 95e4117511..0b7f7ad3ce 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 50ea560af2..6636b04daa 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 11/15] 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 e266e52a26..e0d3e7315e 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 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 12/15] 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 11de586092..2df6152106 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 bb324ba9c18816539c1120e91d66fe9c8a2f4965 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Apr 2019 14:55:43 -0400 Subject: [PATCH 13/15] 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 29a783aecf..5b029cb80d 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 f0bfe858c7..6ce5e4987b 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 f9821e4aa3..2547ecb249 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 14/15] 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 6ce5e4987b..bf0683d0e4 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 15/15] 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 bf0683d0e4..2bd5a1135d 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 0b7f7ad3ce..01596f8084 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 c05a347904..6e0108aefc 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;