From cd5f3e9740291e5ca848400b4a3b1558fa937170 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 29 Aug 2019 23:17:24 +0800 Subject: [PATCH] update cell color api design --- .../src/SerializeAddon.ts | 140 ++++++------------ src/public/Terminal.ts | 130 ++++++++-------- typings/xterm.d.ts | 116 +++++++++++---- 3 files changed, 201 insertions(+), 185 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b8b57b6b..0fa5c3dd 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell, CellColor, CellStyle } from 'xterm'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { @@ -12,19 +12,14 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } -class NullBufferCell implements IBufferCell { - char: string = ''; - width: number = 0; - foregroundColor: CellColor = CellColor.getDefault(); - backgroundColor: CellColor = CellColor.getDefault(); - style: CellStyle = CellStyle.default; -} - abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { - let oldCell: IBufferCell = new NullBufferCell(); + // we need two of them to flip between old and new cell + const cell1 = this._buffer.getNullCell(); + const cell2 = this._buffer.getNullCell(); + let oldCell = cell1; this._serializeStart(endRow - startRow); @@ -35,23 +30,22 @@ abstract class BaseSerializeHandler { if (line) { for (let col = 0; col < line.length; col++) { - const cell = line.getCell(col); + const newCell = line.getCell(col, oldCell === cell1 ? cell2 : cell1); - if (!cell) { + if (!newCell) { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } - if (!cell.foregroundColor.equals(oldCell.foregroundColor) - || !cell.backgroundColor.equals(oldCell.backgroundColor)) { - this._cellColorChanged(cell, oldCell, row, col); + if (!newCell.equalFg(oldCell) || !newCell.equalBg(oldCell)) { + this._cellFgBgChanged(newCell, oldCell, row, col); } - if (cell.style !== oldCell.style) { - this._cellStyleChanged(cell, oldCell, row, col); + if (!newCell.equalFlags(oldCell)) { + this._cellFlagsChanged(newCell, oldCell, row, col); } - this._nextCell(cell, oldCell, row, col); + this._nextCell(newCell, oldCell, row, col); - oldCell = cell; + oldCell = newCell; } } @@ -65,9 +59,9 @@ abstract class BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _lineStart(row: number): void { } @@ -80,33 +74,11 @@ abstract class BaseSerializeHandler { protected _serializeFinished(): string { return ''; } } -function fgColor256to16(c: number): number { - if (0 <= c && c <= 7) { - return 30 + c; - } else if (8 <= c && c <= 15) { - return 82 + c; - } - return -1; -} - -function bgColor256to16(c: number): number { - if (0 <= c && c <= 7) { - return 40 + c; - } else if (8 <= c && c <= 15) { - return 92 + c; - } - return -1; -} - -function isDefaultColorStyle(cell: IBufferCell) { - return cell.foregroundColor.isDefault() && cell.backgroundColor.isDefault() && (cell.style === CellStyle.default); -} - class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); private _currentRow: string = ''; - private _sgrSeq: string[] = []; + private _sgrSeq: number[] = []; constructor(buffer: IBuffer) { super(buffer); @@ -121,76 +93,54 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow = ''; } - protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const styleChangedMask = cell.style ^ oldCell.style; - const style = cell.style; + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (isDefaultColorStyle(cell)) { - return; - } + if (cell.isDefaultAttibutes() || cell.equalFlags(oldCell)) { return; } - if (styleChangedMask & CellStyle.inverse) { - sgrSeq.push((style & CellStyle.inverse) ? '7' : '27'); - } - if (styleChangedMask & CellStyle.bold) { - sgrSeq.push((style & CellStyle.bold) ? '1' : '22'); - } - if (styleChangedMask & CellStyle.underline) { - sgrSeq.push((style & CellStyle.underline) ? '4' : '24'); - } - if (styleChangedMask & CellStyle.blink) { - sgrSeq.push((style & CellStyle.blink) ? '5' : '25'); - } - if (styleChangedMask & CellStyle.invisible) { - sgrSeq.push((style & CellStyle.invisible) ? '8' : '28'); - } - if (styleChangedMask & CellStyle.italic) { - sgrSeq.push((style & CellStyle.italic) ? '3' : '23'); - } - if (styleChangedMask & CellStyle.dim) { - sgrSeq.push((style & CellStyle.dim) ? '2' : '22'); - } + if (cell.flags.inverse !== oldCell.flags.inverse) { sgrSeq.push(cell.flags.inverse ? 7 : 27); } + if (cell.flags.bold !== oldCell.flags.bold) { sgrSeq.push(cell.flags.bold ? 1 : 22); } + if (cell.flags.underline !== oldCell.flags.underline) { sgrSeq.push(cell.flags.underline ? 4 : 24); } + if (cell.flags.blink !== oldCell.flags.blink) { sgrSeq.push(cell.flags.blink ? 5 : 25); } + if (cell.flags.invisible !== oldCell.flags.invisible) { sgrSeq.push(cell.flags.invisible ? 8 : 28); } + if (cell.flags.italic !== oldCell.flags.italic) { sgrSeq.push(cell.flags.italic ? 3 : 23); } + if (cell.flags.dim !== oldCell.flags.dim) { sgrSeq.push(cell.flags.dim ? 2 : 22); } } - protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); - const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (isDefaultColorStyle(cell)) { - return; - } + if (cell.isDefaultAttibutes()) { return; } - if (foregroundColorChanged) { - const foregroundColor = cell.foregroundColor; - switch (foregroundColor.type) { - case 'default': sgrSeq.push('39'); break; - case 'palette16': sgrSeq.push(fgColor256to16(foregroundColor.paletteId()).toString()); break; - case 'palette256': sgrSeq.push(`38;5;${foregroundColor.paletteId()}`); break; - case 'rgb': const [red, green, blue] = foregroundColor.rgbColor(); sgrSeq.push(`38;2;${red};${green};${blue}`); break; + if (!cell.equalFg(oldCell)) { + const color = cell.fg.color; + switch (cell.fg.colorMode) { + case 'RGB': sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; + case 'P256': sgrSeq.push(38, 5, color); break; + case 'P16': sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); break; + default: sgrSeq.push(39); break; } } - if (backgroundColorChanged) { - const backgroundColor = cell.backgroundColor; - switch (backgroundColor.type) { - case 'default': sgrSeq.push('49'); break; - case 'palette16': sgrSeq.push(bgColor256to16(backgroundColor.paletteId()).toString()); break; - case 'palette256': sgrSeq.push(`48;5;${backgroundColor.paletteId()}`); break; - case 'rgb': const [red, green, blue] = backgroundColor.rgbColor(); sgrSeq.push(`48;2;${red};${green};${blue}`); break; + if (!cell.equalBg(oldCell)) { + const color = cell.bg.color; + switch (cell.bg.colorMode) { + case 'RGB': sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; + case 'P256': sgrSeq.push(48, 5, color); break; + case 'P16': sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); break; + default: sgrSeq.push(49); break; } } } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); - const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); - const styleChanged = cell.style !== oldCell.style; + const fgChanged = !cell.equalFg(oldCell); + const bgChanged = !cell.equalBg(oldCell); + const flagsChanged = !cell.equalFlags(oldCell); - if ((foregroundColorChanged || backgroundColorChanged || styleChanged) && isDefaultColorStyle(cell)) { + if (cell.isDefaultAttibutes() && (fgChanged || bgChanged || flagsChanged)) { this._currentRow += '\x1b[0m'; } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index baae367c..e1aef88e 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, CellColor as ICellColorApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IBufferCellColor as IBufferCellColorApi, IBufferCellFlags as IBufferCellFlagsApi } from 'xterm'; import { ITerminal } from '../Types'; -import { IBufferLine, ICellData } from 'common/Types'; +import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; @@ -14,6 +14,7 @@ import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { IParams } from 'common/parser/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -200,6 +201,7 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } + public getNullCell(): IBufferCellApi { return new BufferCellApiView(new CellData()); } } class BufferLineApiView implements IBufferLineApi { @@ -207,81 +209,81 @@ class BufferLineApiView implements IBufferLineApi { public get isWrapped(): boolean { return this._line.isWrapped; } public get length(): number { return this._line.length; } - public getCell(x: number): IBufferCellApi | undefined { + public getCell(x: number, cell?: BufferCellApiView): IBufferCellApi | undefined { if (x < 0 || x >= this._line.length) { return undefined; } - const cell = new CellData(); - this._line.loadCell(x, cell); - return new BufferCellApiView(cell); + if (cell) { + this._line.loadCell(x, cell.cell); + return cell; + } + return new BufferCellApiView(this._line.loadCell(x, new CellData())); } public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { return this._line.translateToString(trimRight, startColumn, endColumn); } } -export enum CellStyle { - default = 0, - // foreground style - inverse = 0x4000000 >>> 24, - bold = 0x8000000 >>> 24, - underline = 0x10000000 >>> 24, - blink = 0x20000000 >>> 24, - invisible = 0x40000000 >>> 24, - // background style - italic = 0x4000000 >>> 16, - dim = 0x8000000 >>> 16 -} - -const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; - -export class CellColor implements ICellColorApi { - readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; - readonly value: number = 0; - - constructor(value: number) { - this.value = value; - switch (value & Attributes.CM_MASK) { - case Attributes.CM_P16: this.type = 'palette16'; break; - case Attributes.CM_P256: this.type = 'palette256'; break; - case Attributes.CM_RGB: this.type = 'rgb'; break; - case Attributes.CM_DEFAULT: this.type = 'default'; break; - default: throw new Error('Invalid CellColor value'); - } - } - public isDefault(): boolean { return this.value === 0; } - public equals(c: ICellColorApi): boolean { return this.value === c.value; } - public paletteId(): number { - switch (this.type) { - case 'default': - case 'palette16': - case 'palette256': return this.value & Attributes.PCOLOR_MASK; - } - return -1; - } - public rgbColor(): [number, number, number] { - if (this.type === 'rgb') { - return CellData.toColorRGB(this.value); - } - return [-1, -1, -1]; - } - - public static getDefault(): ICellColorApi { return new CellColor(0); } -} +const fgFlagMask = FgFlags.BOLD | FgFlags.BLINK | FgFlags.INVERSE | FgFlags.INVISIBLE | FgFlags.UNDERLINE; +const bgFlagMask = BgFlags.DIM | BgFlags.ITALIC; +const colorMask = Attributes.CM_MASK | Attributes.RGB_MASK; class BufferCellApiView implements IBufferCellApi { - constructor(private _cell: ICellData) { } - - public get char(): string { return this._cell.getChars(); } - public get width(): number { return this._cell.getWidth(); } - public get foregroundColor(): ICellColorApi { - return new CellColor(this._cell.fg & COLOR_MASK); + public flags: IBufferCellFlagsApi; + public fg: IBufferCellColorApi; + public bg: IBufferCellColorApi; + constructor(public cell: CellData) { + this.flags = { + get bold(): boolean { return !!(cell.fg & FgFlags.BOLD); }, + get underline(): boolean { return !!(cell.fg & FgFlags.UNDERLINE); }, + get blink(): boolean { return !!(cell.fg & FgFlags.BLINK); }, + get inverse(): boolean { return !!(cell.fg & FgFlags.INVERSE); }, + get invisible(): boolean { return !!(cell.fg & FgFlags.INVISIBLE); }, + get italic(): boolean { return !!(cell.bg & BgFlags.ITALIC); }, + get dim(): boolean { return !!(cell.bg & BgFlags.DIM); } + }; + this.fg = { + get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { + switch (cell.getFgColorMode()) { + case Attributes.CM_RGB: return 'RGB'; + case Attributes.CM_P256: return 'P256'; + case Attributes.CM_P16: return 'P16'; + default: return 'DEFAULT'; + } + }, + get color(): number { return cell.getFgColor(); }, + get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getFgColor()); } + }; + this.bg = { + get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { + switch (cell.getFgColorMode()) { + case Attributes.CM_RGB: return 'RGB'; + case Attributes.CM_P256: return 'P256'; + case Attributes.CM_P16: return 'P16'; + default: return 'DEFAULT'; + } + }, + get color(): number { return cell.getBgColor(); }, + get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getBgColor()); } + }; } - public get backgroundColor(): ICellColorApi { - return new CellColor(this._cell.bg & COLOR_MASK); + public get char(): string { return this.cell.getChars(); } + public get width(): number { return this.cell.getWidth(); } + public isDefaultAttibutes(): boolean { + return this.cell.fg === 0 && this.cell.bg === 0; } - public get style(): CellStyle { - return ((this._cell.bg & BgFlags.FM_MASK) >>> 16) | ((this._cell.fg & FgFlags.FM_MASK) >>> 24); + public equalAttibutes(other: BufferCellApiView): boolean { + return this.cell.fg === other.cell.fg && this.cell.bg === other.cell.bg; + } + public equalFlags(other: BufferCellApiView): boolean { + return (this.cell.fg & fgFlagMask) === (other.cell.fg & fgFlagMask) + && (this.cell.bg & bgFlagMask) === (other.cell.bg & bgFlagMask); + } + public equalFg(other: BufferCellApiView): boolean { + return (this.cell.fg & colorMask) === (other.cell.fg & colorMask); + } + public equalBg(other: BufferCellApiView): boolean { + return (this.cell.bg & colorMask) === (other.cell.bg & colorMask); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6fcc1e53..b0415350 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -900,6 +900,13 @@ declare module 'xterm' { * @param y The line index to get. */ getLine(y: number): IBufferLine | undefined; + + /** + * Creates an empty cell object suitable as a cell reference in + * `line.getCell(x, cell)`. Use this to avoid costly recreation of + * cell objects when dealing with tons of cells. + */ + getNullCell(): IBufferCell; } /** @@ -920,8 +927,9 @@ declare module 'xterm' { * behavior. * * @param x The character index to get. + * @param cell Optional cell object to load data into. */ - getCell(x: number): IBufferCell | undefined; + getCell(x: number, cell?: IBufferCell): IBufferCell | undefined; /** * Gets the line as a string. Note that this is gets only the string for the @@ -934,6 +942,49 @@ declare module 'xterm' { translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; } + /** + * Represents foreground and background color settings of a cell. + */ + interface IBufferCellColor { + /** + * Color mode of the color setting. + * RGB Color is an RGB color, use `.rgb` to grab the different channels. + * P256 Color is an indexed value of the 256 color palette. + * P16 Color is an indexed value of the 8 color palette (+8 for AIX bright colors). + * DEFAULT No color set, thus default color should be used. + */ + colorMode: 'RGB' | 'P256' | 'P16' | 'DEFAULT'; + + /** + * Color value set in the current color mode. + * Note that the color value can only be interpreted in conjunction + * with the color mode: + * RGB color contains 8 bit channels in RGB32 bitorder, e.g. red << 16 | green << 8 | blue + * P256 color contains indexed value 0..255 + * P16 color contains indexed value 0..15 + * DEFAULT color always contains -1 + */ + color: number; + + /** + * Helper to get RGB channels from color mode RGB. Reports channels as [red, green, blue]. + */ + rgb: [number, number, number]; + } + + /** + * Represents style flags of a cell. + */ + interface IBufferCellFlags { + readonly bold: boolean; + readonly underline: boolean; + readonly blink: boolean; + readonly inverse: boolean; + readonly invisible: boolean; + readonly italic: boolean; + readonly dim: boolean; + } + /** * Represents a single cell in the terminal's buffer. */ @@ -952,35 +1003,48 @@ declare module 'xterm' { */ readonly width: number; - readonly foregroundColor: CellColor; - readonly backgroundColor: CellColor; - readonly style: CellStyle; - } + /** + * Text attribute flags like bold, underline etc. + */ + readonly flags: IBufferCellFlags; - export enum CellStyle { - default, - // foreground style - inverse, - bold, - underline, - blink, - invisible, - // background style - italic, - dim - } + /** + * Foreground color. + */ + readonly fg: IBufferCellColor; - export class CellColor { - readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; - readonly value: number; + /** + * Background color. + */ + readonly bg: IBufferCellColor; - constructor(value: number); + /** + * Whether cells have default attributes (flags and colors). + */ + isDefaultAttibutes(): boolean; - isDefault(): boolean; - equals(c: CellColor): boolean; - paletteId(): number; - rgbColor(): [number, number, number]; + /** + * Whether cells have the same text attributes (flags and colors). + * @param other Other cell. + */ + equalAttibutes(other: IBufferCell): boolean; - static getDefault(): CellColor; + /** + * Whether cells have the same text attribute flags. + * @param other Other cell. + */ + equalFlags(other: IBufferCell): boolean; + + /** + * Whether cells have the same foreground color. + * @param other Other cell. + */ + equalFg(other: IBufferCell): boolean; + + /** + * Whether cells have the same background color. + * @param other Other cell. + */ + equalBg(other: IBufferCell): boolean; } }