|
@@ -1,649 +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) {}
|
|
|
|
|
-
|
|
|
|
|
- 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 endLineColumn = row === range.end.y ? endColumn : 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 _cursorStyleRow: number = 0
|
|
|
|
|
- private _cursorStyleCol: number = 0
|
|
|
|
|
- private _backgroundCell: IBufferCell
|
|
|
|
|
- private _firstRow: number = 0
|
|
|
|
|
- private _lastCursorRow: number = 0
|
|
|
|
|
- private _lastCursorCol: number = 0
|
|
|
|
|
- private _lastContentCursorRow: number = 0
|
|
|
|
|
- private _lastContentCursorCol: number = 0
|
|
|
|
|
- private _thisRowLastChar: IBufferCell
|
|
|
|
|
- private _thisRowLastSecondChar: IBufferCell
|
|
|
|
|
- private _nextRowFirstChar: IBufferCell
|
|
|
|
|
-
|
|
|
|
|
- constructor(
|
|
|
|
|
- buffer: IBuffer,
|
|
|
|
|
- private readonly _terminal: ITerminalCore,
|
|
|
|
|
- ) {
|
|
|
|
|
- super(buffer)
|
|
|
|
|
- this._cursorStyle = this._buffer.getNullCell()
|
|
|
|
|
- this._backgroundCell = this._buffer.getNullCell()
|
|
|
|
|
- this._thisRowLastChar = this._buffer.getNullCell()
|
|
|
|
|
- this._thisRowLastSecondChar = this._buffer.getNullCell()
|
|
|
|
|
- this._nextRowFirstChar = 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 {
|
|
|
|
|
- // if there is colorful empty cell at line end, we must pad it back
|
|
|
|
|
- if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
|
|
|
|
- this._currentRow += `\u001b[${this._nullCellCount}X`
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let rowSeparator = ""
|
|
|
|
|
-
|
|
|
|
|
- if (!isLastRow) {
|
|
|
|
|
- // Enable BCE
|
|
|
|
|
- if (row - this._firstRow >= this._terminal.rows) {
|
|
|
|
|
- const line = this._buffer.getLine(this._cursorStyleRow)
|
|
|
|
|
- const cell = line?.getCell(this._cursorStyleCol)
|
|
|
|
|
- if (cell) {
|
|
|
|
|
- this._backgroundCell = cell
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const currentLine = this._buffer.getLine(row)!
|
|
|
|
|
- const nextLine = this._buffer.getLine(row + 1)!
|
|
|
|
|
-
|
|
|
|
|
- if (!nextLine.isWrapped) {
|
|
|
|
|
- rowSeparator = "\r\n"
|
|
|
|
|
- this._lastCursorRow = row + 1
|
|
|
|
|
- this._lastCursorCol = 0
|
|
|
|
|
- } else {
|
|
|
|
|
- rowSeparator = ""
|
|
|
|
|
- const thisRowLastChar = currentLine.getCell(currentLine.length - 1)
|
|
|
|
|
- const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2)
|
|
|
|
|
- const nextRowFirstChar = nextLine.getCell(0)
|
|
|
|
|
-
|
|
|
|
|
- if (thisRowLastChar) this._thisRowLastChar = thisRowLastChar
|
|
|
|
|
- if (thisRowLastSecondChar) this._thisRowLastSecondChar = thisRowLastSecondChar
|
|
|
|
|
- if (nextRowFirstChar) this._nextRowFirstChar = nextRowFirstChar
|
|
|
|
|
-
|
|
|
|
|
- const isNextRowFirstCharDoubleWidth = this._nextRowFirstChar.getWidth() > 1
|
|
|
|
|
-
|
|
|
|
|
- let isValid = false
|
|
|
|
|
-
|
|
|
|
|
- if (
|
|
|
|
|
- this._nextRowFirstChar.getChars() &&
|
|
|
|
|
- (isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0)
|
|
|
|
|
- ) {
|
|
|
|
|
- if (
|
|
|
|
|
- (this._thisRowLastChar.getChars() || this._thisRowLastChar.getWidth() === 0) &&
|
|
|
|
|
- equalBg(this._thisRowLastChar, this._nextRowFirstChar)
|
|
|
|
|
- ) {
|
|
|
|
|
- isValid = true
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (
|
|
|
|
|
- isNextRowFirstCharDoubleWidth &&
|
|
|
|
|
- (this._thisRowLastSecondChar.getChars() || this._thisRowLastSecondChar.getWidth() === 0) &&
|
|
|
|
|
- equalBg(this._thisRowLastChar, this._nextRowFirstChar) &&
|
|
|
|
|
- equalBg(this._thisRowLastSecondChar, this._nextRowFirstChar)
|
|
|
|
|
- ) {
|
|
|
|
|
- isValid = true
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!isValid) {
|
|
|
|
|
- rowSeparator = "-".repeat(this._nullCellCount + 1)
|
|
|
|
|
- rowSeparator += "\u001b[1D\u001b[1X"
|
|
|
|
|
-
|
|
|
|
|
- if (this._nullCellCount > 0) {
|
|
|
|
|
- rowSeparator += "\u001b[A"
|
|
|
|
|
- rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`
|
|
|
|
|
- rowSeparator += `\u001b[${this._nullCellCount}X`
|
|
|
|
|
- rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`
|
|
|
|
|
- rowSeparator += "\u001b[B"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- this._lastContentCursorRow = row + 1
|
|
|
|
|
- this._lastContentCursorCol = 0
|
|
|
|
|
- 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 (fgChanged) {
|
|
|
|
|
- const color = cell.getFgColor()
|
|
|
|
|
- const mode = cell.getFgColorMode()
|
|
|
|
|
- if (mode === 2) {
|
|
|
|
|
- // RGB
|
|
|
|
|
- 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) {
|
|
|
|
|
- // RGB
|
|
|
|
|
- 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)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- 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)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return sgrSeq
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private _isAttributeDefault(cell: IBufferCell): boolean {
|
|
|
|
|
- return (
|
|
|
|
|
- cell.getFgColorMode() === 0 &&
|
|
|
|
|
- cell.getBgColorMode() === 0 &&
|
|
|
|
|
- !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 isEmptyCell = cell.getChars() === ""
|
|
|
|
|
-
|
|
|
|
|
- const sgrSeq = this._diffStyle(cell, this._cursorStyle)
|
|
|
|
|
-
|
|
|
|
|
- const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
|
|
|
|
|
-
|
|
|
|
|
- if (styleChanged) {
|
|
|
|
|
- if (this._nullCellCount > 0) {
|
|
|
|
|
- if (!equalBg(this._cursorStyle, this._backgroundCell)) {
|
|
|
|
|
- this._currentRow += `\u001b[${this._nullCellCount}X`
|
|
|
|
|
- }
|
|
|
|
|
- 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
|
|
|
|
|
- this._cursorStyleRow = row
|
|
|
|
|
- this._cursorStyleCol = col
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (isEmptyCell) {
|
|
|
|
|
- this._nullCellCount += cell.getWidth()
|
|
|
|
|
- } else {
|
|
|
|
|
- if (this._nullCellCount > 0) {
|
|
|
|
|
- if (equalBg(this._cursorStyle, this._backgroundCell)) {
|
|
|
|
|
- this._currentRow += `\u001b[${this._nullCellCount}C`
|
|
|
|
|
- } else {
|
|
|
|
|
- this._currentRow += `\u001b[${this._nullCellCount}X`
|
|
|
|
|
- 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) {
|
|
|
|
|
- // Get cursor position relative to viewport (1-indexed for ANSI)
|
|
|
|
|
- // cursorY is relative to the viewport, cursorX is column position
|
|
|
|
|
- const cursorRow = this._buffer.cursorY + 1 // 1-indexed
|
|
|
|
|
- const cursorCol = this._buffer.cursorX + 1 // 1-indexed
|
|
|
|
|
-
|
|
|
|
|
- // Use absolute cursor positioning (CUP - Cursor Position)
|
|
|
|
|
- // This is more reliable than relative moves which depend on knowing
|
|
|
|
|
- // exactly where the cursor ended up after all the content
|
|
|
|
|
- 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 activeBuffer = buffer.active || buffer.normal
|
|
|
|
|
- if (!activeBuffer) {
|
|
|
|
|
- return ""
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let content = options?.range
|
|
|
|
|
- ? this._serializeBufferByRange(activeBuffer, options.range, true)
|
|
|
|
|
- : this._serializeBufferByScrollback(activeBuffer, options?.scrollback)
|
|
|
|
|
-
|
|
|
|
|
- // Handle alternate buffer if active and not excluded
|
|
|
|
|
- if (!options?.excludeAltBuffer) {
|
|
|
|
|
- const altBuffer = buffer.alternate
|
|
|
|
|
- if (altBuffer && buffer.active?.type === "alternate") {
|
|
|
|
|
- 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,
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|