diff options
| author | Adam <[email protected]> | 2025-12-22 19:38:50 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-22 19:39:00 -0600 |
| commit | 794fe8f381c846f5241800363023d892c12cf495 (patch) | |
| tree | bff98689edfa635a2a9f39cb4ea61639b97f5b2d /packages/desktop/src/addons | |
| parent | a4eebf9f08262f6bf63017710e2e6d9672ec6708 (diff) | |
| download | opencode-794fe8f381c846f5241800363023d892c12cf495.tar.gz opencode-794fe8f381c846f5241800363023d892c12cf495.zip | |
chore: rename packages/desktop -> packages/app
Diffstat (limited to 'packages/desktop/src/addons')
| -rw-r--r-- | packages/desktop/src/addons/serialize.test.ts | 272 | ||||
| -rw-r--r-- | packages/desktop/src/addons/serialize.ts | 595 |
2 files changed, 0 insertions, 867 deletions
diff --git a/packages/desktop/src/addons/serialize.test.ts b/packages/desktop/src/addons/serialize.test.ts deleted file mode 100644 index ad165f43f..000000000 --- a/packages/desktop/src/addons/serialize.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { describe, test, expect, beforeAll, afterEach } from "bun:test" -import { Terminal, Ghostty } from "ghostty-web" -import { SerializeAddon } from "./serialize" - -let ghostty: Ghostty -beforeAll(async () => { - ghostty = await Ghostty.load() -}) - -const terminals: Terminal[] = [] - -afterEach(() => { - for (const term of terminals) { - term.dispose() - } - terminals.length = 0 - document.body.innerHTML = "" -}) - -function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } { - const container = document.createElement("div") - document.body.appendChild(container) - - const term = new Terminal({ cols, rows, ghostty }) - const addon = new SerializeAddon() - term.loadAddon(addon) - term.open(container) - terminals.push(term) - - return { term, addon, container } -} - -function writeAndWait(term: Terminal, data: string): Promise<void> { - return new Promise((resolve) => { - term.write(data, resolve) - }) -} - -describe("SerializeAddon", () => { - describe("ANSI color preservation", () => { - test("should preserve text attributes (bold, italic, underline)", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - expect(origLine!.getCell(0)!.isBold()).toBe(1) - expect(origLine!.getCell(5)!.isItalic()).toBe(1) - expect(origLine!.getCell(12)!.isUnderline()).toBe(1) - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - - const boldCell = line!.getCell(0) - expect(boldCell!.getChars()).toBe("B") - expect(boldCell!.isBold()).toBe(1) - - const italicCell = line!.getCell(5) - expect(italicCell!.getChars()).toBe("I") - expect(italicCell!.isItalic()).toBe(1) - - const underCell = line!.getCell(12) - expect(underCell!.getChars()).toBe("U") - expect(underCell!.isUnderline()).toBe(1) - }) - - test("should preserve basic 16-color foreground colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedFg = origLine!.getCell(0)!.getFgColor() - const origGreenFg = origLine!.getCell(3)!.getFgColor() - const origBlueFg = origLine!.getCell(8)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - expect(line).toBeDefined() - - const redCell = line!.getCell(0) - expect(redCell!.getChars()).toBe("R") - expect(redCell!.getFgColor()).toBe(origRedFg) - - const greenCell = line!.getCell(3) - expect(greenCell!.getChars()).toBe("G") - expect(greenCell!.getFgColor()).toBe(origGreenFg) - - const blueCell = line!.getCell(8) - expect(blueCell!.getChars()).toBe("B") - expect(blueCell!.getFgColor()).toBe(origBlueFg) - }) - - test("should preserve 256-color palette colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedFg = origLine!.getCell(0)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - const redCell = line!.getCell(0) - expect(redCell!.getChars()).toBe("R") - expect(redCell!.getFgColor()).toBe(origRedFg) - }) - - test("should preserve RGB/truecolor colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRgbFg = origLine!.getCell(0)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - const rgbCell = line!.getCell(0) - expect(rgbCell!.getChars()).toBe("R") - expect(rgbCell!.getFgColor()).toBe(origRgbFg) - }) - - test("should preserve background colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedBg = origLine!.getCell(0)!.getBgColor() - const origGreenBg = origLine!.getCell(6)!.getBgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - - const redBgCell = line!.getCell(0) - expect(redBgCell!.getChars()).toBe("R") - expect(redBgCell!.getBgColor()).toBe(origRedBg) - - const greenBgCell = line!.getCell(6) - expect(greenBgCell!.getChars()).toBe("G") - expect(greenBgCell!.getBgColor()).toBe(origGreenBg) - }) - - test("should handle combined colors and attributes", async () => { - const { term, addon } = createTerminal() - - const input = - "\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL " - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origFg = origLine!.getCell(0)!.getFgColor() - const origBg = origLine!.getCell(0)!.getBgColor() - expect(origLine!.getCell(0)!.isBold()).toBe(1) - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "") - - expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, cleanSerialized) - - const line = term2.buffer.active.getLine(0) - const comboCell = line!.getCell(0) - - expect(comboCell!.getChars()).toBe("C") - expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m") - }) - }) - - describe("round-trip serialization", () => { - test("should not produce ECH sequences", async () => { - const { term, addon } = createTerminal() - - await writeAndWait(term, "\x1b[31mHello\x1b[0m World") - - const serialized = addon.serialize() - - const hasECH = /\x1b\[\d+X/.test(serialized) - expect(hasECH).toBe(false) - }) - - test("multi-line content should not have garbage characters", async () => { - const { term, addon } = createTerminal() - - const content = [ - "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path", - "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la", - "total 42", - ].join("\r\n") - - await writeAndWait(term, content) - - const serialized = addon.serialize() - - expect(/\x1b\[\d+X/.test(serialized)).toBe(false) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - for (let row = 0; row < 3; row++) { - const line = term2.buffer.active.getLine(row)?.translateToString(true) - expect(line?.includes("𑼝")).toBe(false) - } - - expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path") - expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la") - expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42") - }) - - test("serialized output written to new terminal should match original colors", async () => { - const { term, addon } = createTerminal(40, 5) - - const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! " - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origHelloFg = origLine!.getCell(0)!.getFgColor() - const origWorldFg = origLine!.getCell(6)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal(40, 5) - terminals.push(term2) - await writeAndWait(term2, serialized) - - const newLine = term2.buffer.active.getLine(0) - - expect(newLine!.getCell(0)!.getChars()).toBe("H") - expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg) - - expect(newLine!.getCell(6)!.getChars()).toBe("W") - expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg) - - expect(newLine!.getCell(11)!.getChars()).toBe("!") - }) - }) -}) diff --git a/packages/desktop/src/addons/serialize.ts b/packages/desktop/src/addons/serialize.ts deleted file mode 100644 index cb1ff8442..000000000 --- a/packages/desktop/src/addons/serialize.ts +++ /dev/null @@ -1,595 +0,0 @@ -/** - * SerializeAddon - Serialize terminal buffer contents - * - * Port of xterm.js addon-serialize for ghostty-web. - * Enables serialization of terminal contents to a string that can - * be written back to restore terminal state. - * - * Usage: - * ```typescript - * const serializeAddon = new SerializeAddon(); - * term.loadAddon(serializeAddon); - * const content = serializeAddon.serialize(); - * ``` - */ - -import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web" - -// ============================================================================ -// Buffer Types (matching ghostty-web internal interfaces) -// ============================================================================ - -interface IBuffer { - readonly type: "normal" | "alternate" - readonly cursorX: number - readonly cursorY: number - readonly viewportY: number - readonly baseY: number - readonly length: number - getLine(y: number): IBufferLine | undefined - getNullCell(): IBufferCell -} - -interface IBufferLine { - readonly length: number - readonly isWrapped: boolean - getCell(x: number): IBufferCell | undefined - translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string -} - -interface IBufferCell { - getChars(): string - getCode(): number - getWidth(): number - getFgColorMode(): number - getBgColorMode(): number - getFgColor(): number - getBgColor(): number - isBold(): number - isItalic(): number - isUnderline(): number - isStrikethrough(): number - isBlink(): number - isInverse(): number - isInvisible(): number - isFaint(): number - isDim(): boolean -} - -// ============================================================================ -// Types -// ============================================================================ - -export interface ISerializeOptions { - /** - * The row range to serialize. When 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. - */ - scrollback?: number - /** - * Whether to exclude the terminal modes from the serialization. - * Default: false - */ - excludeModes?: boolean - /** - * Whether to exclude the alt buffer from the serialization. - * Default: false - */ - excludeAltBuffer?: boolean -} - -export interface ISerializeRange { - /** - * The line to start serializing (inclusive). - */ - start: number - /** - * The line to end serializing (inclusive). - */ - end: number -} - -export interface IHTMLSerializeOptions { - /** - * The number of rows in the scrollback buffer to serialize, starting from - * the bottom of the scrollback buffer. - */ - scrollback?: number - /** - * Whether to only serialize the selection. - * Default: false - */ - onlySelection?: boolean - /** - * Whether to include the global background of the terminal. - * Default: false - */ - includeGlobalBackground?: boolean - /** - * The range to serialize. This is prioritized over onlySelection. - */ - range?: { - startLine: number - endLine: number - startCol: number - } -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -function constrain(value: number, low: number, high: number): number { - return Math.max(low, Math.min(value, high)) -} - -function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor() -} - -function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor() -} - -function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { - return ( - !!cell1.isInverse() === !!cell2.isInverse() && - !!cell1.isBold() === !!cell2.isBold() && - !!cell1.isUnderline() === !!cell2.isUnderline() && - !!cell1.isBlink() === !!cell2.isBlink() && - !!cell1.isInvisible() === !!cell2.isInvisible() && - !!cell1.isItalic() === !!cell2.isItalic() && - !!cell1.isDim() === !!cell2.isDim() && - !!cell1.isStrikethrough() === !!cell2.isStrikethrough() - ) -} - -// ============================================================================ -// Base Serialize Handler -// ============================================================================ - -abstract class BaseSerializeHandler { - constructor(protected readonly _buffer: IBuffer) {} - - private _isRealContent(codepoint: number): boolean { - if (codepoint === 0) return false - if (codepoint >= 0xf000) return false - return true - } - - private _findLastContentColumn(line: IBufferLine): number { - let lastContent = -1 - for (let col = 0; col < line.length; col++) { - const cell = line.getCell(col) - if (cell && this._isRealContent(cell.getCode())) { - lastContent = col - } - } - return lastContent + 1 - } - - public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string { - let oldCell = this._buffer.getNullCell() - - 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.y ? startColumn : 0 - const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line) - const endLineColumn = Math.min(maxColumn, line.length) - for (let col = startLineColumn; col < endLineColumn; col++) { - const c = line.getCell(col) - if (!c) { - continue - } - this._nextCell(c, oldCell, row, col) - oldCell = c - } - } - this._rowEnd(row, row === endRow) - } - - this._afterSerialize() - - 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(_excludeFinalCursorPosition?: boolean): string { - return "" - } -} - -// ============================================================================ -// String Serialize Handler -// ============================================================================ - -class StringSerializeHandler extends BaseSerializeHandler { - private _rowIndex: number = 0 - private _allRows: string[] = [] - private _allRowSeparators: string[] = [] - private _currentRow: string = "" - private _nullCellCount: number = 0 - private _cursorStyle: IBufferCell - private _firstRow: number = 0 - private _lastCursorRow: number = 0 - private _lastCursorCol: number = 0 - private _lastContentCursorRow: number = 0 - private _lastContentCursorCol: number = 0 - - constructor( - buffer: IBuffer, - private readonly _terminal: ITerminalCore, - ) { - super(buffer) - this._cursorStyle = this._buffer.getNullCell() - } - - protected _beforeSerialize(rows: number, start: number, _end: number): void { - this._allRows = new Array<string>(rows) - this._lastContentCursorRow = start - this._lastCursorRow = start - this._firstRow = start - } - - protected _rowEnd(row: number, isLastRow: boolean): void { - let rowSeparator = "" - - if (!isLastRow) { - const nextLine = this._buffer.getLine(row + 1) - - if (!nextLine?.isWrapped) { - rowSeparator = "\r\n" - this._lastCursorRow = row + 1 - this._lastCursorCol = 0 - } - } - - this._allRows[this._rowIndex] = this._currentRow - this._allRowSeparators[this._rowIndex++] = rowSeparator - this._currentRow = "" - this._nullCellCount = 0 - } - - private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { - const sgrSeq: number[] = [] - const fgChanged = !equalFg(cell, oldCell) - const bgChanged = !equalBg(cell, oldCell) - const flagsChanged = !equalFlags(cell, oldCell) - - if (fgChanged || bgChanged || flagsChanged) { - if (this._isAttributeDefault(cell)) { - if (!this._isAttributeDefault(oldCell)) { - sgrSeq.push(0) - } - } else { - if (flagsChanged) { - if (!!cell.isInverse() !== !!oldCell.isInverse()) { - sgrSeq.push(cell.isInverse() ? 7 : 27) - } - if (!!cell.isBold() !== !!oldCell.isBold()) { - sgrSeq.push(cell.isBold() ? 1 : 22) - } - if (!!cell.isUnderline() !== !!oldCell.isUnderline()) { - sgrSeq.push(cell.isUnderline() ? 4 : 24) - } - if (!!cell.isBlink() !== !!oldCell.isBlink()) { - sgrSeq.push(cell.isBlink() ? 5 : 25) - } - if (!!cell.isInvisible() !== !!oldCell.isInvisible()) { - sgrSeq.push(cell.isInvisible() ? 8 : 28) - } - if (!!cell.isItalic() !== !!oldCell.isItalic()) { - sgrSeq.push(cell.isItalic() ? 3 : 23) - } - if (!!cell.isDim() !== !!oldCell.isDim()) { - sgrSeq.push(cell.isDim() ? 2 : 22) - } - if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) { - sgrSeq.push(cell.isStrikethrough() ? 9 : 29) - } - } - if (fgChanged) { - const color = cell.getFgColor() - const mode = cell.getFgColorMode() - if (mode === 2 || mode === 3 || mode === -1) { - sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(38, 5, color) - } else { - sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)) - } - } else { - sgrSeq.push(39) - } - } - if (bgChanged) { - const color = cell.getBgColor() - const mode = cell.getBgColorMode() - if (mode === 2 || mode === 3 || mode === -1) { - sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(48, 5, color) - } else { - sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)) - } - } else { - sgrSeq.push(49) - } - } - } - } - - return sgrSeq - } - - private _isAttributeDefault(cell: IBufferCell): boolean { - const mode = cell.getFgColorMode() - const bgMode = cell.getBgColorMode() - - if (mode === 0 && bgMode === 0) { - return ( - !cell.isBold() && - !cell.isItalic() && - !cell.isUnderline() && - !cell.isBlink() && - !cell.isInverse() && - !cell.isInvisible() && - !cell.isDim() && - !cell.isStrikethrough() - ) - } - - const fgColor = cell.getFgColor() - const bgColor = cell.getBgColor() - const nullCell = this._buffer.getNullCell() - const nullFg = nullCell.getFgColor() - const nullBg = nullCell.getBgColor() - - return ( - fgColor === nullFg && - bgColor === nullBg && - !cell.isBold() && - !cell.isItalic() && - !cell.isUnderline() && - !cell.isBlink() && - !cell.isInverse() && - !cell.isInvisible() && - !cell.isDim() && - !cell.isStrikethrough() - ) - } - - protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void { - const isPlaceHolderCell = cell.getWidth() === 0 - - if (isPlaceHolderCell) { - return - } - - const codepoint = cell.getCode() - const isGarbage = codepoint >= 0xf000 - const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage - - const sgrSeq = this._diffStyle(cell, this._cursorStyle) - - const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0 - - if (styleChanged) { - if (this._nullCellCount > 0) { - this._currentRow += `\u001b[${this._nullCellCount}C` - this._nullCellCount = 0 - } - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col - - this._currentRow += `\u001b[${sgrSeq.join(";")}m` - - const line = this._buffer.getLine(row) - const cellFromLine = line?.getCell(col) - if (cellFromLine) { - this._cursorStyle = cellFromLine - } - } - - if (isEmptyCell) { - this._nullCellCount += cell.getWidth() - } else { - if (this._nullCellCount > 0) { - this._currentRow += `\u001b[${this._nullCellCount}C` - this._nullCellCount = 0 - } - - this._currentRow += cell.getChars() - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth() - } - } - - protected _serializeString(excludeFinalCursorPosition?: boolean): string { - let rowEnd = this._allRows.length - - if (this._buffer.length - this._firstRow <= this._terminal.rows) { - rowEnd = this._lastContentCursorRow + 1 - this._firstRow - this._lastCursorCol = this._lastContentCursorCol - this._lastCursorRow = this._lastContentCursorRow - } - - let content = "" - - for (let i = 0; i < rowEnd; i++) { - content += this._allRows[i] - if (i + 1 < rowEnd) { - content += this._allRowSeparators[i] - } - } - - if (!excludeFinalCursorPosition) { - const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY - const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER) - const cursorCol = this._buffer.cursorX + 1 - content += `\u001b[${cursorRow};${cursorCol}H` - } - - return content - } -} - -// ============================================================================ -// SerializeAddon Class -// ============================================================================ - -export class SerializeAddon implements ITerminalAddon { - private _terminal?: ITerminalCore - - /** - * Activate the addon (called by Terminal.loadAddon) - */ - public activate(terminal: ITerminalCore): void { - this._terminal = terminal - } - - /** - * Dispose the addon and clean up resources - */ - public dispose(): void { - this._terminal = undefined - } - - /** - * Serializes terminal rows into a string that can be written back to the - * terminal to restore the state. The cursor will also be positioned to the - * correct cell. - * - * @param options Custom options to allow control over what gets serialized. - */ - public serialize(options?: ISerializeOptions): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const terminal = this._terminal as any - const buffer = terminal.buffer - - if (!buffer) { - return "" - } - - const normalBuffer = buffer.normal || buffer.active - const altBuffer = buffer.alternate - - if (!normalBuffer) { - return "" - } - - let content = options?.range - ? this._serializeBufferByRange(normalBuffer, options.range, true) - : this._serializeBufferByScrollback(normalBuffer, options?.scrollback) - - if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) { - const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined) - content += `\u001b[?1049h\u001b[H${alternateContent}` - } - - return content - } - - /** - * Serializes terminal content as plain text (no escape sequences) - * @param options Custom options to allow control over what gets serialized. - */ - public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const terminal = this._terminal as any - const buffer = terminal.buffer - - if (!buffer) { - return "" - } - - const activeBuffer = buffer.active || buffer.normal - if (!activeBuffer) { - return "" - } - - const maxRows = activeBuffer.length - const scrollback = options?.scrollback - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows) - - const startRow = maxRows - correctRows - const endRow = maxRows - 1 - const lines: string[] = [] - - for (let row = startRow; row <= endRow; row++) { - const line = activeBuffer.getLine(row) - if (line) { - const text = line.translateToString(options?.trimWhitespace ?? true) - lines.push(text) - } - } - - // Trim trailing empty lines if requested - if (options?.trimWhitespace) { - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop() - } - } - - return lines.join("\n") - } - - private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length - const rows = this._terminal?.rows ?? 24 - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows) - return this._serializeBufferByRange( - buffer, - { - start: maxRows - correctRows, - end: maxRows - 1, - }, - false, - ) - } - - private _serializeBufferByRange( - buffer: IBuffer, - range: ISerializeRange, - excludeFinalCursorPosition: boolean, - ): string { - const handler = new StringSerializeHandler(buffer, this._terminal!) - const cols = this._terminal?.cols ?? 80 - return handler.serialize( - { - start: { x: 0, y: range.start }, - end: { x: cols, y: range.end }, - }, - excludeFinalCursorPosition, - ) - } -} |
