From a39facfe28a6d555b3a60338fb7fcc08118ce506 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 15 Mar 2022 12:42:47 -0700 Subject: [PATCH] Restore cursor style too during serialize Fixes #3677 --- .../src/SerializeAddon.test.ts | 231 +++++++++--------- .../src/SerializeAddon.ts | 18 +- 2 files changed, 134 insertions(+), 115 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index e35751d1..67b83884 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -43,7 +43,7 @@ class TestSelectionService { } } -describe('xterm-addon-serialize html', () => { +describe('xterm-addon-serialize', () => { let cm: ColorManager; let dom: jsdom.JSDOM; let document: Document; @@ -83,123 +83,132 @@ describe('xterm-addon-serialize html', () => { (terminal as any)._core._selectionService = selectionService; }); - it('empty terminal with selection turned off', () => { - const output = serializeAddon.serializeAsHTML(); - assert.notEqual(output, ''); - assert.equal((output.match(/
{10}<\/span><\/div>/g) || []).length, 2); - }); - - it('empty terminal with no selection', () => { - const output = serializeAddon.serializeAsHTML({ - onlySelection: true + describe('text', () => { + it('restoring cursor styles', async () => { + await writeP(terminal, sgr('32') + '> ' + sgr('0')); + assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m'); }); - assert.equal(output, ''); }); - it('basic terminal with selection', async () => { - await writeP(terminal, ' terminal '); - terminal.select(1, 0, 8); - - const output = serializeAddon.serializeAsHTML({ - onlySelection: true + describe('html', () => { + it('empty terminal with selection turned off', () => { + const output = serializeAddon.serializeAsHTML(); + assert.notEqual(output, ''); + assert.equal((output.match(/
{10}<\/span><\/div>/g) || []).length, 2); }); - assert.equal((output.match(/
terminal<\/span><\/div>/g) || []).length, 1, output); - }); - it('cells with bold styling', async () => { - await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with italic styling', async () => { - await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with inverse styling', async () => { - await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with underline styling', async () => { - await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with invisible styling', async () => { - await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with dim styling', async () => { - await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with strikethrough styling', async () => { - await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with combined styling', async () => { - await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/ <\/span>/g) || []).length, 1, output); - assert.equal((output.match(/termi<\/span>/g) || []).length, 1, output); - assert.equal((output.match(/nal<\/span>/g) || []).length, 1, output); - }); - - it('cells with color styling', async () => { - await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('cells with background styling', async () => { - await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); - }); - - it('empty terminal with default options', async () => { - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); - }); - - it('empty terminal with custom options', async () => { - terminal.options.fontFamily = 'verdana'; - terminal.options.fontSize = 20; - terminal.options.theme = { - foreground: '#ff00ff', - background: '#00ff00' - }; - const output = serializeAddon.serializeAsHTML({ - includeGlobalBackground: true + it('empty terminal with no selection', () => { + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal(output, ''); }); - assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output); - }); - it('empty terminal with background included', async () => { - const output = serializeAddon.serializeAsHTML({ - includeGlobalBackground: true + it('basic terminal with selection', async () => { + await writeP(terminal, ' terminal '); + terminal.select(1, 0, 8); + + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal((output.match(/
terminal<\/span><\/div>/g) || []).length, 1, output); + }); + + it('cells with bold styling', async () => { + await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with italic styling', async () => { + await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with inverse styling', async () => { + await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with underline styling', async () => { + await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with invisible styling', async () => { + await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with dim styling', async () => { + await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with strikethrough styling', async () => { + await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with combined styling', async () => { + await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/ <\/span>/g) || []).length, 1, output); + assert.equal((output.match(/termi<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/nal<\/span>/g) || []).length, 1, output); + }); + + it('cells with color styling', async () => { + await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with background styling', async () => { + await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('empty terminal with default options', async () => { + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); + }); + + it('empty terminal with custom options', async () => { + terminal.options.fontFamily = 'verdana'; + terminal.options.fontSize = 20; + terminal.options.theme = { + foreground: '#ff00ff', + background: '#00ff00' + }; + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output); + }); + + it('empty terminal with background included', async () => { + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); }); - assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); }); }); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 8d6f8b36..ed71e4e1 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -7,6 +7,7 @@ import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm'; import { IColorSet } from 'browser/Types'; +import { IAttributeData } from 'common/Types'; function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); @@ -62,17 +63,17 @@ abstract class BaseSerializeHandler { protected _serializeString(): string { return ''; } } -function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor(); } -function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor(); } -function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.isInverse() === cell2.isInverse() && cell1.isBold() === cell2.isBold() && cell1.isUnderline() === cell2.isUnderline() @@ -229,7 +230,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } - private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { + private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] { const sgrSeq: number[] = []; const fgChanged = !equalFg(cell, oldCell); const bgChanged = !equalBg(cell, oldCell); @@ -393,6 +394,15 @@ class StringSerializeHandler extends BaseSerializeHandler { moveRight(realCursorCol - this._lastCursorCol); } + // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677 + // HACK: Internal API access since it's awkward to expose this in the API and serialize will + // likely be the only consumer + const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData; + const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle); + if (sgrSeq.length > 0) { + content += `\u001b[${sgrSeq.join(';')}m`; + } + return content; } }