Skip to content

Commit

Permalink
Merge pull request #4877 from Tyriar/serialize_fixes
Browse files Browse the repository at this point in the history
Add range API to serialize addon
  • Loading branch information
Tyriar committed Nov 9, 2023
2 parents 49eab6b + b595940 commit fb2c39c
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 58 deletions.
31 changes: 30 additions & 1 deletion addons/addon-serialize/src/SerializeAddon.test.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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', () => {
Expand Down
108 changes: 53 additions & 55 deletions addons/addon-serialize/src/SerializeAddon.ts
Expand Up @@ -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';

Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<IHTMLSerializeOptions>): string {
Expand All @@ -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 }
});
}

Expand Down Expand Up @@ -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}`;
}
}
Expand All @@ -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 = '';

Expand Down
21 changes: 19 additions & 2 deletions addons/addon-serialize/typings/addon-serialize.d.ts
Expand Up @@ -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' {
/**
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
}

0 comments on commit fb2c39c

Please sign in to comment.