From d81b4999dc8381a87d79188758670d1589981f81 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 7 Aug 2019 22:56:18 +0800 Subject: [PATCH] add frontground/background color support for SerializeAddon --- .../src/SerializeAddon.api.ts | 307 +++++++++++++++++- .../src/SerializeAddon.ts | 190 ++++++++++- src/common/buffer/Constants.ts | 14 +- 3 files changed, 492 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 5543696b..eec5b6fc 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -21,6 +21,7 @@ describe('SerializeAddon', () => { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, + devtools: true, args: [`--window-size=${width},${height}`] }); page = (await browser.pages())[0]; @@ -40,8 +41,7 @@ describe('SerializeAddon', () => { this.timeout(20000); const rows = 10; const cols = 10; - const blankline = ' '.repeat(cols); - const lines = newArray(blankline, rows); + const lines = newArray('', rows); await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); await page.evaluate(` @@ -69,7 +69,7 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); - it('serialize n rows of content', async function (): Promise { + it('serialize half rows of content', async function (): Promise { this.timeout(20000); const rows = 10; const halfRows = rows >> 1; @@ -101,6 +101,254 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); }); + + it('serialize all rows of content with color16', async function (): Promise { + this.timeout(20000); + const rows = 16; + const cols = 10; + const color16 = [ + 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color + 40, 41, 42, 43, 44, 45, 46, 47 // Set background color + ]; + const lines = newArray( + (index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`), + rows + ); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with fg/bg flags', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence + mkSGR(INVERSE) + line, + mkSGR(BOLD) + line, + mkSGR(UNDERLINED) + line, + mkSGR(BLINK) + line, + mkSGR(INVISIBLE) + line, + mkSGR(NO_INVERSE) + line, + mkSGR(NO_BOLD) + line, + mkSGR(NO_UNDERLINED) + line, + mkSGR(NO_BLINK) + line, + mkSGR(NO_INVISIBLE) + line + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256', async function (): Promise { + this.timeout(20000); + const rows = 32; + const cols = 10; + const lines = newArray( + (index: number) => digitsString(cols, index, `\x1b[38;5;${index}m`), + rows + ); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color16 and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_RED) + line, // fg Red, + mkSGR(UNDERLINED) + line, // fg Red, Underlined + mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined + mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green, Underlined + mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse + mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color16 and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_RED) + line, // fg Red + mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_P16_RED) + line, // fg Red + mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256 and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P256_RED) + line, // fg Red 256, + mkSGR(UNDERLINED) + line, // fg Red 256, Underlined + mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined + mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined + mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse + mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256 and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P256_RED) + line, // fg Red 256 + mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_P256_RED) + line, // fg Red 256 + mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with colorRGB and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_RGB_RED) + line, // fg Red RGB, + mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined + mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined + mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined + mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse + mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with colorRGB and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_RGB_RED) + line, // fg Red RGB + mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_RGB_RED) + line, // fg Red RGB + mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -125,10 +373,57 @@ function newArray(initial: T | ((index: number) => T), count: number): T[] { return array; } -function digitsString(length: number, from: number = 0): string { - let s = ''; +function digitsString(length: number, from: number = 0, sgr: string = ''): string { + let s = sgr; for (let i = 0; i < length; i++) { - s += (from++) % 10; + s += `${(from++) % 10}`; } return s; } + +function mkSGR(...seq: string[]): string { + return `\x1b[${seq.join(';')}m`; +} + +const NORMAL = '0'; + +const FG_P16_RED = '31'; +const FG_P16_GREEN = '32'; +const FG_P16_YELLOW = '33'; +const FG_P256_RED = '38;5;1'; +const FG_P256_GREEN = '38;5;2'; +const FG_P256_YELLOW = '38;5;3'; +const FG_RGB_RED = '38;2;255;0;0'; +const FG_RGB_GREEN = '38;2;0;255;0'; +const FG_RGB_YELLOW = '38;2;255;255;0'; +const FG_RESET = '39'; + + +const BG_P16_RED = '41'; +const BG_P16_GREEN = '42'; +const BG_P16_YELLOW = '43'; +const BG_P256_RED = '48;5;1'; +const BG_P256_GREEN = '48;5;2'; +const BG_P256_YELLOW = '48;5;3'; +const BG_RGB_RED = '48;2;255;0;0'; +const BG_RGB_GREEN = '48;2;0;255;0'; +const BG_RGB_YELLOW = '48;2;255;255;0'; +const BG_RESET = '49'; + +const INVERSE = '7'; +const BOLD = '1'; +const UNDERLINED = '4'; +const BLINK = '5'; +const INVISIBLE = '8'; + +const NO_INVERSE = '27'; +const NO_BOLD = '22'; +const NO_UNDERLINED = '24'; +const NO_BLINK = '25'; +const NO_INVISIBLE = '28'; + +const ITALIC = '3'; +const DIM = '2'; + +const NO_ITALIC = '23'; +const NO_DIM = '22'; diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 44438c98..88ea852f 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -4,8 +4,10 @@ */ import { Terminal, ITerminalAddon } from 'xterm'; -// import { IBufferLine } from 'common/Types'; +import { ICellData } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; import { IBuffer } from 'common/buffer/Types'; +import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { @@ -14,20 +16,184 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } -class SerializeHandler { +class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } - serialize(start: number, end: number): string { - const rows = end - start; - const lines: string[] = new Array(rows); + serialize(startRow: number, endRow: number): string { + let oldCell = new CellData(); - for (let i = start; i < end; i++) { - const line = this._buffer.lines.get(i); + this._serializeStart(endRow - startRow); - lines[i - start] = line ? line.translateToString() : ''; + for (let row = startRow; row < endRow; row++) { + const line = this._buffer.lines.get(row); + + this._lineStart(row); + + if (line) { + for (let col = 0; col < line.length; col++) { + const cell = new CellData(); + + line.loadCell(col, cell); + + if (oldCell.fg !== cell.fg) { + this._fgChanged(cell, oldCell, row, col); + } + if (oldCell.bg !== cell.bg) { + this._bgChanged(cell, oldCell, row, col); + } + + this._cellChanged(cell, oldCell, row, col); + + oldCell = cell; + } + } + + this._lineEnd(row); } - return lines.join('\r\n'); + this._serializeEnd(); + + return this._serializeFinished(); + } + + protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _lineStart(row: number): void { } + + protected _lineEnd(row: number): void { } + + protected _serializeStart(rows: number): void { } + + protected _serializeEnd(): void { } + + protected _serializeFinished(): string { return ''; } +} + +const FG_FM_MASK = FgFlags.FM_MASK; +const BG_FM_MASK = BgFlags.FM_MASK; +const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; + +class StringSerializeHandler extends BaseSerializeHandler { + private _rowIndex: number = 0; + private _allRows: string[] = new Array(); + private _currentRow: string = ''; + private _sgrSeq: string[] = []; + + constructor(buffer: IBuffer) { + super(buffer); + } + + protected _serializeStart(rows: number): void { + this._allRows = new Array(rows); + } + + protected _lineEnd(row: number): void { + this._allRows[this._rowIndex++] = this._currentRow; + this._currentRow = ''; + } + + protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const fgFlagsChanged = (cell.fg ^ oldCell.fg) & FG_FM_MASK; + const fgColorChanged = (cell.fg ^ oldCell.fg) & COLOR_MASK; + const sgrSeq = this._sgrSeq; + + if ((cell.fg === 0) && (cell.bg === 0)) { + return; + } + + if (fgFlagsChanged) { + if (fgFlagsChanged & FgFlags.INVERSE) { + sgrSeq.push(cell.isInverse() ? '7' : '27'); + } + if (fgFlagsChanged & FgFlags.BOLD) { + sgrSeq.push(cell.isBold() ? '1' : '22'); + } + if (fgFlagsChanged & FgFlags.UNDERLINE) { + sgrSeq.push(cell.isUnderline() ? '4' : '24'); + } + if (fgFlagsChanged & FgFlags.BLINK) { + sgrSeq.push(cell.isBlink() ? '5' : '25'); + } + if (fgFlagsChanged & FgFlags.INVISIBLE) { + sgrSeq.push(cell.isInvisible() ? '8' : '28'); + } + } + + if (fgColorChanged) { + const fgColor = cell.getFgColor(); + + if (cell.isFgDefault()) { + sgrSeq.push('39'); + } else if (cell.isFgPalette()) { + switch (cell.getFgColorMode()) { + case Attributes.CM_P16: sgrSeq.push(`${30 + fgColor}`); break; + case Attributes.CM_P256: sgrSeq.push(`38;5;${fgColor}`); break; + } + } else if (cell.isFgRGB()) { + const [r, g, b] = CellData.toColorRGB(fgColor); + sgrSeq.push(`38;2;${r};${g};${b}`); + } + } + } + + protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const bgFlagsChanged = (cell.bg ^ oldCell.bg) & BG_FM_MASK; + const bgColorChanged = (cell.bg ^ oldCell.bg) & COLOR_MASK; + const sgrSeq = this._sgrSeq; + + if ((cell.bg === 0) && (cell.fg === 0)) { + return; + } + + if (bgFlagsChanged) { + if (bgFlagsChanged & BgFlags.ITALIC) { + sgrSeq.push(cell.isItalic() ? '3' : '23'); + } + if (bgFlagsChanged & BgFlags.DIM) { + sgrSeq.push(cell.isDim() ? '2' : '22'); + } + } + + if (bgColorChanged) { + const bgColor = cell.getBgColor(); + + if (cell.isBgDefault()) { + sgrSeq.push('49'); + } else if (cell.isBgPalette()) { + switch (cell.getBgColorMode()) { + case Attributes.CM_P16: sgrSeq.push(`${40 + bgColor}`); break; + case Attributes.CM_P256: sgrSeq.push(`48;5;${bgColor}`); break; + } + } else if (cell.isFgRGB()) { + const [r, g, b] = CellData.toColorRGB(bgColor); + sgrSeq.push(`48;2;${r};${g};${b}`); + } + } + } + + protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const fgChanged = cell.fg !== oldCell.fg; + const bgChanged = cell.bg !== oldCell.bg; + const isfgBgNormal = (cell.fg === 0) && (cell.bg === 0); + + if ((fgChanged || bgChanged) && isfgBgNormal) { + this._currentRow += '\x1b[0m'; + } + + if (this._sgrSeq.length) { + this._currentRow += `\x1b[${this._sgrSeq.join(';')}m`; + this._sgrSeq = []; + } + + this._currentRow += cell.getChars(); + } + + protected _serializeFinished(): string { + return this._allRows.join('\r\n'); } } @@ -41,13 +207,15 @@ export class SerializeAddon implements ITerminalAddon { } public serialize(rows?: number): string { - // TODO: Add frontground/background color support later + // TODO: Add re-position cursor support + // TODO: Add word wrap mode support + // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } const maxRows = this._terminal.rows; - const handler = new SerializeHandler((this._terminal)._core.buffer); + const handler = new StringSerializeHandler((this._terminal)._core.buffer); rows = crop(rows, 0, maxRows, maxRows); diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 276a5c54..81ac773a 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -116,7 +116,12 @@ export const enum FgFlags { BOLD = 0x8000000, UNDERLINE = 0x10000000, BLINK = 0x20000000, - INVISIBLE = 0x40000000 + INVISIBLE = 0x40000000, + + /** + * bit 27..31 (32th bit unused) + */ + FM_MASK = 0x7C000000 } export const enum BgFlags { @@ -124,5 +129,10 @@ export const enum BgFlags { * bit 27..32 (upper 4 unused) */ ITALIC = 0x4000000, - DIM = 0x8000000 + DIM = 0x8000000, + + /** + * bit 27..32 (upper 4 unused) + */ + FM_MASK = 0xFC000000 }