add frontground/background color support for SerializeAddon

This commit is contained in:
javacs3
2019-11-21 20:41:46 +08:00
parent f088a9953f
commit d81b4999dc
3 changed files with 492 additions and 19 deletions
@@ -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<string>(blankline, rows);
const lines = newArray<string>('', 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<any> {
it('serialize half rows of content', async function (): Promise<any> {
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<any> {
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<string>(
(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<any> {
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<any> {
this.timeout(20000);
const rows = 32;
const cols = 10;
const lines = newArray<string>(
(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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<void> {
@@ -125,10 +373,57 @@ function newArray<T>(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';
@@ -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<string>(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<string>();
private _currentRow: string = '';
private _sgrSeq: string[] = [];
constructor(buffer: IBuffer) {
super(buffer);
}
protected _serializeStart(rows: number): void {
this._allRows = new Array<string>(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((<any>this._terminal)._core.buffer);
const handler = new StringSerializeHandler((<any>this._terminal)._core.buffer);
rows = crop(rows, 0, maxRows, maxRows);
+12 -2
View File
@@ -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
}