Merge pull request #1895 from jerch/true_color

True color support
This commit is contained in:
Daniel Imms
2019-04-07 13:45:08 -04:00
committed by GitHub
17 changed files with 689 additions and 443 deletions
+9 -9
View File
@@ -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,13 +37,13 @@ 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()).getAsCharData;
const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData();
buffer.fillViewportRows();
assert.equal(buffer.lines.length, INIT_ROWS);
for (let y = 0; y < INIT_ROWS; y++) {
assert.equal(buffer.lines.get(y).length, INIT_COLS);
for (let x = 0; x < INIT_COLS; x++) {
assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData, blankLineChar);
assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData(), blankLineChar);
}
}
});
@@ -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;
@@ -683,7 +683,7 @@ describe('Buffer', () => {
beforeEach(() => {
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
@@ -742,7 +742,7 @@ describe('Buffer', () => {
terminal.options.scrollback = 10;
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.y = 9;
buffer.ybase = 10;
@@ -877,7 +877,7 @@ describe('Buffer', () => {
beforeEach(() => {
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
@@ -940,7 +940,7 @@ describe('Buffer', () => {
terminal.options.scrollback = 10;
// Add 10 empty rows to start
for (let i = 0; i < 10; i++) {
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
buffer.ybase = 10;
});
+30 -17
View File
@@ -4,15 +4,18 @@
*/
import { CircularList, IInsertEvent, IDeleteEvent } 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 { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
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;
@@ -55,7 +58,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]);
@@ -77,19 +80,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);
}
@@ -121,10 +134,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._rows;
while (i--) {
@@ -154,7 +167,7 @@ export class Buffer implements IBuffer {
*/
public resize(newCols: number, newRows: number): void {
// store reference to null cell with default attrs
const nullCell = this.getNullCell(DEFAULT_ATTR);
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Increase max length if needed before adjustments to allow space to fill
// as required.
@@ -269,7 +282,7 @@ export class Buffer implements IBuffer {
}
private _reflowLarger(newCols: number, newRows: number): void {
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y);
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
@@ -278,7 +291,7 @@ export class Buffer implements IBuffer {
}
private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {
const nullCell = this.getNullCell(DEFAULT_ATTR);
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Adjust viewport based on number of items removed
let viewportAdjustments = countRemoved;
while (viewportAdjustments-- > 0) {
@@ -300,7 +313,7 @@ export class Buffer implements IBuffer {
}
private _reflowSmaller(newCols: number, newRows: number): void {
const nullCell = this.getNullCell(DEFAULT_ATTR);
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
const toInsert = [];
@@ -341,7 +354,7 @@ export class Buffer implements IBuffer {
// Add the new lines
const newLines: BufferLine[] = [];
for (let i = 0; i < linesToAdd; i++) {
const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine;
const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;
newLines.push(newLine);
}
if (newLines.length > 0) {
+5 -5
View File
@@ -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, '', 2, ''.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '', 2, ''.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);
});
});
});
+162 -46
View File
@@ -2,7 +2,7 @@
* 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';
@@ -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,15 +72,131 @@ 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
}
const WIDTH_MASK_SHIFT = 22;
export const 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 const enum FgFlags {
/**
* bit 27..31 (32th bit unused)
*/
INVERSE = 0x4000000,
BOLD = 0x8000000,
UNDERLINE = 0x10000000,
BLINK = 0x20000000,
INVISIBLE = 0x40000000
}
export const enum BgFlags {
/**
* bit 27..32 (upper 4 unused)
*/
ITALIC = 0x4000000,
DIM = 0x8000000
}
export class AttributeData implements IAttributeData {
static toColorRGB(value: number): IColorRGB {
return [
value >>> Attributes.RED_SHIFT & 255,
value >>> Attributes.GREEN_SHIFT & 255,
value & 255
];
}
static fromColorRGB(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 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; }
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(): 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 this.fg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
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 this.bg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
}
/**
* CellData - represents a single Cell in the terminal buffer.
*/
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,21 +213,21 @@ export class CellData 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 '';
}
@@ -125,7 +241,7 @@ export class CellData 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 */
@@ -144,7 +260,7 @@ export class CellData implements ICellData {
if (0xD800 <= code && code <= 0xDBFF) {
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
if (0xDC00 <= second && second <= 0xDFFF) {
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT);
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
combined = true;
}
@@ -152,11 +268,11 @@ export class CellData 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);
}
}
@@ -204,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
];
@@ -225,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);
}
}
@@ -236,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. */
@@ -260,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;
}
/**
@@ -270,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 '';
@@ -303,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;
@@ -313,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;
@@ -327,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;
}
@@ -340,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;
}
@@ -476,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;
@@ -516,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;
}
+3 -5
View File
@@ -3,10 +3,9 @@
* @license MIT
*/
import { BufferLine, CellData } from './BufferLine';
import { BufferLine } from './BufferLine';
import { CircularList, IDeleteEvent } from './common/CircularList';
import { IBufferLine } from './Types';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
import { IBufferLine, ICellData } from './Types';
export interface INewLayoutResult {
layout: number[];
@@ -19,8 +18,7 @@ export interface INewLayoutResult {
* @param lines The buffer lines.
* @param newCols The columns after resize.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, newCols: number, bufferAbsoluteY: number): number[] {
const nullCell = CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
+2 -2
View File
@@ -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;
}
+119 -15
View File
@@ -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, AttributeData } 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()).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()).fg >> 9) & 0x1ff).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');
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()).getFgColor())).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()).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()).fg & 0x1ff).to.equal(2);
expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).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.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.getFgColor(), i);
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);
});
it('colormode palette 256', () => {
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.getFgColor(), i);
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.getFgColor(), -1);
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
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(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]);
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.getFgColor(), -1);
assert.equal(term.curAttrData.getBgColorMode(), 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(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]);
});
});
});
+73 -76
View File
@@ -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 } from './common/TypedArrayUtils';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder';
import { CellData } from './BufferLine';
import { CellData, Attributes, FgFlags, BgFlags, AttributeData } 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.getWidth(cols - 1) === 2) {
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0);
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg);
}
}
// write current char to buffer and advance cursor
bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr, 0);
bufferRow.setCellFromCodePoint(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.setCellFromCodePoint(buffer.x++, 0, 0, curAttr, 0);
bufferRow.setCellFromCodePoint(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.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 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._workCell);
line.replaceCells(buffer.x,
buffer.x + (params[0] || 1),
(this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR)
(this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA)
);
// FIXME: no updateRange here?
}
@@ -1311,7 +1309,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();
@@ -1575,127 +1573,124 @@ 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 |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
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 |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
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;
}
/**
@@ -1782,7 +1777,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; // ??
@@ -1845,7 +1840,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;
}
@@ -1857,7 +1853,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;
}
+4 -4
View File
@@ -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));
}
});
});
+16 -79
View File
@@ -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';
import { applyWindowsMode } from './WindowsMode';
// Let it work inside Node.js for automated testing purposes.
@@ -175,7 +175,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;
@@ -300,7 +301,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;
@@ -344,9 +346,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;
}
/**
@@ -1204,8 +1207,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;
@@ -1780,23 +1784,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
@@ -1872,7 +1865,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 {
@@ -1917,46 +1910,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' ||
@@ -1979,19 +1932,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);
}
+42 -11
View File
@@ -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 {
@@ -521,11 +520,43 @@ 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
getFgColorMode(): number;
getBgColorMode(): number;
isFgRGB(): boolean;
isBgRGB(): boolean;
isFgPalette(): boolean;
isBgPalette(): boolean;
isFgDefault(): boolean;
isBgDefault(): boolean;
// colors
getFgColor(): number;
getBgColor(): number;
}
/** Cell data */
export interface ICellData extends IAttributeData {
content: number;
combinedData: string;
isCombined(): number;
getWidth(): number;
+48 -22
View File
@@ -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 } from '../BufferLine';
import { CellData, AttributeData } from '../BufferLine';
import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -258,17 +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): void {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void {
// skip cache right away if we draw in RGB
if (cell.isFgRGB() || 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.getChars() || WHITESPACE_CELL_CHAR;
this._currentGlyphIdentifier.code = cell.getCode() || 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,
@@ -277,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);
}
}
@@ -292,29 +309,38 @@ 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, 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
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()) {
let fg = cell.getFgColor();
if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.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.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
this._ctx.restore();
+11 -11
View File
@@ -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._workCell);
const chars = this._workCell.getChars();
const width = this._workCell.getWidth();
const attr = this._workCell.fg >> 9;
if (width === 0) {
if (this._workCell.getWidth() === 0) {
// If this character is of width 0, skip it.
continue;
}
// End of range
if (attr !== rangeAttr) {
if (this._workCell.fg !== rangeAttrFG || this._workCell.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._workCell.fg;
rangeAttrBG = this._workCell.bg;
}
currentStringIndex += chars.length;
currentStringIndex += this._workCell.getChars().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
+66 -85
View File
@@ -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 } 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++) {
@@ -75,13 +68,7 @@ export class TextRenderLayer extends BaseRenderLayer {
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
for (let x = 0; x < terminal.cols; x++) {
line.loadCell(x, this._workCell);
let code: number = this._workCell.getCode() || WHITESPACE_CELL_CODE;
// Can either represent character(s) for a single cell or multiple cells
// if indicated by a character joiner.
let chars = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
const attr = this._workCell.fg;
let width = this._workCell.getWidth();
let cell = this._workCell;
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
@@ -89,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.getWidth() === 0) {
continue;
}
@@ -102,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._workCell.fg;
cell.bg = this._workCell.bg;
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
@@ -119,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,8 +115,10 @@ 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._workCell).getCode() === NULL_CELL_CODE) {
width = 2;
if (lastCharX < line.length - 1 && line.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
// 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
@@ -137,32 +127,10 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// 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;
@@ -183,14 +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)) {
nextFillStyle = this._colors.ansi[bg].css;
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[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) {
@@ -225,29 +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.getWidth());
this._ctx.restore();
}
this.drawChars(
terminal, chars, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
);
this.drawChars(terminal, cell, x, y);
});
}
@@ -273,21 +252,23 @@ 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.getWidth() !== 1) {
return false;
}
// We assume that any ascii character will not overlap
if (code < 256) {
if (cell.getCode() < 256) {
return false;
}
const chars = cell.getChars();
// Deliver from cache if available
if (this._characterOverlapCache.hasOwnProperty(char)) {
return this._characterOverlapCache[char];
if (this._characterOverlapCache.hasOwnProperty(chars)) {
return this._characterOverlapCache[chars];
}
// Setup the font
@@ -297,13 +278,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(chars).width) > this._characterWidth;
// Restore the original context
this._ctx.restore();
// Cache and return
this._characterOverlapCache[char] = overlaps;
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
+59 -17
View File
@@ -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, ITerminalOptions } from '../../Types';
import { DEFAULT_COLOR } from '../atlas/Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -73,7 +71,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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>'
@@ -81,7 +81,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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>'
@@ -89,9 +91,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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>`
@@ -100,9 +105,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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>`
@@ -111,38 +119,72 @@ 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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
);
});
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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
);
});
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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
);
});
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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
}
});
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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="color:rgb(1,2,3);background-color:rgb(4,5,6);">a</span>'
);
});
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, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="background-color:rgb(1,2,3);color:rgb(4,5,6);">a</span>'
);
});
});
});
+32 -32
View File
@@ -4,10 +4,9 @@
*/
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
import { FLAGS } from '../Types';
import { IBufferLine, ITerminalOptions } from '../../Types';
import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types';
import { CellData } from '../../BufferLine';
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
import { CellData, AttributeData } from '../../BufferLine';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
@@ -45,7 +44,6 @@ export class DomRendererRowFactory {
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
const attr = this._workCell.fg;
const width = this._workCell.getWidth();
// The character to the left is a wide character, drawing is owned by the char at x-1
@@ -58,10 +56,6 @@ export class DomRendererRowFactory {
charElement.style.width = `${cellWidth * width}px`;
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
@@ -82,29 +76,11 @@ 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 && this._terminalOptions.enableBold) {
// Convert the FG color to the bold variant. This should not happen when
// the fg is the inverse default color as there is no bold variant.
if (fg < 8 && this._terminalOptions.drawBoldTextInBrightColors) {
fg += 8;
}
if (this._workCell.isBold() && this._terminalOptions.enableBold) {
charElement.classList.add(BOLD_CLASS);
}
if (flags & FLAGS.ITALIC) {
if (this._workCell.isItalic()) {
charElement.classList.add(ITALIC_CLASS);
}
@@ -113,12 +89,36 @@ export class DomRendererRowFactory {
}
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
if (fg !== DEFAULT_COLOR) {
charElement.classList.add(`xterm-fg-${fg}`);
const swapColor = this._workCell.isInverse();
// fg
if (this._workCell.isFgRGB()) {
let style = charElement.getAttribute('style') || '';
style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`;
charElement.setAttribute('style', style);
} else if (this._workCell.isFgPalette()) {
let fg = this._workCell.getFgColor();
if (this._workCell.isBold() && fg < 8 && !swapColor &&
this._terminalOptions.enableBold && this._terminalOptions.drawBoldTextInBrightColors) {
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._workCell.isBgRGB()) {
let style = charElement.getAttribute('style') || '';
style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`;
charElement.setAttribute('style', style);
} else if (this._workCell.isBgPalette()) {
charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`);
} else if (swapColor) {
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}
fragment.appendChild(charElement);
}
return fragment;
+8 -7
View File
@@ -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 '../common/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<IBufferLine>): 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.');
}
}