summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/addons
diff options
context:
space:
mode:
Diffstat (limited to 'packages/desktop/src/addons')
-rw-r--r--packages/desktop/src/addons/serialize.test.ts272
-rw-r--r--packages/desktop/src/addons/serialize.ts595
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,
- )
- }
-}