diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts index d41cfa12ed..ac485e549f 100644 --- a/addons/addon-serialize/src/SerializeAddon.test.ts +++ b/addons/addon-serialize/src/SerializeAddon.test.ts @@ -9,7 +9,6 @@ import { SerializeAddon } from './SerializeAddon'; import { Terminal } from 'browser/public/Terminal'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { IBufferService } from 'common/services/Services'; -import { OptionsService } from 'common/services/OptionsService'; import { ThemeService } from 'browser/services/ThemeService'; function sgr(...seq: string[]): string { @@ -83,6 +82,36 @@ describe('SerializeAddon', () => { await writeP(terminal, sgr('32') + '> ' + sgr('0')); assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m'); }); + + describe('ISerializeOptions.range', () => { + it('should serialize the top line', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 0, + end: 0 + } + }), 'hello'); + }); + it('should serialize multiple lines from the top', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 0, + end: 1 + } + }), 'hello\r\nworld'); + }); + it('should serialize lines in the middle', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 1, + end: 1 + } + }), 'world'); + }); + }); }); describe('html', () => { diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index 961d8546e6..e654eddbee 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -6,7 +6,7 @@ */ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm'; -import type { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize'; +import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize'; import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService'; import { IAttributeData, IColor } from 'common/Types'; @@ -21,24 +21,24 @@ abstract class BaseSerializeHandler { ) { } - public serialize(range: IBufferRange): string { + public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string { // 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; - const startRow = range.start.x; - const endRow = range.end.x; - const startColumn = range.start.y; - const endColumn = range.end.y; + const startRow = range.start.y; + const endRow = range.end.y; + const startColumn = range.start.x; + const endColumn = range.end.x; this._beforeSerialize(endRow - startRow, startRow, endRow); for (let row = startRow; row <= endRow; row++) { const line = this._buffer.getLine(row); if (line) { - const startLineColumn = row !== range.start.x ? 0 : startColumn; - const endLineColumn = row !== range.end.x ? line.length : endColumn; + const startLineColumn = row === range.start.y ? startColumn : 0; + const endLineColumn = row === range.end.y ? endColumn: line.length; for (let col = startLineColumn; col < endLineColumn; col++) { const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); if (!c) { @@ -54,14 +54,14 @@ abstract class BaseSerializeHandler { this._afterSerialize(); - return this._serializeString(); + return this._serializeString(excludeFinalCursorPosition); } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _rowEnd(row: number, isLastRow: boolean): void { } protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { } protected _afterSerialize(): void { } - protected _serializeString(): string { return ''; } + protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; } } function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { @@ -353,7 +353,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } - protected _serializeString(): string { + protected _serializeString(excludeFinalCursorPosition: boolean): string { let rowEnd = this._allRows.length; // the fixup is only required for data without scrollback @@ -374,29 +374,31 @@ class StringSerializeHandler extends BaseSerializeHandler { } // restore the cursor - const realCursorRow = this._buffer.baseY + this._buffer.cursorY; - const realCursorCol = this._buffer.cursorX; + if (!excludeFinalCursorPosition) { + const realCursorRow = this._buffer.baseY + this._buffer.cursorY; + const realCursorCol = this._buffer.cursorX; - const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); - const moveRight = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}C`; - } else if (offset < 0) { - content += `\u001b[${-offset}D`; - } - }; - const moveDown = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}B`; - } else if (offset < 0) { - content += `\u001b[${-offset}A`; - } - }; + const moveRight = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}C`; + } else if (offset < 0) { + content += `\u001b[${-offset}D`; + } + }; + const moveDown = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}B`; + } else if (offset < 0) { + content += `\u001b[${-offset}A`; + } + }; - if (cursorMoved) { - moveDown(realCursorRow - this._lastCursorRow); - moveRight(realCursorCol - this._lastCursorCol); + if (cursorMoved) { + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); + } } // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677 @@ -419,14 +421,21 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { this._terminal = terminal; } - private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { + private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { const maxRows = buffer.length; - const handler = new StringSerializeHandler(buffer, terminal); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return this._serializeBufferByRange(terminal, buffer, { + start: maxRows - correctRows, + end: maxRows - 1 + }, false); + } + + private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string { + const handler = new StringSerializeHandler(buffer, terminal); return handler.serialize({ - start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows - 1, y: terminal.cols } - }); + start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line }, + end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line } + }, excludeFinalCursorPosition); } private _serializeBufferAsHTML(terminal: Terminal, options: Partial): string { @@ -438,16 +447,16 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { const scrollback = options.scrollback; const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ - start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows - 1, y: terminal.cols } + start: { x: 0, y: maxRows - correctRows }, + end: { x: terminal.cols, y: maxRows - 1 } }); } const selection = this._terminal?.getSelectionPosition(); if (selection !== undefined) { return handler.serialize({ - start: { x: selection.start.y, y: selection.start.x }, - end: { x: selection.end.y, y: selection.end.x } + start: { x: selection.start.x, y: selection.start.y }, + end: { x: selection.end.x, y: selection.end.y } }); } @@ -490,12 +499,14 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { } // Normal buffer - let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); + let content = options?.range + ? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true) + : this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback); // Alternate buffer if (!options?.excludeAltBuffer) { if (this._terminal.buffer.active.type === 'alternate') { - const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined); + const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined); content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`; } } @@ -519,19 +530,6 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { public dispose(): void { } } - -interface ISerializeOptions { - scrollback?: number; - excludeModes?: boolean; - excludeAltBuffer?: boolean; -} - -interface IHTMLSerializeOptions { - scrollback: number; - onlySelection: boolean; - includeGlobalBackground: boolean; -} - export class HTMLSerializeHandler extends BaseSerializeHandler { private _currentRow: string = ''; diff --git a/addons/addon-serialize/typings/addon-serialize.d.ts b/addons/addon-serialize/typings/addon-serialize.d.ts index 0b127b5061..90b8b4286f 100644 --- a/addons/addon-serialize/typings/addon-serialize.d.ts +++ b/addons/addon-serialize/typings/addon-serialize.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IMarker, IBufferRange } from '@xterm/xterm'; declare module '@xterm/addon-serialize' { /** @@ -48,10 +48,16 @@ declare module '@xterm/addon-serialize' { } export interface ISerializeOptions { + /** + * The row range to serialize. The an explicit range is specified, the cursor will get its final + * repositioning. + */ + range?: ISerializeRange; + /** * The number of rows in the scrollback buffer to serialize, starting from the bottom of the * scrollback buffer. When not specified, all available rows in the scrollback buffer will be - * serialized. + * serialized. This will be ignored if {@link range} is specified. */ scrollback?: number; @@ -85,4 +91,15 @@ declare module '@xterm/addon-serialize' { */ includeGlobalBackground: boolean; } + + export interface ISerializeRange { + /** + * The line to start serializing (inclusive). + */ + start: IMarker | number; + /** + * The line to end serializing (inclusive). + */ + end: IMarker | number; + } }